mirror of
https://github.com/the-djmaze/snappymail.git
synced 2024-11-10 17:13:38 +08:00
403 lines
9.6 KiB
JavaScript
403 lines
9.6 KiB
JavaScript
import ko from 'ko';
|
|
import { addEventsListeners, addShortcut, registerShortcut } from 'Common/Globals';
|
|
import { isArray } from 'Common/Utils';
|
|
import { koComputable } from 'External/ko';
|
|
|
|
/*
|
|
oCallbacks:
|
|
ItemSelect
|
|
MiddleClick
|
|
canSelect
|
|
ItemGetUid
|
|
UpOrDown
|
|
*/
|
|
|
|
let shiftStart;
|
|
|
|
export class Selector {
|
|
/**
|
|
* @param {koProperty} koList
|
|
* @param {koProperty} koSelectedItem
|
|
* @param {koProperty} koFocusedItem
|
|
* @param {string} sItemSelector
|
|
* @param {string} sItemCheckedSelector
|
|
*/
|
|
constructor(
|
|
koList,
|
|
koSelectedItem,
|
|
koFocusedItem,
|
|
sItemSelector,
|
|
sItemCheckedSelector
|
|
) {
|
|
koFocusedItem = (koFocusedItem || ko.observable(null)).extend({ toggleSubscribeProperty: [this, 'focused'] });
|
|
koSelectedItem = (koSelectedItem || ko.observable(null)).extend({ toggleSubscribeProperty: [null, 'selected'] });
|
|
|
|
this.list = koList;
|
|
this.listChecked = koComputable(() => koList.filter(item => item.checked())).extend({ rateLimit: 0 });
|
|
|
|
this.focusedItem = koFocusedItem;
|
|
this.selectedItem = koSelectedItem;
|
|
|
|
this.iSelectNextHelper = 0;
|
|
this.iFocusedNextHelper = 0;
|
|
// this.oContentScrollable = null;
|
|
|
|
this.sItemSelector = sItemSelector;
|
|
this.sItemCheckedSelector = sItemCheckedSelector;
|
|
this.sItemFocusedSelector = sItemSelector + '.focused';
|
|
|
|
this.sLastUid = '';
|
|
this.oCallbacks = {};
|
|
|
|
const
|
|
itemSelected = item => {
|
|
if (koList.hasChecked()) {
|
|
item || this.oCallbacks.ItemSelect?.(null);
|
|
} else if (item) {
|
|
this.oCallbacks.ItemSelect?.(item);
|
|
}
|
|
},
|
|
|
|
itemSelectedThrottle = (item => itemSelected(item)).debounce(300);
|
|
|
|
this.listChecked.subscribe(items => {
|
|
if (items.length) {
|
|
koSelectedItem() ? koSelectedItem(null) : koSelectedItem.valueHasMutated?.();
|
|
} else {
|
|
this.autoSelect();
|
|
}
|
|
});
|
|
|
|
let selectedItemUseCallback = true;
|
|
|
|
koSelectedItem.subscribe(item => {
|
|
if (item) {
|
|
// koList.forEach(subItem => subItem.checked(false));
|
|
selectedItemUseCallback && itemSelectedThrottle(item);
|
|
} else {
|
|
selectedItemUseCallback && itemSelected();
|
|
}
|
|
});
|
|
|
|
koFocusedItem.subscribe(item => item && (this.sLastUid = this.getItemUid(item)));
|
|
|
|
/**
|
|
* Below code is used to keep checked/focused/selected states when array is refreshed.
|
|
*/
|
|
|
|
let aCheckedCache = [],
|
|
mFocused = null,
|
|
mSelected = null;
|
|
|
|
// Before removing old list
|
|
koList.subscribe(
|
|
items => {
|
|
if (isArray(items)) {
|
|
items.forEach(item => {
|
|
const uid = this.getItemUid(item);
|
|
if (uid) {
|
|
item.checked() && aCheckedCache.push(uid);
|
|
if (!mFocused && item.focused()) {
|
|
mFocused = uid;
|
|
}
|
|
if (!mSelected && item.selected()) {
|
|
mSelected = uid;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
},
|
|
this,
|
|
'beforeChange'
|
|
);
|
|
|
|
koList.subscribe(aItems => {
|
|
selectedItemUseCallback = false;
|
|
|
|
this.unselect();
|
|
|
|
if (isArray(aItems)) {
|
|
let temp,
|
|
isChecked,
|
|
next = this.iFocusedNextHelper || this.iSelectNextHelper;
|
|
|
|
aItems.forEach(item => {
|
|
const uid = this.getItemUid(item);
|
|
if (uid) {
|
|
|
|
if (mFocused === uid) {
|
|
koFocusedItem(item);
|
|
mFocused = null;
|
|
}
|
|
|
|
if (aCheckedCache.includes(uid)) {
|
|
item.checked(true);
|
|
isChecked = true;
|
|
}
|
|
|
|
if (!isChecked && mSelected === uid) {
|
|
koSelectedItem(item);
|
|
mSelected = null;
|
|
}
|
|
}
|
|
});
|
|
|
|
selectedItemUseCallback = true;
|
|
|
|
if (next && aItems.length && !koFocusedItem()) {
|
|
temp = aItems[-1 === next ? aItems.length - 1 : 0];
|
|
if (temp) {
|
|
this.iSelectNextHelper && koSelectedItem(temp);
|
|
|
|
koFocusedItem(temp);
|
|
|
|
this.scrollToFocused();
|
|
setTimeout(this.scrollToFocused, 100);
|
|
}
|
|
|
|
this.iSelectNextHelper = 0;
|
|
this.iFocusedNextHelper = 0;
|
|
}
|
|
|
|
!isChecked && !koSelectedItem() && this.autoSelect();
|
|
}
|
|
|
|
aCheckedCache = [];
|
|
mFocused = null;
|
|
mSelected = null;
|
|
selectedItemUseCallback = true;
|
|
});
|
|
}
|
|
|
|
unselect() {
|
|
this.selectedItem(null);
|
|
this.focusedItem(null);
|
|
}
|
|
|
|
init(contentScrollable, keyScope = 'all') {
|
|
this.oContentScrollable = contentScrollable;
|
|
|
|
if (contentScrollable) {
|
|
let getItem = selector => {
|
|
let el = event.target.closestWithin(selector, contentScrollable);
|
|
return el ? ko.dataFor(el) : null;
|
|
};
|
|
|
|
addEventsListeners(contentScrollable, {
|
|
click: event => {
|
|
const el = event.target.closestWithin(this.sItemSelector, contentScrollable);
|
|
let item = el && ko.dataFor(el);
|
|
el && (this.oCallbacks.click || (()=>1))(event, item) && this.actionClick(item, event);
|
|
|
|
item = getItem(this.sItemCheckedSelector);
|
|
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);
|
|
this.oCallbacks.MiddleClick?.(item);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
registerShortcut('enter,open', '', keyScope, () => {
|
|
const focused = this.focusedItem();
|
|
if (focused && !focused.selected()) {
|
|
this.actionClick(focused);
|
|
return false;
|
|
}
|
|
});
|
|
|
|
addShortcut('arrowup,arrowdown', 'meta', keyScope, () => false);
|
|
|
|
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;
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @returns {boolean}
|
|
*/
|
|
autoSelect(bForce) {
|
|
(bForce || (this.oCallbacks.canSelect || (()=>1))())
|
|
&& this.focusedItem()
|
|
&& this.selectedItem(this.focusedItem());
|
|
}
|
|
|
|
/**
|
|
* @param {Object} oItem
|
|
* @returns {string}
|
|
*/
|
|
getItemUid(item) {
|
|
return (item && this.oCallbacks.ItemGetUid?.(item)?.toString()) || '';
|
|
}
|
|
|
|
/**
|
|
* @param {string} sEventKey
|
|
* @param {boolean} bShiftKey
|
|
* @param {boolean=} bForceSelect = false
|
|
*/
|
|
newSelectPosition(sEventKey, bShiftKey, bForceSelect) {
|
|
let result;
|
|
|
|
const up = 'ArrowUp' === sEventKey,
|
|
isArrow = up || 'ArrowDown' === sEventKey,
|
|
pageStep = 10,
|
|
list = this.list(),
|
|
listLen = list.length,
|
|
focused = this.focusedItem();
|
|
|
|
bShiftKey || (shiftStart = -1);
|
|
|
|
if (' ' === sEventKey) {
|
|
focused?.checked(!focused.checked());
|
|
} else if (listLen) {
|
|
if (focused) {
|
|
if (isArrow) {
|
|
let i = list.indexOf(focused);
|
|
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]);
|
|
} else if (++i < listLen) {
|
|
result = list[i];
|
|
}
|
|
bShiftKey && result?.checked(true);
|
|
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)];
|
|
}
|
|
} else if ('PageUp' === sEventKey) {
|
|
let i = list.indexOf(focused);
|
|
if (i > 0) {
|
|
result = list[Math.max(0, i - pageStep)];
|
|
}
|
|
}
|
|
} else if (
|
|
'Home' == sEventKey ||
|
|
'PageUp' == sEventKey
|
|
) {
|
|
result = list[0];
|
|
} else if (
|
|
'End' === sEventKey ||
|
|
'PageDown' === sEventKey
|
|
) {
|
|
result = list[list.length - 1];
|
|
}
|
|
|
|
if (result) {
|
|
this.focusedItem(result);
|
|
!this.list.hasChecked() && this.autoSelect(bForceSelect);
|
|
this.scrollToFocused();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @returns {boolean}
|
|
*/
|
|
scrollToFocused() {
|
|
const scrollable = this.oContentScrollable;
|
|
if (scrollable) {
|
|
let focused = scrollable.querySelector(this.sItemFocusedSelector);
|
|
if (focused) {
|
|
const fRect = focused.getBoundingClientRect(),
|
|
sRect = scrollable.getBoundingClientRect();
|
|
if (fRect.top < sRect.top) {
|
|
focused.scrollIntoView(true);
|
|
} else if (fRect.bottom > sRect.bottom) {
|
|
focused.scrollIntoView(false);
|
|
}
|
|
} else {
|
|
scrollable.scrollTop = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @returns {boolean}
|
|
*/
|
|
scrollToTop() {
|
|
this.oContentScrollable && (this.oContentScrollable.scrollTop = 0);
|
|
}
|
|
|
|
/**
|
|
* @param {Object} item
|
|
* @param {Object=} event
|
|
*/
|
|
actionClick(item, event) {
|
|
if (item) {
|
|
let select = true;
|
|
if (event && !event.altKey) {
|
|
if (event.shiftKey && !event.ctrlKey && !event.metaKey) {
|
|
const uid = this.getItemUid(item);
|
|
if (uid && this.sLastUid && uid !== this.sLastUid) {
|
|
let changeRange = false,
|
|
isInRange = false,
|
|
checked = !item.checked(),
|
|
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;
|
|
this.focusedItem(item);
|
|
return;
|
|
}
|
|
if (!event.shiftKey && (event.ctrlKey || event.metaKey)) {
|
|
select = false;
|
|
this.focusedItem(item);
|
|
const selected = this.selectedItem();
|
|
if (selected && item !== selected) {
|
|
selected.checked(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
select ? this.selectMessageItem(item) : item.checked(!item.checked());
|
|
}
|
|
}
|
|
|
|
on(eventName, callback) {
|
|
this.oCallbacks[eventName] = callback;
|
|
}
|
|
|
|
selectMessageItem(messageItem) {
|
|
this.focusedItem(messageItem);
|
|
this.selectedItem(messageItem);
|
|
this.scrollToFocused();
|
|
}
|
|
}
|