mirror of
https://github.com/the-djmaze/snappymail.git
synced 2024-11-10 17:13:38 +08:00
761 lines
16 KiB
JavaScript
761 lines
16 KiB
JavaScript
|
|
import $ from '$';
|
|
import _ from '_';
|
|
import key from 'key';
|
|
import ko from 'ko';
|
|
import {EventKeyCode} from 'Common/Enums';
|
|
import {isArray, inArray, noop, noopTrue} from 'Common/Utils';
|
|
|
|
class Selector
|
|
{
|
|
/**
|
|
* @constructor
|
|
* @param {koProperty} koList
|
|
* @param {koProperty} koSelectedItem
|
|
* @param {koProperty} koFocusedItem
|
|
* @param {string} sItemSelector
|
|
* @param {string} sItemSelectedSelector
|
|
* @param {string} sItemCheckedSelector
|
|
* @param {string} sItemFocusedSelector
|
|
*/
|
|
constructor(koList, koSelectedItem, koFocusedItem,
|
|
sItemSelector, sItemSelectedSelector, sItemCheckedSelector, sItemFocusedSelector)
|
|
{
|
|
this.list = koList;
|
|
|
|
this.listChecked = ko.computed(() => _.filter(this.list(), (item) => item.checked())).extend({rateLimit: 0});
|
|
this.isListChecked = ko.computed(() => 0 < this.listChecked().length);
|
|
|
|
this.focusedItem = koFocusedItem || ko.observable(null);
|
|
this.selectedItem = koSelectedItem || ko.observable(null);
|
|
this.selectedItemUseCallback = true;
|
|
|
|
this.itemSelectedThrottle = _.debounce(_.bind(this.itemSelected, this), 300);
|
|
|
|
this.listChecked.subscribe((items) => {
|
|
if (0 < items.length)
|
|
{
|
|
if (null === this.selectedItem())
|
|
{
|
|
if (this.selectedItem.valueHasMutated)
|
|
{
|
|
this.selectedItem.valueHasMutated();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
this.selectedItem(null);
|
|
}
|
|
}
|
|
else if (this.autoSelect() && this.focusedItem())
|
|
{
|
|
this.selectedItem(this.focusedItem());
|
|
}
|
|
}, this);
|
|
|
|
this.selectedItem.subscribe((item) => {
|
|
|
|
if (item)
|
|
{
|
|
if (this.isListChecked())
|
|
{
|
|
_.each(this.listChecked(), (subItem) => {
|
|
subItem.checked(false);
|
|
});
|
|
}
|
|
|
|
if (this.selectedItemUseCallback)
|
|
{
|
|
this.itemSelectedThrottle(item);
|
|
}
|
|
}
|
|
else if (this.selectedItemUseCallback)
|
|
{
|
|
this.itemSelected(null);
|
|
}
|
|
|
|
}, this);
|
|
|
|
this.selectedItem = this.selectedItem.extend({toggleSubscribe: [null,
|
|
(prev) => {
|
|
if (prev)
|
|
{
|
|
prev.selected(false);
|
|
}
|
|
}, (next) => {
|
|
if (next)
|
|
{
|
|
next.selected(true);
|
|
}
|
|
}
|
|
]});
|
|
|
|
this.focusedItem = this.focusedItem.extend({toggleSubscribe: [null,
|
|
(prev) => {
|
|
if (prev)
|
|
{
|
|
prev.focused(false);
|
|
}
|
|
}, (next) => {
|
|
if (next)
|
|
{
|
|
next.focused(true);
|
|
}
|
|
}
|
|
]});
|
|
|
|
this.iSelectNextHelper = 0;
|
|
this.iFocusedNextHelper = 0;
|
|
this.oContentVisible = null;
|
|
this.oContentScrollable = null;
|
|
|
|
this.sItemSelector = sItemSelector;
|
|
this.sItemSelectedSelector = sItemSelectedSelector;
|
|
this.sItemCheckedSelector = sItemCheckedSelector;
|
|
this.sItemFocusedSelector = sItemFocusedSelector;
|
|
|
|
this.sLastUid = '';
|
|
this.oCallbacks = {};
|
|
|
|
this.focusedItem.subscribe((item) => {
|
|
if (item)
|
|
{
|
|
this.sLastUid = this.getItemUid(item);
|
|
}
|
|
}, this);
|
|
|
|
let
|
|
aCache = [],
|
|
aCheckedCache = [],
|
|
mFocused = null,
|
|
mSelected = null;
|
|
|
|
this.list.subscribe((items) => {
|
|
|
|
if (isArray(items))
|
|
{
|
|
_.each(items, (item) => {
|
|
if (item)
|
|
{
|
|
const uid = this.getItemUid(item);
|
|
|
|
aCache.push(uid);
|
|
if (item.checked())
|
|
{
|
|
aCheckedCache.push(uid);
|
|
}
|
|
if (null === mFocused && item.focused())
|
|
{
|
|
mFocused = uid;
|
|
}
|
|
if (null === mSelected && item.selected())
|
|
{
|
|
mSelected = uid;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}, this, 'beforeChange');
|
|
|
|
this.list.subscribe((aItems) => {
|
|
|
|
var
|
|
oTemp = null,
|
|
bGetNext = false,
|
|
aUids = [],
|
|
mNextFocused = mFocused,
|
|
bChecked = false,
|
|
bSelected = false,
|
|
iLen = 0;
|
|
|
|
this.selectedItemUseCallback = false;
|
|
|
|
this.focusedItem(null);
|
|
this.selectedItem(null);
|
|
|
|
if (isArray(aItems))
|
|
{
|
|
iLen = aCheckedCache.length;
|
|
|
|
_.each(aItems, (oItem) => {
|
|
|
|
var sUid = this.getItemUid(oItem);
|
|
aUids.push(sUid);
|
|
|
|
if (null !== mFocused && mFocused === sUid)
|
|
{
|
|
this.focusedItem(oItem);
|
|
mFocused = null;
|
|
}
|
|
|
|
if (0 < iLen && -1 < inArray(sUid, aCheckedCache))
|
|
{
|
|
bChecked = true;
|
|
oItem.checked(true);
|
|
iLen -= 1;
|
|
}
|
|
|
|
if (!bChecked && null !== mSelected && mSelected === sUid)
|
|
{
|
|
bSelected = true;
|
|
this.selectedItem(oItem);
|
|
mSelected = null;
|
|
}
|
|
});
|
|
|
|
this.selectedItemUseCallback = true;
|
|
|
|
if (!bChecked && !bSelected && this.autoSelect())
|
|
{
|
|
if (this.focusedItem())
|
|
{
|
|
this.selectedItem(this.focusedItem());
|
|
}
|
|
else if (0 < aItems.length)
|
|
{
|
|
if (null !== mNextFocused)
|
|
{
|
|
bGetNext = false;
|
|
mNextFocused = _.find(aCache, (sUid) => {
|
|
if (bGetNext && -1 < inArray(sUid, aUids))
|
|
{
|
|
return sUid;
|
|
}
|
|
else if (mNextFocused === sUid)
|
|
{
|
|
bGetNext = true;
|
|
}
|
|
return false;
|
|
});
|
|
|
|
if (mNextFocused)
|
|
{
|
|
oTemp = _.find(aItems, (oItem) => mNextFocused === this.getItemUid(oItem));
|
|
}
|
|
}
|
|
|
|
this.selectedItem(oTemp || null);
|
|
this.focusedItem(this.selectedItem());
|
|
}
|
|
}
|
|
|
|
if ((0 !== this.iSelectNextHelper || 0 !== this.iFocusedNextHelper) && 0 < aItems.length && !this.focusedItem())
|
|
{
|
|
oTemp = null;
|
|
if (0 !== this.iFocusedNextHelper)
|
|
{
|
|
oTemp = aItems[-1 === this.iFocusedNextHelper ? aItems.length - 1 : 0] || null;
|
|
}
|
|
|
|
if (!oTemp && 0 !== this.iSelectNextHelper)
|
|
{
|
|
oTemp = aItems[-1 === this.iSelectNextHelper ? aItems.length - 1 : 0] || null;
|
|
}
|
|
|
|
if (oTemp)
|
|
{
|
|
if (0 !== this.iSelectNextHelper)
|
|
{
|
|
this.selectedItem(oTemp || null);
|
|
}
|
|
|
|
this.focusedItem(oTemp || null);
|
|
|
|
this.scrollToFocused();
|
|
|
|
_.delay(() => this.scrollToFocused(), 100);
|
|
}
|
|
|
|
this.iSelectNextHelper = 0;
|
|
this.iFocusedNextHelper = 0;
|
|
}
|
|
}
|
|
|
|
aCache = [];
|
|
aCheckedCache = [];
|
|
mFocused = null;
|
|
mSelected = null;
|
|
|
|
}, this);
|
|
}
|
|
|
|
itemSelected(item) {
|
|
|
|
if (this.isListChecked())
|
|
{
|
|
if (!item)
|
|
{
|
|
(this.oCallbacks.onItemSelect || noop)(item || null);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (item)
|
|
{
|
|
(this.oCallbacks.onItemSelect || noop)(item);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} forceSelect
|
|
*/
|
|
goDown(forceSelect) {
|
|
this.newSelectPosition(EventKeyCode.Down, false, forceSelect);
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} forceSelect
|
|
*/
|
|
goUp(forceSelect) {
|
|
this.newSelectPosition(EventKeyCode.Up, false, forceSelect);
|
|
}
|
|
|
|
unselect() {
|
|
this.selectedItem(null);
|
|
this.focusedItem(null);
|
|
}
|
|
|
|
init(contentVisible, contentScrollable, keyScope = 'all') {
|
|
|
|
this.oContentVisible = contentVisible;
|
|
this.oContentScrollable = contentScrollable;
|
|
|
|
if (this.oContentVisible && this.oContentScrollable)
|
|
{
|
|
$(this.oContentVisible)
|
|
.on('selectstart', (event) => {
|
|
if (event && event.preventDefault)
|
|
{
|
|
event.preventDefault();
|
|
}
|
|
})
|
|
.on('click', this.sItemSelector, (event) => {
|
|
this.actionClick(ko.dataFor(event.currentTarget), event);
|
|
})
|
|
.on('click', this.sItemCheckedSelector, (event) => {
|
|
const item = ko.dataFor(event.currentTarget);
|
|
if (item)
|
|
{
|
|
if (event && event.shiftKey)
|
|
{
|
|
this.actionClick(item, event);
|
|
}
|
|
else
|
|
{
|
|
this.focusedItem(item);
|
|
item.checked(!item.checked());
|
|
}
|
|
}
|
|
});
|
|
|
|
key('enter', keyScope, () => {
|
|
if (this.focusedItem() && !this.focusedItem().selected())
|
|
{
|
|
this.actionClick(this.focusedItem());
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
key('ctrl+up, command+up, ctrl+down, command+down', keyScope, () => false);
|
|
|
|
key('up, shift+up, down, shift+down, home, end, pageup, pagedown, insert, space', keyScope, (event, handler) => {
|
|
if (event && handler && handler.shortcut)
|
|
{
|
|
let eventKey = 0;
|
|
switch (handler.shortcut)
|
|
{
|
|
case 'up':
|
|
case 'shift+up':
|
|
eventKey = EventKeyCode.Up;
|
|
break;
|
|
case 'down':
|
|
case 'shift+down':
|
|
eventKey = EventKeyCode.Down;
|
|
break;
|
|
case 'insert':
|
|
eventKey = EventKeyCode.Insert;
|
|
break;
|
|
case 'space':
|
|
eventKey = EventKeyCode.Space;
|
|
break;
|
|
case 'home':
|
|
eventKey = EventKeyCode.Home;
|
|
break;
|
|
case 'end':
|
|
eventKey = EventKeyCode.End;
|
|
break;
|
|
case 'pageup':
|
|
eventKey = EventKeyCode.PageUp;
|
|
break;
|
|
case 'pagedown':
|
|
eventKey = EventKeyCode.PageDown;
|
|
break;
|
|
// no default
|
|
}
|
|
|
|
if (0 < eventKey)
|
|
{
|
|
this.newSelectPosition(eventKey, key.shift);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @returns {boolean}
|
|
*/
|
|
autoSelect() {
|
|
return !!(this.oCallbacks.onAutoSelect || noopTrue)();
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} up
|
|
*/
|
|
doUpUpOrDownDown(up) {
|
|
(this.oCallbacks.onUpUpOrDownDown || noopTrue)(!!up);
|
|
}
|
|
|
|
/**
|
|
* @param {Object} oItem
|
|
* @returns {string}
|
|
*/
|
|
getItemUid(item) {
|
|
|
|
let uid = '';
|
|
|
|
const getItemUidCallback = this.oCallbacks.onItemGetUid || null;
|
|
if (getItemUidCallback && item)
|
|
{
|
|
uid = getItemUidCallback(item);
|
|
}
|
|
|
|
return uid.toString();
|
|
}
|
|
|
|
/**
|
|
* @param {number} iEventKeyCode
|
|
* @param {boolean} bShiftKey
|
|
* @param {boolean=} bForceSelect = false
|
|
*/
|
|
newSelectPosition(iEventKeyCode, bShiftKey, bForceSelect) {
|
|
|
|
var
|
|
iIndex = 0,
|
|
iPageStep = 10,
|
|
bNext = false,
|
|
bStop = false,
|
|
oResult = null,
|
|
aList = this.list(),
|
|
iListLen = aList ? aList.length : 0,
|
|
oFocused = this.focusedItem();
|
|
|
|
if (0 < iListLen)
|
|
{
|
|
if (!oFocused)
|
|
{
|
|
if (EventKeyCode.Down === iEventKeyCode || EventKeyCode.Insert === iEventKeyCode ||
|
|
EventKeyCode.Space === iEventKeyCode || EventKeyCode.Home === iEventKeyCode ||
|
|
EventKeyCode.PageUp === iEventKeyCode)
|
|
{
|
|
oResult = aList[0];
|
|
}
|
|
else if (EventKeyCode.Up === iEventKeyCode || EventKeyCode.End === iEventKeyCode ||
|
|
EventKeyCode.PageDown === iEventKeyCode)
|
|
{
|
|
oResult = aList[aList.length - 1];
|
|
}
|
|
}
|
|
else if (oFocused)
|
|
{
|
|
if (EventKeyCode.Down === iEventKeyCode || EventKeyCode.Up === iEventKeyCode ||
|
|
EventKeyCode.Insert === iEventKeyCode || EventKeyCode.Space === iEventKeyCode)
|
|
{
|
|
_.each(aList, (item) => {
|
|
if (!bStop)
|
|
{
|
|
switch (iEventKeyCode)
|
|
{
|
|
case EventKeyCode.Up:
|
|
if (oFocused === item)
|
|
{
|
|
bStop = true;
|
|
}
|
|
else
|
|
{
|
|
oResult = item;
|
|
}
|
|
break;
|
|
case EventKeyCode.Down:
|
|
case EventKeyCode.Insert:
|
|
if (bNext)
|
|
{
|
|
oResult = item;
|
|
bStop = true;
|
|
}
|
|
else if (oFocused === item)
|
|
{
|
|
bNext = true;
|
|
}
|
|
break;
|
|
// no default
|
|
}
|
|
}
|
|
});
|
|
|
|
if (!oResult && (EventKeyCode.Down === iEventKeyCode || EventKeyCode.Up === iEventKeyCode))
|
|
{
|
|
this.doUpUpOrDownDown(EventKeyCode.Up === iEventKeyCode);
|
|
}
|
|
}
|
|
else if (EventKeyCode.Home === iEventKeyCode || EventKeyCode.End === iEventKeyCode)
|
|
{
|
|
if (EventKeyCode.Home === iEventKeyCode)
|
|
{
|
|
oResult = aList[0];
|
|
}
|
|
else if (EventKeyCode.End === iEventKeyCode)
|
|
{
|
|
oResult = aList[aList.length - 1];
|
|
}
|
|
}
|
|
else if (EventKeyCode.PageDown === iEventKeyCode)
|
|
{
|
|
for (; iIndex < iListLen; iIndex++)
|
|
{
|
|
if (oFocused === aList[iIndex])
|
|
{
|
|
iIndex += iPageStep;
|
|
iIndex = iListLen - 1 < iIndex ? iListLen - 1 : iIndex;
|
|
oResult = aList[iIndex];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else if (EventKeyCode.PageUp === iEventKeyCode)
|
|
{
|
|
for (iIndex = iListLen; 0 <= iIndex; iIndex--)
|
|
{
|
|
if (oFocused === aList[iIndex])
|
|
{
|
|
iIndex -= iPageStep;
|
|
iIndex = 0 > iIndex ? 0 : iIndex;
|
|
oResult = aList[iIndex];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (oResult)
|
|
{
|
|
this.focusedItem(oResult);
|
|
|
|
if (oFocused)
|
|
{
|
|
if (bShiftKey)
|
|
{
|
|
if (EventKeyCode.Up === iEventKeyCode || EventKeyCode.Down === iEventKeyCode)
|
|
{
|
|
oFocused.checked(!oFocused.checked());
|
|
}
|
|
}
|
|
else if (EventKeyCode.Insert === iEventKeyCode || EventKeyCode.Space === iEventKeyCode)
|
|
{
|
|
oFocused.checked(!oFocused.checked());
|
|
}
|
|
}
|
|
|
|
if ((this.autoSelect() || !!bForceSelect) &&
|
|
!this.isListChecked() && EventKeyCode.Space !== iEventKeyCode)
|
|
{
|
|
this.selectedItem(oResult);
|
|
}
|
|
|
|
this.scrollToFocused();
|
|
}
|
|
else if (oFocused)
|
|
{
|
|
if (bShiftKey && (EventKeyCode.Up === iEventKeyCode || EventKeyCode.Down === iEventKeyCode))
|
|
{
|
|
oFocused.checked(!oFocused.checked());
|
|
}
|
|
else if (EventKeyCode.Insert === iEventKeyCode || EventKeyCode.Space === iEventKeyCode)
|
|
{
|
|
oFocused.checked(!oFocused.checked());
|
|
}
|
|
|
|
this.focusedItem(oFocused);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @returns {boolean}
|
|
*/
|
|
scrollToFocused() {
|
|
|
|
if (!this.oContentVisible || !this.oContentScrollable)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const
|
|
offset = 20,
|
|
list = this.list(),
|
|
$focused = $(this.sItemFocusedSelector, this.oContentScrollable),
|
|
pos = $focused.position(),
|
|
visibleHeight = this.oContentVisible.height(),
|
|
focusedHeight = $focused.outerHeight();
|
|
|
|
if (list && list[0] && list[0].focused())
|
|
{
|
|
this.oContentScrollable.scrollTop(0);
|
|
return true;
|
|
}
|
|
else if (pos && (0 > pos.top || pos.top + focusedHeight > visibleHeight))
|
|
{
|
|
this.oContentScrollable.scrollTop(0 > pos.top ?
|
|
this.oContentScrollable.scrollTop() + pos.top - offset :
|
|
this.oContentScrollable.scrollTop() + pos.top - visibleHeight + focusedHeight + offset
|
|
);
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param {boolean=} fast = false
|
|
* @returns {boolean}
|
|
*/
|
|
scrollToTop(fast = false) {
|
|
|
|
if (!this.oContentVisible || !this.oContentScrollable)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (fast || 50 > this.oContentScrollable.scrollTop())
|
|
{
|
|
this.oContentScrollable.scrollTop(0);
|
|
}
|
|
else
|
|
{
|
|
this.oContentScrollable.stop().animate({scrollTop: 0}, 200);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
eventClickFunction(item, event) {
|
|
|
|
let
|
|
index = 0,
|
|
length = 0,
|
|
changeRange = false,
|
|
isInRange = false,
|
|
list = [],
|
|
checked = false,
|
|
listItem = null,
|
|
lineUid = '';
|
|
|
|
const uid = this.getItemUid(item);
|
|
if (event && event.shiftKey)
|
|
{
|
|
if ('' !== uid && '' !== this.sLastUid && uid !== this.sLastUid)
|
|
{
|
|
list = this.list();
|
|
checked = item.checked();
|
|
|
|
for (index = 0, length = list.length; index < length; index++)
|
|
{
|
|
listItem = list[index];
|
|
lineUid = this.getItemUid(listItem);
|
|
|
|
changeRange = false;
|
|
if (lineUid === this.sLastUid || lineUid === uid)
|
|
{
|
|
changeRange = true;
|
|
}
|
|
|
|
if (changeRange)
|
|
{
|
|
isInRange = !isInRange;
|
|
}
|
|
|
|
if (isInRange || changeRange)
|
|
{
|
|
listItem.checked(checked);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
this.sLastUid = '' === uid ? '' : uid;
|
|
}
|
|
|
|
/**
|
|
* @param {Object} item
|
|
* @param {Object=} event
|
|
*/
|
|
actionClick(item, event = null) {
|
|
|
|
if (item)
|
|
{
|
|
let click = true;
|
|
if (event)
|
|
{
|
|
if (event.shiftKey && !(event.ctrlKey || event.metaKey) && !event.altKey)
|
|
{
|
|
click = false;
|
|
if ('' === this.sLastUid)
|
|
{
|
|
this.sLastUid = this.getItemUid(item);
|
|
}
|
|
|
|
item.checked(!item.checked());
|
|
this.eventClickFunction(item, event);
|
|
|
|
this.focusedItem(item);
|
|
}
|
|
else if ((event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey)
|
|
{
|
|
click = false;
|
|
this.focusedItem(item);
|
|
|
|
if (this.selectedItem() && item !== this.selectedItem())
|
|
{
|
|
this.selectedItem().checked(true);
|
|
}
|
|
|
|
item.checked(!item.checked());
|
|
}
|
|
}
|
|
|
|
if (click)
|
|
{
|
|
this.selectMessageItem(item);
|
|
}
|
|
}
|
|
}
|
|
|
|
on(eventName, callback) {
|
|
this.oCallbacks[eventName] = callback;
|
|
}
|
|
|
|
selectMessageItem(messageItem) {
|
|
this.focusedItem(messageItem);
|
|
this.selectedItem(messageItem);
|
|
this.scrollToFocused();
|
|
}
|
|
}
|
|
|
|
export {Selector, Selector as default};
|