snappymail/dev/Common/Selector.js

424 lines
10 KiB
JavaScript
Raw Normal View History

2015-11-19 01:32:29 +08:00
import ko from 'ko';
import { addEventsListeners, addShortcut, registerShortcut } from 'Common/Globals';
import { isArray } from 'Common/Utils';
import { koComputable } from 'External/ko';
2015-11-19 01:32:29 +08:00
2021-07-19 17:57:47 +08:00
/*
oCallbacks:
ItemSelect
MiddleClick
AutoSelect
ItemGetUid
UpOrDown
*/
let shiftStart;
2021-01-22 23:32:08 +08:00
export class Selector {
2015-11-19 01:32:29 +08:00
/**
* @param {koProperty} koList
* @param {koProperty} koSelectedItem
* @param {koProperty} koFocusedItem
* @param {string} sItemSelector
* @param {string} sItemCheckedSelector
* @param {string} sItemFocusedSelector
*/
2019-07-05 03:19:24 +08:00
constructor(
koList,
koSelectedItem,
koFocusedItem,
sItemSelector,
sItemCheckedSelector,
sItemFocusedSelector
) {
2022-09-09 18:07:05 +08:00
koFocusedItem = (koFocusedItem || ko.observable(null)).extend({ toggleSubscribeProperty: [this, 'focused'] });
koSelectedItem = (koSelectedItem || ko.observable(null)).extend({ toggleSubscribeProperty: [null, 'selected'] });
2015-11-19 01:32:29 +08:00
2022-09-09 18:07:05 +08:00
this.list = koList;
this.listChecked = koComputable(() => koList.filter(item => item.checked())).extend({ rateLimit: 0 });
2015-11-19 01:32:29 +08:00
2022-09-09 18:07:05 +08:00
this.focusedItem = koFocusedItem;
this.selectedItem = koSelectedItem;
this.iSelectNextHelper = 0;
this.iFocusedNextHelper = 0;
2022-09-09 18:07:05 +08:00
// this.oContentScrollable = null;
this.sItemSelector = sItemSelector;
this.sItemCheckedSelector = sItemCheckedSelector;
this.sItemFocusedSelector = sItemFocusedSelector;
this.sLastUid = '';
this.oCallbacks = {};
2022-10-10 19:52:56 +08:00
const
itemSelected = item => {
if (koList.hasChecked()) {
item || this.oCallbacks.ItemSelect?.(null);
} else if (item) {
this.oCallbacks.ItemSelect?.(item);
}
},
itemSelectedThrottle = (item => itemSelected(item)).debounce(300);
2015-11-19 01:32:29 +08:00
2022-09-09 18:07:05 +08:00
this.listChecked.subscribe(items => {
if (items.length) {
2022-10-10 19:52:56 +08:00
koSelectedItem() ? koSelectedItem(null) : koSelectedItem.valueHasMutated?.();
2022-09-09 23:04:52 +08:00
} else if (this.autoSelect()) {
2022-09-09 18:07:05 +08:00
koSelectedItem(koFocusedItem());
2015-11-19 01:32:29 +08:00
}
2022-09-09 23:04:52 +08:00
});
2015-11-19 01:32:29 +08:00
2022-09-09 18:07:05 +08:00
let selectedItemUseCallback = true;
koSelectedItem.subscribe(item => {
2019-07-05 03:19:24 +08:00
if (item) {
2022-09-09 18:07:05 +08:00
koList.forEach(subItem => subItem.checked(false));
2022-10-10 19:52:56 +08:00
selectedItemUseCallback && itemSelectedThrottle(item);
} else {
selectedItemUseCallback && itemSelected();
2015-11-19 01:32:29 +08:00
}
2022-09-09 23:04:52 +08:00
});
2015-11-19 01:32:29 +08:00
2022-09-09 23:04:52 +08:00
koFocusedItem.subscribe(item => item && (this.sLastUid = this.getItemUid(item)));
2015-11-19 01:32:29 +08:00
2022-09-09 18:07:05 +08:00
/**
* Below code is used to keep checked/focused/selected states when array is refreshed.
*/
2015-11-19 01:32:29 +08:00
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
2022-09-09 18:07:05 +08:00
// Before removing old list
koList.subscribe(
items => {
if (isArray(items)) {
items.forEach(item => {
2022-09-09 18:07:05 +08:00
const uid = this.getItemUid(item);
if (uid) {
2019-07-05 03:19:24 +08:00
aCache.push(uid);
item.checked() && aCheckedCache.push(uid);
2022-09-09 18:07:05 +08:00
if (!mFocused && item.focused()) {
2019-07-05 03:19:24 +08:00
mFocused = uid;
}
2022-09-09 18:07:05 +08:00
if (!mSelected && item.selected()) {
2019-07-05 03:19:24 +08:00
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
2022-09-09 18:07:05 +08:00
koList.subscribe(aItems => {
selectedItemUseCallback = false;
2015-11-19 01:32:29 +08:00
2022-09-09 18:07:05 +08:00
koFocusedItem(null);
koSelectedItem(null);
2015-11-19 01:32:29 +08:00
if (isArray(aItems)) {
2022-09-09 18:07:05 +08:00
let temp,
isChecked;
2015-11-19 01:32:29 +08:00
aItems.forEach(item => {
const uid = this.getItemUid(item);
2022-09-09 18:07:05 +08:00
if (uid) {
2015-11-19 01:32:29 +08:00
2022-09-09 18:07:05 +08:00
if (mFocused === uid) {
koFocusedItem(item);
mFocused = null;
}
2015-11-19 01:32:29 +08:00
2022-09-09 18:07:05 +08:00
if (aCheckedCache.includes(uid)) {
item.checked(true);
isChecked = true;
}
2015-11-19 01:32:29 +08:00
2022-09-09 18:07:05 +08:00
if (!isChecked && mSelected === uid) {
koSelectedItem(item);
mSelected = null;
}
2015-11-19 01:32:29 +08:00
}
});
2022-09-09 18:07:05 +08:00
selectedItemUseCallback = true;
2015-11-19 01:32:29 +08:00
2019-07-05 03:19:24 +08:00
if (
2022-09-09 18:07:05 +08:00
(this.iSelectNextHelper || this.iFocusedNextHelper) &&
aItems.length &&
2022-09-09 18:07:05 +08:00
!koFocusedItem()
2019-07-05 03:19:24 +08:00
) {
temp = null;
2022-09-09 18:07:05 +08:00
if (this.iFocusedNextHelper) {
temp = aItems[-1 === this.iFocusedNextHelper ? aItems.length - 1 : 0];
2015-11-19 01:32:29 +08:00
}
2022-09-09 18:07:05 +08:00
if (!temp && this.iSelectNextHelper) {
temp = aItems[-1 === this.iSelectNextHelper ? aItems.length - 1 : 0];
2015-11-19 01:32:29 +08:00
}
2019-07-05 03:19:24 +08:00
if (temp) {
2022-09-09 18:07:05 +08:00
if (this.iSelectNextHelper) {
koSelectedItem(temp);
2015-11-19 01:32:29 +08:00
}
2022-09-09 18:07:05 +08:00
koFocusedItem(temp);
2015-11-19 01:32:29 +08:00
this.scrollToFocused();
setTimeout(this.scrollToFocused, 100);
2015-11-19 01:32:29 +08:00
}
this.iSelectNextHelper = 0;
this.iFocusedNextHelper = 0;
}
2022-09-09 18:07:05 +08:00
2022-09-09 23:04:52 +08:00
if (this.autoSelect() && !isChecked && !koSelectedItem()) {
2022-09-09 18:07:05 +08:00
koSelectedItem(koFocusedItem());
}
2015-11-19 01:32:29 +08:00
}
aCache = [];
aCheckedCache = [];
mFocused = null;
mSelected = null;
2022-09-09 18:07:05 +08:00
selectedItemUseCallback = true;
2016-09-10 06:38:16 +08:00
});
2015-11-19 01:32:29 +08:00
}
unselect() {
this.selectedItem(null);
this.focusedItem(null);
}
2020-08-27 21:45:47 +08:00
init(contentScrollable, keyScope = 'all') {
this.oContentScrollable = contentScrollable;
2015-11-19 01:32:29 +08:00
2020-08-27 21:45:47 +08:00
if (contentScrollable) {
2021-07-19 17:57:47 +08:00
let getItem = selector => {
let el = event.target.closestWithin(selector, contentScrollable);
return el ? ko.dataFor(el) : null;
};
addEventsListeners(contentScrollable, {
click: event => {
let el = event.target.closestWithin(this.sItemSelector, contentScrollable);
el && this.actionClick(ko.dataFor(el), event);
const item = getItem(this.sItemCheckedSelector);
2019-07-05 03:19:24 +08:00
if (item) {
if (event.shiftKey) {
this.actionClick(item, event);
} else {
this.focusedItem(item);
item.checked(!item.checked());
}
}
},
auxclick: event => {
if (1 == event.button) {
const item = getItem(this.sItemSelector);
if (item) {
this.focusedItem(item);
2022-09-08 19:37:06 +08:00
this.oCallbacks.MiddleClick?.(item);
}
2015-11-19 01:32:29 +08:00
}
}
});
2015-11-19 01:32:29 +08:00
registerShortcut('enter,open', '', keyScope, () => {
2020-08-27 21:45:47 +08:00
const focused = this.focusedItem();
if (focused && !focused.selected()) {
this.actionClick(focused);
2015-11-19 01:32:29 +08:00
return false;
}
});
addShortcut('arrowup,arrowdown', 'meta', keyScope, () => false);
2016-07-01 06:50:11 +08:00
addShortcut('arrowup,arrowdown', 'shift', keyScope, event => {
this.newSelectPosition(event.key, true);
return false;
});
registerShortcut('arrowup,arrowdown,home,end,pageup,pagedown,space', '', keyScope, event => {
this.newSelectPosition(event.key, false);
return false;
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() {
2022-09-10 04:20:40 +08:00
return (this.oCallbacks.AutoSelect || (()=>1))() && this.focusedItem();
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) {
2022-09-10 04:20:40 +08:00
return (item && this.oCallbacks.ItemGetUid?.(item)?.toString()) || '';
2015-11-19 01:32:29 +08:00
}
/**
* @param {string} sEventKey
2015-11-19 01:32:29 +08:00
* @param {boolean} bShiftKey
* @param {boolean=} bForceSelect = false
*/
newSelectPosition(sEventKey, bShiftKey, bForceSelect) {
let isArrow = 'ArrowUp' === sEventKey || 'ArrowDown' === sEventKey,
result;
2019-07-05 03:19:24 +08:00
const pageStep = 10,
list = this.list(),
listLen = list.length,
focused = this.focusedItem();
2022-09-09 18:07:05 +08:00
bShiftKey || (shiftStart = -1);
if (' ' === sEventKey) {
focused?.checked(!focused.checked());
} else if (listLen) {
if (focused) {
if (isArrow) {
2022-09-09 18:07:05 +08:00
let i = list.indexOf(focused),
up = 'ArrowUp' == sEventKey;
if (bShiftKey) {
shiftStart = -1 < shiftStart ? shiftStart : i;
shiftStart == i
? focused.checked(true)
: ((up ? shiftStart < i : shiftStart > i) && focused.checked(false));
}
if (up) {
i > 0 && (result = list[--i]);
2022-09-09 18:07:05 +08:00
} else if (++i < listLen) {
result = list[i];
2015-11-19 01:32:29 +08:00
}
bShiftKey && result?.checked(true);
2022-09-09 18:07:05 +08:00
result || this.oCallbacks.UpOrDown?.(up);
} else if ('Home' === sEventKey) {
result = list[0];
} else if ('End' === sEventKey) {
result = list[list.length - 1];
} else if ('PageDown' === sEventKey) {
let i = list.indexOf(focused);
if (i < listLen - 1) {
result = list[Math.min(i + pageStep, listLen - 1)];
2015-11-19 01:32:29 +08:00
}
} else if ('PageUp' === sEventKey) {
let i = list.indexOf(focused);
if (i > 0) {
result = list[Math.max(0, i - pageStep)];
2015-11-19 01:32:29 +08:00
}
}
} else if (
'Home' == sEventKey ||
'PageUp' == sEventKey
) {
result = list[0];
} else if (
'End' === sEventKey ||
'PageDown' === sEventKey
) {
result = list[list.length - 1];
2015-11-19 01:32:29 +08:00
}
if (result) {
this.focusedItem(result);
if ((this.autoSelect() || bForceSelect) && !this.list.hasChecked()) {
this.selectedItem(result);
}
this.scrollToFocused();
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() {
2020-08-27 21:45:47 +08:00
const scrollable = this.oContentScrollable;
if (scrollable) {
2022-09-10 04:20:40 +08:00
let focused = scrollable.querySelector(this.sItemFocusedSelector);
2020-08-27 21:45:47 +08:00
if (focused) {
const fRect = focused.getBoundingClientRect(),
sRect = scrollable.getBoundingClientRect();
if (fRect.top < sRect.top) {
2022-09-10 04:20:40 +08:00
focused.scrollIntoView(true);
2020-08-27 21:45:47 +08:00
} else if (fRect.bottom > sRect.bottom) {
2022-09-10 04:20:40 +08:00
focused.scrollIntoView(false);
2020-08-27 21:45:47 +08:00
}
} else {
scrollable.scrollTop = 0;
}
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
*/
scrollToTop() {
2020-08-27 21:45:47 +08:00
this.oContentScrollable && (this.oContentScrollable.scrollTop = 0);
2015-11-19 01:32:29 +08:00
}
/**
* @param {Object} item
* @param {Object=} event
*/
2022-10-10 19:52:56 +08:00
actionClick(item, event) {
2019-07-05 03:19:24 +08:00
if (item) {
2022-10-10 19:52:56 +08:00
let select = true;
if (event && !event.altKey) {
if (event.shiftKey && !event.ctrlKey && !event.metaKey) {
2022-09-10 04:20:40 +08:00
const uid = this.getItemUid(item);
if (uid && this.sLastUid && uid !== this.sLastUid) {
let changeRange = false,
isInRange = false,
2022-10-10 19:52:56 +08:00
checked = !item.checked(),
2022-09-10 04:20:40 +08:00
lineUid = '';
this.list().forEach(listItem => {
lineUid = this.getItemUid(listItem);
changeRange = (lineUid === this.sLastUid || lineUid === uid);
if (isInRange || changeRange) {
if (changeRange) {
isInRange = !isInRange;
}
listItem.checked(checked);
}
});
}
this.sLastUid = uid;
2015-11-19 01:32:29 +08:00
this.focusedItem(item);
2022-10-16 00:33:45 +08:00
return;
}
if (!event.shiftKey && (event.ctrlKey || event.metaKey)) {
2022-10-10 19:52:56 +08:00
select = false;
2015-11-19 01:32:29 +08:00
this.focusedItem(item);
2022-10-10 19:52:56 +08:00
const selected = this.selectedItem();
if (selected && item !== selected) {
selected.checked(true);
2015-11-19 01:32:29 +08:00
}
}
}
2022-10-10 19:52:56 +08:00
select ? this.selectMessageItem(item) : item.checked(!item.checked());
2015-11-19 01:32:29 +08:00
}
}
on(eventName, callback) {
this.oCallbacks[eventName] = callback;
}
selectMessageItem(messageItem) {
this.focusedItem(messageItem);
this.selectedItem(messageItem);
this.scrollToFocused();
}
}