diff --git a/common/css/edit-flow-admin.css b/common/css/edit-flow-admin.css index 4cd35b711..c060c08c8 100644 --- a/common/css/edit-flow-admin.css +++ b/common/css/edit-flow-admin.css @@ -20,3 +20,61 @@ display: block; } +/* +Users list style + */ + +.list { + margin: 0; +} + +.pagination li { + display:inline-block; + padding-top:5px; +} + +.page-item { + !important; + padding-right: 4px; + padding-left: 4px; +} + +.user-item { + display: block; + padding: 10px 5px 5px 5px; + margin: 0; + border-bottom: 1px solid #ccc; +} + +.user-item:hover { + background: #EAF2FA; +} + +.user-item-name { + !important; + display: block; + font-size: 14px; + margin: 0; +} + +.user-item-email { + !important; + display: block; + color: #ccc; + font-size: 12px; + margin: 0; +} + +.user-item input { + float: right; +} + +.users-list-infos { + text-align: right; + display: block; + border-bottom: 1px solid #ccc; +} + +.users-total-info-value{ + font-weight: bold; +} \ No newline at end of file diff --git a/common/js/jquery.twbsPagination.js b/common/js/jquery.twbsPagination.js new file mode 100644 index 000000000..2451a6109 --- /dev/null +++ b/common/js/jquery.twbsPagination.js @@ -0,0 +1,364 @@ +/*! + * jQuery pagination plugin v1.4.2 + * http://josecebe.github.io/twbs-pagination/ + * + * Copyright 2014-2018, Eugene Simakin + * Released under Apache 2.0 license + * http://apache.org/licenses/LICENSE-2.0.html + */ +(function ($, window, document, undefined) { + + 'use strict'; + + var old = $.fn.twbsPagination; + + // PROTOTYPE AND CONSTRUCTOR + + var TwbsPagination = function (element, options) { + this.$element = $(element); + this.options = $.extend({}, $.fn.twbsPagination.defaults, options); + + if (this.options.startPage < 1 || this.options.startPage > this.options.totalPages) { + throw new Error('Start page option is incorrect'); + } + + this.options.totalPages = parseInt(this.options.totalPages); + if (isNaN(this.options.totalPages)) { + throw new Error('Total pages option is not correct!'); + } + + this.options.visiblePages = parseInt(this.options.visiblePages); + if (isNaN(this.options.visiblePages)) { + throw new Error('Visible pages option is not correct!'); + } + + if (this.options.beforePageClick instanceof Function) { + this.$element.first().on('beforePage', this.options.beforePageClick); + } + + if (this.options.onPageClick instanceof Function) { + this.$element.first().on('page', this.options.onPageClick); + } + + // hide if only one page exists + if (this.options.hideOnlyOnePage && this.options.totalPages == 1) { + if (this.options.initiateStartPageClick) { + this.$element.trigger('page', 1); + } + return this; + } + + if (this.options.href) { + this.options.startPage = this.getPageFromQueryString(); + if (!this.options.startPage) { + this.options.startPage = 1; + } + } + + var tagName = (typeof this.$element.prop === 'function') ? + this.$element.prop('tagName') : this.$element.attr('tagName'); + + if (tagName === 'UL') { + this.$listContainer = this.$element; + } else { + var elements = this.$element; + var $newListContainer = $([]); + elements.each(function(index) { + var $newElem = $(""); + $(this).append($newElem); + $newListContainer.push($newElem[0]); + }); + this.$listContainer = $newListContainer; + this.$element = $newListContainer; + } + + this.$listContainer.addClass(this.options.paginationClass); + + if (this.options.initiateStartPageClick) { + this.show(this.options.startPage); + } else { + this.currentPage = this.options.startPage; + this.render(this.getPages(this.options.startPage)); + this.setupEvents(); + } + + return this; + }; + + TwbsPagination.prototype = { + + constructor: TwbsPagination, + + destroy: function () { + this.$element.empty(); + this.$element.removeData('twbs-pagination'); + this.$element.off('page'); + + return this; + }, + + show: function (page) { + if (page < 1 || page > this.options.totalPages) { + throw new Error('Page is incorrect.'); + } + this.currentPage = page; + + this.$element.trigger('beforePage', page); + + var pages = this.getPages(page); + this.render(pages); + this.setupEvents(); + + this.$element.trigger('page', page); + + return pages; + }, + + enable: function () { + this.show(this.currentPage); + }, + + disable: function () { + var _this = this; + this.$listContainer.off('click').on('click', 'li', function (evt) { + evt.preventDefault(); + }); + this.$listContainer.children().each(function () { + var $this = $(this); + if (!$this.hasClass(_this.options.activeClass)) { + $(this).addClass(_this.options.disabledClass); + } + }); + }, + + buildListItems: function (pages) { + var listItems = []; + + if (this.options.first) { + listItems.push(this.buildItem('first', 1)); + } + + if (this.options.prev) { + var prev = pages.currentPage > 1 ? pages.currentPage - 1 : this.options.loop ? this.options.totalPages : 1; + listItems.push(this.buildItem('prev', prev)); + } + + for (var i = 0; i < pages.numeric.length; i++) { + listItems.push(this.buildItem('page', pages.numeric[i])); + } + + if (this.options.next) { + var next = pages.currentPage < this.options.totalPages ? pages.currentPage + 1 : this.options.loop ? 1 : this.options.totalPages; + listItems.push(this.buildItem('next', next)); + } + + if (this.options.last) { + listItems.push(this.buildItem('last', this.options.totalPages)); + } + + return listItems; + }, + + buildItem: function (type, page) { + var $itemContainer = $('
  • '), + $itemContent = $(''), + itemText = this.options[type] ? this.makeText(this.options[type], page) : page; + + $itemContainer.addClass(this.options[type + 'Class']); + $itemContainer.data('page', page); + $itemContainer.data('page-type', type); + $itemContainer.append($itemContent.attr('href', this.makeHref(page)).addClass(this.options.anchorClass).html(itemText)); + + return $itemContainer; + }, + + getPages: function (currentPage) { + var pages = []; + + var half = Math.floor(this.options.visiblePages / 2); + var start = currentPage - half + 1 - this.options.visiblePages % 2; + var end = currentPage + half; + + var visiblePages = this.options.visiblePages; + if (visiblePages > this.options.totalPages) { + visiblePages = this.options.totalPages; + } + + // handle boundary case + if (start <= 0) { + start = 1; + end = visiblePages; + } + if (end > this.options.totalPages) { + start = this.options.totalPages - visiblePages + 1; + end = this.options.totalPages; + } + + var itPage = start; + while (itPage <= end) { + pages.push(itPage); + itPage++; + } + + return {"currentPage": currentPage, "numeric": pages}; + }, + + render: function (pages) { + var _this = this; + this.$listContainer.children().remove(); + var items = this.buildListItems(pages); + $.each(items, function(key, item){ + _this.$listContainer.append(item); + }); + + this.$listContainer.children().each(function () { + var $this = $(this), + pageType = $this.data('page-type'); + + switch (pageType) { + case 'page': + if ($this.data('page') === pages.currentPage) { + $this.addClass(_this.options.activeClass); + } + break; + case 'first': + $this.toggleClass(_this.options.disabledClass, pages.currentPage === 1); + break; + case 'last': + $this.toggleClass(_this.options.disabledClass, pages.currentPage === _this.options.totalPages); + break; + case 'prev': + $this.toggleClass(_this.options.disabledClass, !_this.options.loop && pages.currentPage === 1); + break; + case 'next': + $this.toggleClass(_this.options.disabledClass, + !_this.options.loop && pages.currentPage === _this.options.totalPages); + break; + default: + break; + } + + }); + }, + + setupEvents: function () { + var _this = this; + this.$listContainer.off('click').on('click', 'li', function (evt) { + var $this = $(this); + if ($this.hasClass(_this.options.disabledClass) || $this.hasClass(_this.options.activeClass)) { + return false; + } + // Prevent click event if href is not set. + !_this.options.href && evt.preventDefault(); + _this.show(parseInt($this.data('page'))); + }); + }, + + changeTotalPages: function(totalPages, currentPage) { + this.options.totalPages = totalPages; + return this.show(currentPage); + }, + + makeHref: function (page) { + return this.options.href ? this.generateQueryString(page) : "#"; + }, + + makeText: function (text, page) { + return text.replace(this.options.pageVariable, page) + .replace(this.options.totalPagesVariable, this.options.totalPages) + }, + + getPageFromQueryString: function (searchStr) { + var search = this.getSearchString(searchStr), + regex = new RegExp(this.options.pageVariable + '(=([^&#]*)|&|#|$)'), + page = regex.exec(search); + if (!page || !page[2]) { + return null; + } + page = decodeURIComponent(page[2]); + page = parseInt(page); + if (isNaN(page)) { + return null; + } + return page; + }, + + generateQueryString: function (pageNumber, searchStr) { + var search = this.getSearchString(searchStr), + regex = new RegExp(this.options.pageVariable + '=*[^&#]*'); + if (!search) return ''; + return '?' + search.replace(regex, this.options.pageVariable + '=' + pageNumber); + }, + + getSearchString: function (searchStr) { + var search = searchStr || window.location.search; + if (search === '') { + return null; + } + if (search.indexOf('?') === 0) search = search.substr(1); + return search; + }, + + getCurrentPage: function () { + return this.currentPage; + }, + + getTotalPages: function () { + return this.options.totalPages; + } + }; + + // PLUGIN DEFINITION + + $.fn.twbsPagination = function (option) { + var args = Array.prototype.slice.call(arguments, 1); + var methodReturn; + + var $this = $(this); + var data = $this.data('twbs-pagination'); + var options = typeof option === 'object' ? option : {}; + + if (!data) $this.data('twbs-pagination', (data = new TwbsPagination(this, options) )); + if (typeof option === 'string') methodReturn = data[ option ].apply(data, args); + + return ( methodReturn === undefined ) ? $this : methodReturn; + }; + + $.fn.twbsPagination.defaults = { + totalPages: 1, + startPage: 1, + visiblePages: 5, + initiateStartPageClick: true, + hideOnlyOnePage: false, + href: false, + pageVariable: '{{page}}', + totalPagesVariable: '{{total_pages}}', + page: null, + first: 'First', + prev: 'Previous', + next: 'Next', + last: 'Last', + loop: false, + beforePageClick: null, + onPageClick: null, + paginationClass: 'pagination', + nextClass: 'page-item next', + prevClass: 'page-item prev', + lastClass: 'page-item last', + firstClass: 'page-item first', + pageClass: 'page-item', + activeClass: 'active', + disabledClass: 'disabled', + anchorClass: 'page-link' + }; + + $.fn.twbsPagination.Constructor = TwbsPagination; + + $.fn.twbsPagination.noConflict = function () { + $.fn.twbsPagination = old; + return this; + }; + + $.fn.twbsPagination.version = "1.4.2"; + +})(window.jQuery, window, document); diff --git a/common/js/list.js b/common/js/list.js new file mode 100644 index 000000000..d40457fb3 --- /dev/null +++ b/common/js/list.js @@ -0,0 +1,1758 @@ +/*! List.js v1.5.0 (http://listjs.com) by Jonny Strömberg (http://javve.com) */ +var List = +/******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; + +/******/ // The require function +/******/ function __webpack_require__(moduleId) { + +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) +/******/ return installedModules[moduleId].exports; + +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; + +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); + +/******/ // Flag the module as loaded +/******/ module.l = true; + +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } + + +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; + +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; + +/******/ // identity function for calling harmony imports with the correct context +/******/ __webpack_require__.i = function(value) { return value; }; + +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { +/******/ configurable: false, +/******/ enumerable: true, +/******/ get: getter +/******/ }); +/******/ } +/******/ }; + +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; + +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; + +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; + +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = 11); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ (function(module, exports, __webpack_require__) { + +/** + * Module dependencies. + */ + +var index = __webpack_require__(4); + +/** + * Whitespace regexp. + */ + +var re = /\s+/; + +/** + * toString reference. + */ + +var toString = Object.prototype.toString; + +/** + * Wrap `el` in a `ClassList`. + * + * @param {Element} el + * @return {ClassList} + * @api public + */ + +module.exports = function(el){ + return new ClassList(el); +}; + +/** + * Initialize a new ClassList for `el`. + * + * @param {Element} el + * @api private + */ + +function ClassList(el) { + if (!el || !el.nodeType) { + throw new Error('A DOM element reference is required'); + } + this.el = el; + this.list = el.classList; +} + +/** + * Add class `name` if not already present. + * + * @param {String} name + * @return {ClassList} + * @api public + */ + +ClassList.prototype.add = function(name){ + // classList + if (this.list) { + this.list.add(name); + return this; + } + + // fallback + var arr = this.array(); + var i = index(arr, name); + if (!~i) arr.push(name); + this.el.className = arr.join(' '); + return this; +}; + +/** + * Remove class `name` when present, or + * pass a regular expression to remove + * any which match. + * + * @param {String|RegExp} name + * @return {ClassList} + * @api public + */ + +ClassList.prototype.remove = function(name){ + // classList + if (this.list) { + this.list.remove(name); + return this; + } + + // fallback + var arr = this.array(); + var i = index(arr, name); + if (~i) arr.splice(i, 1); + this.el.className = arr.join(' '); + return this; +}; + + +/** + * Toggle class `name`, can force state via `force`. + * + * For browsers that support classList, but do not support `force` yet, + * the mistake will be detected and corrected. + * + * @param {String} name + * @param {Boolean} force + * @return {ClassList} + * @api public + */ + +ClassList.prototype.toggle = function(name, force){ + // classList + if (this.list) { + if ("undefined" !== typeof force) { + if (force !== this.list.toggle(name, force)) { + this.list.toggle(name); // toggle again to correct + } + } else { + this.list.toggle(name); + } + return this; + } + + // fallback + if ("undefined" !== typeof force) { + if (!force) { + this.remove(name); + } else { + this.add(name); + } + } else { + if (this.has(name)) { + this.remove(name); + } else { + this.add(name); + } + } + + return this; +}; + +/** + * Return an array of classes. + * + * @return {Array} + * @api public + */ + +ClassList.prototype.array = function(){ + var className = this.el.getAttribute('class') || ''; + var str = className.replace(/^\s+|\s+$/g, ''); + var arr = str.split(re); + if ('' === arr[0]) arr.shift(); + return arr; +}; + +/** + * Check if class `name` is present. + * + * @param {String} name + * @return {ClassList} + * @api public + */ + +ClassList.prototype.has = +ClassList.prototype.contains = function(name){ + return this.list ? this.list.contains(name) : !! ~index(this.array(), name); +}; + + +/***/ }), +/* 1 */ +/***/ (function(module, exports, __webpack_require__) { + +var bind = window.addEventListener ? 'addEventListener' : 'attachEvent', + unbind = window.removeEventListener ? 'removeEventListener' : 'detachEvent', + prefix = bind !== 'addEventListener' ? 'on' : '', + toArray = __webpack_require__(5); + +/** + * Bind `el` event `type` to `fn`. + * + * @param {Element} el, NodeList, HTMLCollection or Array + * @param {String} type + * @param {Function} fn + * @param {Boolean} capture + * @api public + */ + +exports.bind = function(el, type, fn, capture){ + el = toArray(el); + for ( var i = 0; i < el.length; i++ ) { + el[i][bind](prefix + type, fn, capture || false); + } +}; + +/** + * Unbind `el` event `type`'s callback `fn`. + * + * @param {Element} el, NodeList, HTMLCollection or Array + * @param {String} type + * @param {Function} fn + * @param {Boolean} capture + * @api public + */ + +exports.unbind = function(el, type, fn, capture){ + el = toArray(el); + for ( var i = 0; i < el.length; i++ ) { + el[i][unbind](prefix + type, fn, capture || false); + } +}; + + +/***/ }), +/* 2 */ +/***/ (function(module, exports) { + +module.exports = function(list) { + return function(initValues, element, notCreate) { + var item = this; + + this._values = {}; + + this.found = false; // Show if list.searched == true and this.found == true + this.filtered = false;// Show if list.filtered == true and this.filtered == true + + var init = function(initValues, element, notCreate) { + if (element === undefined) { + if (notCreate) { + item.values(initValues, notCreate); + } else { + item.values(initValues); + } + } else { + item.elm = element; + var values = list.templater.get(item, initValues); + item.values(values); + } + }; + + this.values = function(newValues, notCreate) { + if (newValues !== undefined) { + for(var name in newValues) { + item._values[name] = newValues[name]; + } + if (notCreate !== true) { + list.templater.set(item, item.values()); + } + } else { + return item._values; + } + }; + + this.show = function() { + list.templater.show(item); + }; + + this.hide = function() { + list.templater.hide(item); + }; + + this.matching = function() { + return ( + (list.filtered && list.searched && item.found && item.filtered) || + (list.filtered && !list.searched && item.filtered) || + (!list.filtered && list.searched && item.found) || + (!list.filtered && !list.searched) + ); + }; + + this.visible = function() { + return (item.elm && (item.elm.parentNode == list.list)) ? true : false; + }; + + init(initValues, element, notCreate); + }; +}; + + +/***/ }), +/* 3 */ +/***/ (function(module, exports) { + +/** + * A cross-browser implementation of getElementsByClass. + * Heavily based on Dustin Diaz's function: http://dustindiaz.com/getelementsbyclass. + * + * Find all elements with class `className` inside `container`. + * Use `single = true` to increase performance in older browsers + * when only one element is needed. + * + * @param {String} className + * @param {Element} container + * @param {Boolean} single + * @api public + */ + +var getElementsByClassName = function(container, className, single) { + if (single) { + return container.getElementsByClassName(className)[0]; + } else { + return container.getElementsByClassName(className); + } +}; + +var querySelector = function(container, className, single) { + className = '.' + className; + if (single) { + return container.querySelector(className); + } else { + return container.querySelectorAll(className); + } +}; + +var polyfill = function(container, className, single) { + var classElements = [], + tag = '*'; + + var els = container.getElementsByTagName(tag); + var elsLen = els.length; + var pattern = new RegExp("(^|\\s)"+className+"(\\s|$)"); + for (var i = 0, j = 0; i < elsLen; i++) { + if ( pattern.test(els[i].className) ) { + if (single) { + return els[i]; + } else { + classElements[j] = els[i]; + j++; + } + } + } + return classElements; +}; + +module.exports = (function() { + return function(container, className, single, options) { + options = options || {}; + if ((options.test && options.getElementsByClassName) || (!options.test && document.getElementsByClassName)) { + return getElementsByClassName(container, className, single); + } else if ((options.test && options.querySelector) || (!options.test && document.querySelector)) { + return querySelector(container, className, single); + } else { + return polyfill(container, className, single); + } + }; +})(); + + +/***/ }), +/* 4 */ +/***/ (function(module, exports) { + +var indexOf = [].indexOf; + +module.exports = function(arr, obj){ + if (indexOf) return arr.indexOf(obj); + for (var i = 0; i < arr.length; ++i) { + if (arr[i] === obj) return i; + } + return -1; +}; + + +/***/ }), +/* 5 */ +/***/ (function(module, exports) { + +/** + * Source: https://github.com/timoxley/to-array + * + * Convert an array-like object into an `Array`. + * If `collection` is already an `Array`, then will return a clone of `collection`. + * + * @param {Array | Mixed} collection An `Array` or array-like object to convert e.g. `arguments` or `NodeList` + * @return {Array} Naive conversion of `collection` to a new `Array`. + * @api public + */ + +module.exports = function toArray(collection) { + if (typeof collection === 'undefined') return []; + if (collection === null) return [null]; + if (collection === window) return [window]; + if (typeof collection === 'string') return [collection]; + if (isArray(collection)) return collection; + if (typeof collection.length != 'number') return [collection]; + if (typeof collection === 'function' && collection instanceof Function) return [collection]; + + var arr = []; + for (var i = 0; i < collection.length; i++) { + if (Object.prototype.hasOwnProperty.call(collection, i) || i in collection) { + arr.push(collection[i]); + } + } + if (!arr.length) return []; + return arr; +}; + +function isArray(arr) { + return Object.prototype.toString.call(arr) === "[object Array]"; +} + + +/***/ }), +/* 6 */ +/***/ (function(module, exports) { + +module.exports = function(s) { + s = (s === undefined) ? "" : s; + s = (s === null) ? "" : s; + s = s.toString(); + return s; +}; + + +/***/ }), +/* 7 */ +/***/ (function(module, exports) { + +/* + * Source: https://github.com/segmentio/extend + */ + +module.exports = function extend (object) { + // Takes an unlimited number of extenders. + var args = Array.prototype.slice.call(arguments, 1); + + // For each extender, copy their properties on our object. + for (var i = 0, source; source = args[i]; i++) { + if (!source) continue; + for (var property in source) { + object[property] = source[property]; + } + } + + return object; +}; + + +/***/ }), +/* 8 */ +/***/ (function(module, exports) { + +module.exports = function(list) { + var addAsync = function(values, callback, items) { + var valuesToAdd = values.splice(0, 50); + items = items || []; + items = items.concat(list.add(valuesToAdd)); + if (values.length > 0) { + setTimeout(function() { + addAsync(values, callback, items); + }, 1); + } else { + list.update(); + callback(items); + } + }; + return addAsync; +}; + + +/***/ }), +/* 9 */ +/***/ (function(module, exports) { + +module.exports = function(list) { + + // Add handlers + list.handlers.filterStart = list.handlers.filterStart || []; + list.handlers.filterComplete = list.handlers.filterComplete || []; + + return function(filterFunction) { + list.trigger('filterStart'); + list.i = 1; // Reset paging + list.reset.filter(); + if (filterFunction === undefined) { + list.filtered = false; + } else { + list.filtered = true; + var is = list.items; + for (var i = 0, il = is.length; i < il; i++) { + var item = is[i]; + if (filterFunction(item)) { + item.filtered = true; + } else { + item.filtered = false; + } + } + } + list.update(); + list.trigger('filterComplete'); + return list.visibleItems; + }; +}; + + +/***/ }), +/* 10 */ +/***/ (function(module, exports, __webpack_require__) { + + +var classes = __webpack_require__(0), + events = __webpack_require__(1), + extend = __webpack_require__(7), + toString = __webpack_require__(6), + getByClass = __webpack_require__(3), + fuzzy = __webpack_require__(19); + +module.exports = function(list, options) { + options = options || {}; + + options = extend({ + location: 0, + distance: 100, + threshold: 0.4, + multiSearch: true, + searchClass: 'fuzzy-search' + }, options); + + + + var fuzzySearch = { + search: function(searchString, columns) { + // Substract arguments from the searchString or put searchString as only argument + var searchArguments = options.multiSearch ? searchString.replace(/ +$/, '').split(/ +/) : [searchString]; + + for (var k = 0, kl = list.items.length; k < kl; k++) { + fuzzySearch.item(list.items[k], columns, searchArguments); + } + }, + item: function(item, columns, searchArguments) { + var found = true; + for(var i = 0; i < searchArguments.length; i++) { + var foundArgument = false; + for (var j = 0, jl = columns.length; j < jl; j++) { + if (fuzzySearch.values(item.values(), columns[j], searchArguments[i])) { + foundArgument = true; + } + } + if(!foundArgument) { + found = false; + } + } + item.found = found; + }, + values: function(values, value, searchArgument) { + if (values.hasOwnProperty(value)) { + var text = toString(values[value]).toLowerCase(); + + if (fuzzy(text, searchArgument, options)) { + return true; + } + } + return false; + } + }; + + + events.bind(getByClass(list.listContainer, options.searchClass), 'keyup', function(e) { + var target = e.target || e.srcElement; // IE have srcElement + list.search(target.value, fuzzySearch.search); + }); + + return function(str, columns) { + list.search(str, columns, fuzzySearch.search); + }; +}; + + +/***/ }), +/* 11 */ +/***/ (function(module, exports, __webpack_require__) { + +var naturalSort = __webpack_require__(18), + getByClass = __webpack_require__(3), + extend = __webpack_require__(7), + indexOf = __webpack_require__(4), + events = __webpack_require__(1), + toString = __webpack_require__(6), + classes = __webpack_require__(0), + getAttribute = __webpack_require__(17), + toArray = __webpack_require__(5); + +module.exports = function(id, options, values) { + + var self = this, + init, + Item = __webpack_require__(2)(self), + addAsync = __webpack_require__(8)(self), + initPagination = __webpack_require__(12)(self); + + init = { + start: function() { + self.listClass = "list"; + self.searchClass = "search"; + self.sortClass = "sort"; + self.page = 10000; + self.i = 1; + self.items = []; + self.visibleItems = []; + self.matchingItems = []; + self.searched = false; + self.filtered = false; + self.searchColumns = undefined; + self.handlers = { 'updated': [] }; + self.valueNames = []; + self.utils = { + getByClass: getByClass, + extend: extend, + indexOf: indexOf, + events: events, + toString: toString, + naturalSort: naturalSort, + classes: classes, + getAttribute: getAttribute, + toArray: toArray + }; + + self.utils.extend(self, options); + + self.listContainer = (typeof(id) === 'string') ? document.getElementById(id) : id; + if (!self.listContainer) { return; } + self.list = getByClass(self.listContainer, self.listClass, true); + + self.parse = __webpack_require__(13)(self); + self.templater = __webpack_require__(16)(self); + self.search = __webpack_require__(14)(self); + self.filter = __webpack_require__(9)(self); + self.sort = __webpack_require__(15)(self); + self.fuzzySearch = __webpack_require__(10)(self, options.fuzzySearch); + + this.handlers(); + this.items(); + this.pagination(); + + self.update(); + }, + handlers: function() { + for (var handler in self.handlers) { + if (self[handler]) { + self.on(handler, self[handler]); + } + } + }, + items: function() { + self.parse(self.list); + if (values !== undefined) { + self.add(values); + } + }, + pagination: function() { + if (options.pagination !== undefined) { + if (options.pagination === true) { + options.pagination = [{}]; + } + if (options.pagination[0] === undefined){ + options.pagination = [options.pagination]; + } + for (var i = 0, il = options.pagination.length; i < il; i++) { + initPagination(options.pagination[i]); + } + } + } + }; + + /* + * Re-parse the List, use if html have changed + */ + this.reIndex = function() { + self.items = []; + self.visibleItems = []; + self.matchingItems = []; + self.searched = false; + self.filtered = false; + self.parse(self.list); + }; + + this.toJSON = function() { + var json = []; + for (var i = 0, il = self.items.length; i < il; i++) { + json.push(self.items[i].values()); + } + return json; + }; + + + /* + * Add object to list + */ + this.add = function(values, callback) { + if (values.length === 0) { + return; + } + if (callback) { + addAsync(values, callback); + return; + } + var added = [], + notCreate = false; + if (values[0] === undefined){ + values = [values]; + } + for (var i = 0, il = values.length; i < il; i++) { + var item = null; + notCreate = (self.items.length > self.page) ? true : false; + item = new Item(values[i], undefined, notCreate); + self.items.push(item); + added.push(item); + } + self.update(); + return added; + }; + + this.show = function(i, page) { + this.i = i; + this.page = page; + self.update(); + return self; + }; + + /* Removes object from list. + * Loops through the list and removes objects where + * property "valuename" === value + */ + this.remove = function(valueName, value, options) { + var found = 0; + for (var i = 0, il = self.items.length; i < il; i++) { + if (self.items[i].values()[valueName] == value) { + self.templater.remove(self.items[i], options); + self.items.splice(i,1); + il--; + i--; + found++; + } + } + self.update(); + return found; + }; + + /* Gets the objects in the list which + * property "valueName" === value + */ + this.get = function(valueName, value) { + var matchedItems = []; + for (var i = 0, il = self.items.length; i < il; i++) { + var item = self.items[i]; + if (item.values()[valueName] == value) { + matchedItems.push(item); + } + } + return matchedItems; + }; + + /* + * Get size of the list + */ + this.size = function() { + return self.items.length; + }; + + /* + * Removes all items from the list + */ + this.clear = function() { + self.templater.clear(); + self.items = []; + return self; + }; + + this.on = function(event, callback) { + self.handlers[event].push(callback); + return self; + }; + + this.off = function(event, callback) { + var e = self.handlers[event]; + var index = indexOf(e, callback); + if (index > -1) { + e.splice(index, 1); + } + return self; + }; + + this.trigger = function(event) { + var i = self.handlers[event].length; + while(i--) { + self.handlers[event][i](self); + } + return self; + }; + + this.reset = { + filter: function() { + var is = self.items, + il = is.length; + while (il--) { + is[il].filtered = false; + } + return self; + }, + search: function() { + var is = self.items, + il = is.length; + while (il--) { + is[il].found = false; + } + return self; + } + }; + + this.update = function() { + var is = self.items, + il = is.length; + + self.visibleItems = []; + self.matchingItems = []; + self.templater.clear(); + for (var i = 0; i < il; i++) { + if (is[i].matching() && ((self.matchingItems.length+1) >= self.i && self.visibleItems.length < self.page)) { + is[i].show(); + self.visibleItems.push(is[i]); + self.matchingItems.push(is[i]); + } else if (is[i].matching()) { + self.matchingItems.push(is[i]); + is[i].hide(); + } else { + is[i].hide(); + } + } + self.trigger('updated'); + return self; + }; + + init.start(); +}; + + +/***/ }), +/* 12 */ +/***/ (function(module, exports, __webpack_require__) { + +var classes = __webpack_require__(0), + events = __webpack_require__(1), + List = __webpack_require__(11); + +module.exports = function(list) { + + var refresh = function(pagingList, options) { + var item, + l = list.matchingItems.length, + index = list.i, + page = list.page, + pages = Math.ceil(l / page), + currentPage = Math.ceil((index / page)), + innerWindow = options.innerWindow || 2, + left = options.left || options.outerWindow || 0, + right = options.right || options.outerWindow || 0; + + right = pages - right; + + pagingList.clear(); + for (var i = 1; i <= pages; i++) { + var className = (currentPage === i) ? "active" : ""; + + //console.log(i, left, right, currentPage, (currentPage - innerWindow), (currentPage + innerWindow), className); + + if (is.number(i, left, right, currentPage, innerWindow)) { + item = pagingList.add({ + page: i, + dotted: false + })[0]; + if (className) { + classes(item.elm).add(className); + } + addEvent(item.elm, i, page); + } else if (is.dotted(pagingList, i, left, right, currentPage, innerWindow, pagingList.size())) { + item = pagingList.add({ + page: "...", + dotted: true + })[0]; + classes(item.elm).add("disabled"); + } + } + }; + + var is = { + number: function(i, left, right, currentPage, innerWindow) { + return this.left(i, left) || this.right(i, right) || this.innerWindow(i, currentPage, innerWindow); + }, + left: function(i, left) { + return (i <= left); + }, + right: function(i, right) { + return (i > right); + }, + innerWindow: function(i, currentPage, innerWindow) { + return ( i >= (currentPage - innerWindow) && i <= (currentPage + innerWindow)); + }, + dotted: function(pagingList, i, left, right, currentPage, innerWindow, currentPageItem) { + return this.dottedLeft(pagingList, i, left, right, currentPage, innerWindow) || (this.dottedRight(pagingList, i, left, right, currentPage, innerWindow, currentPageItem)); + }, + dottedLeft: function(pagingList, i, left, right, currentPage, innerWindow) { + return ((i == (left + 1)) && !this.innerWindow(i, currentPage, innerWindow) && !this.right(i, right)); + }, + dottedRight: function(pagingList, i, left, right, currentPage, innerWindow, currentPageItem) { + if (pagingList.items[currentPageItem-1].values().dotted) { + return false; + } else { + return ((i == (right)) && !this.innerWindow(i, currentPage, innerWindow) && !this.right(i, right)); + } + } + }; + + var addEvent = function(elm, i, page) { + events.bind(elm, 'click', function() { + list.show((i-1)*page + 1, page); + }); + }; + + return function(options) { + var pagingList = new List(list.listContainer.id, { + listClass: options.paginationClass || 'pagination', + item: "
  • ", + valueNames: ['page', 'dotted'], + searchClass: 'pagination-search-that-is-not-supposed-to-exist', + sortClass: 'pagination-sort-that-is-not-supposed-to-exist' + }); + + list.on('updated', function() { + refresh(pagingList, options); + }); + refresh(pagingList, options); + }; +}; + + +/***/ }), +/* 13 */ +/***/ (function(module, exports, __webpack_require__) { + +module.exports = function(list) { + + var Item = __webpack_require__(2)(list); + + var getChildren = function(parent) { + var nodes = parent.childNodes, + items = []; + for (var i = 0, il = nodes.length; i < il; i++) { + // Only textnodes have a data attribute + if (nodes[i].data === undefined) { + items.push(nodes[i]); + } + } + return items; + }; + + var parse = function(itemElements, valueNames) { + for (var i = 0, il = itemElements.length; i < il; i++) { + list.items.push(new Item(valueNames, itemElements[i])); + } + }; + var parseAsync = function(itemElements, valueNames) { + var itemsToIndex = itemElements.splice(0, 50); // TODO: If < 100 items, what happens in IE etc? + parse(itemsToIndex, valueNames); + if (itemElements.length > 0) { + setTimeout(function() { + parseAsync(itemElements, valueNames); + }, 1); + } else { + list.update(); + list.trigger('parseComplete'); + } + }; + + list.handlers.parseComplete = list.handlers.parseComplete || []; + + return function() { + var itemsToIndex = getChildren(list.list), + valueNames = list.valueNames; + + if (list.indexAsync) { + parseAsync(itemsToIndex, valueNames); + } else { + parse(itemsToIndex, valueNames); + } + }; +}; + + +/***/ }), +/* 14 */ +/***/ (function(module, exports) { + +module.exports = function(list) { + var item, + text, + columns, + searchString, + customSearch; + + var prepare = { + resetList: function() { + list.i = 1; + list.templater.clear(); + customSearch = undefined; + }, + setOptions: function(args) { + if (args.length == 2 && args[1] instanceof Array) { + columns = args[1]; + } else if (args.length == 2 && typeof(args[1]) == "function") { + columns = undefined; + customSearch = args[1]; + } else if (args.length == 3) { + columns = args[1]; + customSearch = args[2]; + } else { + columns = undefined; + } + }, + setColumns: function() { + if (list.items.length === 0) return; + if (columns === undefined) { + columns = (list.searchColumns === undefined) ? prepare.toArray(list.items[0].values()) : list.searchColumns; + } + }, + setSearchString: function(s) { + s = list.utils.toString(s).toLowerCase(); + s = s.replace(/[-[\]{}()*+?.,\\^$|#]/g, "\\$&"); // Escape regular expression characters + searchString = s; + }, + toArray: function(values) { + var tmpColumn = []; + for (var name in values) { + tmpColumn.push(name); + } + return tmpColumn; + } + }; + var search = { + list: function() { + for (var k = 0, kl = list.items.length; k < kl; k++) { + search.item(list.items[k]); + } + }, + item: function(item) { + item.found = false; + for (var j = 0, jl = columns.length; j < jl; j++) { + if (search.values(item.values(), columns[j])) { + item.found = true; + return; + } + } + }, + values: function(values, column) { + if (values.hasOwnProperty(column)) { + text = list.utils.toString(values[column]).toLowerCase(); + if ((searchString !== "") && (text.search(searchString) > -1)) { + return true; + } + } + return false; + }, + reset: function() { + list.reset.search(); + list.searched = false; + } + }; + + var searchMethod = function(str) { + list.trigger('searchStart'); + + prepare.resetList(); + prepare.setSearchString(str); + prepare.setOptions(arguments); // str, cols|searchFunction, searchFunction + prepare.setColumns(); + + if (searchString === "" ) { + search.reset(); + } else { + list.searched = true; + if (customSearch) { + customSearch(searchString, columns); + } else { + search.list(); + } + } + + list.update(); + list.trigger('searchComplete'); + return list.visibleItems; + }; + + list.handlers.searchStart = list.handlers.searchStart || []; + list.handlers.searchComplete = list.handlers.searchComplete || []; + + list.utils.events.bind(list.utils.getByClass(list.listContainer, list.searchClass), 'keyup', function(e) { + var target = e.target || e.srcElement, // IE have srcElement + alreadyCleared = (target.value === "" && !list.searched); + if (!alreadyCleared) { // If oninput already have resetted the list, do nothing + searchMethod(target.value); + } + }); + + // Used to detect click on HTML5 clear button + list.utils.events.bind(list.utils.getByClass(list.listContainer, list.searchClass), 'input', function(e) { + var target = e.target || e.srcElement; + if (target.value === "") { + searchMethod(''); + } + }); + + return searchMethod; +}; + + +/***/ }), +/* 15 */ +/***/ (function(module, exports) { + +module.exports = function(list) { + + var buttons = { + els: undefined, + clear: function() { + for (var i = 0, il = buttons.els.length; i < il; i++) { + list.utils.classes(buttons.els[i]).remove('asc'); + list.utils.classes(buttons.els[i]).remove('desc'); + } + }, + getOrder: function(btn) { + var predefinedOrder = list.utils.getAttribute(btn, 'data-order'); + if (predefinedOrder == "asc" || predefinedOrder == "desc") { + return predefinedOrder; + } else if (list.utils.classes(btn).has('desc')) { + return "asc"; + } else if (list.utils.classes(btn).has('asc')) { + return "desc"; + } else { + return "asc"; + } + }, + getInSensitive: function(btn, options) { + var insensitive = list.utils.getAttribute(btn, 'data-insensitive'); + if (insensitive === "false") { + options.insensitive = false; + } else { + options.insensitive = true; + } + }, + setOrder: function(options) { + for (var i = 0, il = buttons.els.length; i < il; i++) { + var btn = buttons.els[i]; + if (list.utils.getAttribute(btn, 'data-sort') !== options.valueName) { + continue; + } + var predefinedOrder = list.utils.getAttribute(btn, 'data-order'); + if (predefinedOrder == "asc" || predefinedOrder == "desc") { + if (predefinedOrder == options.order) { + list.utils.classes(btn).add(options.order); + } + } else { + list.utils.classes(btn).add(options.order); + } + } + } + }; + + var sort = function() { + list.trigger('sortStart'); + var options = {}; + + var target = arguments[0].currentTarget || arguments[0].srcElement || undefined; + + if (target) { + options.valueName = list.utils.getAttribute(target, 'data-sort'); + buttons.getInSensitive(target, options); + options.order = buttons.getOrder(target); + } else { + options = arguments[1] || options; + options.valueName = arguments[0]; + options.order = options.order || "asc"; + options.insensitive = (typeof options.insensitive == "undefined") ? true : options.insensitive; + } + + buttons.clear(); + buttons.setOrder(options); + + + // caseInsensitive + // alphabet + var customSortFunction = (options.sortFunction || list.sortFunction || null), + multi = ((options.order === 'desc') ? -1 : 1), + sortFunction; + + if (customSortFunction) { + sortFunction = function(itemA, itemB) { + return customSortFunction(itemA, itemB, options) * multi; + }; + } else { + sortFunction = function(itemA, itemB) { + var sort = list.utils.naturalSort; + sort.alphabet = list.alphabet || options.alphabet || undefined; + if (!sort.alphabet && options.insensitive) { + sort = list.utils.naturalSort.caseInsensitive; + } + return sort(itemA.values()[options.valueName], itemB.values()[options.valueName]) * multi; + }; + } + + list.items.sort(sortFunction); + list.update(); + list.trigger('sortComplete'); + }; + + // Add handlers + list.handlers.sortStart = list.handlers.sortStart || []; + list.handlers.sortComplete = list.handlers.sortComplete || []; + + buttons.els = list.utils.getByClass(list.listContainer, list.sortClass); + list.utils.events.bind(buttons.els, 'click', sort); + list.on('searchStart', buttons.clear); + list.on('filterStart', buttons.clear); + + return sort; +}; + + +/***/ }), +/* 16 */ +/***/ (function(module, exports) { + +var Templater = function(list) { + var itemSource, + templater = this; + + var init = function() { + itemSource = templater.getItemSource(list.item); + if (itemSource) { + itemSource = templater.clearSourceItem(itemSource, list.valueNames); + } + }; + + this.clearSourceItem = function(el, valueNames) { + for(var i = 0, il = valueNames.length; i < il; i++) { + var elm; + if (valueNames[i].data) { + for (var j = 0, jl = valueNames[i].data.length; j < jl; j++) { + el.setAttribute('data-'+valueNames[i].data[j], ''); + } + } else if (valueNames[i].attr && valueNames[i].name) { + elm = list.utils.getByClass(el, valueNames[i].name, true); + if (elm) { + elm.setAttribute(valueNames[i].attr, ""); + } + } else { + elm = list.utils.getByClass(el, valueNames[i], true); + if (elm) { + elm.innerHTML = ""; + } + } + elm = undefined; + } + return el; + }; + + this.getItemSource = function(item) { + if (item === undefined) { + var nodes = list.list.childNodes, + items = []; + + for (var i = 0, il = nodes.length; i < il; i++) { + // Only textnodes have a data attribute + if (nodes[i].data === undefined) { + return nodes[i].cloneNode(true); + } + } + } else if (/]/g.exec(item)) { + var tbody = document.createElement('tbody'); + tbody.innerHTML = item; + return tbody.firstChild; + } else if (item.indexOf("<") !== -1) { + var div = document.createElement('div'); + div.innerHTML = item; + return div.firstChild; + } else { + var source = document.getElementById(list.item); + if (source) { + return source; + } + } + return undefined; + }; + + this.get = function(item, valueNames) { + templater.create(item); + var values = {}; + for(var i = 0, il = valueNames.length; i < il; i++) { + var elm; + if (valueNames[i].data) { + for (var j = 0, jl = valueNames[i].data.length; j < jl; j++) { + values[valueNames[i].data[j]] = list.utils.getAttribute(item.elm, 'data-'+valueNames[i].data[j]); + } + } else if (valueNames[i].attr && valueNames[i].name) { + elm = list.utils.getByClass(item.elm, valueNames[i].name, true); + values[valueNames[i].name] = elm ? list.utils.getAttribute(elm, valueNames[i].attr) : ""; + } else { + elm = list.utils.getByClass(item.elm, valueNames[i], true); + values[valueNames[i]] = elm ? elm.innerHTML : ""; + } + elm = undefined; + } + return values; + }; + + this.set = function(item, values) { + var getValueName = function(name) { + for (var i = 0, il = list.valueNames.length; i < il; i++) { + if (list.valueNames[i].data) { + var data = list.valueNames[i].data; + for (var j = 0, jl = data.length; j < jl; j++) { + if (data[j] === name) { + return { data: name }; + } + } + } else if (list.valueNames[i].attr && list.valueNames[i].name && list.valueNames[i].name == name) { + return list.valueNames[i]; + } else if (list.valueNames[i] === name) { + return name; + } + } + }; + var setValue = function(name, value) { + var elm; + var valueName = getValueName(name); + if (!valueName) + return; + if (valueName.data) { + item.elm.setAttribute('data-'+valueName.data, value); + } else if (valueName.attr && valueName.name) { + elm = list.utils.getByClass(item.elm, valueName.name, true); + if (elm) { + elm.setAttribute(valueName.attr, value); + } + } else { + elm = list.utils.getByClass(item.elm, valueName, true); + if (elm) { + elm.innerHTML = value; + } + } + elm = undefined; + }; + if (!templater.create(item)) { + for(var v in values) { + if (values.hasOwnProperty(v)) { + setValue(v, values[v]); + } + } + } + }; + + this.create = function(item) { + if (item.elm !== undefined) { + return false; + } + if (itemSource === undefined) { + throw new Error("The list need to have at list one item on init otherwise you'll have to add a template."); + } + /* If item source does not exists, use the first item in list as + source for new items */ + var newItem = itemSource.cloneNode(true); + newItem.removeAttribute('id'); + item.elm = newItem; + templater.set(item, item.values()); + return true; + }; + this.remove = function(item) { + if (item.elm.parentNode === list.list) { + list.list.removeChild(item.elm); + } + }; + this.show = function(item) { + templater.create(item); + list.list.appendChild(item.elm); + }; + this.hide = function(item) { + if (item.elm !== undefined && item.elm.parentNode === list.list) { + list.list.removeChild(item.elm); + } + }; + this.clear = function() { + /* .innerHTML = ''; fucks up IE */ + if (list.list.hasChildNodes()) { + while (list.list.childNodes.length >= 1) + { + list.list.removeChild(list.list.firstChild); + } + } + }; + + init(); +}; + +module.exports = function(list) { + return new Templater(list); +}; + + +/***/ }), +/* 17 */ +/***/ (function(module, exports) { + +/** + * A cross-browser implementation of getAttribute. + * Source found here: http://stackoverflow.com/a/3755343/361337 written by Vivin Paliath + * + * Return the value for `attr` at `element`. + * + * @param {Element} el + * @param {String} attr + * @api public + */ + +module.exports = function(el, attr) { + var result = (el.getAttribute && el.getAttribute(attr)) || null; + if( !result ) { + var attrs = el.attributes; + var length = attrs.length; + for(var i = 0; i < length; i++) { + if (attr[i] !== undefined) { + if(attr[i].nodeName === attr) { + result = attr[i].nodeValue; + } + } + } + } + return result; +}; + + +/***/ }), +/* 18 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var alphabet; +var alphabetIndexMap; +var alphabetIndexMapLength = 0; + +function isNumberCode(code) { + return code >= 48 && code <= 57; +} + +function naturalCompare(a, b) { + var lengthA = (a += '').length; + var lengthB = (b += '').length; + var aIndex = 0; + var bIndex = 0; + + while (aIndex < lengthA && bIndex < lengthB) { + var charCodeA = a.charCodeAt(aIndex); + var charCodeB = b.charCodeAt(bIndex); + + if (isNumberCode(charCodeA)) { + if (!isNumberCode(charCodeB)) { + return charCodeA - charCodeB; + } + + var numStartA = aIndex; + var numStartB = bIndex; + + while (charCodeA === 48 && ++numStartA < lengthA) { + charCodeA = a.charCodeAt(numStartA); + } + while (charCodeB === 48 && ++numStartB < lengthB) { + charCodeB = b.charCodeAt(numStartB); + } + + var numEndA = numStartA; + var numEndB = numStartB; + + while (numEndA < lengthA && isNumberCode(a.charCodeAt(numEndA))) { + ++numEndA; + } + while (numEndB < lengthB && isNumberCode(b.charCodeAt(numEndB))) { + ++numEndB; + } + + var difference = numEndA - numStartA - numEndB + numStartB; // numA length - numB length + if (difference) { + return difference; + } + + while (numStartA < numEndA) { + difference = a.charCodeAt(numStartA++) - b.charCodeAt(numStartB++); + if (difference) { + return difference; + } + } + + aIndex = numEndA; + bIndex = numEndB; + continue; + } + + if (charCodeA !== charCodeB) { + if ( + charCodeA < alphabetIndexMapLength && + charCodeB < alphabetIndexMapLength && + alphabetIndexMap[charCodeA] !== -1 && + alphabetIndexMap[charCodeB] !== -1 + ) { + return alphabetIndexMap[charCodeA] - alphabetIndexMap[charCodeB]; + } + + return charCodeA - charCodeB; + } + + ++aIndex; + ++bIndex; + } + + return lengthA - lengthB; +} + +naturalCompare.caseInsensitive = naturalCompare.i = function(a, b) { + return naturalCompare(('' + a).toLowerCase(), ('' + b).toLowerCase()); +}; + +Object.defineProperties(naturalCompare, { + alphabet: { + get: function() { + return alphabet; + }, + set: function(value) { + alphabet = value; + alphabetIndexMap = []; + var i = 0; + if (alphabet) { + for (; i < alphabet.length; i++) { + alphabetIndexMap[alphabet.charCodeAt(i)] = i; + } + } + alphabetIndexMapLength = alphabetIndexMap.length; + for (i = 0; i < alphabetIndexMapLength; i++) { + if (alphabetIndexMap[i] === undefined) { + alphabetIndexMap[i] = -1; + } + } + }, + }, +}); + +module.exports = naturalCompare; + + +/***/ }), +/* 19 */ +/***/ (function(module, exports) { + +module.exports = function(text, pattern, options) { + // Aproximately where in the text is the pattern expected to be found? + var Match_Location = options.location || 0; + + //Determines how close the match must be to the fuzzy location (specified above). An exact letter match which is 'distance' characters away from the fuzzy location would score as a complete mismatch. A distance of '0' requires the match be at the exact location specified, a threshold of '1000' would require a perfect match to be within 800 characters of the fuzzy location to be found using a 0.8 threshold. + var Match_Distance = options.distance || 100; + + // At what point does the match algorithm give up. A threshold of '0.0' requires a perfect match (of both letters and location), a threshold of '1.0' would match anything. + var Match_Threshold = options.threshold || 0.4; + + if (pattern === text) return true; // Exact match + if (pattern.length > 32) return false; // This algorithm cannot be used + + // Set starting location at beginning text and initialise the alphabet. + var loc = Match_Location, + s = (function() { + var q = {}, + i; + + for (i = 0; i < pattern.length; i++) { + q[pattern.charAt(i)] = 0; + } + + for (i = 0; i < pattern.length; i++) { + q[pattern.charAt(i)] |= 1 << (pattern.length - i - 1); + } + + return q; + }()); + + // Compute and return the score for a match with e errors and x location. + // Accesses loc and pattern through being a closure. + + function match_bitapScore_(e, x) { + var accuracy = e / pattern.length, + proximity = Math.abs(loc - x); + + if (!Match_Distance) { + // Dodge divide by zero error. + return proximity ? 1.0 : accuracy; + } + return accuracy + (proximity / Match_Distance); + } + + var score_threshold = Match_Threshold, // Highest score beyond which we give up. + best_loc = text.indexOf(pattern, loc); // Is there a nearby exact match? (speedup) + + if (best_loc != -1) { + score_threshold = Math.min(match_bitapScore_(0, best_loc), score_threshold); + // What about in the other direction? (speedup) + best_loc = text.lastIndexOf(pattern, loc + pattern.length); + + if (best_loc != -1) { + score_threshold = Math.min(match_bitapScore_(0, best_loc), score_threshold); + } + } + + // Initialise the bit arrays. + var matchmask = 1 << (pattern.length - 1); + best_loc = -1; + + var bin_min, bin_mid; + var bin_max = pattern.length + text.length; + var last_rd; + for (var d = 0; d < pattern.length; d++) { + // Scan for the best match; each iteration allows for one more error. + // Run a binary search to determine how far from 'loc' we can stray at this + // error level. + bin_min = 0; + bin_mid = bin_max; + while (bin_min < bin_mid) { + if (match_bitapScore_(d, loc + bin_mid) <= score_threshold) { + bin_min = bin_mid; + } else { + bin_max = bin_mid; + } + bin_mid = Math.floor((bin_max - bin_min) / 2 + bin_min); + } + // Use the result from this iteration as the maximum for the next. + bin_max = bin_mid; + var start = Math.max(1, loc - bin_mid + 1); + var finish = Math.min(loc + bin_mid, text.length) + pattern.length; + + var rd = Array(finish + 2); + rd[finish + 1] = (1 << d) - 1; + for (var j = finish; j >= start; j--) { + // The alphabet (s) is a sparse hash, so the following line generates + // warnings. + var charMatch = s[text.charAt(j - 1)]; + if (d === 0) { // First pass: exact match. + rd[j] = ((rd[j + 1] << 1) | 1) & charMatch; + } else { // Subsequent passes: fuzzy match. + rd[j] = (((rd[j + 1] << 1) | 1) & charMatch) | + (((last_rd[j + 1] | last_rd[j]) << 1) | 1) | + last_rd[j + 1]; + } + if (rd[j] & matchmask) { + var score = match_bitapScore_(d, j - 1); + // This match will almost certainly be better than any existing match. + // But check anyway. + if (score <= score_threshold) { + // Told you so. + score_threshold = score; + best_loc = j - 1; + if (best_loc > loc) { + // When passing loc, don't exceed our current distance from loc. + start = Math.max(1, 2 * loc - best_loc); + } else { + // Already passed loc, downhill from here on in. + break; + } + } + } + } + // No hope for a (better) match at greater error levels. + if (match_bitapScore_(d + 1, loc) > score_threshold) { + break; + } + last_rd = rd; + } + + return (best_loc < 0) ? false : true; +}; + + +/***/ }) +/******/ ]); \ No newline at end of file diff --git a/common/php/class-module.php b/common/php/class-module.php index dff2dd17c..f6d9ec19b 100755 --- a/common/php/class-module.php +++ b/common/php/class-module.php @@ -483,6 +483,44 @@ function users_select_form( $selected = null, $args = null ) { 'authors', + 'fields' => array( + 'ID', + ), + ); + + $usersQuery = new WP_User_Query( array('who' => 'authors', 'fields' => 'ID') ); + $users_count = $usersQuery->get_total(); + + ?> + +
    + + + + + + +
    + Total users + +
    + + + + +
    + + There was an error. Please reload the page.

    '); +jQuery( document ).ready( function( $ ) { + // $('#ef-post_following_users_box ul').listFilterizer(); + + var post_id = $( '#post_ID' ).val(); + + // check post_id, only run the following JS if it is in post page + if ( post_id !== undefined ) { + var params = { + action: 'save_notifications', + post_id: post_id, + }; + + $( document ).on( 'click', '.ef-post_following_list li input:checkbox, .ef-following_usergroups li input:checkbox', function() { + var user_group_ids = []; + var parent_this = $( this ); + params.ef_notifications_name = $( this ).attr( 'name' ); + params._nonce = $( '#ef_notifications_nonce' ).val(); + + $( this ) + .parent() + .parent() + .parent() + .find( 'input:checked' ) + .map( function() { + user_group_ids.push( $( this ).val() ); + } ); + + params.user_group_ids = user_group_ids; + + $.ajax( { + type: 'POST', + url: ( ajaxurl ) ? ajaxurl : wpListL10n.url, + data: params, + success: function( x ) { + // This event is used to show an updated list of who will be notified of editorial comments and status updates. + $( '#ef-post_following_box' ).trigger( 'following_list_updated' ); + + var backgroundColor = parent_this.css( 'background-color' ); + $( parent_this.parent().parent() ) + .animate( { backgroundColor: '#CCEEBB' }, 200 ) + .animate( { backgroundColor: backgroundColor }, 200 ); + }, + error: function( r ) { + $( '#ef-post_following_users_box' ).prev().append( '

    There was an error. Please reload the page.

    ' ); + }, + } ); + } ); + + // Options for the list + var options = { + // values used for filters + valueNames: [ 'user-item-name', 'user-item-email', { + name: 'user_checked', + attr: '', + }, { data: [ 'user-item-id' ] } ], + + // searchClass is used for filtering values in the list + searchClass: 'filter-users', + + // item used for user list template + item: '
  • ', + }; + + // Initialize the list.js, 'users' is the html class name to fill in the users list + var userList = new List( 'users', options ); + var usersPerPage = 10; + var totalUsers = 0; + var totalUsersCount = $( '#total-users-count' ).val(); //embedded total users in the hidden html + + /** + * The function will show paginated users list. Each users page will show a number of users defined by the parameter. + * Total users pages will be calculated by dividing totalUsers with usersPerPage. Each users page retrieved using ajax. + * + * @param {number} totalUsers Total users related to the search keyword + * @param {number} usersPerPage Total user shown in a users page + * @param {string} searchKeyword The keyword for users to be shown in the page + */ + function fillPaginatedUsersList( totalUsers, usersPerPage, searchKeyword ) { + // remove pagination if it existed + if ( $( '#users-pagination' ).data( 'twbs-pagination' ) ) { + $( '#users-pagination' ).twbsPagination( 'destroy' ); } - }); - }); -}); \ No newline at end of file + + $( '#users-pagination' ).twbsPagination( { + totalPages: Math.ceil( totalUsers / usersPerPage ), // The total number of user pages + visiblePages: usersPerPage, // Number of users displayed in a page + next: 'Next', + prev: 'Prev', + onPageClick: function( event, page ) { + // clear the users list when the page created + userList.clear(); + + // Data sent to WP through ajax for paginated users list + var data = { + action: 'retrieve_users', + post_id: $( '#post_ID' ).val(), + page: page, + users_per_page: usersPerPage, + nonce: $( '#ef_notifications_nonce' ).val(), + search_keyword: searchKeyword, + }; + + jQuery.post( ajax_object.ajax_url, data, function( response ) { + // Add the users retrieved from wordpress db to list + for ( var user of response.users ) { + userList.add( user ); + if ( user.user_checked ) { + $( 'li[data-user-item-id=' + user[ 'user-item-id' ] + '] input:checkbox' ).prop( 'checked', true ); + } + } + + // Fill in users count info + $( '.users-total-info-value' ).text( totalUsers ); + if ( searchKeyword !== '' ) { + $( '.users-total-info-text' ).text( 'Totals users found' ); + } + } ); + }, + } ); + } + + /** + * This will populate users based on a keyword. First it will retrieve the count of users based on the keyword. + * The count then will be used as base to calculate pagination related variables in fillPaginatedUsersList + * + * @param {string} searchKeyword Text based on users for to be shown in the users list. Can contain wildcard. + */ + function fillUsersListByKeyword( searchKeyword ) { + // Data sent to WP through ajax for user counts + var data_user_count = { + action: 'retrieve_users_count_by_keyword', + nonce: $( '#ef_notifications_nonce' ).val(), + // count_users: true, + search_keyword: searchKeyword, + }; + + jQuery.post( ajax_object.ajax_url, data_user_count, function( response ) { + totalUsers = parseInt( response ); + + if ( totalUsers > 0 ) { + fillPaginatedUsersList( totalUsers, usersPerPage, searchKeyword ); + } else { + $( '#users-pagination' ).twbsPagination( 'destroy' ); + $( '.users-total-info-text' ).text( 'Totals users found' ); + $( '.users-total-info-value' ).text( totalUsers ); + } + } ); + } + + // jQuery bind to search users when pressing Enter key + $( '.search-users' ).bind( 'keypress', function( e ) { + if ( e.keyCode == 13 ) { + clearTimeout( $.data( this, 'timer' ) ); + + e.preventDefault(); + var searchKeyword = $( '.search-users' ).val(); + userList.clear(); + + var wait = setTimeout( fillUsersListByKeyword( searchKeyword ), 10000 ); + + $( this ).data( 'timer', wait ); + } + } ); + + // jQuery binding search button click + $( '.btn-search-users' ).click( function( e ) { + clearTimeout( $.data( this, 'timer' ) ); + + e.preventDefault(); + var searchKeyword = $( '.search-users' ).val(); + userList.clear(); + + var wait = setTimeout( fillUsersListByKeyword( searchKeyword ), 10000 ); + + $( this ).data( 'timer', wait ); + } ); + + // Ajax for saving checked/unchecked user + $( document ).on( 'click', '.user-item', function( e ) { + var item_element = $( this ); + var checkbox = item_element.find( "input[type='checkbox']" ); + + // check the checkbox when .user-item element is clicked + if ( ! $( e.target ).is( ':checkbox' ) && ! checkbox.is( ':checked' ) ) { + checkbox.attr( 'checked', true ); + } else if ( ( ! $( e.target ).is( ':checkbox' ) && checkbox.is( ':checked' ) ) ) { + checkbox.attr( 'checked', false ); + } + + var data = { + action: 'save_user_in_notification', + post_id: post_id, + nonce: $( '#ef_notifications_nonce' ).val(), + user_id: $( this ).data( 'user-item-id' ), + }; + + // add the user to notification if the checkbox checked or remove if unchecked + if ( checkbox.is( ':checked' ) ) { + data.follow = true; + } else { + data.follow = false; + } + + jQuery.post( ajaxurl, data ) + .done( function( response ) { + // This event is used to show an updated list of who will be notified of editorial comments and status updates. + $( '#ef-post_following_box' ).trigger( 'following_list_updated' ); + + // Trigger visual effect when ajax successful + var backgroundColor = item_element.parent().css( 'background-color' ); + item_element + .animate( { backgroundColor: '#CCEEBB' }, 200 ) + .animate( { backgroundColor: backgroundColor }, 200 ); + } ) + .fail( function( xhr, status, error ) { + $( '#ef-post_following_users_box' ).prev().append( '

    There was an error. Please reload the page.

    ' ); + } ); + } ); + + // Fill the initial users list on document load + fillPaginatedUsersList( totalUsersCount, usersPerPage, '' ); + }// checks post_id +} ); diff --git a/modules/notifications/notifications.php b/modules/notifications/notifications.php index 8bc794590..550f7a868 100644 --- a/modules/notifications/notifications.php +++ b/modules/notifications/notifications.php @@ -100,8 +100,14 @@ function init() { //Ajax for saving notifiction updates add_action( 'wp_ajax_save_notifications', array( $this, 'ajax_save_post_subscriptions' ) ); add_action( 'wp_ajax_ef_notifications_user_post_subscription', array( $this, 'handle_user_post_subscription' ) ); - - } + + //Ajax for retrieving users + add_action('wp_ajax_retrieve_users', array($this, 'ajax_retrieve_users')); + //Ajax to save user notification + add_action('wp_ajax_save_user_in_notification', array( $this, 'ajax_save_user_in_notification' )); + //Ajax for retrieving total users count by search keyword + add_action('wp_ajax_retrieve_users_count_by_keyword', array( $this, 'ajax_retrieve_users_count_by_keyword' )); + } /** * Load the capabilities onto users the first time the module is run @@ -190,7 +196,16 @@ function enqueue_admin_scripts() { if ( $this->is_whitelisted_functional_view() ) { wp_enqueue_script( 'jquery-listfilterizer' ); wp_enqueue_script( 'jquery-quicksearch' ); - wp_enqueue_script( 'edit-flow-notifications-js', $this->module_url . 'lib/notifications.js', array( 'jquery', 'jquery-listfilterizer', 'jquery-quicksearch' ), EDIT_FLOW_VERSION, true ); + + wp_enqueue_script( 'list'); + wp_enqueue_script( 'jquery-twbsPagination'); + + wp_enqueue_script( 'edit-flow-notifications-js', $this->module_url . 'lib/notifications.js', array( 'jquery', 'jquery-listfilterizer', 'jquery-quicksearch', 'list', 'jquery-twbsPagination' ), EDIT_FLOW_VERSION, true ); + + + wp_localize_script( 'edit-flow-notifications-js', 'ajax_object', + array( 'ajax_url' => admin_url( 'admin-ajax.php' ), 'ajax_nonce' => wp_create_nonce( "edit-flow-users-list-notifications-ajax" ) ) + ); } } @@ -336,7 +351,8 @@ function notifications_meta_box() { $select_form_args = array( 'list_class' => 'ef-post_following_list', ); - $this->users_select_form( $followers, $select_form_args ); ?> +// $this->users_select_form( $followers, $select_form_args ); + $this->users_list(); ?> module_enabled( 'user_groups' ) && in_array( $this->get_current_post_type(), $this->get_post_types_for_module( $edit_flow->user_groups->module ) ) ): ?> @@ -354,7 +370,130 @@ function notifications_meta_box() { 'authors', + 'fields' => array( + 'ID', + ), + 'search' => '*' . $search_keyword .'*', + 'search_columns' => array('display_name', 'user_email'), + 'count_total' => true + ); + + $usersQuery = new WP_User_Query( $args ); + $users_count = $usersQuery->get_total(); + wp_send_json($users_count); + + } + + /** + * Ajax endpoint for retrieving users in notification + */ + function ajax_retrieve_users(){ + + check_ajax_referer("save_user_usergroups" , "nonce"); + + $post_id = isset( $_POST['post_id'] ) ? intval($_POST['post_id']) : 0; + $selected = $this->get_following_users( $post_id, 'id' ); + + $search_keyword = isset( $_POST['search_keyword']) ? sanitize_text_field($_POST['search_keyword']) : ''; + + $users_per_page = isset( $_POST['users_per_page']) ? intval($_POST['users_per_page']) : 0; + $page = isset( $_POST['page']) ? intval($_POST['page']) : 0; + $offset = $users_per_page * ($page - 1); + + $args = array( + 'number' => $users_per_page, + 'offset' => $offset, + 'who' => 'authors', + 'fields' => array( + 'ID', + 'display_name', + 'user_email' + ), + 'orderby' => 'display_name', + 'search' => '*' . $search_keyword .'*', + 'search_columns' => array('display_name', 'user_email'), + ); + + $users = get_users($args); + + if ( ! is_array($selected)){ + $selected = array(); + } + + // Compile users with selected users on top of the list + $users_with_selection = array(); + + foreach ($users as $user){ + + $user_arr['user-item-id'] = $user->ID; + $user_arr['user-item-name'] = $user->display_name; + $user_arr['user-item-email'] = $user->user_email; + + if ( in_array($user->ID, $selected) ){ + $user_arr['user_checked'] = true; + } else { + $user_arr['user_checked'] = false; + } + + array_push($users_with_selection, $user_arr); + } + + wp_send_json(['users' => $users_with_selection]); + + } + + function ajax_save_user_in_notification(){ + + check_ajax_referer('save_user_usergroups', 'nonce'); + + $post_id = isset( $_POST['post_id'] ) ? intval( $_POST['post_id'] ) : 0; + $post = get_post( $post_id ); + $user_id = isset( $_POST['user_id']) ? intval( $_POST['user_id'] ) : 0; + $follow = isset( $_POST['follow']) ? filter_var($_POST['follow'], FILTER_VALIDATE_BOOLEAN) : false; + + // Add selected user, current user, and author to notification if they are set to receive notifications + $users = array(); + $users[] = $user_id; + + // Add current user to notified users + $current_user = wp_get_current_user(); + if ( $current_user && apply_filters( 'ef_notification_auto_subscribe_current_user', true, 'subscription_action' ) ) + $users[] = $current_user->ID; + + // Add post author to notified users + if ( apply_filters( 'ef_notification_auto_subscribe_post_author', true, 'subscription_action' ) ) + $users[] = $post->post_author; + + $users = array_unique( array_map( 'intval', $users ) ); + + // check if the post used for notification is valid + $valid_post = ! is_null( $post ) && ! wp_is_post_revision( $post_id ) && ! wp_is_post_autosave( $post_id ); + if ( ! $valid_post || ! current_user_can( $this->edit_post_subscriptions_cap ) ) { + die(); + } + + // do follow or unfollow based on checkbox in the frontend + if ( $follow ) { + $this->follow_post_user( $post, $users, true ); + } else { + $this->unfollow_post_user($post, $user_id); + } + + wp_send_json_success('change to notification list is saved'); + } + /** * Called when a notification editorial metadata checkbox is checked. Handles saving of a user/usergroup to a post. */ @@ -475,8 +614,8 @@ function save_post_following_usergroups( $post, $usergroups = null ) { $usergroups = array_map( 'intval', $usergroups ); $follow = $this->follow_post_usergroups($post, $usergroups, false); - } - + } + /** * Set up and send post status change notification email */ diff --git a/modules/user-groups/lib/user-groups.js b/modules/user-groups/lib/user-groups.js index 04403ad6a..48e671121 100644 --- a/modules/user-groups/lib/user-groups.js +++ b/modules/user-groups/lib/user-groups.js @@ -1,9 +1,182 @@ -jQuery(document).ready(function () { - jQuery('ul#ef-post_following_users li').quicksearch({ - position: 'before', - attached: 'ul#ef-post_following_users', - loaderText: '', - delay: 100 - }) - jQuery('#ef-usergroup-users ul').listFilterizer(); -}); \ No newline at end of file +jQuery( document ).ready( function( $ ) { + // jQuery('ul#ef-post_following_users li').quicksearch({ + // position: 'before', + // attached: 'ul#ef-post_following_users', + // loaderText: '', + // delay: 100 + // }) + // jQuery('#ef-usergroup-users ul').listFilterizer(); + + var usergroup_id = $( '#usergroup_id' ).val(); + + if ( usergroup_id !== undefined ) { + // Options for the list + var options = { + // values used for filters + valueNames: [ 'user-item-name', 'user-item-email', { + name: 'user_checked', + attr: '', + }, { data: [ 'user-item-id' ] } ], + + // searchClass is used for filtering values in the list + searchClass: 'filter-users', + + // item used for user list template + item: '
  • ', + }; + + // Initialize the list.js, 'users' is the html class name to fill in the users list + var userList = new List( 'users', options ); + var usersPerPage = 10; + var totalUsers = 0; + var totalUsersCount = $( '#total-users-count' ).val(); //embedded total users in the hidden html + + /** + * The function will show paginated users list. Each users page will show a number of users defined by the parameter. + * Total users pages will be calculated by dividing totalUsers with usersPerPage. Each users page retrieved using ajax. + * + * @param {number} totalUsers Total users related to the search keyword + * @param {number} usersPerPage Total user shown in a users page + * @param {string} searchKeyword The keyword for users to be shown in the page + */ + function fillPaginatedUsersList( totalUsers, usersPerPage, searchKeyword ) { + // remove pagination if it existed + if ( $( '#users-pagination' ).data( 'twbs-pagination' ) ) { + $( '#users-pagination' ).twbsPagination( 'destroy' ); + } + + $( '#users-pagination' ).twbsPagination( { + totalPages: Math.ceil( totalUsers / usersPerPage ), // The total number of user pages + visiblePages: usersPerPage, // Number of users displayed in a page + next: 'Next', + prev: 'Prev', + onPageClick: function( event, page ) { + // clear the users list when the page created + userList.clear(); + + // Data sent to WP through ajax for paginated users list + var data = { + action: 'retrieve_users_in_usergroup', + usergroup_id: usergroup_id, + page: page, + users_per_page: usersPerPage, + nonce: ajax_object.ajax_nonce, + search_keyword: searchKeyword, + }; + + jQuery.post( ajax_object.ajax_url, data, function( response ) { + // Add the users retrieved from wordpress db to list + for ( var user of response.users ) { + userList.add( user ); + if ( user.user_checked ) { + $( 'li[data-user-item-id=' + user[ 'user-item-id' ] + '] input:checkbox' ).prop( 'checked', true ); + } + } + + // Fill in users count info + $( '.users-total-info-value' ).text( totalUsers ); + if ( searchKeyword !== '' ) { + $( '.users-total-info-text' ).text( 'Totals users found' ); + } + } ); + }, + } ); + } + + /** + * This will populate users based on a keyword. First it will retrieve the count of users based on the keyword. + * The count then will be used as base to calculate pagination related variables in fillPaginatedUsersList + * + * @param {string} searchKeyword Text based on users for to be shown in the users list. Can contain wildcard. + */ + function fillUsersListByKeyword( searchKeyword ) { + // Data sent to WP through ajax for user counts + var data_user_count = { + action: 'retrieve_users_count_in_usergroup_by_keyword', + nonce: ajax_object.ajax_nonce, + search_keyword: searchKeyword, + }; + + jQuery.post( ajax_object.ajax_url, data_user_count, function( response ) { + totalUsers = parseInt( response ); + + if ( totalUsers > 0 ) { + fillPaginatedUsersList( totalUsers, usersPerPage, searchKeyword ); + } else { + $( '#users-pagination' ).twbsPagination( 'destroy' ); + $( '.users-total-info-text' ).text( 'Totals users found' ); + $( '.users-total-info-value' ).text( totalUsers ); + } + } ); + } + + // jQuery bind to search users when pressing Enter key + $( '.search-users' ).bind( 'keypress', function( e ) { + if ( e.keyCode == 13 ) { + clearTimeout( $.data( this, 'timer' ) ); + + e.preventDefault(); + var searchKeyword = $( '.search-users' ).val(); + userList.clear(); + + var wait = setTimeout( fillUsersListByKeyword( searchKeyword ), 10000 ); + + $( this ).data( 'timer', wait ); + } + } ); + + // jQuery binding search button click + $( '.btn-search-users' ).click( function( e ) { + clearTimeout( $.data( this, 'timer' ) ); + + e.preventDefault(); + var searchKeyword = $( '.search-users' ).val(); + userList.clear(); + + var wait = setTimeout( fillUsersListByKeyword( searchKeyword ), 10000 ); + + $( this ).data( 'timer', wait ); + } ); + + // Ajax for saving checked/unchecked user + $( document ).on( 'click', '.user-item', function( e ) { + var item_element = $( this ); + var checkbox = item_element.find( "input[type='checkbox']" ); + + // check the checkbox when .user-item element is clicked + if ( ! $( e.target ).is( ':checkbox' ) && ! checkbox.is( ':checked' ) ) { + checkbox.attr( 'checked', true ); + } else if ( ( ! $( e.target ).is( ':checkbox' ) && checkbox.is( ':checked' ) ) ) { + checkbox.attr( 'checked', false ); + } + + var params = { + action: 'save_user_to_usergroup', + usergroup_id: usergroup_id, + nonce: ajax_object.ajax_nonce, + user_id: ( $( this ).data( 'user-item-id' ) ), + }; + + if ( checkbox.is( ':checked' ) ) { + params.add = true; + } else { + params.remove = true; + } + + jQuery.post( ajaxurl, params ) + .done( function( response ) { + // Trigger visual effect when ajax successful + var backgroundColor = item_element.parent().css( 'background-color' ); + item_element + .animate( { backgroundColor: '#CCEEBB' }, 200 ) + .animate( { backgroundColor: backgroundColor }, 200 ); + } ) + .fail( function( xhr, status, error ) { + $( '#ef-post_following_users_box' ).prev().append( '

    There was an error. Please reload the page.

    ' ); + } ); + } ); + + // Fill the initial users list on document load + fillPaginatedUsersList( totalUsersCount, usersPerPage, '' ); + } // check on usergroup +} ); diff --git a/modules/user-groups/user-groups.php b/modules/user-groups/user-groups.php index e85fa9ff6..45438e7ac 100644 --- a/modules/user-groups/user-groups.php +++ b/modules/user-groups/user-groups.php @@ -101,8 +101,16 @@ function init() { // Javascript and CSS if we need it add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ) ); - add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_styles' ) ); - + add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_styles' ) ); + + // Ajax for retrieving users + add_action( 'wp_ajax_retrieve_users_in_usergroup', array( $this, 'ajax_retrieve_users_in_usergroups')); + + // Ajax for saving user to usergroup + add_action( 'wp_ajax_save_user_to_usergroup', array( $this, 'ajax_save_user_to_usergroup' ) ); + //Ajax for retrieving total users count by search keyword + add_action( 'wp_ajax_retrieve_users_count_in_usergroup_by_keyword', array( $this, 'ajax_retrieve_users_count_in_usergroup_by_keyword' )); + } /** @@ -231,7 +239,18 @@ function enqueue_admin_scripts() { if ( $this->is_whitelisted_functional_view() || $this->is_whitelisted_settings_view( $this->module->name ) ) { wp_enqueue_script( 'jquery-listfilterizer' ); wp_enqueue_script( 'jquery-quicksearch' ); - wp_enqueue_script( 'edit-flow-user-groups-js', $this->module_url . 'lib/user-groups.js', array( 'jquery', 'jquery-listfilterizer', 'jquery-quicksearch' ), EDIT_FLOW_VERSION, true ); + + wp_enqueue_script( 'list'); + wp_enqueue_script( 'jquery-twbsPagination'); + + + wp_enqueue_script( 'edit-flow-user-groups-js', $this->module_url . 'lib/user-groups.js', array( 'jquery', 'jquery-color', 'jquery-listfilterizer', 'jquery-quicksearch', 'list', 'jquery-twbsPagination' ), EDIT_FLOW_VERSION, true ); + + wp_localize_script( 'edit-flow-user-groups-js', 'ajax_object', + array( 'ajax_url' => admin_url( 'admin-ajax.php' ), + 'ajax_nonce' => wp_create_nonce( "edit-flow-users-list-usergroups-ajax" ) + ) + ); } if ( $this->is_whitelisted_settings_view( $this->module->name ) ) @@ -384,9 +403,11 @@ function handle_edit_usergroup() { 'description' => $description, ); // Gracefully handle the case where all users have been unsubscribed from the user group - $users = isset( $_POST['usergroup_users'] ) ? (array)$_POST['usergroup_users'] : array(); - $users = array_map( 'intval', $users ); - $usergroup = $this->update_usergroup( $existing_usergroup->term_id, $args, $users ); +// $users = isset( $_POST['usergroup_users'] ) ? (array)$_POST['usergroup_users'] : array(); +// $users = array_map( 'intval', $users ); +// $users = null; +// $usergroup = $this->update_usergroup( $existing_usergroup->term_id, $args, $users ); + $usergroup = $this->update_usergroup( $existing_usergroup->term_id, $args ); if ( is_wp_error( $usergroup ) ) wp_die( __( 'Error updating user group.', 'edit-flow' ) ); @@ -559,11 +580,12 @@ function print_configure_view() { 'input_id' => 'usergroup_users' ); ?> - users_select_form( $usergroup->user_ids , $select_form_args ); ?> + users_list(); ?> + users_select_form( $usergroup->user_ids , $select_form_args ); ?>
    - +
    - + inline_edit(); ?> 'authors', + 'fields' => array( + 'ID', + ), + 'search' => '*' . $search_keyword .'*', + 'search_columns' => array('display_name', 'user_email'), + 'count_total' => true + ); + + $usersQuery = new WP_User_Query( $args ); + $users_count = $usersQuery->get_total(); + + wp_send_json($users_count); + + } + + /** + * Ajax endpoint for retrieving users in usergroups. + */ + function ajax_retrieve_users_in_usergroups(){ + + check_ajax_referer('edit-flow-users-list-usergroups-ajax', 'nonce'); + + // find user ids who are selected in the usergroup + $usergroup_id = isset( $_POST['usergroup_id'] ) ? intval($_POST['usergroup_id']) : 0; + $usergroup_by_id = $this->get_usergroup_by('id', $usergroup_id); + $usergroup_by_id_arr = ($usergroup_by_id !== false) ? $usergroup_by_id->to_array() : array(); + $selected = $usergroup_by_id_arr['user_ids']; + + $search_keyword = isset( $_POST['search_keyword']) ? sanitize_text_field($_POST['search_keyword']) : ''; + + $users_per_page = isset( $_POST['users_per_page']) ? intval($_POST['users_per_page']) : 0; + $page = isset( $_POST['page']) ? intval($_POST['page']) : 0; + $offset = $users_per_page * ($page - 1); + + $args = array( + 'number' => $users_per_page, + 'offset' => $offset, + 'who' => 'authors', + 'fields' => array( + 'ID', + 'display_name', + 'user_email' + ), + 'orderby' => 'display_name', + 'search' => '*' . $search_keyword .'*', + 'search_columns' => array('display_name', 'user_email'), + // 'include' => $selected + ); + + $users = get_users($args); + + if ( ! is_array($selected)){ + $selected = array(); + } + + $users_with_selection = array(); + + foreach ($users as $user){ + + $user_arr['user-item-id'] = $user->ID; + $user_arr['user-item-name'] = $user->display_name; + $user_arr['user-item-email'] = $user->user_email; + + if ( in_array($user->ID, $selected) ){ + $user_arr['user_checked'] = true; + } else { + $user_arr['user_checked'] = false; + } + + array_push($users_with_selection, $user_arr); + } + + wp_send_json(['users' => $users_with_selection]); + + } + + /* + * Ajax handling saving and removing user from usergroup + */ + function ajax_save_user_to_usergroup(){ + check_ajax_referer('edit-flow-users-list-usergroups-ajax', 'nonce'); + + $add = isset( $_POST['add']) ? filter_var($_POST['add'], FILTER_VALIDATE_BOOLEAN) : false; + $remove = isset( $_POST['remove']) ? filter_var($_POST['remove'], FILTER_VALIDATE_BOOLEAN) : false; + + $user_id = isset( $_POST['user_id'] ) ? intval( $_POST['user_id']) : 0; + $usergroup_id = isset( $_POST['usergroup_id'] ) ? intval( $_POST['usergroup_id']) : 0; + + if($add){ + $this->add_user_to_usergroup($user_id, $usergroup_id); + } + + if($remove){ + $this->remove_user_from_usergroup($user_id, $usergroup_id); + } + + } /** * Adds a form to the user profile page to allow adding usergroup selecting options