getlackey/lackey-cms

View on GitHub
modules/core/client/js/table.js

Summary

Maintainability
F
6 days
Test Coverage
/* jslint node:true, esnext:true, es6:true, browser:true, loopfunc:true*/
/* eslint no-param-reassign:0 no-alert:0 */
'use strict';
/*
    Copyright 2016 Enigma Marketing Services Limited

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
*/
const
    template = require('core/client/js/template'),
    lackey = require('core/client/js'),
    qs = require('query-string'),
    xhr = require('core/client/js/xhr'),
    growl = require('cms/client/js/growl'),
    api = require('core/client/js/api'),
    modal = require('core/client/js/modal'),
    createMedia = require('cms/client/js/new-media'),
    editProfile = require('cms/client/js/profile'),
    createContent = require('cms/client/js/new-page'),
    createUser = require('cms/client/js/new-user');

function replaceState(href, qFilter) {
    let loc = document.location,
        url = loc.protocol + '//' + loc.host + loc.pathname + '?' + qs.stringify(href) + qFilter;
    window.history.replaceState({}, document.title, url);
}

class Table {

    constructor(element) {
        var
            self = this;
        this._search = lackey.select('[data-lky-hook="table.filter"]')[0];
        this._root = element;
        this._initial = JSON.parse(element.getAttribute('data-lky-init'));
        this._paging = element.getAttribute('data-lky-paging') ? JSON.parse(element.getAttribute('data-lky-paging')) : {};
        this._columns = JSON.parse(element.getAttribute('data-lky-columns'));
        this._apiEndpoint = element.getAttribute('data-lky-table');
        this._advFilters = document.querySelectorAll('[data-filter]');
        this._selectFills = document.querySelectorAll('[data-selectfill]');
        this.pageNumber = 0;
        this._modal = false;
        this.sorting();
        this.sort = {};
        this.data = {};
        this.perPage = 10;
        this.filter = '';
        this.ready = false;
        this.advFilters = [];
        this.qFilter = '';
        this.colCount = element.querySelectorAll('th').length;
        this.advFilters.add = function (filter) {
            var added = false;
            this.forEach((advFilter) => {
                if (advFilter.key === filter.key && filter.operator === 'equal') {
                    advFilter.value.push(filter.value);
                    added = true;
                } else if (advFilter.key === filter.key && advFilter.operator === filter.operator) {
                    advFilter.value = filter.value;
                    added = true;
                }
            });
            if (!added) {
                this.push({
                    key: filter.key,
                    operator: filter.operator,
                    value: (filter.operator === 'equal') ? [filter.value] : filter.value
                });
            }
        };
        this.advFilters.remove = function (filter) {
            this.forEach((advFilter, index, object) => {
                if (advFilter.key === filter.key && filter.operator === 'equal' && advFilter.value.length > 1) {
                    advFilter.value.splice(advFilter.value.indexOf(filter.value), 1);
                } else if (advFilter.key === filter.key && advFilter.operator === filter.operator) {
                    object.splice(index, 1);
                }
            });
            self.pageq();
        };
        this.cols = {};
        this.operators = {
            equal: (a, b) => {
                if (Array.isArray(b)) {
                    return b.indexOf(a) > -1;
                }
                return a === b;
            },
            greater: (a, b) => {
                return a > b;
            },
            lower: (a, b) => {
                return a < b;
            },
            like: (a, b) => {
                return a.toLowerCase().indexOf(b.toLowerCase()) > -1;
            }
        };
        this.applyJs = {
            createMedia: (root) => {
                createMedia(root, function () {
                    self.getData().then(() => {
                        if (self.sort.field) {
                            self.sortData();
                        }
                    });
                });
            },
            editProfile: (root, resolve, row) => {
                editProfile(root, () => {
                    this.getModal(row, function () {
                        resolve();
                    });

                    self.getData().then(() => {
                        if (self.sort.field) {
                            self.sortData();
                        }
                    });
                });
            },
            createContent: (root) => {
                createContent(root);
            },
            createUser: (root) => {
                createUser(root, function () {
                    self.getData().then(() => {
                        if (self.sort.field) {
                            self.sortData();
                        }
                    });
                });
            }
        };

        for(var i = 0; i < self._selectFills.length; i += 1) {
            self._selectFills[i].addOption = function (option, val) {
                var opt = document.createElement('option');
                opt.value = val || option;
                opt.innerHTML = option;
                this.appendChild(opt);
            };
            self._selectFills[i]._original = self._selectFills[i].innerHTML;
        }

        lackey
            .hook('table.actions')
            .addEventListener('click', function () {
                var nav = document.querySelector('.overflow-menu');

                var unbind, hide, mouseEnter, mouseLeave, timeout;

                nav.setAttribute('data-visible', '');

                unbind = function () {
                    nav.removeEventListener('mouseleave', mouseLeave);
                    nav.removeEventListener('mouseenter', mouseEnter);
                };

                hide = function () {
                    unbind();
                    nav.removeAttribute('data-visible');
                };

                mouseLeave = function () {
                    clearTimeout(timeout);
                    timeout = setTimeout(hide, 500);
                };

                mouseEnter = function () {
                    clearTimeout(timeout);
                };

                clearTimeout(timeout);
                timeout = setTimeout(hide, 2000);

                nav.addEventListener('mouseleave', mouseLeave);
                nav.addEventListener('mouseenter', mouseEnter);
            });

        lackey.bind('button.reset', 'click', () => {
           this.resetFilters();
        });

        this.setupFilters();
        let waiting = null;

        this.footerWidth();

        lackey.bind('[data-lky-hook="open-modal"]', 'click', (event, hook) => {
            event.preventDefault();
            if (hook.dataset.lkyTemplate) {
                self.getModal(hook);
            } else {
                window.location = hook.href;
            }
        });

        this.initialDraw()
            .then(() => {
                self._search
                    .addEventListener('keyup', () => {
                        /* istanbul ignore next */
                        if (waiting) {
                            clearTimeout(waiting);
                        }

                        self.filter = self._search.value;

                        waiting = setTimeout(() => {
                            clearTimeout(waiting);
                            waiting = null;
                            self.pageq();
                        }, 500);
                    });

                if (document.location.search && document.location.search.length) {
                    self.setProps(qs.parse(document.location.search));
                }
                self.getData()
                    .then(() => {
                        self.ready = true;
                    });
                window.addEventListener('popstate', () => {
                    self.query(qs.parse(document.location.search), true);
                    return true;
                });
            });
    }

    initialDraw() {
        return this.drawRows({
            host: xhr.base,
            table: this._initial
        });
    }

    api() {
        let self = this;
        lackey.bind('[data-lky-api]', 'click', (event, hook) => {
            event.preventDefault();
            let apiAction = hook.getAttribute('data-lky-api').split(':'),
                itemId = apiAction[1].split('/').pop();

            if (apiAction[0] === 'DELETE') {
                growl({
                    status: 'info',
                    message: 'Are you sure? There is no undo',
                    type: 'yesno'
                }).then(() => {
                        api
                            .delete(apiAction[1])
                            .then(() => {
                                self.removeItem(parseInt(itemId));
                                if (self._modal) {
                                    growl({
                                        status: 'success',
                                        message: 'Deleted'
                                    });
                                    top.document.body.removeChild(self._modal);
                                    self._modal = false;
                                }
                            }, error => {
                                growl({
                                    status: 'error',
                                    message: error.message || error.toString()
                                });
                            });
                });
            }
        });
    }

    footerWidth() {
        document.querySelector('td.pagination').setAttribute('colspan', this.colCount);
    }

    setupFilters() {
        var self = this;
        for(var i = 0; i < self._advFilters.length; i += 1) {
            self._advFilters[i].addEventListener('change', function () {
                var actions = this.dataset.filter.split(':');
                self.advFilters.add({
                    key: actions[0],
                    operator: actions[1],
                    value: this.value
                });
                if (this.nodeName === 'SELECT') {
                    self.filterRemoveBtn(this, {
                        key: actions[0],
                        operator: actions[1],
                        value: this.value
                    });
                    this.querySelector('[value="' + this.value + '"]').disabled = true;
                    this.value = '';
                }
                self.pageq();
            });
        }
    }

    filterRemoveBtn(select, filter) {
        var self = this,
            rmBtn = document.createElement('button');
        rmBtn.dataset.removefilter = filter.key + ':' + filter.operator + ':' + filter.value;
        rmBtn.innerHTML = filter.value;
        rmBtn.addEventListener('click', function () {
            var filterParts = this.dataset.removefilter.split(':');

            self.advFilters.remove({
                key: filterParts[0],
                operator: filterParts[1],
                value: filterParts[2]
            });
            this.parentElement.removeChild(this);
            select.querySelector('[value="' + filterParts[2] + '"]').disabled = false;
        });
        select.parentElement.querySelector('.tag-container').appendChild(rmBtn);
    }

    rowActions() {
        var self = this,
            rows = lackey.select('[data-lky-hook="tableRowLink"]');
        rows.forEach((row) => {
            row.addEventListener('click', function () {
                if (row.dataset.lkyTemplate) {
                   self.getModal(this);
                } else {
                    window.location = row.dataset.lkyHref;
                }
            });
        });
    }

    getModal(row, cb) {
        var callback,
            self = this,
            js;

        cb = cb || function () {};

        callback = function (root, vars, resolve) {
            self._modal = root;
            self.api();
            if (js) {
                self.applyJs[js](root, resolve, row);
            }
        };

         xhr.basedGet(row.dataset.lkyHref + '.json', true)
            .then((data) => {
                data = JSON.parse(data);
                modal.open(row.dataset.lkyTemplate, {
                    data: data.data,
                    closeBtn: true
                }, callback);
                cb();
                if (row.dataset.lkyJavascript) {
                    js = row.dataset.lkyJavascript;
                }
            });
    }

    removeItem(id) {
        var self = this,
            rows = this.data.rows;

        self.data.rows = rows.filter((row) => {
            return row.id !== id;
        });
        self.pageq();
    }

    page(pageNumber) {
        let self = this;
        this
            .pageq({
                page: pageNumber
            })
            .then(() => {
                self.pageNumber = pageNumber;
            });
    }

    getData() {
        let self = this,
            total = this._initial.paging.total,
            limit = 50,
            calls = Math.ceil(total / limit),
            promises = [],
            data = false;

        for (var i = 0; i < calls; i += 1) {
            promises.push(self.getDataPart(limit, i * limit));
        }

        return Promise.all(promises)
            .then(values => {
                values.forEach(function (stuff) {
                    if (!data) {
                        data = stuff;
                    } else {
                        data.rows = data.rows.concat(stuff.rows);
                    }
                });
                return data;
            })
            .then((response) => {
                self.data = response;
                self.getColumns();
                self.pageq();
            });
    }

    getDataPart(limit, offset) {
        var self = this,
            path = this._apiEndpoint;

        return api
            .read(path + '?limit=' + limit + '&offset=' + offset + '&format=table' + self.qFilter);
    }

    pageq(options) {
        var self = this,
            page = this.pageNumber,
            cloneData = this.data.rows.slice(),
            pages,
            response,
            context;

        if (options) {
            if (options.page || options.page === 0) {
                page = options.page;
            }
        }

        if (self.filter && self.filter.length > 2) {
            cloneData = cloneData.filter(function (item) {
                var found = false;
                item.columns.forEach(function (col) {
                    if (col.value && col.value.date) {
                        var test = col.value.date.toString();
                        if (test && test.toLowerCase().indexOf(self.filter.toLowerCase()) > -1) {
                            found = true;
                        }
                    } else if (col.value && col.value.toLowerCase().indexOf(self.filter.toLowerCase()) > -1) {
                        found = true;
                    }
                });
                return found;
            });
        }

        if (this.advFilters.length > 0) {
            this.advFilters.forEach((filter) => {
                cloneData = cloneData.filter((item) => {
                    if (item.columns[self.cols[filter.key]].value && item.columns[self.cols[filter.key]].value.date) {
                        return self.operators[filter.operator](new Date(item.columns[self.cols[filter.key]].value.date), new Date(filter.value));
                    } else if (item.columns[self.cols[filter.key]].value){
                        return self.operators[filter.operator](item.columns[self.cols[filter.key]].value, filter.value);
                    }
                });
            });
        }
        this.data.paging.total = cloneData.length;
        this.data.paging.actions = self._paging.actions;
        this.data.paging.perPage = self.perPage;
        this.data.paging.startNo = (page * self.perPage) + 1;
        this.data.paging.pages = Math.ceil((cloneData.length / this.data.paging.perPage));
        if ((page + 1) > this.data.paging.pages) {
            page = 0;
        }
        pages = cloneData.splice(page * this.perPage, this.perPage);
        page += 1;
        this.data.paging.start = page - 3;
        this.data.paging.finish = page + 3;
        this.data.paging.page = page;

        response = {
            perPage: this.data.perPage,
            paging: this.data.paging,
            columns: this.data.columns,
            rows: pages
        };
        context = {
            table: response,
            host: xhr.base
        };
        response.rows.forEach(row =>
            row.columns.forEach((cell) => {
                if (cell.value && cell.value.date) {
                    cell.value.date = new Date(cell.value.date);
                }
            }));

        return this.drawRows(context)
            .then(() => {
                self.api();
                self.rowActions();
            })
            .then(() => {
                self.drawPaging(context);
            })
            .then(() => {
                var push = {};
                push.page = page;
                push.perPage = this.data.paging.perPage;
                if (self.filter && self.filter.length > 2) {
                    push.q = self.filter;
                }
                replaceState(push, self.qFilter);
            });
    }

    sortData(field, dir) {
        var direction = dir || this.sort.dir || 'desc';

        field = field || this.sort.field;

        this.sort.dir = direction;
        this.sort.field = field;
        this.data.rows.sort(function (a, b) {
            var fieldA,
                fieldB;

            if (a.columns[field].value && a.columns[field].value.date) {
                fieldA = a.columns[field].value.date;
            } else if (a.columns[field].value) {
                fieldA = a.columns[field].value.toLocaleLowerCase();
            }

            if (b.columns[field].value && b.columns[field].value.date) {
                fieldB = b.columns[field].value.date;
            } else if (b.columns[field].value) {
                fieldB = b.columns[field].value.toLocaleLowerCase();
            }
            if (direction === 'desc') {
                if (!fieldA) {
                    return -1;
                }
                if (!fieldB) {
                    return 1;
                }
                if (fieldA < fieldB) {
                    return -1;
                }
                if (fieldA > fieldB) {
                    return 1;
                }
                return 0;
            } else {
                if (!fieldB) {
                    return -1;
                }
                if (!fieldA) {
                    return 1;
                }
                if (fieldB < fieldA) {
                    return -1;
                }
                if (fieldB > fieldA) {
                    return 1;
                }
                return 0;
            }
        });
        this.pageq();
    }

    drawPaging(context) {
        let body = lackey.hook('table-footer', this._root),
            self = this;
        body.innerHTML = '';

        return template.render(body.getAttribute('data-lky-template'), context).then((rows) => {
            rows.forEach((row) => {
                body.appendChild(row);
                self.paging(row);
                self.footerWidth();
            });
        });
    }

    drawRows(context) {
        let body = lackey.hook('table-body', this._root);
        body.innerHTML = '';

        return template.render(body.getAttribute('data-lky-template'), context).then((rows) => {
            rows.forEach((row) => {
                body.appendChild(row);
            });
        });
    }

    get pagingArea() {
        return lackey.hook('table-footer', this._root);
    }

    paging(area) {
        let self = this;
        lackey.bind('lky:table-paging', 'click', (event, hook) => {
            event.stopPropagation();
            event.preventDefault();
            self.page(hook.getAttribute('data-page') - 1);
            return false;
        }, area || this.pagingArea);

         lackey.bind('lky:table-perPage', 'change', (event, hook) => {
            event.stopPropagation();
            event.preventDefault();
            self.perPage = hook.value;
            self.pageq();
        }, area || this.pagingArea);
    }

    sorting() {
        let self = this;
        lackey.bind('th[data-sort]', 'click', (event, hook) => {
            event.stopPropagation();
            event.preventDefault();
            if (self.ready) {
                var direction = hook.getAttribute('data-direction');

                if (!direction || direction === 'asc') {
                    direction = 'desc';
                } else {
                    direction = 'asc';
                }
                Array.prototype.forEach.call(document.querySelectorAll('th[data-sort]'), function (th) {
                    th.setAttribute('data-direction', '');
                });
                hook.setAttribute('data-direction', direction);
                self.sortData(hook.getAttribute('data-sort'), direction);
            }
            return false;
        });
    }

    setProps(options) {
        var self = this;
        Object.keys(options).forEach((property) => {
            if (property === 'page') {
                self.pageNumber = parseInt(options[property]) - 1;
            } else if (property === 'q') {
                self.filter = options[property];
                self._search.value = options[property];
            } else if (property === 'perPage') {
                self.perPage = options[property];
            } else {
                self.qFilter = self.qFilter + '&' + property + '=' + options[property];
            }
        });
    }

    selectFiller(data) {
        var self = this,
            i,
            getOptions = (options, column) => {
                data.forEach((row) => {
                    if (row.columns[self.cols[column]].value && row.columns[self.cols[column]].value.date) {
                        var date = new Date(row.columns[self.cols[column]].value.date).toDateString();
                        if (options.indexOf(date) < 0) {
                            options.push(date);
                        }
                    } else if (row.columns[self.cols[column]].value && options.indexOf(row.columns[self.cols[column]].value) < 0) {
                        options.push(row.columns[self.cols[column]].value);
                    }
                });
                return options;
            },
            addOptions = (options, element) => {
                options.sort().forEach((option) => {
                    element.addOption(option);
                });
            };

        for(i = 0; i < self._selectFills.length; i += 1) {
            var element = self._selectFills[i],
                column = element.dataset.selectfill,
                options = [];

            element.innerHTML = element._original;
            options = getOptions(options, column);
            addOptions(options.sort(), element);
        }
    }

    getColumns() {
        var self = this,
            columns = {},
            i = 0;
        self.data.columns.forEach((column) => {
            columns[column.name] = i;
            i += 1;
        });
        self.cols = columns;
        self.selectFiller(self.data.rows.slice());
    }

    resetFilters() {
        this.advFilters.splice(0,this.advFilters.length);

        let buttons = document.querySelectorAll('[data-removefilter]'),
            inputs = document.querySelectorAll('input[data-filter]'),
            i;

        for (i = 0; i < buttons.length; i += 1) {
            buttons[i].click();
        }
        for (i = 0; i < inputs.length; i += 1) {
            inputs[i].value = '';
        }

        this.pageq();
    }

    static init() {
        return lackey.getWithAttribute('data-lky-table').map((element) => {
            return new Table(element);
        });
    }
}

module.exports = Table;