Files
nzbget/webui/fasttable.js
Andrey Prygunkov 0d67e322a3 #282: extra warning when deleting from history
An extra warning is shown when deleting 50 or more records from history
and there are selected records on other pages.
2016-11-29 18:56:00 +01:00

1524 lines
34 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* This file is part of nzbget. See <http://nzbget.net>.
*
* Copyright (C) 2012-2016 Andrey Prygunkov <hugbug@users.sourceforge.net>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* Some code was borrowed from:
* 1. Greg Weber's uiTableFilter jQuery plugin (http://gregweber.info/projects/uitablefilter)
* 2. Denny Ferrassoli & Charles Christolini's TypeWatch jQuery plugin (http://github.com/dennyferra/TypeWatch)
* 3. Justin Britten's tablesorterFilter jQuery plugin (http://www.justinbritten.com/work/2008/08/tablesorter-filter-results-based-on-search-string/)
* 4. Allan Jardine's Bootstrap Pagination jQuery plugin for DataTables (http://datatables.net/)
*/
/*
* In this module:
* HTML tables with:
* 1) very fast content updates;
* 2) automatic pagination;
* 3) search/filtering;
* 4) drag and drop.
*
* What makes it unique and fast?
* The tables are designed to be updated very often (up to 10 times per second). This has two challenges:
* 1) updating of whole content is slow because the DOM updates are slow.
* 2) if the DOM is updated during user interaction the user input is not processed correctly.
* For example if the table is updated after the user pressed mouse key but before he/she released
* the key, the click is not processed because the element, on which the click was performed,
* doesn't exist after the update of DOM anymore.
*
* How Fasttable solves these problems? The solutions is to update only rows and cells,
* which were changed by keeping the unchanged DOM-elements.
*
* Important: the UI of table must be designed in a way, that the cells which are frequently changed
* (like remaining download size) should not be clickable, whereas the cells which are rarely changed
* (e. g. Download name) can be clickable.
*/
(function($) {
'use strict';
$.fn.fasttable = function(method)
{
if (methods[method])
{
return methods[method].apply( this, Array.prototype.slice.call( arguments, 1 ));
}
else if ( typeof method === 'object' || ! method )
{
return methods.init.apply( this, arguments );
}
else
{
$.error( 'Method ' + method + ' does not exist on jQuery.fasttable' );
}
};
var methods =
{
defaults : function()
{
return defaults;
},
init : function(options)
{
return this.each(function()
{
var $this = $(this);
var data = $this.data('fasttable');
// If the plugin hasn't been initialized yet
if (!data)
{
/*
Do more setup stuff here
*/
var config = {};
config = $.extend(config, defaults, options);
config.filterInput = $(config.filterInput);
config.filterClearButton = $(config.filterClearButton);
config.pagerContainer = $(config.pagerContainer);
config.infoContainer = $(config.infoContainer);
config.dragBox = $(config.dragBox);
config.dragContent = $(config.dragContent);
config.dragBadge = $(config.dragBadge);
config.selector = $('th.table-selector', $this);
var searcher = new FastSearcher();
// Create a timer which gets reset upon every keyup event.
// Perform filter only when the timer's wait is reached (user finished typing or paused long enough to elapse the timer).
// Do not perform the filter is the query has not changed.
// Immediately perform the filter if the ENTER key is pressed.
var timer;
config.filterInput.keyup(function()
{
var timerWait = 500;
var overrideBool = false;
var inputBox = this;
// Was ENTER pushed?
if (inputBox.keyCode == 13)
{
timerWait = 1;
overrideBool = true;
}
var timerCallback = function()
{
var value = inputBox.value.trim();
var data = $this.data('fasttable');
if ((value != data.lastFilter) || overrideBool)
{
applyFilter(data, value);
}
};
// Reset the timer
clearTimeout(timer);
timer = setTimeout(timerCallback, timerWait);
return false;
});
config.filterClearButton.click(function()
{
var data = $this.data('fasttable');
data.config.filterInput.val('');
applyFilter(data, '');
});
config.pagerContainer.on('click', 'li', function (e)
{
e.preventDefault();
var data = $this.data('fasttable');
var pageNum = $(this).text();
if (pageNum.indexOf('Prev') > -1)
{
data.curPage--;
}
else if (pageNum.indexOf('Next') > -1)
{
data.curPage++;
}
else if (isNaN(parseInt(pageNum)))
{
return;
}
else
{
data.curPage = parseInt(pageNum);
}
refresh(data);
});
var data = {
target: $this,
config: config,
pageSize: parseInt(config.pageSize),
maxPages: parseInt(config.maxPages),
pageDots: Util.parseBool(config.pageDots),
curPage: 1,
checkedRows: {},
checkedCount: 0,
lastClickedRowID: null,
searcher: searcher
};
initDragDrop(data);
$this.on('click', 'thead > tr', function(e) { titleCheckClick(data, e); });
$this.on('click', 'tbody > tr', function(e) { itemCheckClick(data, e); });
$this.data('fasttable', data);
}
});
},
destroy : function()
{
return this.each(function()
{
var $this = $(this);
var data = $this.data('fasttable');
// Namespacing FTW
$(window).unbind('.fasttable');
$this.removeData('fasttable');
});
},
update : updateContent,
setPageSize : setPageSize,
setCurPage : setCurPage,
applyFilter : function(filter)
{
applyFilter($(this).data('fasttable'), filter);
},
filteredContent : function()
{
return $(this).data('fasttable').filteredContent;
},
availableContent : function()
{
return $(this).data('fasttable').availableContent;
},
checkedRows : function()
{
return $(this).data('fasttable').checkedRows;
},
checkedCount : function()
{
return $(this).data('fasttable').checkedCount;
},
pageCheckedCount : function()
{
return $(this).data('fasttable').pageCheckedCount;
},
checkRow : function(id, checked)
{
checkRow($(this).data('fasttable'), id, checked);
},
processShortcut : function(key)
{
return processShortcut($(this).data('fasttable'), key);
},
};
function updateContent(content)
{
var data = $(this).data('fasttable');
if (content)
{
data.content = content;
}
refresh(data);
blinkMovedRecords(data);
}
function applyFilter(data, filter)
{
data.lastFilter = filter;
if (data.content)
{
data.curPage = 1;
data.hasFilter = filter !== '';
data.searcher.compile(filter);
refresh(data);
}
if (filter !== '' && data.config.filterInputCallback)
{
data.config.filterInputCallback(filter);
}
if (filter === '' && data.config.filterClearCallback)
{
data.config.filterClearCallback();
}
}
function refresh(data)
{
refilter(data);
validateChecks(data);
updatePager(data);
updateInfo(data);
updateSelector(data);
updateTable(data);
}
function refilter(data)
{
data.availableContent = [];
data.filteredContent = [];
for (var i = 0; i < data.content.length; i++)
{
var item = data.content[i];
if (data.hasFilter && item.search === undefined && data.config.fillSearchCallback)
{
data.config.fillSearchCallback(item);
}
if (!data.hasFilter || data.searcher.exec(item.data))
{
data.availableContent.push(item);
if (!data.config.filterCallback || data.config.filterCallback(item))
{
data.filteredContent.push(item);
}
}
}
}
function updateTable(data)
{
var oldTable = data.target[0];
var newTable = buildTBody(data);
updateTBody(data, oldTable, newTable);
}
function buildTBody(data)
{
var table = $('<table><tbody></tbody></table>')[0];
for (var i=0; i < data.pageContent.length; i++)
{
var item = data.pageContent[i];
var row = table.insertRow(table.rows.length);
row.fasttableID = item.id;
if (data.checkedRows[item.id])
{
row.className = 'checked';
}
if (data.config.renderRowCallback)
{
data.config.renderRowCallback(row, item);
}
if (!item.fields)
{
if (data.config.fillFieldsCallback)
{
data.config.fillFieldsCallback(item);
}
else
{
item.fields = [];
}
}
for (var j=0; j < item.fields.length; j++)
{
var cell = row.insertCell(row.cells.length);
cell.innerHTML = item.fields[j];
if (data.config.renderCellCallback)
{
data.config.renderCellCallback(cell, j, item);
}
}
}
titleCheckRedraw(data);
if (data.config.renderTableCallback)
{
data.config.renderTableCallback(table);
}
return table;
}
function updateTBody(data, oldTable, newTable)
{
var headerRows = $('thead > tr', oldTable).length;
var oldTRs = oldTable.rows;
var newTRs = newTable.rows;
var oldTBody = $('tbody', oldTable)[0];
var oldTRsLength = oldTRs.length - headerRows; // evlt. skip header row
var newTRsLength = newTRs.length;
for (var i=0; i < newTRs.length; )
{
var newTR = newTRs[i];
if (i < oldTRsLength)
{
// update existing row
var oldTR = oldTRs[i + headerRows]; // evlt. skip header row
var oldTDs = oldTR.cells;
var newTDs = newTR.cells;
oldTR.className = newTR.className;
oldTR.fasttableID = newTR.fasttableID;
for (var j=0, n = 0; j < oldTDs.length; j++, n++)
{
var oldTD = oldTDs[j];
var newTD = newTDs[n];
var oldHtml = oldTD.outerHTML;
var newHtml = newTD.outerHTML;
if (oldHtml !== newHtml)
{
oldTR.replaceChild(newTD, oldTD);
n--;
}
}
i++;
}
else
{
// add new row
oldTBody.appendChild(newTR);
}
}
var maxTRs = newTRsLength + headerRows; // evlt. skip header row;
while (oldTRs.length > maxTRs)
{
oldTable.deleteRow(oldTRs.length - 1);
}
}
function updatePager(data)
{
data.pageCount = Math.ceil(data.filteredContent.length / data.pageSize);
if (data.curPage < 1)
{
data.curPage = 1;
}
if (data.curPage > data.pageCount)
{
data.curPage = data.pageCount;
}
var startIndex = (data.curPage - 1) * data.pageSize;
data.pageContent = data.filteredContent.slice(startIndex, startIndex + data.pageSize);
var pagerObj = data.config.pagerContainer;
var pagerHtml = buildPagerHtml(data);
var oldPager = pagerObj[0];
var newPager = $(pagerHtml)[0];
updatePagerContent(data, oldPager, newPager);
}
function buildPagerHtml(data)
{
var iListLength = data.maxPages;
var iStart, iEnd, iHalf = Math.floor(iListLength/2);
if (data.pageCount < iListLength)
{
iStart = 1;
iEnd = data.pageCount;
}
else if (data.curPage -1 <= iHalf)
{
iStart = 1;
iEnd = iListLength;
}
else if (data.curPage - 1 >= (data.pageCount-iHalf))
{
iStart = data.pageCount - iListLength + 1;
iEnd = data.pageCount;
}
else
{
iStart = data.curPage - 1 - iHalf + 1;
iEnd = iStart + iListLength - 1;
}
var pager = '<ul>';
pager += '<li' + (data.curPage === 1 || data.curPage === 0 ? ' class="disabled"' : '') +
'><a href="#" title="Previous page' + (data.config.shortcuts ? ' [Left]' : '') + '">&larr; Prev</a></li>';
if (iStart > 1)
{
pager += '<li><a href="#"' + (data.config.shortcuts ? ' title="First page [Shift+Left]"' : '') + '>1</a></li>';
if (iStart > 2 && data.pageDots)
{
pager += '<li class="disabled"><a href="#">&#133;</a></li>';
}
}
for (var j=iStart; j<=iEnd; j++)
{
pager += '<li' + ((j===data.curPage) ? ' class="active"' : '') +
'><a href="#"' +
(data.config.shortcuts && j === 1 ? ' title="First page [Shift+Left]"' :
data.config.shortcuts && j === data.pageCount ? ' title="Last page [Shift+Right]"' : '') +
'>' + j + '</a></li>';
}
if (iEnd != data.pageCount)
{
if (iEnd < data.pageCount - 1 && data.pageDots)
{
pager += '<li class="disabled"><a href="#">&#133;</a></li>';
}
pager += '<li><a href="#"' + (data.config.shortcuts ? ' title="Last page [Shift+Right]"' : '') + '>' + data.pageCount + '</a></li>';
}
pager += '<li' + (data.curPage === data.pageCount || data.pageCount === 0 ? ' class="disabled"' : '') +
'><a href="#" title="Next page' + (data.config.shortcuts ? ' [Right]' : '') + '">Next &rarr;</a></li>';
pager += '</ul>';
return pager;
}
function updatePagerContent(data, oldPager, newPager)
{
var oldLIs = oldPager.getElementsByTagName('li');
var newLIs = newPager.getElementsByTagName('li');
var oldLIsLength = oldLIs.length;
var newLIsLength = newLIs.length;
for (var i=0, n=0; i < newLIs.length; i++, n++)
{
var newLI = newLIs[i];
if (n < oldLIsLength)
{
// update existing LI
var oldLI = oldLIs[n];
var oldHtml = oldLI.outerHTML;
var newHtml = newLI.outerHTML;
if (oldHtml !== newHtml)
{
oldPager.replaceChild(newLI, oldLI);
i--;
}
}
else
{
// add new LI
oldPager.appendChild(newLI);
i--;
}
}
while (oldLIs.length > newLIsLength)
{
oldPager.removeChild(oldPager.lastChild);
}
}
function updateInfo(data)
{
if (data.content.length === 0)
{
var infoText = data.config.infoEmpty;
}
else if (data.curPage === 0)
{
var infoText = 'No matching records found (total ' + data.content.length + ')';
}
else
{
var firstRecord = (data.curPage - 1) * data.pageSize + 1;
var lastRecord = firstRecord + data.pageContent.length - 1;
var infoText = 'Showing records ' + firstRecord + '-' + lastRecord + ' from ' + data.filteredContent.length;
if (data.filteredContent.length != data.content.length)
{
infoText += ' filtered (total ' + data.content.length + ')';
}
}
data.config.infoContainer.html(infoText);
if (data.config.updateInfoCallback)
{
data.config.updateInfoCallback({
total: data.content.length,
available: data.availableContent.length,
filtered: data.filteredContent.length,
firstRecord: firstRecord,
lastRecord: lastRecord
});
}
}
function updateSelector(data)
{
data.pageCheckedCount = 0;
if (data.checkedCount > 0 && data.filteredContent.length > 0)
{
for (var i = (data.curPage - 1) * data.pageSize; i < Math.min(data.curPage * data.pageSize, data.filteredContent.length); i++)
{
data.pageCheckedCount += data.checkedRows[data.filteredContent[i].id] ? 1 : 0;
}
}
data.config.selector.css('display', data.pageCheckedCount === data.checkedCount ? 'none' : '');
if (data.checkedCount !== data.pageCheckedCount)
{
data.config.selector.text('' + (data.checkedCount - data.pageCheckedCount) +
(data.checkedCount - data.pageCheckedCount > 1 ? ' records' : ' record') +
' selected on other pages');
}
}
function setPageSize(pageSize, maxPages, pageDots)
{
var data = $(this).data('fasttable');
data.pageSize = parseInt(pageSize);
data.curPage = 1;
if (maxPages !== undefined)
{
data.maxPages = maxPages;
}
if (pageDots !== undefined)
{
data.pageDots = pageDots;
}
refresh(data);
}
function setCurPage(page)
{
var data = $(this).data('fasttable');
data.curPage = parseInt(page);
refresh(data);
}
function checkedIds(data)
{
var checkedRows = data.checkedRows;
var checkedIds = [];
for (var i = 0; i < data.content.length; i++)
{
var id = data.content[i].id;
if (checkedRows[id])
{
checkedIds.push(id);
}
}
return checkedIds;
}
function titleCheckRedraw(data)
{
var filteredContent = data.filteredContent;
var checkedRows = data.checkedRows;
var hasSelectedItems = false;
var hasUnselectedItems = false;
for (var i = 0; i < filteredContent.length; i++)
{
if (checkedRows[filteredContent[i].id])
{
hasSelectedItems = true;
}
else
{
hasUnselectedItems = true;
}
}
var headerRow = $('thead > tr', data.target);
if (hasSelectedItems && hasUnselectedItems)
{
headerRow.removeClass('checked').addClass('checkremove');
}
else if (hasSelectedItems)
{
headerRow.removeClass('checkremove').addClass('checked');
}
else
{
headerRow.removeClass('checked').removeClass('checkremove');
}
}
function itemCheckClick(data, event)
{
var checkmark = $(event.target).hasClass('check');
if (data.dragging || (!checkmark && !data.config.rowSelect))
{
return;
}
var row = $(event.target).closest('tr', data.target)[0];
var id = row.fasttableID;
var doToggle = true;
var checkedRows = data.checkedRows;
if (event.shiftKey && data.lastClickedRowID != null)
{
var checked = checkedRows[id];
doToggle = !checkRange(data, id, data.lastClickedRowID, !checked);
}
if (doToggle)
{
toggleCheck(data, id);
}
data.lastClickedRowID = id;
refresh(data);
}
function titleCheckClick(data, event)
{
var checkmark = $(event.target).hasClass('check');
if (data.dragging || (!checkmark && !data.config.rowSelect))
{
return;
}
var filteredContent = data.filteredContent;
var checkedRows = data.checkedRows;
var hasSelectedItems = false;
for (var i = 0; i < filteredContent.length; i++)
{
if (checkedRows[filteredContent[i].id])
{
hasSelectedItems = true;
break;
}
}
data.lastClickedRowID = null;
checkAll(data, !hasSelectedItems);
}
function toggleCheck(data, id)
{
var checkedRows = data.checkedRows;
var index = checkedRows[id];
if (checkedRows[id])
{
checkedRows[id] = undefined;
data.checkedCount--;
}
else
{
checkedRows[id] = true;
data.checkedCount++;
}
}
function checkAll(data, checked)
{
var filteredContent = data.filteredContent;
for (var i = 0; i < filteredContent.length; i++)
{
checkRow(data, filteredContent[i].id, checked);
}
refresh(data);
}
function checkRange(data, from, to, checked)
{
var filteredContent = data.filteredContent;
var indexFrom = indexOfID(filteredContent, from);
var indexTo = indexOfID(filteredContent, to);
if (indexFrom === -1 || indexTo === -1)
{
return false;
}
if (indexTo < indexFrom)
{
var tmp = indexTo; indexTo = indexFrom; indexFrom = tmp;
}
for (var i = indexFrom; i <= indexTo; i++)
{
checkRow(data, filteredContent[i].id, checked);
}
return true;
}
function checkRow(data, id, checked)
{
if (checked)
{
if (!data.checkedRows[id])
{
data.checkedCount++;
}
data.checkedRows[id] = true;
}
else
{
if (data.checkedRows[id])
{
data.checkedCount--;
}
data.checkedRows[id] = undefined;
}
}
function indexOfID(content, id)
{
for (var i = 0; i < content.length; i++)
{
if (id === content[i].id)
{
return i;
}
}
return -1;
}
function validateChecks(data)
{
var filteredContent = data.filteredContent;
var checkedRows = data.checkedRows;
data.checkedRows = {}
data.checkedCount = 0;
for (var i = 0; i < data.content.length; i++)
{
if (checkedRows[data.content[i].id])
{
data.checkedRows[data.content[i].id] = true;
data.checkedCount++;
}
}
}
//*************** DRAG-N-DROP
function initDragDrop(data)
{
data.target[0].addEventListener('mousedown', function(e) { mouseDown(data, e); }, true);
data.target[0].addEventListener('touchstart', function(e) { mouseDown(data, e); }, true);
data.moveIds = [];
data.dropAfter = false;
data.dropId = null;
data.dragging = false;
data.dragRow = $('');
data.cancelDrag = false;
data.downPos = null;
data.blinkIds = [];
data.blinkState = null;
data.wantBlink = false;
}
function touchToMouse(e)
{
if (e.type === 'touchstart' || e.type === 'touchmove' || e.type === 'touchend')
{
e.clientX = e.changedTouches[0].clientX;
e.clientY = e.changedTouches[0].clientY;
}
}
function mouseDown(data, e)
{
data.dragging = false;
data.dropId = null;
data.dragRow = $(e.target).closest('tr', data.target);
var checkmark = $(e.target).hasClass('check') ||
($(e.target).find('.check').length > 0 && !$('body').hasClass('phone'));
var head = $(e.target).closest('tr', data.target).parent().is('thead');
if (head || !(checkmark || (data.config.rowSelect && e.type === 'mousedown')) ||
data.dragRow.length != 1 || e.ctrlKey || e.altKey || e.metaKey)
{
return;
}
touchToMouse(e);
if (e.type === 'mousedown')
{
e.preventDefault();
}
if (!data.config.dragEndCallback)
{
return;
}
data.downPos = { x: e.clientX, y: e.clientY };
data.mouseMove = function(e) { mouseMove(data, e); };
data.mouseUp = function(e) { mouseUp(data, e); };
data.keyDown = function(e) { keyDown(data, e); };
document.addEventListener('mousemove', data.mouseMove, true);
document.addEventListener('touchmove', data.mouseMove, true);
document.addEventListener('mouseup', data.mouseUp, true);
document.addEventListener('touchend', data.mouseUp, true);
document.addEventListener('touchcancel', data.mouseUp, true);
document.addEventListener('keydown', data.keyDown, true);
}
function mouseMove(data, e)
{
touchToMouse(e);
e.preventDefault();
if (e.touches && e.touches.length > 1)
{
data.cancelDrag = true;
mouseUp(data, e);
return;
}
if (!data.dragging)
{
if (Math.abs(data.downPos.x - e.clientX) < 5 &&
Math.abs(data.downPos.y - e.clientY) < 5)
{
return;
}
startDrag(data, e);
if (data.dragCancel)
{
mouseUp(data, e);
return;
}
}
updateDrag(data, e.clientX, e.clientY);
autoScroll(data, e.clientX, e.clientY);
}
function startDrag(data, e)
{
if (data.config.dragStartCallback)
{
data.config.dragStartCallback();
}
var offsetX = $(document).scrollLeft();
var offsetY = $(document).scrollTop();
var rf = data.dragRow.offset();
data.dragOffset = { x: data.downPos.x - rf.left + offsetX,
y: Math.min(Math.max(data.downPos.y - rf.top + offsetY, 0), data.dragRow.height()) };
var checkedRows = data.checkedRows;
var chkIds = checkedIds(data);
var id = data.dragRow[0].fasttableID;
data.moveIds = checkedRows[id] ? chkIds : [id];
data.dragging = true;
data.cancelDrag = false;
buildDragBox(data);
data.config.dragBox.css('display', 'block');
data.dragRow.addClass('drag-source');
$('html').addClass('drag-progress');
data.oldOverflowX = $('body').css('overflow-x');
$('body').css('overflow-x', 'hidden');
}
function buildDragBox(data)
{
var tr = data.dragRow.clone();
var table = data.target.clone();
$('tr', table).remove();
$('thead', table).remove();
$('tbody', table).append(tr);
table.css('margin', 0);
data.config.dragContent.html(table);
data.config.dragBadge.text(data.moveIds.length);
data.config.dragBadge.css('display', data.moveIds.length > 1 ? 'block' : 'none');
data.config.dragBox.css({left: data.target.offset().left, width: data.dragRow.width()});
var tds = $('td', tr);
$('td', data.dragRow).each(function(ind, el) { $(tds[ind]).css('width', $(el).width()); });
}
function updateDrag(data, x, y)
{
var offsetX = $(document).scrollLeft();
var offsetY = $(document).scrollTop();
var posX = x + offsetX;
var posY = y + offsetY;
data.config.dragBox.css({
left: posX - data.dragOffset.x,
top: Math.max(Math.min(posY - data.dragOffset.y, offsetY + $(window).height() - data.config.dragBox.height() - 2), offsetY + 2)});
var dt = data.config.dragBox.offset().top;
var dh = data.config.dragBox.height();
var rows = $('tbody > tr', data.target);
for (var i = 0; i < rows.length; i++)
{
var row = $(rows[i]);
var rt = row.offset().top;
var rh = row.height();
if (row[0] !== data.dragRow[0])
{
if ((dt >= rt && dt <= rt + rh / 2) ||
(dt < rt && i == 0))
{
data.dropAfter = false;
row.before(data.dragRow);
data.dropId = row[0].fasttableID;
break;
}
if ((dt + dh >= rt + rh / 2 && dt + dh <= rt + rh) ||
(dt + dh > rt + rh && i === rows.length - 1))
{
data.dropAfter = true;
row.after(data.dragRow);
data.dropId = row[0].fasttableID;
break;
}
}
}
if (data.dropId === null)
{
data.dropId = data.dragRow[0].fasttableID;
data.dropAfter = true;
}
}
function autoScroll(data, x, y)
{
// works properly only if the table lays directly on the page (not in another scrollable div)
data.scrollStep = (y > $(window).height() - 20 ? 1 : y < 20 ? -1 : 0) * 5;
if (data.scrollStep !== 0 && !data.scrollTimer)
{
var scroll = function()
{
$(document).scrollTop($(document).scrollTop() + data.scrollStep);
updateDrag(data, x, y + data.scrollStep);
data.scrollTimer = data.scrollStep == 0 ? null : setTimeout(scroll, 10);
}
data.scrollTimer = setTimeout(scroll, 500);
}
}
function mouseUp(data, e)
{
document.removeEventListener('mousemove', data.mouseMove, true);
document.removeEventListener('touchmove', data.mouseMove, true);
document.removeEventListener('mouseup', data.mouseUp, true);
document.removeEventListener('touchend', data.mouseUp, true);
document.removeEventListener('touchcancel', data.mouseUp, true);
document.removeEventListener('keydown', data.keyDown, true);
if (!data.dragging)
{
return;
}
data.dragging = false;
data.cancelDrag = data.cancelDrag || e.type === 'touchcancel';
data.dragRow.removeClass('drag-source');
$('html').removeClass('drag-progress');
$('body').css('overflow-x', data.oldOverflowX);
data.config.dragBox.hide();
data.scrollStep = 0;
clearTimeout(data.scrollTimer);
data.scrollTimer = null;
moveRecords(data);
}
function keyDown(data, e)
{
if (e.keyCode == 27) // ESC-key
{
data.cancelDrag = true;
e.preventDefault();
mouseUp(data, e);
}
}
function moveRecords(data)
{
if (data.dropId !== null && !data.cancelDrag &&
!(data.moveIds.length == 1 && data.dropId == data.moveIds[0]))
{
data.blinkIds = data.moveIds;
data.moveIds = [];
data.blinkState = data.config.dragBlink === 'none' ? 0 : 3;
data.wantBlink = data.blinkState > 0;
moveRows(data);
}
else
{
data.dropId = null;
}
if (data.dropId === null)
{
data.moveIds = [];
}
refresh(data);
data.config.dragEndCallback(data.dropId !== null ?
{
ids: data.blinkIds,
position: data.dropId,
direction: data.dropAfter ? 'after' : 'before'
} : null);
if (data.config.dragBlink === 'direct')
{
data.target.fasttable('update');
}
}
function moveRows(data)
{
var movedIds = data.blinkIds;
var movedRecords = [];
for (var i = 0; i < data.content.length; i++)
{
var item = data.content[i];
if (movedIds.indexOf(item.id) > -1)
{
movedRecords.push(item);
data.content.splice(i, 1);
i--;
if (item.id === data.dropId)
{
if (i >= 0)
{
data.dropId = data.content[i].id;
data.dropAfter = true;
}
else if (i + 1 < data.content.length)
{
data.dropId = data.content[i + 1].id;
data.dropAfter = false;
}
else
{
data.dropId = null;
}
}
}
}
if (data.dropId === null)
{
// restore content
for (var j = 0; j < movedRecords.length; j++)
{
data.content.push(movedRecords[j]);
}
return;
}
for (var i = 0; i < data.content.length; i++)
{
if (data.content[i].id === data.dropId)
{
for (var j = movedRecords.length - 1; j >= 0; j--)
{
data.content.splice(data.dropAfter ? i + 1 : i, 0, movedRecords[j]);
}
break;
}
}
}
function blinkMovedRecords(data)
{
if (data.blinkIds.length > 0)
{
blinkProgress(data, data.wantBlink);
data.wantBlink = false;
}
}
function blinkProgress(data, recur)
{
var rows = $('tr', data.target);
rows.removeClass('drag-finish');
rows.each(function(ind, el)
{
var id = el.fasttableID;
if (data.blinkIds.indexOf(id) > -1 &&
(data.blinkState === 1 || data.blinkState === 3 || data.blinkState === 5))
{
$(el).addClass('drag-finish');
}
});
if (recur && data.blinkState > 0)
{
setTimeout(function()
{
data.blinkState -= 1;
blinkProgress(data, true);
},
150);
}
if (data.blinkState === 0)
{
data.blinkIds = [];
}
}
//*************** KEYBOARD
function processShortcut(data, key)
{
switch (key)
{
case 'Left': data.curPage = Math.max(data.curPage - 1, 1); refresh(data); return true;
case 'Shift+Left': data.curPage = 1; refresh(data); return true;
case 'Right': data.curPage = Math.min(data.curPage + 1, data.pageCount); refresh(data); return true;
case 'Shift+Right': data.curPage = data.pageCount; refresh(data); return true;
case 'Shift+F': data.config.filterInput.focus(); return true;
case 'Shift+C': data.config.filterClearButton.click(); return true;
}
}
//*************** CONFIG
var defaults =
{
filterInput: '#TableFilter',
filterClearButton: '#TableClear',
pagerContainer: '#TablePager',
infoContainer: '#TableInfo',
dragBox: '#TableDragBox',
dragContent: '#TableDragContent',
dragBadge: '#TableDragBadge',
dragBlink: 'none', // none, direct, update
pageSize: 10,
maxPages: 5,
pageDots: true,
rowSelect: false,
shortcuts: false,
infoEmpty: 'No records',
renderRowCallback: undefined,
renderCellCallback: undefined,
renderTableCallback: undefined,
fillFieldsCallback: undefined,
updateInfoCallback: undefined,
filterInputCallback: undefined,
filterClearCallback: undefined,
fillSearchCallback: undefined,
filterCallback: undefined,
dragStartCallback: undefined,
dragEndCallback: undefined
};
})(jQuery);
function FastSearcher()
{
'use strict';
this.source;
this.len;
this.p;
this.initLexer = function(source)
{
this.source = source;
this.len = source.length;
this.p = 0;
}
this.nextToken = function()
{
while (this.p < this.len)
{
var ch = this.source[this.p++];
switch (ch) {
case ' ':
case '\t':
continue;
case '-':
case '(':
case ')':
case '|':
return ch;
default:
this.p--;
var token = '';
var quote = false;
while (this.p < this.len)
{
var ch = this.source[this.p++];
if (quote)
{
if (ch === '"')
{
quote = false;
ch = '';
}
}
else
{
if (ch === '"')
{
quote = true;
ch = '';
}
else if (' \t()|'.indexOf(ch) > -1)
{
this.p--;
return token;
}
}
token += ch;
}
return token;
}
}
return null;
}
this.compile = function(searchstr)
{
var _this = this;
this.initLexer(searchstr);
function expression(greedy)
{
var node = null;
while (true)
{
var token = _this.nextToken();
var node2 = null;
switch (token)
{
case null:
case ')':
return node;
case '-':
node2 = expression(false);
node2 = node2 ? _this.not(node2) : node2;
break;
case '(':
node2 = expression(true);
break;
case '|':
node2 = expression(false);
break;
default:
node2 = _this.term(token);
}
if (node && node2)
{
node = token === '|' ? _this.or(node, node2) : _this.and(node, node2);
}
else if (node2)
{
node = node2;
}
if (!greedy && node)
{
return node;
}
}
}
this.root = expression(true);
}
this.root = null;
this.data = null;
this.exec = function(data) {
this.data = data;
return this.root ? this.root.eval() : true;
}
this.and = function(L, R) {
return {
L: L, R: R,
eval: function() { return this.L.eval() && this.R.eval(); }
};
}
this.or = function(L, R) {
return {
L: L, R: R,
eval: function() { return this.L.eval() || this.R.eval(); }
};
}
this.not = function(M) {
return {
M: M,
eval: function() { return !this.M.eval();}
};
}
this.term = function(term) {
return this.compileTerm(term);
}
var COMMANDS = [ ':', '>=', '<=', '<>', '>', '<', '=' ];
this.compileTerm = function(term) {
var _this = this;
var text = term.toLowerCase();
var field;
var command;
var commandIndex;
for (var i = 0; i < COMMANDS.length; i++)
{
var cmd = COMMANDS[i];
var p = term.indexOf(cmd);
if (p > -1 && (p < commandIndex || commandIndex === undefined))
{
commandIndex = p;
command = cmd;
}
}
if (command !== undefined)
{
field = term.substring(0, commandIndex);
text = text.substring(commandIndex + command.length);
}
return {
command: command,
text: text,
field: field,
eval: function() { return _this.evalTerm(this); }
};
}
this.evalTerm = function(term) {
var text = term.text;
var field = term.field;
var content = this.fieldValue(this.data, field);
if (content === undefined)
{
return false;
}
switch (term.command)
{
case undefined:
case ':':
return content.toString().toLowerCase().indexOf(text) > -1;
case '=':
return content.toString().toLowerCase() == text;
case '<>':
return content.toString().toLowerCase() != text;
case '>':
return parseInt(content) > parseInt(text);
case '>=':
return parseInt(content) >= parseInt(text);
case '<':
return parseInt(content) < parseInt(text);
case '<=':
return parseInt(content) <= parseInt(text);
default:
return false;
}
}
this.fieldValue = function(data, field) {
var value = '';
if (field !== undefined)
{
value = data[field];
if (value === undefined)
{
if (this.nameMap === undefined)
{
this.buildNameMap(data);
}
value = data[this.nameMap[field.toLowerCase()]];
}
}
else
{
if (data._search === true)
{
for (var prop in data)
{
value += ' ' + data[prop];
}
}
else
{
for (var i = 0; i < data._search.length; i++)
{
value += ' ' + data[data._search[i]];
}
}
}
return value;
}
this.nameMap;
this.buildNameMap = function(data)
{
this.nameMap = {};
for (var prop in data)
{
this.nameMap[prop.toLowerCase()] = prop;
}
}
}