var g_listviews = {};

function Listview(opt)
{
    $WH.cO(this, opt);

    if (this.id)
    {
        var divId = (this.tabs ? 'tab-' : 'lv-') + this.id;
        if (this.parent)
        {
            var d = $WH.ce('div');
            d.id = divId;
            $WH.ae($WH.ge(this.parent), d);
            this.container = d;
        }
        else
            this.container = $WH.ge(divId);
    }
    else
        return;

    var get = $WH.g_getGets();
    if ((get.debug != null || g_user.debug) && g_user.roles & U_GROUP_MODERATOR)
        this.debug = true;

    if (this.template && Listview.templates[this.template])
        this.template = Listview.templates[this.template];
    else
        return;

    g_listviews[this.id] = this;

    if (this.data == null)
        this.data = [];

    if (this.poundable == null)
    {
        if (this.template.poundable != null)
            this.poundable = this.template.poundable;
        else
            this.poundable = true;
    }

    if (this.searchable == null)
    {
        if (this.template.searchable != null)
            this.searchable = this.template.searchable;
        else
            this.searchable = false;
    }

    if (this.filtrable == null)
    {
        if (this.template.filtrable != null)
            this.filtrable = this.template.filtrable;
        else
            this.filtrable = false;
    }

    if (this.sortable == null)
    {
        if (this.template.sortable != null)
            this.sortable = this.template.sortable;
        else
            this.sortable = true;
    }

    if (this.customPound == null)
    {
        if (this.template.customPound != null)
            this.customPound = this.template.customPound;
        else
            this.customPound = false;
    }

    if (this.data.length == 1)
    {
        this.filtrable  = false;
        this.searchable = false;
    }

    if (this.searchable && this.searchDelay == null)
    {
        if (this.template.searchDelay != null)
            this.searchDelay = this.template.searchDelay;
        else
            this.searchDelay = 333;
    }

    if (this.clickable == null)
    {
        if (this.template.clickable != null)
            this.clickable = this.template.clickable;
        else
            this.clickable = true;
    }

    if (this.hideBands == null)
        this.hideBands = this.template.hideBands;

    if (this.hideNav == null)
        this.hideNav = this.template.hideNav;

    if (this.hideHeader == null)
        this.hideHeader = this.template.hideHeader;

    if (this.hideCount == null)
        this.hideCount = this.template.hideCount;

    if (this.computeDataFunc == null && this.template.computeDataFunc != null)
        this.computeDataFunc = this.template.computeDataFunc;

    if (this.createCbControls == null && this.template.createCbControls != null)
        this.createCbControls = this.template.createCbControls;

    if (this.template.onBeforeCreate != null)
    {
        if (this.onBeforeCreate == null)
            this.onBeforeCreate = this.template.onBeforeCreate;
        else
            this.onBeforeCreate = [this.template.onBeforeCreate, this.onBeforeCreate];
    }

    if (this.onAfterCreate == null && this.template.onAfterCreate != null)
        this.onAfterCreate = this.template.onAfterCreate;

    if (this.onNoData == null && this.template.onNoData != null)
        this.onNoData = this.template.onNoData;

    if (this.createNote == null && this.template.createNote != null)
        this.createNote = this.template.createNote;

    if (this.sortOptions == null && this.template.sortOptions != null)
        this.sortOptions = this.template.sortOptions;

    if (this.customFilter == null && this.template.customFilter != null)
        this.customFilter = this.template.customFilter;

    if (this.onSearchSubmit == null && this.template.onSearchSubmit != null)
        this.onSearchSubmit = this.template.onSearchSubmit;

    if (this.getItemLink == null && this.template.getItemLink != null)
        this.getItemLink = this.template.getItemLink;

    if (this.clip == null && this.template.clip != null)
        this.clip = this.template.clip;

    if (this.clip || this.template.compute || this.id == 'topics' || this.id == 'recipes')
        this.debug = false; // Don't add columns to picker windows

    if (this.mode == null)
        this.mode = this.template.mode;

    if (this.template.noStyle != null)
        this.noStyle = this.template.noStyle;

    if (this.nItemsPerPage == null)
    {
        if (this.template.nItemsPerPage != null)
            this.nItemsPerPage = this.template.nItemsPerPage;
        else
            this.nItemsPerPage = 50;
    }

    this.nItemsPerPage |= 0;
    if (this.nItemsPerPage <= 0)
        this.nItemsPerPage = 0;

    this.nFilters = 0;
    this.resetRowVisibility();

    if (this.mode == Listview.MODE_TILED)
    {
        if (this.nItemsPerRow == null)
        {
            var ipr = this.template.nItemsPerRow;
            this.nItemsPerRow = (ipr != null ? ipr : 4);
        }
        this.nItemsPerRow |= 0;
        if (this.nItemsPerRow <= 1)
            this.nItemsPerRow = 1;
    }
    else if (this.mode == Listview.MODE_CALENDAR)
    {
        this.dates         = [];
        this.nItemsPerRow  = 7; // Days per row
        this.nItemsPerPage = 1; // Months per page
        this.nDaysPerMonth = [];

        if (this.template.startOnMonth != null)
            this.startOnMonth = this.template.startOnMonth;
        else
            this.startOnMonth = new Date();

        this.startOnMonth.setDate(1);
        this.startOnMonth.setHours(0, 0, 0, 0);

        if (this.nMonthsToDisplay == null)
        {
            if (this.template.nMonthsToDisplay != null)
                this.nMonthsToDisplay = this.template.nMonthsToDisplay;
            else
                this.nMonthsToDisplay = 1;
        }

        var y = this.startOnMonth.getFullYear(),
            m = this.startOnMonth.getMonth();

        for (var j = 0; j < this.nMonthsToDisplay; ++j)
        {
            var date = new Date(y, m + j, 32);
            this.nDaysPerMonth[j] = 32 - date.getDate();
            for (var i = 1; i <= this.nDaysPerMonth[j]; ++i)
                this.dates.push({ date: new Date(y, m + j, i) });
        }

        if (this.template.rowOffset != null)
            this.rowOffset = this.template.rowOffset;
    }
    else
        this.nItemsPerRow = 1;

    this.columns = [];
    for (var i = 0, len = this.template.columns.length; i < len; ++i)
    {
        var
            ori = this.template.columns[i],
            cpy = {};

            $WH.cO(cpy, ori);

        this.columns.push(cpy);
    }

    // ************************
    // Extra Columns

    if (this.extraCols != null)
    {
        for (var i = 0, len = this.extraCols.length; i < len; ++i)
        {
            var pos = null;
            var col = this.extraCols[i];

            if (col.after || col.before)
            {
                var index = $WH.in_array(this.columns, (col.after ? col.after : col.before), function(x) { return x.id; } );

                if (index != -1)
                    pos = (col.after ? index + 1 : index);
            }

            if (pos == null)
                pos = this.columns.length;

            if (col.id == 'debug-id')
                this.columns.splice(0, 0, col);
            else
                this.columns.splice(pos, 0, col);
        }
    }

    // ************************
    // Visibility

    this.visibility = [];

    var visibleCols = [],
        hiddenCols  = [];

    if (this.visibleCols != null)
        $WH.array_walk(this.visibleCols, function(x) { visibleCols[x] = 1; });

    if (this.hiddenCols != null)
        $WH.array_walk(this.hiddenCols, function(x) { hiddenCols[x] = 1; });

    if ($.isArray(this.sortOptions))
    {
        for (var i = 0, len = this.sortOptions.length; i < len; ++i)
        {
            var sortOpt = this.sortOptions[i];
            if (visibleCols[sortOpt.id] != null || (!sortOpt.hidden && hiddenCols[sortOpt.id] == null))
                this.visibility.push(i);
        }
    }
    else
    {
        for (var i = 0, len = this.columns.length; i < len; ++i)
        {
            var col = this.columns[i];
            if (visibleCols[col.id] != null || (!col.hidden && hiddenCols[col.id] == null))
                this.visibility.push(i);
        }
    }

    // ************************
    // Sort

    if (this.sort == null && this.template.sort)
    {
        this.sort = this.template.sort.slice(0);
    }

    if (this.sort != null)
    {
        var sortParam = this.sort;
        this.sort = [];
        for (var i = 0, len = sortParam.length; i < len; ++i)
        {
            var col = parseInt(sortParam[i]);
            if (isNaN(col))
            {
                var desc = 0;
                if (sortParam[i].charAt(0) == '-')
                {
                    desc = 1;
                    sortParam[i] = sortParam[i].substring(1);
                }
                var index = $WH.in_array(this.columns, sortParam[i], function(x) { return x.id; } );
                if (index != -1)
                {
                    if (desc)
                        this.sort.push(-(index + 1));
                    else
                        this.sort.push(index + 1);
                }
            }
            else
                this.sort.push(col);
        }
    }
    else
        this.sort = [];

    if (this.debug)
    {
        this.columns.splice(0, 0, {
            id: 'debug-id',
            compute: function(data, td)
            {
                if (data.id)
                {
                    let pre = $WH.ce('pre', { style: { display: 'inline', margin: '0' }}, $WH.ct(data.id));
                    $WH.clickToCopy(pre);
                    $WH.ae(td, pre);
                }
            },
            getVisibleText: function(data)
            {
                return data.id || '';
            },
            getValue: function(data)
            {
                return data.id || 0;
            },
            sortFunc: function(a, b, col)
            {
                if (a.id == null)
                    return -1;
                else if (b.id == null)
                    return 1;

                return $WH.strcmp(a.id, b.id);
            },
            name: 'ID',
            width: '5%',
            tooltip: 'ID'
        });

        this.visibility.splice(0, 0, -1);

        for (var i = 0, len = this.visibility.length; i < len; ++i)
            this.visibility[i] = this.visibility[i] + 1;

        for (var i = 0, len = this.sort.length; i < len; ++i) {
            if (this.sort[i] < 0)
                this.sort[i] = this.sort[i] - 1;
            else
                this.sort[i] = this.sort[i] + 1;
        }
    }

    if (this.tabs)
    {
        this.tabIndex = this.tabs.add(this.getTabName(), {
            id:     this.id,
            onLoad: this.initialize.bind(this)
        });

        this.tabClick = Tabs.trackClick.bind(this.tabs, this.tabs.tabs[this.tabIndex]);
    }
    else
        this.initialize();
}

Listview.MODE_DEFAULT  = 0;
Listview.MODE_CHECKBOX = 1;
Listview.MODE_DIV      = 2;
Listview.MODE_TILED    = 3;
Listview.MODE_CALENDAR = 4;
Listview.MODE_FLEXGRID = 5;

Listview.prototype = {
    initialize: function()
    {
        if (this.data.length)
        {
            if (this.computeDataFunc != null)
            {
                for (var i = 0, len = this.data.length; i < len; ++i)
                    this.computeDataFunc(this.data[i]);
            }
        }

        if (this.tabs)
        {
            this.pounded = (this.tabs.poundedTab == this.tabIndex);
            if (this.pounded)
                this.readPound();
        }
        else
            this.readPound();

        this.applySort();

        var obcResult;
        if (this.onBeforeCreate != null)
        {
            if (typeof this.onBeforeCreate == 'function')
                obcResult = this.onBeforeCreate();
            else
            {
                for (var i = 0; i < this.onBeforeCreate.length; ++i)
                    (this.onBeforeCreate[i].bind(this))();
            }
        }

        this.noData = $WH.ce('div');
        this.noData.className = 'listview-nodata text';

        if (this.mode == Listview.MODE_DIV)
        {
            this.mainContainer = this.mainDiv = $WH.ce('div');
            if (!this.noStyle)
                this.mainContainer.className = 'listview-mode-div';
        }
        else if (this.mode == Listview.MODE_FLEXGRID)
        {
            this.mainContainer = this.mainDiv = $WH.ce('div', { className: 'listview-mode-flexgrid' });
            this.mainContainer.setAttribute('data-cell-min-width', this.template.cellMinWidth);
            if (this.clickable)
                this.mainContainer.className += ' clickable';

            var layout = $('.layout');
            var totalWidth = parseInt(layout.css('max-width')) - (parseInt(layout.css('padding-left')) || 0) - (parseInt(layout.css('padding-right')) || 0);
            var slots = Math.floor(totalWidth / this.template.cellMinWidth);
            var extraStyle = '.listview-mode-flexgrid[data-cell-min-width="' + this.template.cellMinWidth + '"] > div {min-width:' + this.template.cellMinWidth + "px;width:" + (100 / slots) + "%}";
            while (slots--)
            {
                if (slots)
                {
                    extraStyle += "\n@media screen and (max-width: " + (((slots + 1) * this.template.cellMinWidth) - 1 + 40) + "px) {";
                    extraStyle += '\n    .listview-mode-flexgrid[data-cell-min-width="' + this.template.cellMinWidth + '"] > div {width:' + (100 / slots) + "%}";
                    extraStyle += "\n}"
                }
            }

            $("<style/>").text(extraStyle).appendTo(document.head)
        }
        else
        {
            this.mainContainer = this.table = $WH.ce('table');
            this.thead = $WH.ce('thead');
            this.tbody = $WH.ce('tbody');

            if (this.clickable)
                this.tbody.className = 'clickable';

            if (this.mode == Listview.MODE_TILED || this.mode == Listview.MODE_CALENDAR)
            {
                if (!this.noStyle)
                    this.table.className = 'listview-mode-' + (this.mode == Listview.MODE_TILED ? 'tiled' : 'calendar');

                var
                    width = (100 / this.nItemsPerRow) + '%',
                    colGroup = $WH.ce('colgroup'),
                    col;

                for (var i = 0; i < this.nItemsPerRow; ++i)
                {
                    col = $WH.ce('col');
                    col.style.width = width;
                    $WH.ae(colGroup, col);
                }

                $WH.ae(this.mainContainer, colGroup);

                if (this.sortOptions)
                    setTimeout((function() { this.updateSortArrow() }).bind(this), 0);
            }
            else
            {
                if (!this.noStyle)
                    this.table.className = 'listview-mode-default';

                this.createHeader();
                this.updateSortArrow();
            }

            $WH.ae(this.table, this.thead);
            $WH.ae(this.table, this.tbody);
        }

        this.createBands();

        if (this.customFilter != null)
            this.updateFilters();

        this.updateNav();
        this.refreshRows();
        this.updateBrowseSession();

        if (this.onAfterCreate != null)
            this.onAfterCreate(obcResult);
    },

    createHeader: function()
    {
        var tr = $WH.ce('tr');

        if (this.mode == Listview.MODE_CHECKBOX)
        {
            var
                th  = $WH.ce('th'),
                div = $WH.ce('div'),
                a   = $WH.ce('a');

            th.style.width = '33px';

            a.href = 'javascript:;';
            a.className = 'listview-cb';
            $WH.ns(a);
            $WH.ae(a, $WH.ct(String.fromCharCode(160)));

            $WH.ae(div, a);
            $WH.ae(th, div);
            $WH.ae(tr, th);
        }

        for (var i = 0, len = this.visibility.length; i < len; ++i)
        {
            var
                reali = this.visibility[i],
                col = this.columns[reali],
                th = $WH.ce('th');
                div = $WH.ce('div'),
                a = $WH.ce('a'),
                outerSpan = $WH.ce('span'),
                innerSpan = $WH.ce('span');

            col.__th = th;

            if (this.filtrable && (col.filtrable == null || col.filtrable))
            {
                a.onmouseup = Listview.headerClick.bind(this, col, reali);
                a.onclick = a.oncontextmenu = $WH.rf;
            }
            else if (this.sortable)
            {
                a.href = 'javascript:;';
                a.onclick = this.sortBy.bind(this, reali + 1);
            }

            if (a.onclick)
            {
                a.onmouseover = Listview.headerOver.bind(this, a, col);
                a.onmouseout  = $WH.Tooltip.hide;
                $WH.ns(a);
            }
            else
                a.className = 'static';

            if (col.width != null)
                th.style.width = col.width;

            if (col.align != null)
                th.style.textAlign = col.align;

            if (col.span != null)
                th.colSpan = col.span;

            $WH.ae(innerSpan, $WH.ct(col.name));
            $WH.ae(outerSpan, innerSpan);

            $WH.ae(a, outerSpan);
            $WH.ae(div, a);
            $WH.ae(th, div);

            $WH.ae(tr, th);
        }

        if (this.hideHeader)
            this.thead.style.display = 'none';

        $WH.ae(this.thead, tr);
    },

    createSortOptions: function(parent)
    {
        if (!$.isArray(this.sortOptions))
            return;

        var div = $WH.ce('div');
        div.className = 'listview-sort-options';
        div.innerHTML = LANG.lvnote_sort;
        var sp = $WH.ce('span');
        sp.className = 'listview-sort-options-choices';
        var activeSort = null;
        if ($.isArray(this.sort))
            activeSort = this.sort[0];

        var a;
        var sorts = [];
        for (var i = 0; i < this.sortOptions.length; i++)
        {
            if (this.sortOptions[i].hidden)
                continue;

            a = $WH.ce('a');
            a.href = 'javascript:;';
            a.innerHTML = this.sortOptions[i].name;
            a.onclick = this.sortGallery.bind(this, a, i + 1);
            if (activeSort === i + 1)
                a.className = 'active';

            sorts.push(a);
        }

        for (i = 0; i < sorts.length; i++)
            $WH.ae(sp, sorts[i]);


        $WH.ae(div, sp);
        $WH.aef(parent, div);
    },

    sortGallery: function(el, colNo)
    {
        var btn = $(el);
        btn.siblings('a').removeClass('active');
        btn.addClass('active');
        this.sortBy(colNo);
    },

    createBands: function()
    {
        var
            bandTop = $WH.ce('div'),
            bandBot = $WH.ce('div'),
            noteTop = $WH.ce('div'),
            noteBot = $WH.ce('div');

        this.bandTop = bandTop;
        this.bandBot = bandBot;
        this.noteTop = noteTop;
        this.noteBot = noteBot;

        bandTop.className = 'listview-band-top';
        bandBot.className = 'listview-band-bottom';

        this.navTop = this.createNav(true);
        this.navBot = this.createNav(false);

        noteTop.className = noteBot.className = 'listview-note';

        if (this.note)
        {
            noteTop.innerHTML = this.note;
            var e = $WH.g_getGets();
            if (this.note.indexOf('fi_toggle()') > -1 && !e.filter)
                fi_toggle();
        }
        else if (this.createNote)
            this.createNote(noteTop, noteBot);

        this.createSortOptions(noteTop);

        if (this.debug)
        {
            $WH.ae(noteTop, $WH.ct(" ("));
            var ids = $WH.ce('a');
            ids.onclick = this.getList.bind(this);
            $WH.ae(ids, $WH.ct("CSV"));
            $WH.ae(noteTop, ids);
            $WH.ae(noteTop, $WH.ct(")"));
        }

        if (this._errors)
        {
            var sp = $WH.ce('small'),
                b  = $WH.ce('b');

            b.className = 'q10 icon-report';
            if (noteTop.innerHTML)
                b.style.marginLeft = '10px';

            g_addTooltip(sp, LANG.lvnote_witherrors, 'q');

            $WH.st(b, LANG.error);
            $WH.ae(sp, b);
            $WH.ae(noteTop, sp);
        }

        if (!noteTop.firstChild && !(this.createCbControls || this.mode == Listview.MODE_CHECKBOX))
            $WH.ae(noteTop, $WH.ct(String.fromCharCode(160)));
        if (!(this.createCbControls || this.mode == Listview.MODE_CHECKBOX))
            $WH.ae(noteBot, $WH.ct(String.fromCharCode(160)));

        $WH.ae(bandTop, this.navTop);
        if (this.searchable)
        {
            var
                FI_FUNC = this.updateFilters.bind(this, true),
                FI_PH   = (this._truncated ? LANG.lvsearchdisplayedresults : LANG.lvsearchresults),
                sp      = $WH.ce('span'),
                em      = $WH.ce('em'),
                a       = $WH.ce('a'),
                input   = $WH.ce('input');

            sp.className = 'listview-quicksearch';

            if (this.tabClick)
                $(sp).click(this.tabClick);

            $WH.ae(sp, em);

            a.href = 'javascript:;';
            a.onclick = function() {
                var foo = this.nextSibling;
                foo.value = '';
                foo.placeholder = FI_PH;
                FI_FUNC();
            };
            a.style.display = 'none';
            $WH.ae(a, $WH.ce('span'));
            $WH.ae(sp, a);
            $WH.ns(a);

            input.setAttribute('type', 'text');
            input.placeholder = FI_PH;
            input.style.width = (this._truncated ? '19em' : '15em');
            g_onAfterTyping(input, FI_FUNC, this.searchDelay);

            input.onmouseover = function()
            {
                if ($WH.trim(this.value) != '')
                    this.className = '';
            };

            input.onfocus = function()
            {
                this.className = '';
            };

            input.onblur = function()
            {
                if ($WH.trim(this.value) == '')
                {
                    this.value = '';
                }
            };

            input.onkeypress = this.submitSearch.bind(this);

            $WH.ae(sp, input);

            this.quickSearchBox   = input;
            this.quickSearchGlass = em;
            this.quickSearchClear = a;

            $WH.ae(bandTop, sp);
        }
        $WH.ae(bandTop, noteTop);

        $WH.ae(bandBot, this.navBot);
        $WH.ae(bandBot, noteBot);

        if (this.createCbControls || this.mode == Listview.MODE_CHECKBOX)
        {
            if (this.note)
                noteTop.style.paddingBottom = '5px';

            this.cbBarTop = this.createCbBar(true);
            this.cbBarBot = this.createCbBar(false);

            $WH.ae(bandTop, this.cbBarTop);
            $WH.ae(bandBot, this.cbBarBot);

            if (!this.noteTop.firstChild && !this.cbBarTop.firstChild)
                this.noteTop.innerHTML = '&nbsp;';
            if (!this.noteBot.firstChild && !this.cbBarBot.firstChild)
                this.noteBot.innerHTML = '&nbsp;';

            if (this.noteTop.firstChild && this.cbBarTop.firstChild)
                this.noteTop.style.paddingBottom = '6px';
            if (this.noteBot.firstChild && this.cbBarBot.firstChild)
                this.noteBot.style.paddingBottom = '6px';
        }

        if (this.hideBands & 1)
            bandTop.style.display = 'none';
        if (this.hideBands & 2)
            bandBot.style.display = 'none';

        $WH.ae(this.container, this.bandTop);
        if (this.clip)
        {
            var clipDiv = $WH.ce('div');
            clipDiv.className = 'listview-clip';
            clipDiv.style.width  = this.clip.w + 'px';
            clipDiv.style.height = this.clip.h + 'px';
            this.clipDiv = clipDiv;

            $WH.ae(clipDiv, this.mainContainer);
            $WH.ae(clipDiv, this.noData);
            $WH.ae(this.container, clipDiv);
        }
        else
        {
            $WH.ae(this.container, this.mainContainer);
            $WH.ae(this.container, this.noData);
        }
        $WH.ae(this.container, this.bandBot);
    },

    createNav: function(top)
    {
        var
            div = $WH.ce('div'),
            a1 = $WH.ce('a'),
            a2 = $WH.ce('a'),
            a3 = $WH.ce('a'),
            a4 = $WH.ce('a'),
            span = $WH.ce('span'),
            b1 = $WH.ce('b'),
            b2 = $WH.ce('b'),
            b3 = $WH.ce('b');

        div.className = 'listview-nav';

        a1.href = a2.href = a3.href = a4.href = 'javascript:;';

        $WH.ae(a1, $WH.ct(String.fromCharCode(171) + LANG.lvpage_first));
        $WH.ae(a2, $WH.ct(String.fromCharCode(8249) + LANG.lvpage_previous));
        $WH.ae(a3, $WH.ct(LANG.lvpage_next + String.fromCharCode(8250)));
        $WH.ae(a4, $WH.ct(LANG.lvpage_last + String.fromCharCode(187)));

        $WH.ns(a1);
        $WH.ns(a2);
        $WH.ns(a3);
        $WH.ns(a4);

        a1.onclick = this.firstPage.bind(this);
        a2.onclick = this.previousPage.bind(this);
        a3.onclick = this.nextPage.bind(this);
        a4.onclick = this.lastPage.bind(this);

        if (this.mode == Listview.MODE_CALENDAR)
        {
            $WH.ae(b1, $WH.ct('a'));
            $WH.ae(span, b1);
        }
        else
        {
            $WH.ae(b1, $WH.ct('a'));
            $WH.ae(b2, $WH.ct('a'));
            $WH.ae(b3, $WH.ct('a'));
            $WH.ae(span, b1);
            $WH.ae(span, $WH.ct(LANG.hyphen));
            $WH.ae(span, b2);
            $WH.ae(span, $WH.ct(LANG.lvpage_of));
            $WH.ae(span, b3);
        }

        $WH.ae(div, a1);
        $WH.ae(div, a2);
        $WH.ae(div, span);
        $WH.ae(div, a3);
        $WH.ae(div, a4);

        if (top)
        {
            if (this.hideNav & 1)
                div.style.display = 'none';
        }
        else
        {
            if (this.hideNav & 2)
                div.style.display = 'none';
        }

        if (this.tabClick)
            $('a', div).click(this.tabClick);

        return div;
    },

    createCbBar: function(topBar)
    {
        var div = $WH.ce('div');

        if (this.createCbControls)
            this.createCbControls(div, topBar);

        if (div.firstChild) // Not empty
        {
            div.className = 'listview-withselected' + (topBar ? '' : '2');
        }

        return div;
    },

    refreshRows: function()
    {
        var target = null;
        switch (this.mode)
        {
            case Listview.MODE_DIV:
                target = this.mainContainer;
                break;
            case Listview.MODE_FLEXGRID:
                target = this.mainDiv;
                break;
            default:
                target = this.tbody
        }
        if (!target)
            return;

        $WH.ee(target);

        if (this.nRowsVisible == 0)
        {
            if (!this.filtered)
            {
                this.bandTop.style.display = this.bandBot.style.display = 'none';

                this.mainContainer.style.display = 'none';
            }

            this.noData.style.display = '';
            this.showNoData();

            return;
        }

        var
            starti,
            endi,
            func;

        if (!(this.hideBands & 1))
            this.bandTop.style.display = '';
        if (!(this.hideBands & 2))
            this.bandBot.style.display = '';

        if (this.nDaysPerMonth && this.nDaysPerMonth.length)
        {
            starti = 0;
            for (var i = 0; i < this.rowOffset; ++i)
                starti += this.nDaysPerMonth[i];

            endi = starti + this.nDaysPerMonth[i];
        }
        else if (this.nItemsPerPage > 0)
        {
            starti = this.rowOffset;
            endi   = Math.min(starti + this.nRowsVisible, starti + this.nItemsPerPage);

            if (this.filtered && this.rowOffset > 0) // Adjusts start and end position when listview is filtered
            {
                for (var i = 0, count = 0; i < this.data.length && count < this.rowOffset; ++i)
                {
                    var row = this.data[i];

                    if (row.__hidden || row.__deleted)
                        ++starti;
                    else
                        ++count;
                }
                endi += (starti - this.rowOffset);
            }
        }
        else
        {
            starti = 0;
            endi   = this.nRowsVisible;
        }

        var nItemsToDisplay = endi - starti;

        if (this.mode == Listview.MODE_DIV)
        {
            for (var j = 0; j < nItemsToDisplay; ++j)
            {
                var
                    i   = starti + j,
                    row = this.data[i];

                if (!row)
                    break;

                if (row.__hidden || row.__deleted)
                {
                    ++nItemsToDisplay;
                    continue;
                }

                $WH.ae(this.mainDiv, this.getDiv(i));
            }
        }
        else if (this.mode == Listview.MODE_FLEXGRID)
        {
            for (var j = 0; j < nItemsToDisplay; ++j)
            {
                var
                    i   = starti + j,
                    row = this.data[i];

                if (!row)
                    break;

                if (row.__hidden || row.__deleted)
                {
                    ++nItemsToDisplay;
                    continue;
                }

                $WH.ae(this.mainDiv, this.getDiv(i));
            }
        }
        else if (this.mode == Listview.MODE_TILED)
        {
            var
                k  = 0,
                tr = $WH.ce('tr');

            for (var j = 0; j < nItemsToDisplay; ++j)
            {
                var
                    i   = starti + j,
                    row = this.data[i];

                if (!row)
                    break;

                if (row.__hidden || row.__deleted)
                {
                    ++nItemsToDisplay;
                    continue;
                }

                $WH.ae(tr, this.getCell(i));

                if (++k == this.nItemsPerRow)
                {
                    $WH.ae(this.tbody, tr);
                    if (j + 1 < nItemsToDisplay)
                        tr = $WH.ce('tr');

                    k = 0;
                }
            }

            if (k != 0)
            {
                for (; k < 4; ++k)
                {
                    var foo = $WH.ce('td');
                    foo.className = 'empty-cell';
                    $WH.ae(tr, foo);
                }
                $WH.ae(this.tbody, tr);
            }
        }
        else if (this.mode == Listview.MODE_CALENDAR)
        {
            var tr = $WH.ce('tr');

            for (var i = 0; i < 7; ++i)
            {
                var th = $WH.ce('th');
                $WH.st(th, LANG.date_days[i]);
                $WH.ae(tr, th);
            }

            $WH.ae(this.tbody, tr);
            tr = $WH.ce('tr');

            for (var k = 0; k < this.dates[starti].date.getLocaleDay(); ++k)
            {
                var foo = $WH.ce('td');
                foo.className = 'empty-cell';
                $WH.ae(tr, foo);
            }

            for (var j = starti; j < endi; ++j)
            {
                $WH.ae(tr, this.getEvent(j));

                if (++k == 7)
                {
                    $WH.ae(this.tbody, tr);
                    tr = $WH.ce('tr');
                    k  = 0;
                }
            }

            if (k != 0)
            {
                for (; k < 7; ++k)
                {
                    var foo = $WH.ce('td');
                    foo.className = 'empty-cell';
                    $WH.ae(tr, foo);
                }
                $WH.ae(this.tbody, tr);
            }
        }
        else // DEFAULT || CHECKBOX
        {
            for (var j = 0; j < nItemsToDisplay; ++j)
            {
                var
                i   = starti + j,
                row = this.data[i];

                if (!row)
                    break;

                if (row.__hidden || row.__deleted)
                {
                    ++nItemsToDisplay;
                    continue;
                }

                $WH.ae(this.tbody, this.getRow(i));
            }
        }

        this.mainContainer.style.display = '';
        this.noData.style.display = 'none';
    },

    showNoData: function()
    {
        var div = this.noData;
        $WH.ee(div);

        var result = -1;
        if (this.onNoData)
            result = (this.onNoData.bind(this, div))();

        if (result == -1)
            $WH.ae(this.noData, $WH.ct(this.filtered ? LANG.lvnodata2 : LANG.lvnodata));
    },

    getDiv: function(i)
    {
        var row = this.data[i];

        if (row.__div == null || this.minPatchVersion != row.__minPatch)
            this.createDiv(row, i);

        return row.__div;
    },

    createDiv: function(row, i)
    {
        var div = $WH.ce('div');
        row.__div = div;
        if (this.minPatchVersion)
            row.__minPatch = this.minPatchVersion;

        (this.template.compute.bind(this, row, div, i))();
    },

    getCell: function(i)
    {
        var row = this.data[i];

        if (row.__td == null)
            this.createCell(row, i);

        return row.__td;
    },

    createCell: function(row, i)
    {
        var td = $WH.ce('td');
        row.__td = td;

        (this.template.compute.bind(this, row, td, i))();
    },

    getEvent: function(i)
    {
        var row = this.dates[i];

        if (row.__td == null)
            this.createEvent(row, i);

        return row.__td;
    },

    createEvent: function(row, i)
    {
        row.events = $WH.array_filter(this.data, function(holiday) {
            if (holiday.__hidden || holiday.__deleted)
                return false;

            var dates = Listview.funcBox.getEventNextDates(holiday.startDate, holiday.endDate, holiday.rec || 0, row.date);
            if (dates[0] && dates[1])
            {
                dates[0].setHours(0, 0, 0, 0);
                dates[1].setHours(0, 0, 0, 0);
                return dates[0] <= row.date && dates[1] >= row.date;
            }

            return false;
        });

        var td = $WH.ce('td');
        row.__td = td;

        if (row.date.getFullYear() == g_serverTime.getFullYear() && row.date.getMonth() == g_serverTime.getMonth() && row.date.getDate() == g_serverTime.getDate())
            td.className = 'calendar-today';

        var div = $WH.ce('div');
        div.className = 'calendar-date';
        $WH.st(div, row.date.getDate());
        $WH.ae(td, div);

        div = $WH.ce('div');
        div.className = 'calendar-event';
        $WH.ae(td, div);

        (this.template.compute.bind(this, row, div, i))();

        if (this.getItemLink)
            td.onclick = this.itemClick.bind(this, row);
    },

    getRow: function(i)
    {
        var row = this.data[i];

        if (row.__tr == null)
            this.createRow(row);

        return row.__tr;
    },

    setRow: function(newRow)
    {
        if (this.data[newRow.pos])
        {
            this.data[newRow.pos] = newRow;
            this.data[newRow.pos].__tr = newRow.__tr;
            this.createRow(this.data[newRow.pos]);
            this.refreshRows();
        }
    },

    createRow: function(row)
    {
        var tr = $WH.ce('tr');
        row.__tr = tr;

        if (this.mode == Listview.MODE_CHECKBOX)
        {
            var td = $WH.ce('td');

            if (!row.__nochk)
            {
                td.className = 'listview-cb';
                td.onclick   = Listview.cbCellClick;

                var cb = $WH.ce('input');
                $WH.ns(cb);
                cb.type = 'checkbox';
                cb.onclick = Listview.cbClick;

                if (row.__chk)
                {
                    cb.checked = true;
                }

                row.__cb = cb;

                $WH.ae(td, cb);
            }

            $WH.ae(tr, td);
        }

        for (var i = 0, len = this.visibility.length; i < len; ++i)
        {
            var
                reali = this.visibility[i],
                col = this.columns[reali],
                td = $WH.ce('td'),
                result;

            if (col.align != null)
                td.style.textAlign = col.align;

            if (col.compute)
                result = (col.compute.bind(this, row, td, tr, reali))();
            else
            {
                if (row[col.value] != null)
                    result = row[col.value];
                else
                    result = -1;
            }

            if (result != -1 && result != null)
                td.insertBefore($WH.ct(result), td.firstChild);

            $WH.ae(tr, td);
        }

        if (this.mode == Listview.MODE_CHECKBOX && row.__chk)
            tr.className = 'checked';

        if (row.frommerge == 1) {
            tr.className += ' mergerow';
        }

        if (this.getItemLink)
            tr.onclick = this.itemClick.bind(this, row);
    },

    itemClick: function(row, e)
    {
        e = $WH.$E(e);

        var i  = 0,
            el = e._target;

        while (el && i < 3)
        {
            if (el.nodeName == 'A')
                return;

            el = el.parentNode;
        }

        location.href = this.getItemLink(row);
    },

    submitSearch: function(e)
    {
        e = $WH.$E(e);

        if (!this.onSearchSubmit || e.keyCode != 13)
            return;

        for (var i = 0, len = this.data.length; i < len; ++i)
        {
            if (this.data[i].__hidden)
                continue;

            (this.onSearchSubmit.bind(this, this.data[i]))();
        }
    },

    validatePage: function()
    {
        var
            rpp = this.nItemsPerPage,
            ro  = this.rowOffset,
            len = this.nRowsVisible;

        if (ro < 0)
            this.rowOffset = 0;
        else if (this.mode == Listview.MODE_CALENDAR)
            this.rowOffset = Math.min(ro, this.nDaysPerMonth.length - 1);
        else
            this.rowOffset = this.getRowOffset(ro + rpp > len ? len - 1 : ro);
    },

    getRowOffset: function(rowNo)
    {
        var rpp = this.nItemsPerPage;

        return (rpp > 0 && rowNo > 0 ? Math.floor(rowNo / rpp) * rpp : 0);
    },

    resetRowVisibility: function()
    {
        for (var i = 0, len = this.data.length; i < len; ++i)
            this.data[i].__hidden = false;

        this.filtered     = false;
        this.rowOffset    = 0;
        this.nRowsVisible = this.data.length;
    },

    getColText: function(row, col)
    {
        var text = '';
        if (this.template.getVisibleText)
            text = $WH.trim(this.template.getVisibleText(row) + ' ');

        if (col.getVisibleText)
            return text + col.getVisibleText(row);

        if (col.getValue)
            return text + col.getValue(row);

        if (col.value)
            return text + row[col.value];

        if (col.compute)
            return text + col.compute(row, $WH.ce('td'), $WH.ce('tr'));

        return '';
    },

    resetFilters: function()
    {
        for (var j = 0, len2 = this.visibility.length; j < len2; ++j)
        {
            var realj = this.visibility[j];
            var col = this.columns[realj];

            if (col.__filter)
            {
                col.__th.firstChild.firstChild.className = '';
                col.__filter = null;
                --(this.nFilters);
            }
        }
    },

    updateFilters: function(refresh)
    {
        $WH.Tooltip.hide();
        this.resetRowVisibility();

        var
            searchText,
            parts,
            nParts;

        if (this.searchable)
        {
            this.quickSearchBox.parentNode.style.display = '';

            searchText = $WH.trim(this.quickSearchBox.value);

            if (searchText)
            {
                this.quickSearchGlass.style.display = 'none';
                this.quickSearchClear.style.display = '';

                searchText = searchText.toLowerCase().replace(/\s+/g, ' ');

                parts  = searchText.split(' ');
                nParts = parts.length;
            }
            else
            {
                this.quickSearchGlass.style.display = '';
                this.quickSearchClear.style.display = 'none';
            }
        }
        else if (this.quickSearchBox)
            this.quickSearchBox.parentNode.style.display = 'none';

        if (!searchText && this.nFilters == 0 && this.customFilter == null)
        {
            if (refresh)
            {
                this.updateNav();
                this.refreshRows();
                this.updateBrowseSession();
            }

            return;
        }

        // Numerical
        var filterFuncs = {
            1: function(x, y)    { return x  > y;           },
            2: function(x, y)    { return x == y;           },
            3: function(x, y)    { return x  < y;           },
            4: function(x, y)    { return x >= y;           },
            5: function(x, y)    { return x <= y;           },
            6: function(x, y, z) { return y <= x && x <= z; }
        };

        // Range
        var filterFuncs2 = {
            1: function(min, max, y)    { return max  > y;             },
            2: function(min, max, y)    { return min <= y && y <= max; },
            3: function(min, max, y)    { return min  < y;             },
            4: function(min, max, y)    { return max >= y;             },
            5: function(min, max, y)    { return min <= y;             },
            6: function(min, max, y, z) { return y <= max && min <= z; }
        };

        var nRowsVisible = 0;

        for (var i = 0, len = this.data.length; i < len; ++i)
        {
            var
                row = this.data[i],
                nFilterMatches = 0;
                nSearchMatches = 0,
                matches = [];

            row.__hidden = true;

            if (this.customFilter && !this.customFilter(row, i))
                continue;

            for (var j = 0, len2 = this.visibility.length; j < len2; ++j)
            {
                var realj = this.visibility[j];
                var col = this.columns[realj];

                if (col.__filter)
                {
                    var
                        filter = col.__filter,
                        result = false;

                    if (col.type != null && col.type == 'range')
                    {
                        var minValue = col.getMinValue(row),
                            maxValue = col.getMaxValue(row);

                        result = (filterFuncs2[filter.type])(minValue, maxValue, filter.value, filter.value2);
                    }
                    else if (col.type == null || col.type == 'num' || filter.type > 0)
                    {
                        var value = null;

                        if (col.getValue)
                            value = col.getValue(row);
                        else if (col.value)
                            value = parseFloat(row[col.value]);

                        if (!value)
                            value = 0;

                        result = (filterFuncs[filter.type])(value, filter.value, filter.value2);
                    }
                    else
                    {
                        var text = this.getColText(row, col);

                        if (text)
                        {
                            text = text.toString().toLowerCase();

                            if (filter.invert)
                                result = text.match(filter.regex) != null;
                            else
                            {
                                var foo = 0;

                                for (var k = 0, len3 = filter.words.length; k < len3; ++k)
                                {
                                    if (text.indexOf(filter.words[k]) != -1)
                                        ++foo;
                                    else
                                        break;
                                }

                                result = (foo == filter.words.length);
                            }
                        }
                    }

                    if (filter.invert)
                        result = !result;

                    if (result)
                        ++nFilterMatches;
                    else
                        break;
                }

                if (searchText)
                {
                    var text = this.getColText(row, col);
                    if (text)
                    {
                        text = text.toString().toLowerCase();

                        for (var k = 0, len3 = parts.length; k < len3; ++k)
                        {
                            if (!matches[k])
                            {
                                if (text.indexOf(parts[k]) != -1)
                                {
                                    matches[k] = 1;
                                    ++nSearchMatches;
                                }
                            }
                        }
                    }
                }
            }

            if (row.__alwaysvisible ||
               ((this.nFilters == 0 || nFilterMatches == this.nFilters) &&
               (!searchText || nSearchMatches == nParts)))
            {
                row.__hidden = false;
                ++nRowsVisible;
            }
        }

        this.filtered     = (nRowsVisible < this.data.length);
        this.nRowsVisible = nRowsVisible;

        if (refresh)
        {
            this.updateNav();
            this.refreshRows();
            this.updateBrowseSession();
        }
    },

    changePage: function()
    {
        this.validatePage();

        this.refreshRows();
        this.updateNav();
        this.updatePound();
        this.updateBrowseSession();

        var
            scroll = $WH.g_getScroll(),
            c = $WH.ac(this.container);

        if (scroll.y > c[1])
            scrollTo(scroll.x, c[1]);
    },

    firstPage: function()
    {
        this.rowOffset = 0;
        this.changePage();

        return false;
    },

    previousPage: function()
    {
        this.rowOffset -= this.nItemsPerPage;
        this.changePage();

        return false;
    },

    nextPage: function()
    {
        this.rowOffset += this.nItemsPerPage;
        this.changePage();

        return false;
    },

    lastPage: function()
    {
        this.rowOffset = 99999999;
        this.changePage();

        return false;
    },

    addSort: function(arr, colNo)
    {
        var i = $WH.in_array(arr, Math.abs(colNo), function(x) { return Math.abs(x); });

        if (i != -1)
        {
            colNo = arr[i];
            arr.splice(i, 1);
        }

        arr.splice(0, 0, colNo);
    },

    sortBy: function(colNo)
    {
        var sorts = this.sortOptions || this.columns;
        if (colNo <= 0 || colNo > sorts.length)
            return;

        if (Math.abs(this.sort[0]) == colNo)
            this.sort[0] = -this.sort[0];
        else
        {
            var defaultSort = -1;
            if (sorts[colNo - 1].type == 'text')
                defaultSort = 1;

            this.addSort(this.sort, defaultSort * colNo);
        }

        this.applySort();
        this.refreshRows();
        this.updateSortArrow();
        this.updatePound();
    },

    applySort: function()
    {
        if (this.sort.length == 0)
            return;

        Listview.sort        = this.sort;
        Listview.columns     = this.columns;
        Listview.sortOptions = this.sortOptions;

        if (this.indexCreated)
            this.data.sort(Listview.sortIndexedRows.bind(this));
        else
            this.data.sort(Listview.sortRows.bind(this));

        this.updateSortIndex();
        this.updateBrowseSession();
    },

    setSort: function(sort, refresh, updatePound)
    {
        if (this.sort.toString() != sort.toString())
        {
            this.sort = sort;
            this.applySort();

            if (refresh)
                this.refreshRows();

            if (updatePound)
                this.updatePound();
        }
    },

    readPound: function()
    {
        if (!this.poundable || !location.hash.length)
            return false;

        var _ = location.hash.substr(1);
        if (this.tabs)
        {
            var n = _.lastIndexOf(':');

            if (n == -1)
                return false;

            _ = _.substr(n + 1);
        }

        var num = parseInt(_);
        if (!isNaN(num))
        {
            this.rowOffset = num;
            this.validatePage();

            if (this.poundable != 2)
            {
                var sort = [];
                var matches = _.match(/(\+|\-)[0-9]+/g);
                if (matches != null)
                {
                    var sorts = this.sortOptions || this.columns;
                    for (var i = matches.length - 1; i >= 0; --i)
                    {
                        var colNo = parseInt(matches[i]) | 0;
                        var _ = Math.abs(colNo);
                        if (_ <= 0 || _ > sorts.length)
                            break;

                        this.addSort(sort, colNo);
                    }

                    this.setSort(sort, false, false);
                }
            }

            if (this.tabs)
                this.tabs.setTabPound(this.tabIndex, this.getTabPound());
        }
    },

    updateBrowseSession: function ()
    {
        if ((!window.JSON) || (!$WH.localStorage.isSupported()))
            return;

     // if ((typeof fi_filters == 'undefined') && (location.pathname != '/search')) { aowow - we still don't flatten our urls
        if ((typeof fi_filters == 'undefined') && !(/\?search/.test(location.search)))
            return;

        if (this.data.length < 3)
            return;

        if (typeof this.getItemLink != 'function')
            return;

        var max    = 5;
        var path   = location.pathname + location.search;
        var expire = (new Date(g_serverTime.getTime() - 1000 * 60 * 60 * 24 * 3)).getTime();
        var lv     = $WH.localStorage.get('lvBrowse');

        if (lv)
        {
            lv = window.JSON.parse(lv);
            for (var i = 0; i < lv.length; i++)
            {
                if ((lv[i].path == path) || (lv[i].when < expire))
                    lv.splice(i--, 1);
            }

            if (lv.length >= max)
                lv.splice(max - 1, lv.length - max - 1);
        }
        else
            lv = [];

        var urls = [],
            url,
            pattern = /^\?[-a-z]+=\d+/i;

        for (var i = 0; i < this.data.length; i++)
        {
            if (url = pattern.exec(this.getItemLink(this.data[i])))
                urls.push(url[0]);
        }

        if (urls.length < 3)
            return;

        lv.unshift({
            path: path,
            hash: location.hash,
            when: g_serverTime.getTime(),
            urls: urls
        });

        $WH.localStorage.set('lvBrowse', window.JSON.stringify(lv));
    },

    updateSortArrow: function()
    {
        if (!this.sort.length || !this.thead || this.mode == Listview.MODE_CALENDAR /* || this.searchSort */)
            return;

        var i = $WH.in_array(this.visibility, Math.abs(this.sort[0]) - 1);

        if (i == -1)
            return;

        if (this.mode == Listview.MODE_TILED)
        {
            if (!this.sortOptions)
                return;

            var a = $('.listview-sort-options a', this.noteTop).get(i);
            if (this.lsa && this.lsa != a)
                this.lsa.className = '';

            a.className = this.sort[0] < 0 ? 'active sortdesc' : 'active sortasc';
            this.lsa = a;
            return;
        }

        if (this.mode == Listview.MODE_CHECKBOX && i < this.thead.firstChild.childNodes.length - 1)
            i += 1;

        var span = this.thead.firstChild.childNodes[i].firstChild.firstChild.firstChild;

        if (this.lsa && this.lsa != span) // lastSortArrow
            this.lsa.className = '';

        span.className = (this.sort[0] < 0 ? 'sortdesc' : 'sortasc');
        this.lsa = span;
    },

    updateSortIndex: function()
    {
        var _ = this.data;

        for (var i = 0, len = _.length; i < len; ++i)
            _[i].__si = i; // sortIndex

        this.indexCreated = true;
    },

    updateTabName: function()
    {
        if (this.tabs && this.tabIndex != null)
            this.tabs.setTabName(this.tabIndex, this.getTabName());
    },

    updatePound: function(useCurrentSort)
    {
        if (!this.poundable)
            return;

        var _  = '',
            id = '';

        if (useCurrentSort)
        {
            if (location.hash.length && this.tabs)
            {
                var n = location.hash.lastIndexOf(':');
                if (n != -1 && !isNaN(parseInt(location.hash.substr(n + 1))))
                    _ = location.hash.substr(n + 1);
            }
        }
        else
            _ = this.getTabPound();

        if (this.customPound)
            id = this.customPound;
        else if (this.tabs)
            id = this.id;

        if (_ && this.tabs)
            this.tabs.setTabPound(this.tabIndex, _);

        location.replace('#' + id + (id && _ ? ':' : '') + _);
    },

    updateNav: function()
    {
        var
            arr      = [this.navTop, this.navBot],
            _        = this.nItemsPerPage,
            __       = this.rowOffset,
            ___      = this.nRowsVisible,
            first    = 0,
            previous = 0,
            next     = 0,
            last     = 0,
            date     = new Date();

        if (___ > 0)
        {
            if (!(this.hideNav & 1))
                arr[0].style.display = '';
            if (!(this.hideNav & 2))
                arr[1].style.display = '';
        }
        else
            arr[0].style.display = arr[1].style.display = 'none';

        if (this.mode == Listview.MODE_CALENDAR)
        {
            for (var i = 0; i < this.nDaysPerMonth.length; ++i)
            {
                if (i == __) // Selected month
                {
                    if (i > 0)
                        previous = 1;
                    if (i > 1)
                        first = 1;
                    if (i < this.nDaysPerMonth.length - 1)
                        next = 1;
                    if (i < this.nDaysPerMonth.length - 2)
                        last = 1;
                }
            }

            date.setTime(this.startOnMonth.valueOf());
            date.setMonth(date.getMonth() + __);
        }
        else
        {
            if (_)
            {
                if (__ > 0)
                {
                    previous = 1;
                    if (__ >= _ + _)
                        first = 1;
                }
                if (__ + _ < ___)
                {
                    next = 1;
                    if (__ + _ + _ < ___)
                        last = 1;
                }
            }
        }

        for (var i = 0; i < 2; ++i)
        {
            var childs = arr[i].childNodes;

            childs[0].style.display = (first ? '' : 'none');
            childs[1].style.display = (previous ? '' : 'none');
            childs[3].style.display = (next ? '' : 'none');
            childs[4].style.display = (last ? '' : 'none');

            childs = childs[2].childNodes;

            if (this.mode == Listview.MODE_CALENDAR)
                childs[0].firstChild.nodeValue = LANG.date_months[date.getMonth()] + ' ' + date.getFullYear();
            else
            {
                childs[0].firstChild.nodeValue = __ + 1;
                childs[2].firstChild.nodeValue = _ ? Math.min(__ + _, ___) : ___;
                childs[4].firstChild.nodeValue = ___;
            }
        }
    },

    getTabName: function()
    {
        var
            name = this.name,
            n = this.data.length;

        for (var i = 0, len = this.data.length; i < len; ++i)
            if (this.data[i].__hidden || this.data[i].__deleted)
                --n;

        if (n > 0 && !this.hideCount)
            name += $WH.sprintf(LANG.qty, n);

        return name;
    },

    getTabPound: function()
    {
        var buffer = '';

        buffer += this.rowOffset;

        if (this.poundable != 2 && this.sort.length)
            buffer += ('+' + this.sort.join('+')).replace(/\+\-/g, '-');

        return buffer;
    },

    getCheckedRows: function()
    {
        var checkedRows = [];

        for (var i = 0, len = this.data.length; i < len; ++i)
        {
            var _ = this.data[i];

            if ((_.__cb && _.__cb.checked) || (!_.__cb && _.__chk))
                checkedRows.push(_);
        }

        return checkedRows;
    },

    resetCheckedRows: function()
    {
        for (var i = 0, len = this.data.length; i < len; ++i)
        {
            var _ = this.data[i];

            if (_.__cb)
                _.__cb.checked = false;
            else if (_.__chk)
                _.__chk = null;

            if (_.__tr)
                _.__tr.className = _.__tr.className.replace('checked', '');
        }
    },

    deleteRows: function(rows)
    {
        if (!rows || !rows.length)
            return;

        for (var i = 0, len = rows.length; i < len; ++i)
        {
            var row = rows[i];

            if (!row.__hidden && !row.__hidden)
                this.nRowsVisible -= 1;

            row.__deleted = true;
        }

        this.updateTabName();

        if (this.rowOffset >= this.nRowsVisible)
            this.previousPage();
        else
        {
            this.refreshRows();
            this.updateNav();
            this.updateBrowseSession();
        }
    },

    setData: function(data)
    {
        this.data = data;
        this.indexCreated = false;

        this.resetCheckedRows();
        this.resetRowVisibility();

        if (this.tabs)
        {
            this.pounded = (this.tabs.poundedTab == this.tabIndex);
            if (this.pounded)
                this.readPound();
        }
        else
            this.readPound();

        this.applySort();
        this.updateSortArrow();

        if (this.customFilter != null)
            this.updateFilters();

        this.updateNav();
        this.refreshRows();
        this.updateBrowseSession();
    },

    getClipDiv: function()
    {
        return this.clipDiv;
    },

    getNoteTopDiv: function()
    {
        return this.noteTop;
    },

    focusSearch: function()
    {
        this.quickSearchBox.focus();
    },

    clearSearch: function()
    {
        this.quickSearchBox.value = '';
    },

    getList: function()
    {
        if (!this.debug)
            return;

        var str = '';
        for (var i = 0; i < this.data.length; i++)
            if (!this.data[i].__hidden)
                str += this.data[i].id + ', ';

        listviewIdList.show(str);
    },

    createIndicator: function(v, f, c)
    {
        if (!this.noteIndicators)
        {
            this.noteIndicators = $WH.ce('div');
            this.noteIndicators.className = 'listview-indicators';
            $(this.noteIndicators).insertBefore($(this.noteTop));
        }

        var t = this.tabClick;
        $(this.noteIndicators)
            .append(
                $('<span class="indicator"></span>')
                .html(v)
                .append(!f ? '' :
                    $('<a class="indicator-x" style="outline: none">[x]</a>')
                    .attr('href', (typeof f == 'function' ? 'javascript:;' : f))
                    .click(function () { if (t) t(); if (typeof f == 'function') f(); })
                )
                .css('cursor', (typeof c == 'function' ? 'pointer' : null))
                .click(function () { if (t) t(); if (typeof c == 'function') c(); })
            );

        $(this.noteTop).css('padding-top', '7px');
    },

    removeIndicators: function()
    {
        if (this.noteIndicators)
        {
            $(this.noteIndicators).remove();
            this.noteIndicators = null;
        }

        $(this.noteTop).css('padding-top', '');
    }
};

Listview.sortRows = function(a, b)
{
    var
        sort = Listview.sort,
        cols = Listview.columns;

    for (var i = 0, len = sort.length; i < len; ++i)
    {
        var
            res,
            _ = cols[Math.abs(sort[i]) - 1];

        if (!_)
            _ = this.template;

        if (_.sortFunc)
            res = _.sortFunc(a, b, sort[i]);
        else
            res = $WH.strcmp(a[_.value], b[_.value]);

        if (res != 0)
            return res * sort[i];
    }

    return 0;
},

Listview.sortIndexedRows = function(a, b)
{
    var
        sort = Listview.sort,
        cols = Listview.sortOptions || Listview.columns,
        res;

    for (var idx in sort)
    {
        _ = cols[Math.abs(sort[idx]) - 1];

        if (!_)
            _ = this.template;

        if (_.sortFunc)
            res = _.sortFunc(a, b, sort[0]);
        else
            res = $WH.strcmp(a[_.value], b[_.value]);

        if (res != 0)
            return res * sort[idx];
    }

    return (a.__si - b.__si);
},

Listview.cbSelect = function(v)
{
    for (var i = 0, len = this.data.length; i < len; ++i)
    {
        var _ = this.data[i];
        var v2 = v;

        if (_.__hidden)
            continue;

        if (!_.__nochk && _.__cb)
        {
            var cb = _.__cb,
                tr = cb.parentNode.parentNode;

            if (v2 == null)
                v2 = !cb.checked;

            if (cb.checked != v2)
            {
                cb.checked = v2;
                tr.className = (cb.checked ? tr.className + ' checked' : tr.className.replace('checked', ''));
            }
        }
        else if (v2 == null)
            v2 = true;

        _.__chk = v2;
    }
};

Listview.cbClick = function(e)
{
    setTimeout(Listview.cbUpdate.bind(0, 0, this, this.parentNode.parentNode), 1);
    $WH.sp(e);
};

Listview.cbCellClick = function(e)
{
    setTimeout(Listview.cbUpdate.bind(0, 1, this.firstChild, this.parentNode), 1);
    $WH.sp(e);
};

Listview.cbUpdate = function(toggle, cb, tr)
{
    if (toggle)
        cb.checked = !cb.checked;

    tr.className = (cb.checked ? tr.className + ' checked' : tr.className.replace('checked', ''));
};

Listview.headerClick = function(col, i, e)
{
    e = $WH.$E(e);

    if (this.tabClick)
        this.tabClick();

    if (e._button == 3 || e.shiftKey || e.ctrlKey)
    {
        $WH.Tooltip.hide();
        setTimeout(Listview.headerFilter.bind(this, col, null), 1);
    }
    else
    {
        this.sortBy(i + 1);
    }

    return false;
};

Listview.headerFilter = function(col, res)
{
    var prefilled = '';
    if (col.__filter)
    {
        if (col.__filter.invert)
            prefilled += '!';

        prefilled += col.__filter.text;
    }

    if (res == null)
        var res = prompt($WH.sprintf(LANG.prompt_colfilter1 + (col.type == 'text' ? LANG.prompt_colfilter2 : LANG.prompt_colfilter3), col.name), prefilled);

    if (res != null)
    {
        var filter = {text: '', type: -1};

        res = $WH.trim(res.replace(/\s+/g, ' '));

        if (!res && this.onEmptyFilter)
            this.onEmptyFilter(col);
        else if (res)
        {
            if (res.charAt(0) == '!' || res.charAt(0) == '-')
            {
                filter.invert = 1;
                res = res.substr(1);
            }

            if (col.type == 'text')
            {
                filter.type = 0;
                filter.text = res;

                if (filter.invert)
                    filter.regex = g_createOrRegex(res);
                else
                    filter.words = res.toLowerCase().split(' ');
            }
            var value,
                value2;

            if (res.match(/(>|=|<|>=|<=)\s*([0-9\.]+)/))
            {
                value = parseFloat(RegExp.$2);
                if (!isNaN(value))
                {
                    switch (RegExp.$1)
                    {
                        case '>':
                            filter.type = 1;
                            break;

                        case '=':
                            filter.type = 2;
                            break;

                        case '<':
                            filter.type = 3;
                            break;

                        case '>=':
                            filter.type = 4;
                            break;

                        case '<=':
                            filter.type = 5;
                            break;
                    }
                    filter.value = value;
                    filter.text  = RegExp.$1 + ' ' + value;
                }
            }
            else if (res.match(/([0-9\.]+)\s*\-\s*([0-9\.]+)/))
            {
                value  = parseFloat(RegExp.$1);
                value2 = parseFloat(RegExp.$2);
                if (!isNaN(value) && !isNaN(value2))
                {
                    if (value > value2)
                    {
                        var foo = value;
                        value   = value2;
                        value2  = foo;
                    }

                    if (value == value2)
                    {
                        filter.type  = 2;
                        filter.value = value;
                        filter.text  = '= ' + value;
                    }
                    else
                    {
                        filter.type   = 6;
                        filter.value  = value;
                        filter.value2 = value2;
                        filter.text   = value + ' - ' + value2;
                    }
                }
            }
            else
            {
                var parts = res.toLowerCase().split(' ');
                if (!col.allText && parts.length == 1 && !isNaN(value = parseFloat(parts[0])))
                {
                    filter.type = 2;
                    filter.value = value;
                    filter.text = '= ' + value;
                }
                else if (col.type == 'text')
                {
                    filter.type = 0;
                    filter.text = res;

                    if (filter.invert)
                        filter.regex = g_createOrRegex(res);
                    else
                        filter.words = parts;
                }
            }

            if (filter.type == -1)
            {
                alert(LANG.message_invalidfilter);
                return;
            }
        }

        if (!col.__filter || filter.text != col.__filter.text || filter.invert != col.__filter.invert)
        {
            var a = col.__th.firstChild.firstChild;

            if (res && filter.text)
            {
                if (!col.__filter)
                {
                    a.className = 'q5';
                    ++(this.nFilters);
                }

                col.__filter = filter;
            }
            else
            {
                if (col.__filter)
                {
                    a.className = '';
                    --(this.nFilters);
                }

                col.__filter = null;
            }

            this.updateFilters(1);
        }
    }
};

Listview.headerOver = function(a, col, e)
{
    var buffer = '';

    buffer += '<b class="q1">' + (col.tooltip ? col.tooltip : col.name) + '</b>';

    if (col.__filter)
        buffer += '<br />' + $WH.sprintf((col.__filter.invert ? LANG.tooltip_colfilter2 : LANG.tooltip_colfilter1), col.__filter.text);

    buffer += '<br /><span class="q2">' + LANG.tooltip_lvheader1 + '</span>';
    if (this.filtrable && (col.filtrable == null || col.filtrable))
        buffer += '<br /><span class="q2">' + LANG.tooltip_lvheader2 + '</span>';

    $WH.Tooltip.show(a, buffer, 0, 0, 'q');
};

Listview.extraCols = {
    id: {
        id: 'id',
        name: 'ID',
        width: '5%',
        value: 'id',
        compute: function(data, td)
        {
            if (data.id)
                $WH.ae(td, $WH.ct(data.id));
        }
    },

    date: {
        id: 'obj-date',
        name: LANG.added,
        compute: function(data, td)
        {
            if (data.date)
            {
                if (data.date <= 86400)
                    $WH.ae(td, $WH.ct('???'));
                else
                {
                    var added   = new Date(data.date * 1000);
                    var elapsed = (g_serverTime - added) / 1000;

                    return g_formatDate(td, elapsed, added, null, true);
                }
            }
        },
        sortFunc: function(a, b, col)
        {
            if (a.date == b.date)
                return 0;
            else if (a.date < b.date)
                return -1;
            else
                return 1;
        }
    },

    cost: {
        id: 'cost',
        name: LANG.cost,
        getValue: function(row)
        {
            if (row.cost)
                return (row.cost[2] && row.cost[2][0] ? row.cost[2][0][1] : 0) || (row.cost[1] && row.cost[1][0] ? row.cost[1][0][1] : 0) || row.cost[0];
        },
        compute: function(row, td)
        {
            if (row.cost)
            {
                var money = row.cost[0];
                var side = null;
                var items = row.cost[2];
                var currency = row.cost[1];

                if (row.side != null)
                    side = row.side;
                else if (row.react != null)
                {
                    if (row.react[0] == 1 && row.react[1] == -1) // Alliance only
                        side = 1;
                    else if (row.react[0] == -1 && row.react[1] == 1) // Horde only
                        side = 2;
                }

                Listview.funcBox.appendMoney(td, money, side, items, currency);
            }
        },
        sortFunc: function(a, b, col)
        {
            if (a.cost == null)
                return -1;
            else if (b.cost == null)
                return 1;

            var lena = 0,
                lenb = 0,
                lenc = 0,
                lend = 0;

            if (a.cost[2] != null)
                $WH.array_walk(a.cost[2], function(x, _, __, i) { lena += Math.pow(10, i) + x[1]; });
            if (b.cost[2] != null)
                $WH.array_walk(b.cost[2], function(x, _, __, i) { lenb += Math.pow(10, i) + x[1]; });
            if (a.cost[1] != null)
                $WH.array_walk(a.cost[1], function(x, _, __, i) { lenc += Math.pow(10, i) + x[1]; });
            if (b.cost[1] != null)
                $WH.array_walk(b.cost[1], function(x, _, __, i) { lend += Math.pow(10, i) + x[1]; });

            return $WH.strcmp(lena, lenb) || $WH.strcmp(lenc, lend) || $WH.strcmp(a.cost[0], b.cost[0]);
        }
    },

    count: {
        id: 'count',
        name: LANG.count,
        value: 'count',
        compute: function(row, td)
        {
            if (!(this._totalCount > 0 || row.outof > 0))
                return;

            if (row.outof)
            {
                var d = $WH.ce('div');
                d.className = 'small q0';
                $WH.ae(d, $WH.ct($WH.sprintf(LANG.lvdrop_outof, row.outof)));
                $WH.ae(td, d);
            }
            return row.count;
        },
        getVisibleText: function(row)
        {
            var buff = row.count;
            if (row.outof)
                buff += ' ' + row.outof;

            return buff;
        },
        sortFunc: function(a, b, col)
        {
            if (a.count == null)
                return -1;
            else if (b.count == null)
                return 1;

            return $WH.strcmp(a.count, b.count);
        }
    },

    percent: {
        id: 'percent',
        name: '%',
        value: 'percent',
        compute: function(row, td)
        {
            if (row.count <= 0)
                return '??';

            if (row.pctstack)
            {
                var text = '';
                var data = eval('(' + row.pctstack + ')');

                for (var amt in data)
                {
                    var pct = (data[amt] * row.percent) / 100;

                    if (pct >= 1.95)
                        pct = parseFloat(pct.toFixed(0));
                    else if (pct >= 0.195)
                        pct = parseFloat(pct.toFixed(1));
                    else
                        pct = parseFloat(pct.toFixed(2));

                    text += $WH.sprintf(LANG.stackof_format, amt, pct) + '<br />';
                }

                td.className += ' tip';
                g_addTooltip(td, text);
            }

         // var value = parseFloat(row.percent.toFixed(row.percent >= 1.95 ? 0 : (row.percent >= 0.195 ? 1 : 2)));
            var value = parseFloat(row.percent.toFixed(row.percent >= 1.95 ? 1 : 2)); // aowow: doesn't look as nice but i prefer accuracy

            if (row.pctstack)
                $(td).append($('<span>').addClass('tip').text(value));
            else
                return value;
        },
        getVisibleText: function(row)
        {
            if (row.count <= 0)
                return '??';

            if (row.percent >= 1.95)
                return row.percent.toFixed(1);
         //     return row.percent.toFixed(0);
         // else if (row.percent >= 0.195)
         //     return parseFloat(row.percent.toFixed(1));
            else
                return parseFloat(row.percent.toFixed(2));
        },
        sortFunc: function(a, b, col)
        {
            if (a.count == null)
                return -1;
            else if (b.count == null)
                return 1;

            if (a.percent >= 1.95)
                var acmp = a.percent.toFixed(1);
         //     var acmp = a.percent.toFixed(0);
         // else if (a.percent >= 0.195)
         //     acmp = parseFloat(a.percent.toFixed(1));
            else
                acmp = parseFloat(a.percent.toFixed(2));

            if (b.percent >= 1.95)
                var bcmp = b.percent.toFixed(1);
         //     var bcmp = b.percent.toFixed(0);
         // else if (b.percent >= 0.195)
         //     bcmp = parseFloat(b.percent.toFixed(1));
            else
                bcmp = parseFloat(b.percent.toFixed(2));

            return $WH.strcmp(acmp, bcmp);
        }
    },

    stock: {
        id: 'stock',
        name: LANG.stock,
        width: '10%',
        value: 'stock',
        compute: function(row, td)
        {
            if (row.stock > 0)
                return row.stock;
            else
            {
                td.style.fontFamily = 'Verdana, sans-serif';
                return String.fromCharCode(8734);
            }
        },
        getVisibleText: function(row)
        {
            if (row.stock > 0)
                return row.stock;
            else
                return String.fromCharCode(8734) + ' infinity';
        }
    },

    currency: {
        id: 'currency',
        name: LANG.currency,
        getValue: function(row)
        {
            if (row.currency)
                return (row.currency[0] ? row.currency[0][1] : 0);
        },
        compute: function(row, td)
        {
            if (row.currency)
            {
                var side = null;
                if (row.side != null)
                    side = row.side;
                else if (row.react != null)
                {
                    if (row.react[0] == 1 && row.react[1] == -1) // Alliance only
                        side = 1;
                    else if (row.react[0] == -1 && row.react[1] == 1) // Horde only
                        side = 2;
                }

                Listview.funcBox.appendMoney(td, null, side, null, row.currency);
            }
        },
        sortFunc: function(a, b, col)
        {
            if (a.currency == null)
                return -1;
            else if (b.currency == null)
                return 1;

            var lena = 0,
                lenb = 0;

            $WH.array_walk(a.currency, function(x, _, __, i) { lena += Math.pow(10, i) + x[1]; });
            $WH.array_walk(b.currency, function(x, _, __, i) { lenb += Math.pow(10, i) + x[1]; });

            return $WH.strcmp(lena, lenb);
        }
    },

    mode: {
        id: 'mode',
        name: 'Mode',
        after: 'name',
        type: 'text',
        compute: function(row, td)
        {
            if (row.modes && row.modes.mode)
            {
                if ((row.modes.mode & 120) == 120 || (row.modes.mode & 3) == 3)
                    return LANG.pr_note_all;

                return Listview.extraCols.mode.getVisibleText(row);
            }
        },
        getVisibleText: function(row)
        {
            // TODO: Remove magic numbers.
            var modeNormal = !!(row.modes.mode & 26);
            var modeHeroic = !!(row.modes.mode & 97);
            var player10   = !!(row.modes.mode & 40);
            var player25   = !!(row.modes.mode & 80);

            var specificPlayers;
            if (player10 && !player25)
                specificPlayers = 10;
            else if (player25 && !player10)
                specificPlayers = 25;

            var specificMode;
            if (modeNormal && !modeHeroic)
                specificMode = 'normal';
            else if (modeHeroic && !modeNormal)
                specificMode = 'heroic';

            if (specificMode)
            {
                if (specificPlayers)
                    return $WH.sprintf(LANG['tab_' + specificMode + 'X'], specificPlayers); // e.g. "Heroic 25"
                else
                    return LANG['tab_' + specificMode]; // e.g. "Heroic"
            }

            if (specificPlayers)
                return $WH.sprintf(LANG.lvzone_xman, specificPlayers); // e.g. "25-player"

            return LANG.pr_note_all;
        },
        sortFunc: function(a, b, col)
        {
            if (a.modes && b.modes)
                return -$WH.strcmp(a.modes.mode, b.modes.mode);
        }
    },

    requires:
    {
        id: 'requires',
        name: LANG.requires,
        type: 'text',
        compute: function(item, td)
        {
            if (item.achievement && g_achievements[item.achievement])
            {
                $WH.nw(td);
                td.className = 'small';
                td.style.lineHeight = '18px';

                var a = $WH.ce('a');
                a.href = '?achievement=' + item.achievement;
                a.className = 'icontiny tinyspecial';
                a.style.backgroundImage = 'url(' + g_staticUrl + '/images/wow/icons/tiny/' + g_achievements[item.achievement].icon.toLowerCase() + '.gif)';
                a.style.whiteSpace = 'nowrap';

                $WH.st(a, g_achievements[item.achievement]['name_' + Locale.getName()]);
                $WH.ae(td, a);
            }
        },
        getVisibleText: function(item)
        {
            if (item.achievement && g_achievements[item.achievement])
                return g_achievements[item.achievement].name;
        },
        sortFunc: function(a, b, col)
        {
            return $WH.strcmp(this.getVisibleText(a), this.getVisibleText(b));
        }
    },

    reqskill:
    {
        id: 'reqskill',
        name: LANG.skill,
        width: '10%',
        value: 'reqskill',
        before: 'yield'
    },

    yield:
    {
        id: 'yield',
        name: LANG.yields,
        type: 'text',
        align: 'left',
        span: 2,
        value: 'name',
        compute: function(row, td, tr)
        {
            if (row.yield && g_items[row.yield])
            {
                var i = $WH.ce('td');
                i.style.width = '1px';
                i.style.padding = '0';
                i.style.borderRight = 'none';

                $WH.ae(i, g_items.createIcon(row.yield, 1));
                $WH.ae(tr, i);
                td.style.borderLeft = 'none';

                var wrapper = $WH.ce('div');

                var a = $WH.ce('a');
                a.style.fontFamily = 'Verdana, sans-serif';
                a.href = '?item=' + row.yield;
                a.className = 'q' + g_items[row.yield].quality;
                $WH.ae(a, $WH.ct(g_items[row.yield]['name_' + Locale.getName()]));
                $WH.ae(wrapper, a);
                $WH.ae(td, wrapper);
            }
        },
        getVisibleText: function(row)
        {
            if (row.yield && g_items[row.yield])
                return g_items[row.yield]['name_' + Locale.getName()];
        },
        sortFunc: function(a, b, col)
        {
            if (!a.yield || !g_items[a.yield] || !b.yield || !g_items[b.yield])
                return (a.yield && g_items[a.yield] ? 1 : (b.yield && g_items[b.yield] ? -1 : 0));

            return -$WH.strcmp(g_items[a.yield].quality, g_items[b.yield].quality) ||
                    $WH.strcmp(g_items[a.yield]['name_' + Locale.getName()], g_items[b.yield]['name_' + Locale.getName()]);
        }
    },

    condition:
    {
        id: 'condition',
        name: LANG.tab_conditions,
        type: 'text',
        compute: function(row, td)
        {
            if (!row.condition)
                return;

            td.className = 'small';
            td.style.lineHeight = '18px';
            td.style.textAlign  = 'left';
            td.style.whiteSpace = 'nowrap';

            // tiny links are hard to hit, hmkey?
            td.onclick = (e) => $WH.sp(e);

            var mText = ConditionList.createCell(row.condition);
            Markup.printHtml(mText, td);


        },
        getVisibleText: function(row)
        {
            var buff = '',
                mText;

            if (!row.condition)
                return buff;

            mText = ConditionList.createCell(row.condition);

            return Markup.removeTags(mText);
        },
        sortFunc: function(a, b, col)
        {
            var text1 = this.getVisibleText(a),
                text2 = this.getVisibleText(b);

            if (text1 != '' && text2 == '')
                return -1;

            if (text2 != '' && text1 == '')
                return 1;

            return $WH.strcmp(text1, text2);
        }
    }
};

Listview.funcBox = {
    createSimpleCol: function(i, n, w, v)
    {
        return {
            id:    i,
            name:  (LANG[n] !== undefined ? LANG[n] : n),
            width: w,
            value: v
        };
    },

    initLootTable: function(row)
    {
        var divider;

        if (this._totalCount != null)
            divider = this._totalCount;
        else
            divider = row.outof;

        if (divider == 0)
        {
            if (row.count != -1)
                row.percent = row.count;
            else
                row.percent = 0;
        }
        else
        {
            row.percent = row.count / divider * 100;
        }

        (Listview.funcBox.initModeFilter.bind(this, row))();
    },

// // subtabs here //

    initModeFilter: function(row)
    {
        if (this._lootModes == null)
            this._lootModes = { 99: 0 };

        if (this._distinctModes == null)
            this._distinctModes = { 99: 0 };

        if ((!row.modes || row.modes.mode == 4) && row.classs != 12 && row.commondrop)
        {
            this._lootModes[99]++; // Trash
            this._distinctModes[99]++;
        }
        else if (row.modes)
        {
            for (var i = -2; i <= 5; ++i)
            {
                if (this._lootModes[i] == null)
                    this._lootModes[i] = 0;

                if (row.modes.mode & 1 << parseInt(i) + 2)
                    this._lootModes[i]++;
            }

            if (this._distinctModes[row.modes.mode] == null)
                this._distinctModes[row.modes.mode] = 0;
            this._distinctModes[row.modes.mode]++;
        }
    },

    addModeIndicator: function()
    {
        var nModes = 0;
        for (var i in this._distinctModes)
        {
            if (this._distinctModes[i])
                nModes++;
        }

        if (nModes < 2)
            return;

        var    pm      = location.hash.match(/:mode=([^:]+)/),
            order   = [0,-1,-2,1,3,2,4,5,99],
            langref = {
                "-2": LANG.tab_heroic,
                "-1": LANG.tab_normal,
                   0: LANG.tab_noteworthy,
                   1: $WH.sprintf(LANG.tab_normalX, 10),
                   2: $WH.sprintf(LANG.tab_normalX, 25),
                   3: $WH.sprintf(LANG.tab_heroicX, 10),
                   4: $WH.sprintf(LANG.tab_heroicX, 25),
                   5: LANG.tab_raidfinder,
                  99: '' // No indicator for trash
            };

        var f = function(mode, dm, updatePound)
        {
            g_setSelectedLink(this, 'lootmode');

            lv.customPound  = lv.id + (dm != null ? ':mode=' + g_urlize(langref[dm].replace(' ', '')) : '');
            lv.customFilter = function(item) { return Listview.funcBox.filterMode(item, lv._totalCount, mode) };

            lv.updateFilters(1);
            lv.applySort();
            lv.refreshRows();
            if (updatePound)
                lv.updatePound(1);
        };

        var lv = this,
            modes = [],
            a;

        a = $('<a><span>' + LANG.pr_note_all + '</span></a>');
        a[0].f = f.bind(a[0], null, null, 1);
        a.click(a[0].f);
        var firstCallback = f.bind(a[0], null, null, 0);
        firstCallback();

        modes.push($('<span class="indicator-mode"></span>').append(a).append($('<b>' + LANG.pr_note_all + '</b>')));

        for (var j = 0, len = order.length; j < len; ++j)
        {
            var i = order[j];
            if (!this._lootModes[i])
                continue;

            a = $('<a><span>' + langref[i] + '</span> (' + this._lootModes[i] + ')</a>');
            a[0].f = f.bind(a[0], 1 << i + 2, i, 1);
            a.click(a[0].f);

            if (i == 0)
                firstCallback = f.bind(a[0], 1 << i + 2, i, 0);

            if ((i < -1 || i > 2) && i != 5)
                a.addClass('icon-heroic');

            modes.push($('<span class="indicator-mode"></span>').append(a).append($('<b' + (i < -1 || i > 2 ? ' class="icon-heroic"' : '') + '>' + langref[i] + ' (' + this._lootModes[i] + ')</b>'))); // jQuery is dumb

            if (pm && pm[1] == g_urlize(langref[i].replace(' ', '')))
                (a[0].f)();
        }

        var showNoteworthy = false;
        for (var i = 0, len = modes.length; i < len; ++i)
        {
            a = $('a', modes[i]);
            if (!$('span', a).html() && modes.length == 3)
                showNoteworthy = true;
            else
                this.createIndicator(modes[i], null, a[0].f);
        }

        if (showNoteworthy)
            firstCallback();

        $(this.noteTop).append($('<div class="clear"></div>'));
    },

    filterMode: function(row, total, mode)
    {
        if (total != null && row.count != null)
        {
            if (row._count == null)
                row._count = row.count;

            var count = row._count;

            if (mode != null && row.modes[mode])
            {
                count = row.modes[mode].count;
                total = row.modes[mode].outof;
            }

            row.__tr    = null;
            row.count   = count;
            row.outof   = total;
            if (total)
                row.percent = count / total * 100;
            else
                row.percent = count;
        }

        return (mode != null ? ((!row.modes || row.modes.mode == 4) && row.classs != 12 && row.commondrop ? (mode == 32) : (row.modes && (row.modes.mode & mode))) : true);
    },

    initSubclassFilter: function(row)
    {
        var i = row.classs || 0;
        if (this._itemClasses == null)
            this._itemClasses = {};
        if (this._itemClasses[i] == null)
            this._itemClasses[i] = 0;
        this._itemClasses[i]++;
    },

    addSubclassIndicator: function()
    {
        var it = location.hash.match(/:type=([^:]+)/),
            order = [];

        for (var i in g_item_classes)
            order.push({ i: i, n: g_item_classes[i] });
        order.sort(function(a, b) { return $WH.strcmp(a.n, b.n) });

        var f = function(itemClass, updatePound)
        {
            g_setSelectedLink(this, 'itemclass');

            lv.customPound  = lv.id + (itemClass != null ? ':type=' + itemClass : '');
            lv.customFilter = function(item) { return itemClass == null || itemClass == item.classs };

            lv.updateFilters(1);
            lv.applySort();
            lv.refreshRows();
            if (updatePound)
                lv.updatePound(1);
        }

        var lv = this,
            classes = [],
            a;

        a = $('<a><span>' + LANG.pr_note_all + '</span></a>');
        a[0].f = f.bind(a[0], null, 1);
        a.click(a[0].f);
        var firstCallback = f.bind(a[0], null, 0);
        firstCallback();

        classes.push($('<span class="indicator-mode"></span>').append(a).append($('<b>' + LANG.pr_note_all + '</b>')));

        for (var j = 0, len = order.length; j < len; ++j)
        {
            var i = order[j].i;
            if (!this._itemClasses[i])
                continue;

            a = $('<a><span>' + g_item_classes[i] + '</span> (' + this._itemClasses[i] + ')</a>');
            a[0].f = f.bind(a[0], i, 1);
            a.click(a[0].f);

            classes.push($('<span class="indicator-mode"></span>').append(a).append($('<b>' + g_item_classes[i] + ' (' + this._itemClasses[i] + ')</b>')));

            if (it && it[1] == g_urlize(i))
                (a[0].f)();
        }

        if (classes.length > 2)
        {
            for (var i = 0, len = classes.length; i < len; ++i)
                this.createIndicator(classes[i], null, $('a', classes[i])[0].f);

            $(this.noteTop).css('padding-bottom', '12px');
            $(this.noteIndicators).append($('<div class="clear"></div>')).insertAfter($(this.navTop));
        }
    },

    initSpellFilter: function (row)
    {
        if (this._spellTypes == null)
            this._spellTypes = {};

        if (this._spellTypes[row.cat] == null)
            this._spellTypes[row.cat] = 0;

        this._spellTypes[row.cat]++;
    },

    addSpellIndicator: function ()
    {
        var it = location.hash.match(/:type=([^:]+)/);
        var fn = function (spellCat, updatePound)
        {
            g_setSelectedLink(this, "spellType");

            lv.customPound = lv.id + (spellCat != null ? ":type=" + spellCat : "");
            lv.customFilter = function (spell) { return spellCat == null || spell.cat == spellCat; };
            lv.updateFilters(1);
            lv.applySort();
            lv.refreshRows();
            if (updatePound)
                lv.updatePound(1);
        };

        var lv = this,
            categories = [],
            a;

        a = $("<a><span>" + LANG.pr_note_all + "</span></a>");
        a[0].f = fn.bind(a[0], null, 1);
        a.click(a[0].f);
        var firstCallback = fn.bind(a[0], null, 0);
        firstCallback();
        categories.push($('<span class="indicator-mode"></span>').append(a).append($("<b>" + LANG.pr_note_all + "</b>")));

        for (var i in g_spell_categories)
        {
            if (!this._spellTypes[i])
                continue;

            a = $("<a><span>" + g_spell_categories[i] + "</span> (" + this._spellTypes[i] + ")</a>");
            a[0].f = fn.bind(a[0], i, 1);
            a.click(a[0].f);

            categories.push($('<span class="indicator-mode"></span>').append(a).append($("<b>" + g_spell_categories[i] + " (" + this._spellTypes[i] + ")</b>")));

            if (it && it[1] == i)
                (a[0].f)();
        }

        if (categories.length > 2)
        {
            for (var i = 0, len = categories.length; i < len; ++i)
                this.createIndicator(categories[i], null, $("a", categories[i])[0].f);

            $(this.noteTop).css("padding-bottom", "12px");
            $(this.noteIndicators).append($('<div class="clear"></div>')).insertAfter($(this.navTop));
        }
    },

    initStatisticFilter: function(row)
    {
        if (this._achievTypes == null)
            this._achievTypes = {};
        if (this._achievTypes[row.type] == null)
            this._achievTypes[row.type] = 0;
        this._achievTypes[row.type]++;
    },

    addStatisticIndicator: function()
    {
        var it = location.hash.match(/:type=([^:]+)/),
            order = [];

        for (var i in g_achievement_types)
            order.push({ i: i, n: g_achievement_types[i] });
        order.sort(function(a, b) { return $WH.strcmp(a.n, b.n) });

        var f = function(achievType, updatePound)
        {
            g_setSelectedLink(this, 'achievType');

            lv.customPound  = lv.id + (achievType != null ? ':type=' + achievType : '');
            lv.customFilter = function(achievement) { return achievType == null || achievType == achievement.type };

            lv.updateFilters(1);
            lv.applySort();
            lv.refreshRows();
            if (updatePound)
                lv.updatePound(1);
        };

        var lv = this,
            types = [],
            a;

        a = $('<a><span>' + LANG.pr_note_all + '</span></a>');
        a[0].f = f.bind(a[0], null, 1);
        a.click(a[0].f);
        var firstCallback = f.bind(a[0], null, 0);
        firstCallback();

        types.push($('<span class="indicator-mode"></span>').append(a).append($('<b>' + LANG.pr_note_all + '</b>')));

        for (var j = 0, len = order.length; j < len; ++j)
        {
            var i = order[j].i;
            if (!this._achievTypes[i])
                continue;

            a = $('<a><span>' + g_achievement_types[i] + '</span> (' + this._achievTypes[i] + ')</a>');
            a[0].f = f.bind(a[0], i, 1);
            a.click(a[0].f);

            types.push($('<span class="indicator-mode"></span>').append(a).append($('<b>' + g_achievement_types[i] + ' (' + this._achievTypes[i] + ')</b>')));

            if (it && it[1] == i)
                (a[0].f)();
        }

        if (types.length > 2)
        {
            for (var i = 0, len = types.length; i < len; ++i)
                this.createIndicator(types[i], null, $('a', types[i])[0].f);

            $(this.noteTop).append($('<div class="clear"></div>'));
        }
    },

    initQuestFilter: function(row)
    {
        if (this._questTypes == null)
            this._questTypes = {};

        for (var i = 1; i <= 4; ++i)
        {
            if (this._questTypes[i] == null)
                this._questTypes[i] = 0;

            if (row._type && (row._type & 1 << i - 1))
                this._questTypes[i]++;
        }
    },

    addQuestIndicator: function()
    {
        var it = location.hash.match(/:type=([^:]+)/);

        var f = function(questType, updatePound)
        {
            g_setSelectedLink(this, 'questType');

            lv.customPound  = lv.id + (questType != null ? ':type=' + questType : '');
            lv.customFilter = function(quest) { return questType == null || (quest._type & 1 << questType - 1) };

            lv.updateFilters(1);
            lv.applySort();
            lv.refreshRows();
            if (updatePound)
                lv.updatePound(1);
        };

        var lv = this,
            types = [],
            a;

        a = $('<a><span>' + LANG.pr_note_all + '</span></a>');
        a[0].f = f.bind(a[0], null, 1);
        a.click(a[0].f);
        var firstCallback = f.bind(a[0], null, 0);
        firstCallback();

        types.push($('<span class="indicator-mode"></span>').append(a).append($('<b>' + LANG.pr_note_all + '</b>')));

        for (var i = 1; i <= 4; ++i)
        {
            if (!this._questTypes[i])
                continue;

            a = $('<a><span>' + g_quest_indicators[i] + '</span> (' + this._questTypes[i] + ')</a>');
            a[0].f = f.bind(a[0], i, 1);
            a.click(a[0].f);

            types.push($('<span class="indicator-mode"></span>').append(a).append($('<b>' + g_quest_indicators[i] + ' (' + this._questTypes[i] + ')</b>')));

            if (it && it[1] == i)
                (a[0].f)();
        }

        if (types.length > 2)
        {
            for (var i = 0, len = types.length; i < len; ++i)
                this.createIndicator(types[i], null, $('a', types[i])[0].f);

            $(this.noteTop).css('padding-bottom', '12px');
            $(this.noteIndicators).append($('<div class="clear"></div>')).insertAfter($(this.navTop));
        }
    },

// \\ subtabs here \\

    assocArrCmp: function(a, b, arr)
    {
        if (a == null)
            return -1;
        else if (b == null)
            return 1;

        var n = Math.max(a.length, b.length);
        for (var i = 0; i < n; ++i)
        {
            if (a[i] == null)
                return -1;
            else if (b[i] == null)
                return 1;

            var res = $WH.strcmp(arr[a[i]], arr[b[i]]);
            if (res != 0)
                return res;
        }

        return 0
    },

    assocBinFlags: function(f, arr)
    {
        var res = [];
        for (var i in arr)
        {
            if (!isNaN(i) && (f & 1 << i - 1))
                res.push(i);
        }
        res.sort(function(a, b) { return $WH.strcmp(arr[a], arr[b]); });

        return res;
    },

    location: function(row, td)
    {
        if (row.location == null)
            return -1;

        for (var i = 0, len = row.location.length; i < len; ++i)
        {
            if (i > 0)
                $WH.ae(td, $WH.ct(LANG.comma));

            var zoneId = row.location[i];
            if (zoneId == -1)
                $WH.ae(td, $WH.ct(LANG.ellipsis));
            else
            {
                var a = $WH.ce('a');
                a.className = 'q1';
                a.href = '?zone=' + zoneId;
                $WH.ae(a, $WH.ct(g_zones[zoneId]));
                $WH.ae(td, a);
            }
        }
    },

    arrayText: function(arr, lookup)
    {
        if (arr == null)
            return;
        else if (!$WH.is_array(arr))
            return lookup[arr];

        var buff = '';
        for (var i = 0, len = arr.length; i < len; ++i)
        {
            if (i > 0)
                buff += ' ';

            if (!lookup[arr[i]])
                continue;

            buff += lookup[arr[i]];
        }

        return buff;
    },

    createCenteredIcons: function(arr, td, text, type)
    {
        if (arr != null)
        {
            var d  = $WH.ce('div'),
                d2 = $WH.ce('div');

            $WH.ae(document.body, d);

            if (text && (arr.length != 1 || type != 2))
            {
                var bibi = $WH.ce('div');
                bibi.style.position = 'relative';
                bibi.style.width = '1px';
                var bibi2 = $WH.ce('div');
                bibi2.className = 'q0';
                bibi2.style.position = 'absolute';
                bibi2.style.right = '2px';
                bibi2.style.lineHeight = '26px';
                bibi2.style.fontSize = '11px';
                bibi2.style.whiteSpace = 'nowrap';
                $WH.ae(bibi2, $WH.ct(text));
                $WH.ae(bibi, bibi2);
                $WH.ae(d, bibi);

                d.style.paddingLeft = bibi2.offsetWidth + 'px';
            }

            var iconPool = g_items;
            if (type == 1)
                iconPool = g_spells;

            for (var i = 0, len = arr.length; i < len; ++i)
            {
                var icon;
                if (arr[i] == null)
                {
                    icon = $WH.ce('div');
                    icon.style.width = icon.style.height = '26px';
                }
                else
                {
                    var id,
                        num;

                    if (typeof arr[i] == 'object')
                    {
                        id  = arr[i][0];
                        num = arr[i][1];
                    }
                    else
                        id = arr[i];

                    if (id)
                        icon = iconPool.createIcon(id, 0, num);
                    else
                        icon = Icon.create('inventoryslot_empty', 0, null, 'javascript:;');
                }

                if (arr.length == 1 && type == 2) // Tiny text display
                {
                    if (id && g_items[id])
                    {
                        $WH.ee(d);
                        var item = g_items[id],
                            a    = $WH.ce('a'),
                            sp   = $WH.ce('span');

                            sp.style.paddingTop = '4px';

                        a.href = '?item=' + id;
                        a.className = 'q' + item.quality + ' icontiny tinyspecial';
                        a.style.backgroundImage = 'url(' + g_staticUrl + '/images/wow/icons/tiny/' + item.icon.toLowerCase() + '.gif)';
                        a.style.whiteSpace = 'nowrap';

                        $WH.st(a, item['name_' + Locale.getName()]);
                        $WH.ae(sp, a);

                        if (num > 1)
                            $WH.ae(sp, $WH.ct(' (' + num + ')'));

                        if (text)
                        {
                            var bibi = $WH.ce('span');
                            bibi.className = 'q0';
                            bibi.style.fontSize = '11px';
                            bibi.style.whiteSpace = 'nowrap';
                            $WH.ae(bibi, $WH.ct(text));
                            $WH.ae(d, bibi);
                            if ($(bibi2).length > 0)
                                sp.style.paddingLeft = $(bibi2).width() + 'px';
                        }

                        $WH.ae(d, sp);
                    }
                }
                else
                {
                    icon.style.cssFloat = icon.style.styleFloat = 'left';
                    $WH.ae(d, icon);

                    d.style.margin = '0 auto';
                    d.style.textAlign = 'left';

                    d.style.width = (26 * arr.length) + 'px';
                }
            }

            d2.className = 'clear';

            $WH.ae(td, d);
            $WH.ae(td, d2);

            return true;
        }
    },

    createSocketedIcons: function(sockets, td, gems, match, text)
    {
        var nMatch = 0,
            d      = $WH.ce('div'),
            d2     = $WH.ce('div');

        for (var i = 0, len = sockets.length; i < len; ++i)
        {
            var
                icon,
                gemId = gems[i];

                if (g_items && g_items[gemId])
                    icon = g_items.createIcon(gemId, 0);
                else if ($WH.isset('g_gems') && g_gems && g_gems[gemId])
                    icon = Icon.create(g_gems[gemId].icon, 0, null, '?item=' + gemId);
                else
                    icon = Icon.create(null, 0, null, 'javascript:;');

            icon.className += ' iconsmall-socket-' + g_file_gems[sockets[i]] + (!gems || !gemId ? '-empty' : '');
            icon.style.cssFloat = icon.style.styleFloat = 'left';

            if (match && match[i])
            {
                icon.insertBefore($WH.ce('var'), icon.childNodes[1]);
                ++nMatch;
            }

            $WH.ae(d, icon);
        }

        d.style.margin = '0 auto';
        d.style.textAlign = 'left';

        d.style.width = (26 * sockets.length) + 'px';
        d2.className = 'clear';

        $WH.ae(td, d);
        $WH.ae(td, d2);

        if (text && nMatch == sockets.length)
        {
            d = $WH.ce('div');
            d.style.paddingTop = '4px';
            $WH.ae(d, $WH.ct(text));
            $WH.ae(td, d);
        }
    },

    getItemType: function(itemClass, itemSubclass, itemSubsubclass)
    {
        if (itemSubsubclass != null && g_item_subsubclasses[itemClass] != null && g_item_subsubclasses[itemClass][itemSubclass] != null)
            return {
                url: '?items=' + itemClass + '.' + itemSubclass + '.' + itemSubsubclass,
                text: g_item_subsubclasses[itemClass][itemSubclass][itemSubsubclass]
            };
        else if (itemSubclass != null && g_item_subclasses[itemClass] != null)
            return {
                url: '?items=' + itemClass + '.' + itemSubclass,
                text: g_item_subclasses[itemClass][itemSubclass]
            };
        else
            return {
                url: '?items=' + itemClass,
                text: g_item_classes[itemClass]
            };
    },

    getQuestCategory: function(category)
    {
        return g_quest_sorts[category];
    },

    getQuestReputation: function(faction, quest)
    {
        if (quest.reprewards)
            for (var i = 0, len = quest.reprewards.length; i < len; ++i)
                if (quest.reprewards[i][0] == faction)
                    return quest.reprewards[i][1];
    },

    getFactionCategory: function(category, category2)
    {
        if (category)
            return g_faction_categories[category];
        else
            return g_faction_categories[category2];
    },

    getEventNextDates: function(startDate, endDate, recurrence, fromWhen)
    {
        if (typeof startDate != 'string' || typeof endDate != 'string')
            return [null, null];

        startDate = new Date(startDate.replace(/-/g, '/'));
        endDate   = new Date(endDate.replace(/-/g, '/'));

        if (isNaN(startDate.getTime()) || isNaN(endDate.getTime()))
            return [null, null];

        if (fromWhen == null)
            fromWhen = g_serverTime;

        var offset = 0;
        if (recurrence == -1) // Once by month using day of the week of startDate
        {
            // 2009: Monday .. Monday
            // 2010: Sunday .. Saturday, first full week of the month, unless only Sunday is in the previous month

            var nextEvent = new Date(fromWhen.getFullYear(), fromWhen.getMonth(), 1, startDate.getHours(), startDate.getMinutes(), startDate.getSeconds()); // counts as next until it ends
            for (var i = 0; i < 2; ++i)
            {
                nextEvent.setDate(1);
                nextEvent.setMonth(nextEvent.getMonth() + i); // + 0 = try current month, else try next month
                var day = nextEvent.getDay();
                var tolerance = 1;
                if (nextEvent.getYear() == 2009)
                    tolerance = 0;
                if (day > tolerance)
                {
                    nextEvent.setDate(nextEvent.getDate() + (7 - day)); // first sunday
                }

                var eventEnd = new Date(nextEvent);
                eventEnd.setDate(eventEnd.getDate() + (7 - tolerance)); // 2010, length of 6. tolerance is 1 if it isnt 2009
                if (fromWhen.getTime() < eventEnd.getTime())
                    break; // event hasnt ended yet, so this is still the current one
            }

            offset = nextEvent.getTime() - startDate.getTime();
        }
        else if (recurrence > 0)
        {
            recurrence *= 1000; // sec -> ms

            offset = Math.ceil((fromWhen.getTime() - endDate.getTime()) / recurrence) * recurrence;
        }

        startDate.setTime(startDate.getTime() + offset);
        endDate.setTime(endDate.getTime() + offset);

        return [startDate, endDate];
    },

    createTextRange: function(min, max)
    {
        min |= 0;
        max |= 0;
        if (min > 1 || max > 1)
        {
            if (min != max && max > 0)
                return min + '-' + max;
            else
                return min + '';
        }

        return null;
    },

    coGetColor: function(comment, mode, blog)
    {
        if (comment.user && g_customColors[comment.user])
            return ' comment-' + g_customColors[comment.user];

        switch (mode)
        {
            case -1: // Edit forum post
                var foo = null;
                if (!blog)
                    foo = comment.divPost.childNodes[1].className.match(/comment-([a-z]+)/);
                else
                    foo = comment.divBody[0].className.match(/comment-([a-z]+)/);
                if (foo != null)
                    return ' comment-' + foo[1];
                break;

            case 3: // Post reply (forums)
                if (comment.roles & U_GROUP_PREMIUMISH)
                    return ' comment-gold';
            case 4: // Signature (account settings)
                if (comment.roles & U_GROUP_ADMIN)
                    return ' comment-blue';
                if (comment.roles & U_GROUP_GREEN_TEXT) // Mod, Bureau, Dev
                    return ' comment-green';
                if (comment.roles & U_GROUP_VIP) // VIP
                    return ' comment-gold';
                if (comment.roles & U_GROUP_PREMIUMISH) // Premium, Editor
                    return ' comment-gold';
                break;
        }

        if (comment.roles & U_GROUP_ADMIN)
            return ' comment-blue';
        else if ((comment.commentv2 && comment.sticky) || (comment.roles & U_GROUP_GREEN_TEXT) || (comment.rating >= 10))
            return ' comment-green';
        else if (comment.rating < -2)
            return ' comment-bt';
        else if (comment.roles & U_GROUP_PREMIUMISH)
            return ' comment-gold';

        return '';
    },

    coFlagUpOfDate: function(comment)
    {
        var notificationDiv = comment.commentCell.find('.comment-notification');
        var god = comment.user == g_user.name || ((g_user.roles & (U_GROUP_ADMIN|U_GROUP_MOD)) != 0);

        if (!god) {
            return;
        }

        if (confirm('Mark comment ' + comment.id + ' as up to date?')) {
            comment.container.removeClass('comment-outofdate');
            $.post('?comment=out-of-date', { id: comment.id, remove: 1 }, function(data) {
                    if (data == 'ok')
                    {
                        comment.outofdate = false;
                        Listview.templates.comment.updateCommentCell(comment);
                        MessageBox(notificationDiv, LANG.lvcomment_uptodateresponse);
                    }
                    else
                        MessageBox(notificationDiv, 'Error: ' + data);
                }
            );
        }

    },

    coFlagOutOfDate: function(comment)
    {
        var notificationDiv = comment.commentCell.find('.comment-notification');
        var god = comment.user == g_user.name || ((g_user.roles & (U_GROUP_ADMIN|U_GROUP_BUREAU)) != 0);

        // SpiffyJr, God users don't require a reason
        if (god) {
            if (confirm('Mark comment ' + comment.id + ' as out of date?')) {
                comment.container.addClass('comment-outofdate');
                $.post('?comment=out-of-date', { id: comment.id }, function(data) {
                        if (data == 'ok')
                        {
                            comment.outofdate = true;
                            Listview.templates.comment.updateCommentCell(comment);
                            MessageBox(notificationDiv, LANG.lvcomment_outofdateresponse);
                        }
                        else
                            MessageBox(notificationDiv, 'Error: ' + data);
                    }
                );
            }
            return;
        }

        var reason = null;

        while (true) {
            reason = prompt(LANG.lvcomment_outofdate_tip);

            // null value indicates that the confirm box was cancelled
            if (reason == null || reason == false) {
                return;
            } else if (reason.toString().length > 0) {
                break;
            }

            alert(LANG.youmustprovideareason_stc);
        }

        $.post('?comment=out-of-date', { id: comment.id, reason: reason }, function(data) {
                if (data == 'ok')
                {
                    comment.outofdate = true;
                    Listview.templates.comment.updateCommentCell(comment);
                    MessageBox(notificationDiv, LANG.lvcomment_outofdateresponsequeued);
                }
                else
                    MessageBox(notificationDiv, 'Error: ' + data);
            }
        );
    },

    coDelete: function(comment)
    {
        var god = comment.user == g_user.name || ((g_user.roles & (U_GROUP_ADMIN|U_GROUP_MOD|U_GROUP_BUREAU)) != 0);
        var notificationDiv = comment.commentCell.find('.comment-notification');

        if (god)
        {
            if (!confirm(LANG.confirm_deletecomment))
                return;
            $.post('?comment=delete', { id: comment.id });

            if (!comment.commentv2)
            {
                this.deleteRows([comment]);
                return;
            }

            comment.container.addClass('comment-deleted');
            MessageBox(notificationDiv, LANG.commentdeleted_tip);
            comment.deletedInfo = [g_serverTime, g_user.name];
            comment.deleted = true;
            Listview.templates.comment.updateCommentCell(comment);

            // aowow: custom start
            comment.voteCell.hide();
            comment.repliesCell.hide();
            comment.commentBody.hide();
            comment.repliesControl.hide();
            Listview.templates.comment.updateRepliesControl(comment);
            Listview.templates.comment.updateCommentControls(comment, comment.commentControls);
            if (comment.rating >= -4 && !comment.headerCell.data('events'))
            {
                comment.headerCell.css('cursor', 'pointer');
                comment.headerCell.bind('click', function(e) {
                    if ($WH.$E(e)._target.nodeName == 'A')
                        return;

                    comment.voteCell.toggle();
                    comment.repliesCell.toggle();
                    comment.commentBody.toggle();
                    comment.repliesControl.toggle();
                });
            }
            // aowow: custom end


        }
    },

    coUndelete: function(comment)
    {
        var god = comment.user == g_user.name || ((g_user.roles & (U_GROUP_ADMIN|U_GROUP_MOD)) != 0);
        var notificationDiv = comment.commentCell.find('.comment-notification');

        if (confirm(LANG.votetoundelete_tip))
        {
            $.post('?comment=undelete', { id: comment.id });

            if (god)
            {
                MessageBox(notificationDiv, 'This comment has been restored.');

                if (comment.commentv2)
                {
                    comment.container.removeClass('comment-deleted');
                    comment.deletedInfo = null;
                    comment.deleted = false;
                    Listview.templates.comment.updateCommentCell(comment);

                    // aowow: custom start
                    comment.voteCell.show();
                    comment.repliesCell.show();
                    comment.commentBody.show();
                    comment.repliesControl.show();
                    Listview.templates.comment.updateRepliesControl(comment);
                    if (comment.rating >= -4)
                    {
                        comment.headerCell.css('cursor', 'auto');
                        comment.headerCell.unbind('click');
                    }
                    // aowow custom end
                }
            }
            else
                MessageBox(notificationDiv, LANG.votedtodelete_tip);
        }
    },

    coEdit: function(comment, mode, blog)
    {
        // aowow: custom
        if (comment.commentCell.find('.comment-edit')[0]) {
            return;
        }

        if (blog) {
            comment.divBody.hide();
            comment.divResponse.hide();
        }

        var divEdit = $('<div/>');
        divEdit.addClass('comment-edit');
        comment.divEdit = divEdit[0];

        if (mode == -1) // edit post (forums)
        {
            if (g_users[comment.user] != null)
                comment.roles = g_users[comment.user].roles;
        }

        var ta = Listview.funcBox.coEditAppend(divEdit, comment, mode, blog);

        var divButtons = $('<div/>');
        divButtons.addClass('comment-edit-buttons');

        var ip = $('<button/>', { text: LANG.compose_save });
        ip.click(Listview.funcBox.coEditButton.bind(ip[0], comment, true, mode, blog));
        divButtons.append(ip);

        divButtons.append($WH.ct(' '));

        ip = $('<button/>', { text: LANG.compose_cancel });
        ip.click(Listview.funcBox.coEditButton.bind(ip[0], comment, false, mode, blog));
        divButtons.append(ip);
        divEdit.append(divButtons);

        divEdit.insertAfter(comment.divBody);

        ta.focus();
    },

    coEditAppend: function(div, comment, mode, blog, noRestrictedMarkup)
    {
        var charLimit = Listview.funcBox.coGetCharLimit(mode);

        // Modes:
        // -1: Edit forum post
        //  0: Edit comment
        //  1: Post your comment
        //  2: Public description (Account settings)
        //  3: Post reply (forums)
        //  4: Signature (Account settings)

        if (mode == 1 || mode == 3 || mode == 4) // new comment/forum post/sig
        {
            comment.user = g_user.name;
            comment.roles = g_user.roles;
            comment.rating = 1;
        }
        else if (mode == 2) // public description
        {
            comment.roles = g_user.roles;
            comment.rating = 1;
        }

        if (noRestrictedMarkup)
            comment.roles &= ~U_GROUP_PENDING;

        if (mode == -1 || mode == 0)
        {
            var divMode = $('<div/>', { text: LANG.compose_mode });
            divMode.addClass('comment-edit-modes');

            var aEdit = $('<a/>', { href: 'javascript:;', text: LANG.compose_edit });
            aEdit.click(Listview.funcBox.coModeLink.bind(aEdit[0], 1, mode, comment));
            aEdit.addClass('selected');
            divMode.append(aEdit);

            divMode.append($WH.ct('|'));

            var aPreview = $('<a/>', { href: 'javascript:;', text: LANG.compose_preview });
            aPreview.click(Listview.funcBox.coModeLink.bind(aPreview[0], 2, mode, comment));
            divMode.append(aPreview);

            div.append(divMode);
        }

        var divPreview = $('<div/>', { css: { display: 'none' } });
        divPreview.addClass('text comment-body' + Listview.funcBox.coGetColor(comment, mode, blog));

        var divBody = $('<div/>');
        divBody.addClass('comment-edit-body');

        var tb = $('<div style="float: left" />');
        tb.addClass('toolbar');

        var tm = $('<div style="float: left" />');
        tm.addClass('menu-buttons');

        var ta = $('<textarea/>', { val: comment.body, rows: 10, css: { clear: 'left' } });
        ta.addClass('comment-editbox');
        switch (mode)
        {
            case 1:
                ta.attr('name', 'commentbody');
             // ta.focus(g_revealCaptcha.bind(null, 'klrbetkjerbt46', false, false, 'Listview commentbody'));
                break;

            case 2:
                ta.attr({ name: 'desc', originalValue: comment.body });
                break;

            case 3:
                ta.attr('name', 'body');
             // ta.focus(g_revealCaptcha.bind(null, 'klrbetkjerbt46', false, false, 'Listview body'));
                break;

            case 4:
                ta.attr({ name: 'sig', originalValue: comment.body, rows: 3 });
                ta.css('height', 'auto');
                break;
        }

        if (mode != -1 && mode != 0)
        {
            var h3 = $('<h3/>'),
                ah3 = $('<a/>'),
                preview = $('<div/>'),
                pad = $('<div/>'),
                mobile = screen.availWidth <= 480;

            var func = Listview.funcBox.coLivePreview.bind(ta[0], comment, mode, preview[0]);

            ah3.addClass('disclosure-' + (mobile ? 'off' : 'on'));

            ah3.text(LANG.compose_livepreview);
            h3.append(ah3);
            ah3.attr('href', 'javascript:;');
            ah3.click(function() { func(1); var on = g_toggleDisplay(preview); ah3.toggleClass('disclosure-on', on); ah3.toggleClass('disclosure-off', !on); });

            h3.addClass('first');
            pad.addClass('pad');

            divPreview.append(h3);
            divPreview.append(preview);
            divPreview.append(pad);

            g_onAfterTyping(ta[0], func, 50);

            ta.focus(function() { func(); divPreview.css('display', (mobile ? 'none' : '')); if (mode != 4) { ta.css('height', '22em'); } });
        }
        else if (mode != 4)
            ta.focus(function() { ta.css('height', '22em'); });

        var buttons = [
            {id: 'b',     title: LANG.markup_b,     pre: '[b]',        post: '[/b]'},
            {id: 'i',     title: LANG.markup_i,     pre: '[i]',        post: '[/i]'},
            {id: 'u',     title: LANG.markup_u,     pre: '[u]',        post: '[/u]'},
            {id: 's',     title: LANG.markup_s,     pre: '[s]',        post: '[/s]'},
            {id: 'small', title: LANG.markup_small, pre: '[small]',    post: '[/small]'},
            {id: 'url',   title: LANG.markup_url,   nopending: true, onclick: function() {var url = prompt(LANG.prompt_linkurl, 'http://');if (url) g_insertTag(ta[0], '[url=' + url + ']', '[/url]')}},
            {id: 'quote', title: LANG.markup_quote, pre: '[quote]',    post: '[/quote]'},
            {id: 'code',  title: LANG.markup_code,  pre: '[code]',     post: '[/code]'},
            {id: 'ul',    title: LANG.markup_ul,    pre: '[ul]\n[li]', post: '[/li]\n[/ul]', rep: function(txt) {return txt.replace(/\n/g, '[/li]\n[li]')}},
            {id: 'ol',    title: LANG.markup_ol,    pre: '[ol]\n[li]', post: '[/li]\n[/ol]', rep: function(txt) {return txt.replace(/\n/g, '[/li]\n[li]')}},
            {id: 'li',    title: LANG.markup_li,    pre: '[li]',       post: '[/li]'}
        ];

        if (!blog)
        {
            for (var i = 0, len = buttons.length; i < len; ++i)
            {
                var button = buttons[i];
                if (mode == 4 && button.id == 'quote')
                    break;
                if ((g_user.roles & U_GROUP_PENDING) && button.nopending)
                    continue;

                var but = $('<button/>', { click: function(button, event) { event.preventDefault(); (button.onclick != null ? button.onclick : g_insertTag.bind(0, ta[0], button.pre, button.post, button.rep))(); }.bind(null, button) });
                but[0].setAttribute('type', 'button');
                var img = $('<img/>');
                but.attr('title', button.title);

                img.attr('src', g_staticUrl + '/images/deprecated/pixel.gif');
                img.addClass('toolbar-' + button.id);

                but.append(img);
                tb.append(but);
            }
        }
        else
        {
            for (var i = 0, len = buttons.length; i < len; ++i)
            {
                var button = buttons[i];
                if ((g_user.rolls & U_GROUP_PENDING) && button.nopending)
                    continue;

                var buttonClass = 'tb-' + button.id;
                var but = $('<button/>', { click: function(button, event) { event.preventDefault(); (button.onclick != null ? button.onclick : g_insertTag.bind(0, ta[0], button.pre, button.post, button.rep))(); }.bind(null, button), 'class': buttonClass, title: button.title });
                but[0].setAttribute('type', 'button');
                but.append('<ins/>');

                tb.append(but);
            }

            tb.addClass('formatting button sm');
        }

        var promptId = function(name, tag)
        {
            var id = prompt($WH.sprintf(LANG.markup_prompt, name), '');
            if (id != null)
                g_insertTag(ta[0], '[' + tag + '=' + (parseInt(id) || 0) + ']', '');
        };

        var menu = [
            [0, LANG.markup_links,, [
                [ 9, LANG.types[10][0] + '...', promptId.bind(null, LANG.types[10][1], 'achievement')],
                [11, LANG.types[13][0] + '...', promptId.bind(null, LANG.types[13][1], 'class')],
                [ 7, LANG.types[8][0]  + '...', promptId.bind(null, LANG.types[8][1],  'faction')],
                [ 0, LANG.types[3][0]  + '...', promptId.bind(null, LANG.types[3][1],  'item')],
                [ 1, LANG.types[4][0]  + '...', promptId.bind(null, LANG.types[4][1],  'itemset')],
                [ 2, LANG.types[1][0]  + '...', promptId.bind(null, LANG.types[1][1],  'npc')],
                [ 3, LANG.types[2][0]  + '...', promptId.bind(null, LANG.types[2][1],  'object')],
                [ 8, LANG.types[9][0]  + '...', promptId.bind(null, LANG.types[9][1],  'pet')],
                [ 4, LANG.types[5][0]  + '...', promptId.bind(null, LANG.types[5][1],  'quest')],
                [12, LANG.types[14][0] + '...', promptId.bind(null, LANG.types[14][1], 'race')],
                [13, LANG.types[15][0] + '...', promptId.bind(null, LANG.types[15][1], 'skill')],
                [ 5, LANG.types[6][0]  + '...', promptId.bind(null, LANG.types[6][1],  'spell')],
                [ 6, LANG.types[7][0]  + '...', promptId.bind(null, LANG.types[7][1],  'zone')]
            ]]
        ];

        divBody.append(tb);
        divBody.append(tm);
        divBody.append($('<div style="clear: left" />'));
        divBody.append(ta);
        divBody.append($('<br/>'));

        Menu.addButtons(tm[0], menu);

        if (mode == 4)
            divBody.append($WH.ct($WH.sprintf(LANG.compose_limit2, charLimit, 3)));
        else
            divBody.append($WH.ct($WH.sprintf(LANG.compose_limit, charLimit)));

        var span = $('<span class="comment-remaining"> ' + $WH.sprintf(LANG.compose_remaining, charLimit - comment.body.length) + '</span>');
        divBody.append(span);

        ta.keyup(Listview.funcBox.coUpdateCharLimit.bind(0, ta, span, charLimit));
        ta.keydown(Listview.funcBox.coUpdateCharLimit.bind(0, ta, span, charLimit));

        if ((mode == -1 || mode == 0) && g_user.roles & U_GROUP_MODERATOR)
        {
            var spaceDiv = $('<div/>', { 'class': 'pad' });
            var responseDiv = $('<div/>', { text: (g_user.roles & U_GROUP_ADMIN ? 'Admin' : 'Moderator') + ' response' });
            var response = $('<textarea/>', { val: comment.response, rows: 3, css: { height: '6em' } });
            divBody.append(spaceDiv);
            divBody.append(responseDiv);
            divBody.append(response);
        }

        div.append(divBody);
        div.append($('<br/>'));
        div.append(divPreview);

        $('<div/>')
            .append('<div class="pad"/>')
            .append(
                $('<h3 class="first"/>')
                    .append(
                        $('<a class="disclosure-off"/>')
                            .text(LANG.compose_formattinghelp)
                            .click(function() { g_disclose(this.parentNode.nextSibling, this) })
                    )
            )
            .append(
                $('<div style="display: none"/>')
                    .append(Markup.toHtml('[markupdoc help=user]'))
            )
            .insertAfter(div.parent());

        return ta;
    },

    coLivePreview: function(comment, mode, div, force)
    {
        if (force != 1 && div.style.display == 'none')
            return;

        var ta        = this,
            charLimit = Listview.funcBox.coGetCharLimit(mode),
            str       = (ta.value.length > charLimit ? ta.value.substring(0, charLimit) : ta.value);

        if (mode == 4)
        {
            var foo;
            if ((foo = str.indexOf('\n')) != -1 && (foo = str.indexOf('\n', foo + 1)) != -1 && (foo = str.indexOf('\n', foo + 1)) != -1)
                str = str.substring(0, foo);
        }

        var allowed = Markup.rolesToClass(comment.roles);
        var html = Markup.toHtml(str, {allow: allowed, mode: Markup.MODE_COMMENT, roles: comment.roles});
        if (html)
            div.innerHTML = html;
        else
            div.innerHTML = '<span class="q6">...</span>';
    },

    coEditButton: function(comment, update, mode, blog)
    {
        if (update)
        {
            var tas = $WH.gE(comment.divEdit, 'textarea');
            var ta = tas[0];
            if (!Listview.funcBox.coValidate(ta, mode))
                return;

            if (ta.value != comment.body || (tas[1] && tas[1].value != comment.response))
            {
                var nEdits = 0;
                if (comment.lastEdit != null)
                    nEdits = comment.lastEdit[1];
                ++nEdits;
                comment.lastEdit = [g_serverTime, nEdits, g_user.name];

                if (!comment.commentv2)
                    Listview.funcBox.coUpdateLastEdit(comment);

                var charLimit = Listview.funcBox.coGetCharLimit(mode);

                var allowed = Markup.rolesToClass(comment.roles);
                var bodyHtml = Markup.toHtml((ta.value.length > charLimit ? ta.value.substring(0, charLimit) : ta.value), {allow: allowed, mode: Markup.MODE_COMMENT, roles: comment.roles});
                var respHtml = ((tas[1] && tas[1].value.length > 0) ? Markup.toHtml('[div][/div][wowheadresponse=' + g_user.name + ' roles=' + g_user.roles + ']' + tas[1].value + '[/wowheadresponse]', { allow: Markup.CLASS_STAFF, mode: Markup.MODE_COMMENT, roles: g_user.roles }) : '');

                if (comment.commentv2)
                {
                    comment.body = ta.value;

                    if (g_user.roles & U_GROUP_MODERATOR && tas[1])
                    {
                        comment.response      = tas[1].value;
                        comment.responseuser  = g_user.name;
                        comment.responseroles = g_user.roles;
                    }

                    Listview.templates.comment.updateCommentCell(comment);
                }
                else
                {
                    if (!blog) {
                        comment.divBody.innerHTML = bodyHtml;
                        comment.divResponse.innerHTML = respHtml;
                    }
                    else {
                        comment.divBody.html(bodyHtml);
                        comment.divResponse.html(respHtml);
                    }
                    comment.body = ta.value;

                    if (g_user.roles & U_GROUP_MODERATOR && tas[1])
                        comment.response = tas[1].value;
                }

                var params = 'body=' + $WH.urlencode(comment.body);
                if (comment.response !== undefined)
                    params += '&response=' + $WH.urlencode(comment.response);

                if (mode == -1)
                    new Ajax('?forums=editpost&id=' + comment.id, {method: 'POST', params: params});
                else
                    new Ajax('?comment=edit&id=' + comment.id, {method: 'POST', params: params});
            }
        }

        if (comment.commentv2)
        {
            Listview.templates.comment.updateCommentCell(comment);
        }
        else if (!blog) {
            comment.divBody.style.display = '';
            comment.divResponse.style.display = '';
            comment.divLinks.firstChild.style.display = '';
        }
        else {
            comment.divBody.show();
            comment.divResponse.show();
        }

        if (!comment.commentv2)
        {
            $WH.de(comment.divEdit);
            comment.divEdit = null;
        }
    },

    coGetCharLimit: function(mode)
    {
        if (mode == 2) // Public description (Account settings)
            return 7500;

        if (mode == 4) // Signature (Account settings)
            return 250;

        if (g_user.roles & U_GROUP_STAFF)
            return 16000000;

        var multiplier = 1;

        if (g_user.premium)
            multiplier = 3;

        switch (mode)
        {
            case 0: // Edit comment
            case 1: // Post comment
                return 7500 * multiplier;

            case -1: // Edit forum post
            case 3: // Post forum reply
                return 15000 * multiplier;
        }
    },

    coUpdateCharLimit: function(ta, span, charLimit)
    {
        var text = $(ta).val();
        if (text.length > charLimit)
            $(ta).val(text.substring(0, charLimit));
        else
        {
            $(span).html(' ' + $WH.sprintf(LANG.compose_remaining, charLimit - text.length)).removeClass('q10');
            if (text.length == charLimit)
                $(span).addClass('q10');
        }
    },

    coModeLink: function(mode, m, comment)
    {
        var charLimit = Listview.funcBox.coGetCharLimit(m);
        var markupMode = Markup.MODE_COMMENT;

        $WH.array_walk($WH.gE(this.parentNode, 'a'), function(x) {x.className = ''});
        this.className = 'selected';

        var
            tas = $WH.gE(this.parentNode.parentNode, 'textarea'),
            ta = tas[0],
            taContainer = ta.parentNode,
            preview = $('.comment-body', taContainer.parentNode)[0];

        if (m == 4)
            markupMode = Markup.MODE_SIGNATURE;

        switch (mode)
        {
            case 1:
                taContainer.style.display = '';
                preview.style.display = 'none';
                taContainer.firstChild.focus();
                break;

            case 2:
                taContainer.style.display = 'none';

                var str = (ta.value.length > charLimit ? ta.value.substring(0, charLimit) : ta.value);
                if (m == 4)
                {
                    var foo;
                    if ((foo = str.indexOf('\n')) != -1 && (foo = str.indexOf('\n', foo + 1)) != -1 && (foo = str.indexOf('\n', foo + 1)) != -1)
                        str = str.substring(0, foo);
                }

                var allowed = Markup.rolesToClass(comment.roles);
                var html = Markup.toHtml(str, {allow: allowed, mode: markupMode, roles: comment.roles});
                if (tas[1] && tas[1].value.length > 0)
                    html += Markup.toHtml('[div][/div][wowheadresponse=' + g_user.name + ' roles=' + g_user.roles + ']' + tas[1].value + '[/wowheadresponse]', { allow: Markup.CLASS_STAFF, mode: markupMode, roles: g_user.roles });
                preview.innerHTML = html;
                preview.style.display = '';
                break;
        }
    },

    coValidate: function(ta, mode)
    {
        mode |= 0;

        if (mode == 1 || mode == -1)
        {
            if ($WH.trim(ta.value).length < 10)
            {
                alert(LANG.message_forumposttooshort);
                return false;
            }
        }
        else
        {
            if ($WH.trim(ta.value).length < 10)
            {
                alert(LANG.message_commenttooshort);
                return false;
            }
        }

        var charLimit = Listview.funcBox.coGetCharLimit(mode);

        if (ta.value.length > charLimit)
        {
            if (!confirm(
                $WH.sprintf(mode == 1 ? LANG.confirm_forumposttoolong : LANG.confirm_commenttoolong, charLimit, ta.value.substring(charLimit - 30, charLimit)))
            )
                return false;
        }

        return true;
    },

    coSortNewestFirst: function(me)
    {
     // $WH.sc('comments_sort', 1000, '1', '/', '.wowhead.com');
        $WH.sc('comments_sort', 1000, '1', '/', location.hostname);

        $(me).parent().find('a.selected').removeClass('selected');
        me.className = 'selected';
        this.mainDiv.className += ' listview-aci';
        this.setSort([-5, 4, 6, -1, -2], true, false);
    },

    coSortOldestFirst: function(me)
    {
     // $WH.sc('comments_sort', 1000, '2', '/', '.wowhead.com');
        $WH.sc('comments_sort', 1000, '2', '/', location.hostname);

        $(me).parent().find('a.selected').removeClass('selected');
        me.className = 'selected';
        this.mainDiv.className += ' listview-aci';
        this.setSort([-5, 4, 6, 1, 2], true, false);
    },

    coSortHighestRatedFirst: function(me)
    {
     // $WH.sc('comments_sort', 1000, '3', '/', '.wowhead.com');
        $WH.sc('comments_sort', 1000, '3', '/', location.hostname);

        $(me).parent().find('a.selected').removeClass('selected');
        me.className = 'selected';
        this.mainDiv.className = this.mainDiv.className.replace('listview-aci', '');
        this.setSort([-5, 4, 6, -3, 2], true, false);
    },

    coFilterByPatchVersion: function(select)
    {
        this.minPatchVersion = select.value;
        this.refreshRows();
    },

    coUpdateLastEdit: function(comment)
    {
        var _ = comment.divLastEdit;
        if (!_) return;

        if (comment.lastEdit != null)
        {
            var __ = comment.lastEdit;
            _.childNodes[1].firstChild.nodeValue = __[2];
            _.childNodes[1].href = '?user=' + __[2];

            var editedOn = new Date(__[0]);
            var elapsed = (g_serverTime - editedOn) / 1000;

            if (_.childNodes[3].firstChild)
                $WH.de(_.childNodes[3].firstChild);

            g_formatDate(_.childNodes[3], elapsed, editedOn);

            var extraTxt = '';

            if (comment.rating != null) // Comments only
                extraTxt += $WH.sprintf(LANG.lvcomment_patch, g_getPatchVersion(editedOn));

            if (__[1] > 1)
                extraTxt += LANG.dash + $WH.sprintf(LANG.lvcomment_nedits, __[1]);

            _.childNodes[4].nodeValue = extraTxt;

            _.style.display = '';
        }
        else
            _.style.display = 'none';
    },

    coFormatFileSize: function(size)
    {
        var i = -1;
        var units = 'KMGTPEZY';

        while (size >= 1024 && i < 7)
        {
            size /= 1024;
            ++i;
        }

        if (i < 0)
            return size + ' byte' + (size > 1? 's': '');
        else
            return size.toFixed(1) + ' ' + units[i] + 'B';
    },

    dateEventOver: function(date, event, e)
    {
        var dates = Listview.funcBox.getEventNextDates(event.startDate, event.endDate, event.rec || 0, date),
            buff  = '';

        if (dates[0] && dates[1])
        {
            var t1 = new Date(event.startDate.replace(/-/g, '/')),
                t2 = new Date(event.endDate.replace(/-/g, '/')),
                first,
                last;

            t1.setFullYear(date.getFullYear(), date.getMonth(), date.getDate());
            t2.setFullYear(date.getFullYear(), date.getMonth(), date.getDate());

            if (date.getFullYear() == dates[0].getFullYear() && date.getMonth() == dates[0].getMonth() && date.getDate() == dates[0].getDate())
                first = true;
            if (date.getFullYear() == dates[1].getFullYear() && date.getMonth() == dates[1].getMonth() && date.getDate() == dates[1].getDate())
                last = true;

            if (first && last)
                buff = g_formatTimeSimple(t1, LANG.lvscreenshot_from, 1) + ' ' + g_formatTimeSimple(t2, LANG.date_to, 1);
            else if (first)
                buff = g_formatTimeSimple(t1, LANG.tab_starts);
            else if (last)
                buff = g_formatTimeSimple(t2, LANG.tab_ends);
            else
                buff = LANG.allday;
        }

        $WH.Tooltip.showAtCursor(e, '<span class="q1">' + event.name + '</span><br />' + buff, 0, 0, 'q');
    },

    ssCellOver: function()
    {
        this.className = 'screenshot-caption-over';
    },

    ssCellOut: function()
    {
        this.className = 'screenshot-caption';
    },

    ssCellClick: function(i, e)
    {
        e = $WH.$E(e);

        if (e.shiftKey || e.ctrlKey)
            return;

        var j  = 0,
            el = e._target;

        while (el && j < 3)
        {
            if (el.nodeName == 'A')
                return;
            if (el.nodeName == 'IMG')
                break;
            el = el.parentNode;
        }

        ScreenshotViewer.show({ screenshots: this.data, pos: i });
    },

    ssCreateCb: function(td, row)
    {
        if (row.__nochk)
            return;

        var div = $WH.ce('div');
        div.className = 'listview-cb';
        div.onclick   = Listview.cbCellClick;

        var cb = $WH.ce('input');
        cb.type = 'checkbox';
        cb.onclick = Listview.cbClick;
        $WH.ns(cb);

        if (row.__chk)
        {
            cb.checked = true;
        }

        row.__cb = cb;

        $WH.ae(div, cb);
        $WH.ae(td, div);
    },

    viCellClick: function(i, e)
    {
        e = $WH.$E(e);

        if (e.shiftKey || e.ctrlKey)
            return;

        var j  = 0,
            el = e._target;

        while (el && j < 3)
        {
            if (el.nodeName == 'A')
                return;
            if (el.nodeName == 'IMG')
                break;
            el = el.parentNode;
        }

        VideoViewer.show({ videos: this.data, pos: i });
    },

    moneyHonorOver: function(e)
    {
        $WH.Tooltip.showAtCursor(e, '<b>' + LANG.tooltip_honorpoints + '</b>', 0, 0, 'q1');
    },

    moneyArenaOver: function(e)
    {
        $WH.Tooltip.showAtCursor(e, '<b>' + LANG.tooltip_arenapoints + '</b>', 0, 0, 'q1');
    },

    moneyAchievementOver: function(e)
    {
        $WH.Tooltip.showAtCursor(e, '<b>' + LANG.tooltip_achievementpoints + '</b>', 0, 0, 'q1');
    },


    moneyCurrencyOver: function(currencyId, count, e)
    {
        var buff = g_gatheredcurrencies[currencyId]['name_' + Locale.getName()];

        // aowow: justice / valor points handling removed

        $WH.Tooltip.showAtCursor(e, buff, 0, 0, 'q1');
    },

    appendMoney: function(d, money, side, costItems, costCurrency, achievementPoints)
    {
        var _,
            __,
            ns = 0;

        if (side == 1 || side == 'alliance')
            side = 1;
        else if (side == 2 || side == 'horde')
            side = 2;
        else
            side = 3;

        if (money >= 10000)
        {
            ns = 1;

            _ = $WH.ce('span');
            _.className = 'moneygold';
            $WH.ae(_, $WH.ct($WH.number_format(Math.floor(money / 10000))));
            $WH.ae(d, _);
            money %= 10000;
        }

        if (money >= 100)
        {
            if (ns)
                $WH.ae(d, $WH.ct(' '));
            else
                ns = 1;

            _ = $WH.ce('span');
            _.className = 'moneysilver';
            $WH.ae(_, $WH.ct(Math.floor(money / 100)));
            $WH.ae(d, _);
            money %= 100;
        }

        if (money >= 1)
        {
            if (ns)
                $WH.ae(d, $WH.ct(' '));
            else
                ns = 1;

            _ = $WH.ce('span');
            _.className = 'moneycopper';
            $WH.ae(_, $WH.ct(money));
            $WH.ae(d, _);
        }

        if (costItems != null)
        {
            for (var i = 0; i < costItems.length; ++i)
            {
                if (ns)
                    $WH.ae(d, $WH.ct(' '));
                else
                    ns = 1;

                var itemId = costItems[i][0];
                var count = costItems[i][1];
                var icon = g_items.getIcon(itemId);

                _ = $WH.ce('a');
                _.href = '?item=' + itemId;
                _.className = 'moneyitem';
                _.style.backgroundImage = 'url(' + g_staticUrl + '/images/wow/icons/tiny/' + icon.toLowerCase() + '.gif)';
                $WH.ae(_, $WH.ct(count));
                $WH.ae(d, _);
            }
        }

        if (costCurrency != null)
        {
            for (var i = 0; i < costCurrency.length; ++i)
            {
                if (ns)
                    $WH.ae(d, $WH.ct(' '));
                else
                    ns = 1;

                var currencyId = costCurrency[i][0];
                var count = costCurrency[i][1];
                var icon = ['inv_misc_questionmark', 'inv_misc_questionmark'];
                if (g_gatheredcurrencies[currencyId])
                    icon = g_gatheredcurrencies[currencyId].icon;

//  aowow: replacement
                _ = $WH.ce('a');
                _.href = '?currency=' + currencyId;
                _.onmousemove = $WH.Tooltip.cursorUpdate;
                _.onmouseout = $WH.Tooltip.hide;
                if (currencyId == 103)                      // arena
                {
                    _.className = 'moneyarena tip';
                    _.onmouseover = Listview.funcBox.moneyArenaOver;
                    $WH.ae(_, $WH.ct($WH.number_format(count)));
                }
                else if (currencyId == 104)                 // honor
                {
                    if (side == 3 && icon[0] == icon[1])
                        side = 1;

                    _.className = 'money' + (side == 1 ? 'alliance' : 'horde') + ' tip';
                    _.onmouseover = Listview.funcBox.moneyHonorOver;

                    if (side == 3)
                    {
                        __ = $WH.ce('span');
                        __.className = 'moneyalliance';
                        $WH.ae(__, $WH.ct($WH.number_format(count)));
                        $WH.ae(_, __);
                    }
                    else
                        $WH.ae(_, $WH.ct($WH.number_format(count)));
                }
                else                                        // tokens
                {
                    _.className = 'icontinyr tip q1';
                    _.style.backgroundImage = 'url(' + g_staticUrl + '/images/wow/icons/tiny/' + icon[0].toLowerCase() + '.gif)';
                    _.onmouseover = Listview.funcBox.moneyCurrencyOver.bind(_, currencyId, count);
                    $WH.ae(_, $WH.ct($WH.number_format(count)));
                }
/*  aowow: original
                if (side == 3 && icon[0] == icon[1])
                    side = 1;

                _ = $WH.ce('a');
                _.href = '?currency=' + currencyId;
                _.className = 'icontinyr tip q1';
                _.style.backgroundImage = 'url(' + g_staticUrl + '/images/wow/icons/tiny/' + icon[(side == 3 ? 1 : side - 1)].toLowerCase() + '.gif)';
                _.onmouseover = Listview.funcBox.moneyCurrencyOver.bind(_, currencyId, count);
                _.onmousemove = $WH.Tooltip.cursorUpdate;
                _.onmouseout = $WH.Tooltip.hide;
                $WH.ae(d, _);

                if (side == 3)
                {
                    __ = $WH.ce('span');
                    __.className = 'icontinyr';
                    __.style.backgroundImage = 'url(' + g_staticUrl + '/images/wow/icons/tiny/' + icon[0].toLowerCase() + '.gif)';
                    $WH.ae(_, __);
                    _ = __;
                }
*/
                $WH.ae(d, _);
            }
        }

        // aowow: changed because legitemately passing zero APs from the profiler is a thing
        // (achievementPoints > 0) {
        if (typeof achievementPoints == 'number')
        {
            if (ns)
                $WH.ae(d, $WH.ct(' '));
            else
                ns = 1;

            _ = $WH.ce('span');
            _.className = 'moneyachievement tip';
            _.onmouseover = Listview.funcBox.moneyAchievementOver;
            _.onmousemove = $WH.Tooltip.cursorUpdate;
            _.onmouseout = $WH.Tooltip.hide;
            $WH.ae(_, $WH.ct($WH.number_format(achievementPoints)));
            $WH.ae(d, _);
        }
    },

    getUpperSource: function(source, sm)
    {
        switch (source)
        {
            case 2: // Drop
                if (sm.bd)
                    return LANG.source_bossdrop;
                if (sm.z)
                    return LANG.source_zonedrop;
                break;

            case 4: // Quest
                return LANG.source_quests;

            case 5: // Vendor
                return LANG.source_vendors;
        }

        return g_sources[source];
    },

    getLowerSource: function(source, sm, type)
    {
        switch (source)
        {
            case 3: // PvP
                if (sm.p && g_sources_pvp[sm.p])
                    return { text: g_sources_pvp[sm.p] };
                break;
        }

        switch (type)
        {
            case 0: // None
            case 1: // NPC
            case 2: // Object
                if (sm.z)
                {
                    var res = {
                        url: '?zone=' + sm.z,
                        text: g_zones[sm.z]
                    };

                    if (sm.t && source == 5)
                        res.pretext = LANG.lvitem_vendorin;

                    if (sm.dd && sm.dd != 99)
                    {
                        if (sm.dd < 0) // Dungeon
                            res.posttext = $WH.sprintf(LANG.lvitem_dd, '', (sm.dd < -1 ? LANG.lvitem_heroic : LANG.lvitem_normal));
                        else // Raid
                            res.posttext = $WH.sprintf(LANG.lvitem_dd, (sm.dd & 1 ? LANG.lvitem_raid10 : LANG.lvitem_raid25), (sm.dd > 2 ? LANG.lvitem_heroic : LANG.lvitem_normal));
                    }

                    return res;
                }
                break;

            case 5: // Quest
                return {
                    url: '?quests=' + sm.c2 + '.' + sm.c,
                    text: Listview.funcBox.getQuestCategory(sm.c)
                };
                break;

            case 6: // Spell
                if (sm.c && sm.s)
                    return {
                        url: '?spells=' + sm.c + '.' + sm.s,
                        text: g_spell_skills[sm.s]
                    };
                else
                    return {
                        url: '?spells=0',
                        text: '??'
                    };
                break;
        }
    },

    getExpansionText: function(line)
    {
        var str = '';
        if (line.expansion == 1)
            str += ' bc';
        else if (line.expansion == 2)
            str += ' wotlk wrath';

        return str;
    }
};
