n-gallery
Version:
A nodejs web image viewer/gallery using local image directory
661 lines (489 loc) • 18.7 kB
JavaScript
/** ========================================================================
* Created by song on 14-1-22.
* =========================================================================
* 该类用于选中页面上的元素,支持单选、单个取消、多选(多次单选),全选、反选、全部取消。
* 支持对单个或多个元素及其对应的本地文件进行删除操作,支持更新徽标。
* ========================================================================= */
var SelectItems = (function(global) {
"use strict";
function SelectItems(selector, id_prefix) {
// 选中的元素的id属性中的数字
this.itemSelected = [];
// id前缀(类型)
this.id_prefix = this.type = id_prefix;
// 需要用到的各个jquery选择器,带_class后缀的仅为类名,不带.号
this.sel = {
'item': selector['item'],
'wrapper': selector['wrapper'],
'selected_class': selector['selected_class'],
'wrapper_selected_class': 'img-wrapper-selected',
'del_pop': selector['del_pop'],
'badge_selected': selector['badge_selected'],
'badge_total': selector['badge_total']
};
this.selector = this.sel['item'];
// 将实际的DOM节点绑定到对应的操作上
this.oprationSelector = null;
// 元素总数(包括选中和未选中的)
this.itemTotal = $(this.selector).length;
// 错误消息
this.error_msg = {
0: '未知错误',
1: '未选中任何元素'
}
}
SelectItems.prototype.selectOne = function(selector, shiftKey) {
// 选中/取消一个元素
// ==============
var _this = this,
$el = $(selector),
selClass = _this.sel['selected_class'],
wrapperSelClass = _this.sel['wrapper_selected_class'],
// 只存id中的数字
id = +$el.attr('id').slice(_this.id_prefix.length),
// 上一次单击选中的元素
last = _this.itemSelected[_this.itemSelected.length - 1];
// 按住shift键+单击,连续多选。如果之前并未有任何元素被选中,则作为普通的选中事件
if (shiftKey && last !== undefined) {
var start = Math.min(last, id),
end = Math.max(last, id),
count = 0;
for (var i = start; i <= end; i++) {
if (_this.itemSelected.indexOf(i) != -1) continue;
_this.itemSelected.push(i);
count++;
$('#' + _this.id_prefix + i).addClass(selClass);
$('#' + _this.id_prefix + i).parent().addClass(wrapperSelClass);
}
} else {
if (!$el.hasClass(selClass)) {
$el.addClass(selClass);
$el.parent().addClass(wrapperSelClass);
_this.itemSelected.push(id);
} else {
$el.removeClass(selClass);
$el.parent().removeClass(wrapperSelClass);
_this.itemSelected.splice(_this.itemSelected.indexOf(id), 1);
}
}
_this.updateBadge();
};
SelectItems.prototype.reverseSelect = function() {
// 反选
// ===
var _this = this,
selClass = this.sel['selected_class'],
wrapperSelClass = _this.sel['wrapper_selected_class'],
hash = {};
// 取消已选
this.itemSelected.forEach(function(id) {
$('#' + _this.id_prefix + id).removeClass(selClass);
$('#' + _this.id_prefix + id).parent().removeClass(wrapperSelClass);
hash[+id] = true;
});
this.itemSelected.length = 0;
// 增加未选
$(this.selector).each(function () {
var id = $(this).attr('id');
id = parseInt(/\D*(\d+)/.exec(id)[1]);
if (!hash[id]) {
$('#' + _this.id_prefix + id).addClass(selClass);
$('#' + _this.id_prefix + id).parent().addClass(wrapperSelClass);
_this.itemSelected.push(id);
}
});
this.updateBadge();
};
SelectItems.prototype.allSelect = function() {
// 全选
// ===
var _this = this;
// 已选中的清除掉
this.itemSelected.length = 0;
$(this.selector).each(function () {
$(this).addClass(_this.sel['selected_class']);
$(this).parent().addClass(_this.sel['wrapper_selected_class']);
_this.itemSelected.push(+$(this).attr('id').slice(_this.id_prefix.length));
});
this.updateBadge();
};
SelectItems.prototype.cancelAllSelect = function() {
// 取消全部选择
// ==========
var _this = this;
$(this.selector).each(function () {
$(this).removeClass(_this.sel['selected_class']);
$(this).parent().removeClass(_this.sel['wrapper_selected_class']);
});
this.itemSelected.length = 0;
this.updateBadge();
};
SelectItems.prototype.deleteOne = function(selector, callback) {
// 删除单个文件(网页上元素和本地文件均删除)
// ===================================
var _this = this,
$img = $(selector['item']),
success = false;
$.ajaxSetup({async: false});
$.get('/operationHandler', {pic_path: callback($img)}, function(data) {
if (data.status == 'OK') {
$img.closest(_this.sel['wrapper']).hide('500', function () {
this.remove();
});
_this.itemTotal--;
_this.updateBadge();
success = true;
} else {
if (selector['popover']) {
var popover = selector['popover'];
_this.popover(popover, 2000, {'data-content': data.msg, 'data-placement': 'top'});
}
}
});
return success;
};
/**
*
* @param callback 将单个的item中需要的信息提取出来,以便利用ajax发送给服务器
* @param query Ajax请求中query string里的字段名,如: a?foo=bar,此时query为foo
* @param type album, pic等类型,每种类型对应的操作有一些小区别
* @returns {boolean}
*/
SelectItems.prototype.deleteSelect = function(callback, query, type) {
// 删除选中项
// =========
var _this = this,
popover = this.sel['del_pop'],
wrapper = this.sel['wrapper'],
items = this.itemSelected.map(callback),
ajaxData = {},
success = false;
if (!this.itemSelected.length) {
if (popover) {
_this.popover(popover, 2000, {'data-content': _this.error_msg[1]});
}
return false;
}
$.ajaxSetup({async: false});
ajaxData[query] = JSON.stringify(items);
$.get('/operationHandler', ajaxData, function(data) {
if (data.status == 'OK') {
var last = _this.itemSelected[_this.itemSelected.length - 1],
nextLast = $('#' + _this.id_prefix + last).closest(wrapper).next();
_this.itemSelected.forEach(function (id) {
var $delItem = $('#' + _this.id_prefix + id).closest(wrapper);
if (type == 'album') {
// Duitang Waterfall Woo
var ht = $delItem.data('ht'),
cls = $delItem.attr('class'),
tp = parseInt($delItem[0].style.top),
m = /\bsc(\d+) \bco(\d+)/i.exec(cls),
sc = m[1],
co = m[2];
$.Woo.resetCol(-ht, sc, co, tp);
}
$delItem.remove();
});
_this.itemTotal -= _this.itemSelected.length;
_this.itemSelected.length = 0;
// 页面跳转到被删的最后一个元素的下一个
if (type == 'pic') {
// 不是最后一个元素
if (nextLast.length) {
$('html,body').animate({scrollTop: nextLast.offset().top}, 500);
}
}
if (popover) {
_this.popover(popover, 2000, {'data-content': "Delete Success!"});
}
_this.updateBadge();
success = true;
} else {
if (popover) {
_this.popover(popover, 2000, {'data-content': data.msg});
}
}
});
return success;
};
SelectItems.prototype.updateBadge = function() {
// 辅助函数:更新徽标
// ===============
var badgeS = this.sel['badge_selected'],
badgeT = this.sel['badge_total'];
if (badgeS) {
$(badgeS).text(this.itemSelected.length);
}
if (badgeT) {
$(badgeT).text(this.itemTotal);
}
};
SelectItems.prototype.popover = function(selector, delay, option) {
// 辅助函数:显示提示框
// =================
var $popover = $(selector);
$popover.attr(option);
$popover.popover('show');
if (delay) {
setTimeout(function () {
$popover.popover('destroy')
}, delay);
}
};
SelectItems.prototype.bindOperation = function(selectors) {
// 可选,绑定事件操作,通过点击按钮触发相应的操作
// =======================================
// var operationSelector = {
// select_one: '.img-item',
// select_reverse: '#reverse-selected',
// select_all: '#all-selected',
// select_all_cancel: '#cancel-selected',
// select_delete: '#del-selected',
// select_delete_modal: '#delete-modal',
// select_delete_one: '.img-opr'
// };
var _this = this;
this.oprationSelector = selectors;
// click to select one
selectors.select_one && $(selectors.select_one).click(function(e) {
_this.selectOne($(this).find('img'), e.shiftKey);
e.preventDefault();
});
// select all
selectors.select_all && $(selectors.select_all).click(function(e) {
_this.allSelect();
e.preventDefault();
});
// cancel all selected
selectors.select_all_cancel && $(selectors.select_all_cancel).click(function(e) {
_this.cancelAllSelect();
e.preventDefault();
});
// reverse selected
selectors.select_reverse && $(selectors.select_reverse).click(function(e) {
_this.reverseSelect();
e.preventDefault();
});
// delete selected images
$('#delete-OK').click(function(e) {
e.preventDefault();
var success;
// pic
if (_this.type == 'pic') {
success = _this.deleteSelect(function(id) {
var src = $('#' + _this.id_prefix + id).attr('src');
return src.slice(7); // strip '/local/'
}, 'pics_path', _this.type);
}
// album
else if (_this.type == 'album') {
success = _this.deleteSelect(function (id) {
var href = $('#' + _this.id_prefix + id).closest('.cover').attr('href');
return {type: href.slice(1, href.indexOf('/', 1)), path: href.slice(href.indexOf('/', 1) + 1)};
}, 'albums', _this.type);
}
if (success) {
$(document).trigger('deleteSelectSuccess');
} else {
$(document).trigger('deleteSelectFail');
}
});
// shortcut key enter to confirm delete operation
var $modal = $(selectors.select_delete_modal);
$modal.bind('keydown', 'return', function() {
$('#delete-OK').click();
});
// delete button, show delete modal first. Images will be deleted after user comfirmation
selectors.select_delete && $(selectors.select_delete).click(function(e) {
e.preventDefault();
var success;
if (_this.type == 'pic') {
// delete confirmation warn
var $modal = $(selectors.select_delete_modal);
if ($modal.length) {
$modal.find('.modal-body').html(_this.itemSelected.length + ' images in total');
$modal.modal();
return;
} else {
success = _this.deleteSelect(function(id) {
var src = $('#' + _this.id_prefix + id).attr('src');
return src.slice(7); // strip '/local/'
}, 'pics_path', _this.type);
}
}
else if (_this.type == 'album') {
// delete confirmation warn
var $modal = $(selectors.select_delete_modal);
if ($modal.length) {
var albums_path_html = '',
folder_alert_html = '<span class="alert-danger" style="padding: 5px;margin-left: 20px;">Contains sub folders</span>';
_this.itemSelected.forEach(function (id) {
var href = $('#' + _this.id_prefix + id).closest('.cover').attr('href');
var pos = href.indexOf('/', 1);
albums_path_html += '<p><span class="delete-modal-warn">'
+ href.slice(pos + 1)
+ '</span>'
+ (href.slice(1, pos) == 'folder' ? folder_alert_html : '')
+ '</p>';
});
$modal.find('.modal-body').html(albums_path_html);
$modal.modal();
return;
} else {
success = _this.deleteSelect(function(id) {
var href = $('#' + _this.id_prefix + id).closest('.cover').attr('href');
return {type: href.slice(1, href.indexOf('/', 1)), path: href.slice(href.indexOf('/', 1) + 1)};
}, 'albums', _this.type);
}
}
if (success) {
$(document).trigger('deleteSelectSuccess');
} else {
$(document).trigger('deleteSelectFail');
}
});
// delete one image
selectors.select_delete_one && $(selectors.select_delete_one).click(function (e) {
e.preventDefault();
e.stopPropagation();
var $img = $(this).prev('a.img').find('img');
var success = _this.deleteOne({item: $img, popover: this}, function($img) {
var src = $img.attr('src');
return src.slice(7); // strip '/local/'
});
if (success) {
$(document).trigger('deleteOneSuccess');
} else {
$(document).trigger('deleteOneFail');
}
});
};
SelectItems.prototype.bindHotKeys = function() {
// Optional,bind the shortcut keys
// must be done after bindOperation function
// =========================================
var selectors = this.oprationSelector,
hotKeys = {
'ctrl+r': 'select_reverse',
'ctrl+a': 'select_all',
'ctrl+del': 'select_delete',
'esc': 'select_all_cancel'
};
for (var key in hotKeys) {
(function() {
var selector = selectors[hotKeys[key]];
if (selector) {
$(document).bind('keydown', key, function(e) {
e.preventDefault();
$(selector).click();
});
}
}());
}
};
SelectItems.prototype.unBindHotKeys = function() {
$(document).unbind('keydown');
};
global.selectItems = function selectItems(selector, id_prefix) {
return new SelectItems(selector, id_prefix);
};
return SelectItems;
}(this));
/** ========================================================================
* Created by song on 14-1-22.
* =========================================================================
* 该类用于作为一个计数牌,显示当前屏幕所对应的图片为第几张。
* ========================================================================= */
var ItemScroll = (function () {
function ItemScroll(options) {
// item selector
this.selector = options.selector;
// badge selector
this.badge_sel = options.badge_sel;
// 不同元素节点间的margin
this.margin = options.margin;
// 元素节点的位置偏移量
this.nodes = [];
// 当前屏幕元素的id号
this.currentNum = 1;
}
ItemScroll.prototype.refresh = function () {
// 重新计算所有元素
// =============
var _this = this;
// 记录所有节点的 top offset, bottom offset 和 id
this.nodes = [];
$(_this.selector).each(function () {
var topOffset = $(this).offset().top;
_this.nodes.push({
top: topOffset,
bottom: topOffset + $(this).outerHeight() + _this.margin, // 算上了margin-bottom
id: $(this).attr('id')
});
});
// 更新currentNum和badge_sel
this.process();
};
ItemScroll.prototype.process = function () {
// 更新当前的badge
// =============
var windowHeight = window.innerHeight,
baseline = $(document).scrollTop() + windowHeight / 2, // 图片包含baseline计数牌就为当前的图片的顺序数
nodes = this.nodes;
function isNthNodeInCurWindow(n) { // index from 1
return n > 0 && n <= nodes.length
&& baseline <= nodes[n-1].bottom && baseline >= nodes[n-1].top;
}
if (isNthNodeInCurWindow(this.currentNum)) {
// Do nothing
} else if (isNthNodeInCurWindow(this.currentNum - 1)) {
this.currentNum--;
} else if (isNthNodeInCurWindow(this.currentNum + 1)) {
this.currentNum++;
} else {
for (var i = 1; i <= nodes.length; i++) {
if (isNthNodeInCurWindow(i)) {
this.currentNum = i;
break;
}
}
}
$(this.badge_sel).text(this.currentNum);
};
ItemScroll.prototype.scrollN = function (n) {
// 向上/下滚动N张图片,N为正则向下,为负向上
// ===================================
var nodes = this.nodes,
num = this.currentNum + +n;
if (!n || num < 1 || num > nodes.length) {
return;
}
var node = nodes[num-1],
windowHeight = window.innerHeight,
// 1/2(top + bottom) - 1/2*windowHeight
scrTop = (node.top + node.bottom - this.margin - windowHeight) / 2;
$('body').animate({scrollTop: scrTop}, 200);
};
ItemScroll.prototype.scrollToN = function (n) {
// 滚动到第N张
// =========
n = parseInt(n);
if (isNaN(n) || n < 1 || n > this.nodes.length) {
return false;
}
var node = this.nodes[n-1];
// 当滚动的距离太远时,为了效率,取消动画
if (Math.abs(this.currentNum - n) > 10) {
$(document).scrollTop(node.top - 50);
return true;
}
$('body').animate({scrollTop: node.top - 50}, 200);
return true;
};
ItemScroll.prototype.getCurrentNode = function() {
// 辅助函数: 或许当前窗口内显示的节点对象
// =================================
return this.nodes[this.currentNum-1];
};
return ItemScroll;
}());