snappymail/dev/Common/Selector.js

639 lines
15 KiB
JavaScript
Raw Normal View History

2016-07-02 06:49:59 +08:00
import $ from '$';
import _ from '_';
import key from 'key';
2015-11-19 01:32:29 +08:00
import ko from 'ko';
2019-07-05 03:19:24 +08:00
import { EventKeyCode } from 'Common/Enums';
import { isArray, inArray, noop, noopTrue } from 'Common/Utils';
2015-11-19 01:32:29 +08:00
2019-07-05 03:19:24 +08:00
class Selector {
2016-09-10 06:38:16 +08:00
list;
listChecked;
isListChecked;
focusedItem;
selectedItem;
itemSelectedThrottle;
selectedItemUseCallback = true;
iSelectNextHelper = 0;
iFocusedNextHelper = 0;
oContentVisible;
oContentScrollable;
sItemSelector;
sItemSelectedSelector;
sItemCheckedSelector;
sItemFocusedSelector;
sLastUid = '';
oCallbacks = {};
2015-11-19 01:32:29 +08:00
/**
* @param {koProperty} koList
* @param {koProperty} koSelectedItem
* @param {koProperty} koFocusedItem
* @param {string} sItemSelector
* @param {string} sItemSelectedSelector
* @param {string} sItemCheckedSelector
* @param {string} sItemFocusedSelector
*/
2019-07-05 03:19:24 +08:00
constructor(
koList,
koSelectedItem,
koFocusedItem,
sItemSelector,
sItemSelectedSelector,
sItemCheckedSelector,
sItemFocusedSelector
) {
2015-11-19 01:32:29 +08:00
this.list = koList;
2019-07-05 03:19:24 +08:00
this.listChecked = ko.computed(() => _.filter(this.list(), (item) => item.checked())).extend({ rateLimit: 0 });
2015-11-19 01:32:29 +08:00
this.isListChecked = ko.computed(() => 0 < this.listChecked().length);
this.focusedItem = koFocusedItem || ko.observable(null);
this.selectedItem = koSelectedItem || ko.observable(null);
this.itemSelectedThrottle = _.debounce(_.bind(this.itemSelected, this), 300);
this.listChecked.subscribe((items) => {
2019-07-05 03:19:24 +08:00
if (0 < items.length) {
if (null === this.selectedItem()) {
if (this.selectedItem.valueHasMutated) {
2015-11-19 01:32:29 +08:00
this.selectedItem.valueHasMutated();
}
2019-07-05 03:19:24 +08:00
} else {
2015-11-19 01:32:29 +08:00
this.selectedItem(null);
}
2019-07-05 03:19:24 +08:00
} else if (this.autoSelect() && this.focusedItem()) {
2015-11-19 01:32:29 +08:00
this.selectedItem(this.focusedItem());
}
}, this);
this.selectedItem.subscribe((item) => {
2019-07-05 03:19:24 +08:00
if (item) {
if (this.isListChecked()) {
2015-11-19 01:32:29 +08:00
_.each(this.listChecked(), (subItem) => {
subItem.checked(false);
});
}
2019-07-05 03:19:24 +08:00
if (this.selectedItemUseCallback) {
2015-11-19 01:32:29 +08:00
this.itemSelectedThrottle(item);
}
2019-07-05 03:19:24 +08:00
} else if (this.selectedItemUseCallback) {
2015-11-19 01:32:29 +08:00
this.itemSelected(null);
}
}, this);
2019-07-05 03:19:24 +08:00
this.selectedItem = this.selectedItem.extend({ toggleSubscribeProperty: [this, 'selected'] });
this.focusedItem = this.focusedItem.extend({ toggleSubscribeProperty: [null, 'focused'] });
2015-11-19 01:32:29 +08:00
this.sItemSelector = sItemSelector;
this.sItemSelectedSelector = sItemSelectedSelector;
this.sItemCheckedSelector = sItemCheckedSelector;
this.sItemFocusedSelector = sItemFocusedSelector;
this.focusedItem.subscribe((item) => {
2019-07-05 03:19:24 +08:00
if (item) {
2015-11-19 01:32:29 +08:00
this.sLastUid = this.getItemUid(item);
}
}, this);
2019-07-05 03:19:24 +08:00
let aCache = [],
2015-11-19 01:32:29 +08:00
aCheckedCache = [],
mFocused = null,
2016-06-30 08:02:45 +08:00
mSelected = null;
2015-11-19 01:32:29 +08:00
2019-07-05 03:19:24 +08:00
this.list.subscribe(
(items) => {
if (isArray(items)) {
_.each(items, (item) => {
if (item) {
const uid = this.getItemUid(item);
2015-11-19 01:32:29 +08:00
2019-07-05 03:19:24 +08:00
aCache.push(uid);
if (item.checked()) {
aCheckedCache.push(uid);
}
if (null === mFocused && item.focused()) {
mFocused = uid;
}
if (null === mSelected && item.selected()) {
mSelected = uid;
}
2015-11-19 01:32:29 +08:00
}
2019-07-05 03:19:24 +08:00
});
}
},
this,
'beforeChange'
);
2015-11-19 01:32:29 +08:00
this.list.subscribe((aItems) => {
2019-07-05 03:19:24 +08:00
let temp = null,
getNext = false,
isNextFocused = mFocused,
isChecked = false,
isSelected = false,
len = 0;
2019-07-05 03:19:24 +08:00
const uids = [];
2015-11-19 01:32:29 +08:00
this.selectedItemUseCallback = false;
this.focusedItem(null);
this.selectedItem(null);
2019-07-05 03:19:24 +08:00
if (isArray(aItems)) {
len = aCheckedCache.length;
2015-11-19 01:32:29 +08:00
_.each(aItems, (item) => {
const uid = this.getItemUid(item);
uids.push(uid);
2015-11-19 01:32:29 +08:00
2019-07-05 03:19:24 +08:00
if (null !== mFocused && mFocused === uid) {
this.focusedItem(item);
2015-11-19 01:32:29 +08:00
mFocused = null;
}
2019-07-05 03:19:24 +08:00
if (0 < len && -1 < inArray(uid, aCheckedCache)) {
isChecked = true;
item.checked(true);
len -= 1;
2015-11-19 01:32:29 +08:00
}
2019-07-05 03:19:24 +08:00
if (!isChecked && null !== mSelected && mSelected === uid) {
isSelected = true;
this.selectedItem(item);
2015-11-19 01:32:29 +08:00
mSelected = null;
}
});
this.selectedItemUseCallback = true;
2019-07-05 03:19:24 +08:00
if (!isChecked && !isSelected && this.autoSelect()) {
if (this.focusedItem()) {
2015-11-19 01:32:29 +08:00
this.selectedItem(this.focusedItem());
2019-07-05 03:19:24 +08:00
} else if (0 < aItems.length) {
if (null !== isNextFocused) {
getNext = false;
isNextFocused = _.find(aCache, (sUid) => {
2019-07-05 03:19:24 +08:00
if (getNext && -1 < inArray(sUid, uids)) {
2015-11-19 01:32:29 +08:00
return sUid;
2019-07-05 03:19:24 +08:00
} else if (isNextFocused === sUid) {
getNext = true;
2015-11-19 01:32:29 +08:00
}
return false;
});
2019-07-05 03:19:24 +08:00
if (isNextFocused) {
temp = _.find(aItems, (oItem) => isNextFocused === this.getItemUid(oItem));
2015-11-19 01:32:29 +08:00
}
}
this.selectedItem(temp || null);
2015-11-19 01:32:29 +08:00
this.focusedItem(this.selectedItem());
}
}
2019-07-05 03:19:24 +08:00
if (
(0 !== this.iSelectNextHelper || 0 !== this.iFocusedNextHelper) &&
0 < aItems.length &&
!this.focusedItem()
) {
temp = null;
2019-07-05 03:19:24 +08:00
if (0 !== this.iFocusedNextHelper) {
temp = aItems[-1 === this.iFocusedNextHelper ? aItems.length - 1 : 0] || null;
2015-11-19 01:32:29 +08:00
}
2019-07-05 03:19:24 +08:00
if (!temp && 0 !== this.iSelectNextHelper) {
temp = aItems[-1 === this.iSelectNextHelper ? aItems.length - 1 : 0] || null;
2015-11-19 01:32:29 +08:00
}
2019-07-05 03:19:24 +08:00
if (temp) {
if (0 !== this.iSelectNextHelper) {
this.selectedItem(temp || null);
2015-11-19 01:32:29 +08:00
}
this.focusedItem(temp || null);
2015-11-19 01:32:29 +08:00
this.scrollToFocused();
_.delay(() => this.scrollToFocused(), 100);
}
this.iSelectNextHelper = 0;
this.iFocusedNextHelper = 0;
}
}
aCache = [];
aCheckedCache = [];
mFocused = null;
mSelected = null;
2016-09-10 06:38:16 +08:00
});
2015-11-19 01:32:29 +08:00
}
itemSelected(item) {
2019-07-05 03:19:24 +08:00
if (this.isListChecked()) {
if (!item) {
2016-06-17 07:23:49 +08:00
(this.oCallbacks.onItemSelect || noop)(item || null);
2015-11-19 01:32:29 +08:00
}
2019-07-05 03:19:24 +08:00
} else if (item) {
(this.oCallbacks.onItemSelect || noop)(item);
2015-11-19 01:32:29 +08:00
}
}
/**
* @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;
2019-07-05 03:19:24 +08:00
if (this.oContentVisible && this.oContentScrollable) {
2015-11-19 01:32:29 +08:00
$(this.oContentVisible)
.on('selectstart', (event) => {
2019-07-05 03:19:24 +08:00
if (event && event.preventDefault) {
2015-11-19 01:32:29 +08:00
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);
2019-07-05 03:19:24 +08:00
if (item) {
if (event && event.shiftKey) {
2015-11-19 01:32:29 +08:00
this.actionClick(item, event);
2019-07-05 03:19:24 +08:00
} else {
2015-11-19 01:32:29 +08:00
this.focusedItem(item);
item.checked(!item.checked());
}
}
2016-06-30 08:02:45 +08:00
});
2015-11-19 01:32:29 +08:00
key('enter', keyScope, () => {
2019-07-05 03:19:24 +08:00
if (this.focusedItem() && !this.focusedItem().selected()) {
2015-11-19 01:32:29 +08:00
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) => {
2019-07-05 03:19:24 +08:00
if (event && handler && handler.shortcut) {
2015-11-19 01:32:29 +08:00
let eventKey = 0;
2019-07-05 03:19:24 +08:00
switch (handler.shortcut) {
2015-11-19 01:32:29 +08:00
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;
2016-06-30 08:02:45 +08:00
// no default
2015-11-19 01:32:29 +08:00
}
2019-07-05 03:19:24 +08:00
if (0 < eventKey) {
2015-11-19 01:32:29 +08:00
this.newSelectPosition(eventKey, key.shift);
return false;
}
}
2016-07-01 06:50:11 +08:00
return true;
2015-11-19 01:32:29 +08:00
});
}
}
/**
2016-06-30 08:02:45 +08:00
* @returns {boolean}
2015-11-19 01:32:29 +08:00
*/
autoSelect() {
2016-06-17 07:23:49 +08:00
return !!(this.oCallbacks.onAutoSelect || noopTrue)();
2015-11-19 01:32:29 +08:00
}
/**
* @param {boolean} up
*/
doUpUpOrDownDown(up) {
2016-06-17 07:23:49 +08:00
(this.oCallbacks.onUpUpOrDownDown || noopTrue)(!!up);
2015-11-19 01:32:29 +08:00
}
/**
* @param {Object} oItem
2016-06-30 08:02:45 +08:00
* @returns {string}
2015-11-19 01:32:29 +08:00
*/
getItemUid(item) {
let uid = '';
2016-04-21 01:12:51 +08:00
const getItemUidCallback = this.oCallbacks.onItemGetUid || null;
2019-07-05 03:19:24 +08:00
if (getItemUidCallback && item) {
2015-11-19 01:32:29 +08:00
uid = getItemUidCallback(item);
}
return uid.toString();
}
/**
* @param {number} iEventKeyCode
* @param {boolean} bShiftKey
* @param {boolean=} bForceSelect = false
*/
newSelectPosition(iEventKeyCode, bShiftKey, bForceSelect) {
2019-07-05 03:19:24 +08:00
let index = 0,
isNext = false,
isStop = false,
result = null;
2019-07-05 03:19:24 +08:00
const pageStep = 10,
list = this.list(),
listLen = list ? list.length : 0,
focused = this.focusedItem();
2019-07-05 03:19:24 +08:00
if (0 < listLen) {
if (!focused) {
if (
EventKeyCode.Down === iEventKeyCode ||
EventKeyCode.Insert === iEventKeyCode ||
EventKeyCode.Space === iEventKeyCode ||
EventKeyCode.Home === iEventKeyCode ||
EventKeyCode.PageUp === iEventKeyCode
) {
result = list[0];
2019-07-05 03:19:24 +08:00
} else if (
EventKeyCode.Up === iEventKeyCode ||
EventKeyCode.End === iEventKeyCode ||
EventKeyCode.PageDown === iEventKeyCode
) {
result = list[list.length - 1];
2015-11-19 01:32:29 +08:00
}
2019-07-05 03:19:24 +08:00
} else if (focused) {
if (
EventKeyCode.Down === iEventKeyCode ||
EventKeyCode.Up === iEventKeyCode ||
EventKeyCode.Insert === iEventKeyCode ||
EventKeyCode.Space === iEventKeyCode
) {
_.each(list, (item) => {
2019-07-05 03:19:24 +08:00
if (!isStop) {
switch (iEventKeyCode) {
2016-06-30 08:02:45 +08:00
case EventKeyCode.Up:
2019-07-05 03:19:24 +08:00
if (focused === item) {
isStop = true;
2019-07-05 03:19:24 +08:00
} else {
result = item;
2016-06-30 08:02:45 +08:00
}
break;
case EventKeyCode.Down:
case EventKeyCode.Insert:
2019-07-05 03:19:24 +08:00
if (isNext) {
result = item;
isStop = true;
2019-07-05 03:19:24 +08:00
} else if (focused === item) {
isNext = true;
2016-06-30 08:02:45 +08:00
}
break;
// no default
2015-11-19 01:32:29 +08:00
}
}
});
2019-07-05 03:19:24 +08:00
if (!result && (EventKeyCode.Down === iEventKeyCode || EventKeyCode.Up === iEventKeyCode)) {
2015-11-19 01:32:29 +08:00
this.doUpUpOrDownDown(EventKeyCode.Up === iEventKeyCode);
}
2019-07-05 03:19:24 +08:00
} else if (EventKeyCode.Home === iEventKeyCode || EventKeyCode.End === iEventKeyCode) {
if (EventKeyCode.Home === iEventKeyCode) {
result = list[0];
2019-07-05 03:19:24 +08:00
} else if (EventKeyCode.End === iEventKeyCode) {
result = list[list.length - 1];
2015-11-19 01:32:29 +08:00
}
2019-07-05 03:19:24 +08:00
} else if (EventKeyCode.PageDown === iEventKeyCode) {
for (; index < listLen; index++) {
if (focused === list[index]) {
index += pageStep;
index = listLen - 1 < index ? listLen - 1 : index;
result = list[index];
2015-11-19 01:32:29 +08:00
break;
}
}
2019-07-05 03:19:24 +08:00
} else if (EventKeyCode.PageUp === iEventKeyCode) {
for (index = listLen; 0 <= index; index--) {
if (focused === list[index]) {
index -= pageStep;
index = 0 > index ? 0 : index;
result = list[index];
2015-11-19 01:32:29 +08:00
break;
}
}
}
}
}
2019-07-05 03:19:24 +08:00
if (result) {
this.focusedItem(result);
2015-11-19 01:32:29 +08:00
2019-07-05 03:19:24 +08:00
if (focused) {
if (bShiftKey) {
if (EventKeyCode.Up === iEventKeyCode || EventKeyCode.Down === iEventKeyCode) {
focused.checked(!focused.checked());
2015-11-19 01:32:29 +08:00
}
2019-07-05 03:19:24 +08:00
} else if (EventKeyCode.Insert === iEventKeyCode || EventKeyCode.Space === iEventKeyCode) {
focused.checked(!focused.checked());
2015-11-19 01:32:29 +08:00
}
}
2019-07-05 03:19:24 +08:00
if ((this.autoSelect() || !!bForceSelect) && !this.isListChecked() && EventKeyCode.Space !== iEventKeyCode) {
this.selectedItem(result);
2015-11-19 01:32:29 +08:00
}
this.scrollToFocused();
2019-07-05 03:19:24 +08:00
} else if (focused) {
if (bShiftKey && (EventKeyCode.Up === iEventKeyCode || EventKeyCode.Down === iEventKeyCode)) {
focused.checked(!focused.checked());
2019-07-05 03:19:24 +08:00
} else if (EventKeyCode.Insert === iEventKeyCode || EventKeyCode.Space === iEventKeyCode) {
focused.checked(!focused.checked());
2015-11-19 01:32:29 +08:00
}
this.focusedItem(focused);
2015-11-19 01:32:29 +08:00
}
}
/**
2016-06-30 08:02:45 +08:00
* @returns {boolean}
2015-11-19 01:32:29 +08:00
*/
scrollToFocused() {
2019-07-05 03:19:24 +08:00
if (!this.oContentVisible || !this.oContentScrollable) {
2015-11-19 01:32:29 +08:00
return false;
}
2019-07-05 03:19:24 +08:00
const offset = 20,
2015-11-19 01:32:29 +08:00
list = this.list(),
$focused = $(this.sItemFocusedSelector, this.oContentScrollable),
pos = $focused.position(),
visibleHeight = this.oContentVisible.height(),
2016-06-30 08:02:45 +08:00
focusedHeight = $focused.outerHeight();
2015-11-19 01:32:29 +08:00
2019-07-05 03:19:24 +08:00
if (list && list[0] && list[0].focused()) {
2015-11-19 01:32:29 +08:00
this.oContentScrollable.scrollTop(0);
return true;
2019-07-05 03:19:24 +08:00
} 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
2015-11-19 01:32:29 +08:00
);
return true;
}
return false;
}
/**
* @param {boolean=} fast = false
2016-06-30 08:02:45 +08:00
* @returns {boolean}
2015-11-19 01:32:29 +08:00
*/
scrollToTop(fast = false) {
2019-07-05 03:19:24 +08:00
if (!this.oContentVisible || !this.oContentScrollable) {
2015-11-19 01:32:29 +08:00
return false;
}
2019-07-05 03:19:24 +08:00
if (fast || 50 > this.oContentScrollable.scrollTop()) {
2015-11-19 01:32:29 +08:00
this.oContentScrollable.scrollTop(0);
2019-07-05 03:19:24 +08:00
} else {
this.oContentScrollable.stop().animate({ scrollTop: 0 }, 200);
2015-11-19 01:32:29 +08:00
}
return true;
}
eventClickFunction(item, event) {
2019-07-05 03:19:24 +08:00
let index = 0,
2015-11-19 01:32:29 +08:00
length = 0,
changeRange = false,
isInRange = false,
list = [],
checked = false,
listItem = null,
2016-06-30 08:02:45 +08:00
lineUid = '';
2015-11-19 01:32:29 +08:00
const uid = this.getItemUid(item);
2019-07-05 03:19:24 +08:00
if (event && event.shiftKey) {
if ('' !== uid && '' !== this.sLastUid && uid !== this.sLastUid) {
2015-11-19 01:32:29 +08:00
list = this.list();
checked = item.checked();
2019-07-05 03:19:24 +08:00
for (index = 0, length = list.length; index < length; index++) {
2015-11-19 01:32:29 +08:00
listItem = list[index];
lineUid = this.getItemUid(listItem);
changeRange = false;
2019-07-05 03:19:24 +08:00
if (lineUid === this.sLastUid || lineUid === uid) {
2015-11-19 01:32:29 +08:00
changeRange = true;
}
2019-07-05 03:19:24 +08:00
if (changeRange) {
2015-11-19 01:32:29 +08:00
isInRange = !isInRange;
}
2019-07-05 03:19:24 +08:00
if (isInRange || changeRange) {
2015-11-19 01:32:29 +08:00
listItem.checked(checked);
}
}
}
}
this.sLastUid = '' === uid ? '' : uid;
}
/**
* @param {Object} item
* @param {Object=} event
*/
2016-06-28 04:54:38 +08:00
actionClick(item, event = null) {
2019-07-05 03:19:24 +08:00
if (item) {
2015-11-19 01:32:29 +08:00
let click = true;
2019-07-05 03:19:24 +08:00
if (event) {
if (event.shiftKey && !(event.ctrlKey || event.metaKey) && !event.altKey) {
2015-11-19 01:32:29 +08:00
click = false;
2019-07-05 03:19:24 +08:00
if ('' === this.sLastUid) {
2015-11-19 01:32:29 +08:00
this.sLastUid = this.getItemUid(item);
}
item.checked(!item.checked());
this.eventClickFunction(item, event);
this.focusedItem(item);
2019-07-05 03:19:24 +08:00
} else if ((event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey) {
2015-11-19 01:32:29 +08:00
click = false;
this.focusedItem(item);
2019-07-05 03:19:24 +08:00
if (this.selectedItem() && item !== this.selectedItem()) {
2015-11-19 01:32:29 +08:00
this.selectedItem().checked(true);
}
item.checked(!item.checked());
}
}
2019-07-05 03:19:24 +08:00
if (click) {
2015-11-19 01:32:29 +08:00
this.selectMessageItem(item);
}
}
}
on(eventName, callback) {
this.oCallbacks[eventName] = callback;
}
selectMessageItem(messageItem) {
this.focusedItem(messageItem);
this.selectedItem(messageItem);
this.scrollToFocused();
}
}
2019-07-05 03:19:24 +08:00
export { Selector, Selector as default };