Completed
Push — master ( 2f8415...e6e0b0 )
by Rain
02:22
created

dev/Common/Selector.js   A

Size

Lines of Code 736

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
nc 1
dl 0
loc 736
rs 9.7391
noi 3
1
2
import $ from '$';
3
import _ from '_';
4
import key from 'key';
5
import ko from 'ko';
6
import {EventKeyCode} from 'Common/Enums';
7
import {isArray, inArray, noop, noopTrue} from 'Common/Utils';
8
9
class Selector
10
{
11
	list;
12
	listChecked;
13
	isListChecked;
14
15
	focusedItem;
16
	selectedItem;
17
18
	itemSelectedThrottle;
19
20
	selectedItemUseCallback = true;
21
22
	iSelectNextHelper = 0;
23
	iFocusedNextHelper = 0;
24
	oContentVisible;
25
	oContentScrollable;
26
27
	sItemSelector;
28
	sItemSelectedSelector;
29
	sItemCheckedSelector;
30
	sItemFocusedSelector;
31
32
	sLastUid = '';
33
	oCallbacks = {};
34
35
	/**
36
	 * @param {koProperty} koList
37
	 * @param {koProperty} koSelectedItem
38
	 * @param {koProperty} koFocusedItem
39
	 * @param {string} sItemSelector
40
	 * @param {string} sItemSelectedSelector
41
	 * @param {string} sItemCheckedSelector
42
	 * @param {string} sItemFocusedSelector
43
	 */
44
	constructor(koList, koSelectedItem, koFocusedItem,
45
		sItemSelector, sItemSelectedSelector, sItemCheckedSelector, sItemFocusedSelector)
46
	{
47
		this.list = koList;
48
49
		this.listChecked = ko.computed(() => _.filter(this.list(), (item) => item.checked())).extend({rateLimit: 0});
50
		this.isListChecked = ko.computed(() => 0 < this.listChecked().length);
51
52
		this.focusedItem = koFocusedItem || ko.observable(null);
53
		this.selectedItem = koSelectedItem || ko.observable(null);
54
55
		this.itemSelectedThrottle = _.debounce(_.bind(this.itemSelected, this), 300);
56
57
		this.listChecked.subscribe((items) => {
58
			if (0 < items.length)
59
			{
60
				if (null === this.selectedItem())
61
				{
62
					if (this.selectedItem.valueHasMutated)
63
					{
64
						this.selectedItem.valueHasMutated();
65
					}
66
				}
67
				else
68
				{
69
					this.selectedItem(null);
70
				}
71
			}
72
			else if (this.autoSelect() && this.focusedItem())
73
			{
74
				this.selectedItem(this.focusedItem());
75
			}
76
		}, this);
77
78
		this.selectedItem.subscribe((item) => {
79
80
			if (item)
81
			{
82
				if (this.isListChecked())
83
				{
84
					_.each(this.listChecked(), (subItem) => {
85
						subItem.checked(false);
86
					});
87
				}
88
89
				if (this.selectedItemUseCallback)
90
				{
91
					this.itemSelectedThrottle(item);
92
				}
93
			}
94
			else if (this.selectedItemUseCallback)
95
			{
96
				this.itemSelected(null);
97
			}
98
99
		}, this);
100
101
		this.selectedItem = this.selectedItem.extend({toggleSubscribeProperty: [this, 'selected']});
102
		this.focusedItem = this.focusedItem.extend({toggleSubscribeProperty: [null, 'focused']});
103
104
		this.sItemSelector = sItemSelector;
105
		this.sItemSelectedSelector = sItemSelectedSelector;
106
		this.sItemCheckedSelector = sItemCheckedSelector;
107
		this.sItemFocusedSelector = sItemFocusedSelector;
108
109
		this.focusedItem.subscribe((item) => {
110
			if (item)
111
			{
112
				this.sLastUid = this.getItemUid(item);
113
			}
114
		}, this);
115
116
		let
117
			aCache = [],
118
			aCheckedCache = [],
119
			mFocused = null,
120
			mSelected = null;
121
122
		this.list.subscribe((items) => {
123
124
			if (isArray(items))
125
			{
126
				_.each(items, (item) => {
127
					if (item)
128
					{
129
						const uid = this.getItemUid(item);
130
131
						aCache.push(uid);
132
						if (item.checked())
133
						{
134
							aCheckedCache.push(uid);
135
						}
136
						if (null === mFocused && item.focused())
137
						{
138
							mFocused = uid;
139
						}
140
						if (null === mSelected && item.selected())
141
						{
142
							mSelected = uid;
143
						}
144
					}
145
				});
146
			}
147
		}, this, 'beforeChange');
148
149
		this.list.subscribe((aItems) => {
150
151
			let
152
				temp = null,
153
				getNext = false,
154
				isNextFocused = mFocused,
155
				isChecked = false,
156
				isSelected = false,
157
				len = 0;
158
159
			const
160
				uids = [];
161
162
			this.selectedItemUseCallback = false;
163
164
			this.focusedItem(null);
165
			this.selectedItem(null);
166
167
			if (isArray(aItems))
168
			{
169
				len = aCheckedCache.length;
170
171
				_.each(aItems, (item) => {
172
173
					const uid = this.getItemUid(item);
174
					uids.push(uid);
175
176
					if (null !== mFocused && mFocused === uid)
177
					{
178
						this.focusedItem(item);
179
						mFocused = null;
180
					}
181
182
					if (0 < len && -1 < inArray(uid, aCheckedCache))
183
					{
184
						isChecked = true;
185
						item.checked(true);
186
						len -= 1;
187
					}
188
189
					if (!isChecked && null !== mSelected && mSelected === uid)
190
					{
191
						isSelected = true;
192
						this.selectedItem(item);
193
						mSelected = null;
194
					}
195
				});
196
197
				this.selectedItemUseCallback = true;
198
199
				if (!isChecked && !isSelected && this.autoSelect())
200
				{
201
					if (this.focusedItem())
202
					{
203
						this.selectedItem(this.focusedItem());
204
					}
205
					else if (0 < aItems.length)
206
					{
207
						if (null !== isNextFocused)
208
						{
209
							getNext = false;
210
							isNextFocused = _.find(aCache, (sUid) => {
211
								if (getNext && -1 < inArray(sUid, uids))
212
								{
213
									return sUid;
214
								}
215
								else if (isNextFocused === sUid)
216
								{
217
									getNext = true;
218
								}
219
								return false;
220
							});
221
222
							if (isNextFocused)
223
							{
224
								temp = _.find(aItems, (oItem) => isNextFocused === this.getItemUid(oItem));
225
							}
226
						}
227
228
						this.selectedItem(temp || null);
229
						this.focusedItem(this.selectedItem());
230
					}
231
				}
232
233
				if ((0 !== this.iSelectNextHelper || 0 !== this.iFocusedNextHelper) && 0 < aItems.length && !this.focusedItem())
234
				{
235
					temp = null;
236
					if (0 !== this.iFocusedNextHelper)
237
					{
238
						temp = aItems[-1 === this.iFocusedNextHelper ? aItems.length - 1 : 0] || null;
239
					}
240
241
					if (!temp && 0 !== this.iSelectNextHelper)
242
					{
243
						temp = aItems[-1 === this.iSelectNextHelper ? aItems.length - 1 : 0] || null;
244
					}
245
246
					if (temp)
247
					{
248
						if (0 !== this.iSelectNextHelper)
249
						{
250
							this.selectedItem(temp || null);
251
						}
252
253
						this.focusedItem(temp || null);
254
255
						this.scrollToFocused();
256
257
						_.delay(() => this.scrollToFocused(), 100);
258
					}
259
260
					this.iSelectNextHelper = 0;
261
					this.iFocusedNextHelper = 0;
262
				}
263
			}
264
265
			aCache = [];
266
			aCheckedCache = [];
267
			mFocused = null;
268
			mSelected = null;
269
270
		});
271
	}
272
273
	itemSelected(item) {
274
275
		if (this.isListChecked())
276
		{
277
			if (!item)
278
			{
279
				(this.oCallbacks.onItemSelect || noop)(item || null);
280
			}
281
		}
282
		else if (item)
283
		{
284
			(this.oCallbacks.onItemSelect || noop)(item);
285
		}
286
	}
287
288
	/**
289
	 * @param {boolean} forceSelect
290
	 */
291
	goDown(forceSelect) {
292
		this.newSelectPosition(EventKeyCode.Down, false, forceSelect);
293
	}
294
295
	/**
296
	 * @param {boolean} forceSelect
297
	 */
298
	goUp(forceSelect) {
299
		this.newSelectPosition(EventKeyCode.Up, false, forceSelect);
300
	}
301
302
	unselect() {
303
		this.selectedItem(null);
304
		this.focusedItem(null);
305
	}
306
307
	init(contentVisible, contentScrollable, keyScope = 'all') {
308
309
		this.oContentVisible = contentVisible;
310
		this.oContentScrollable = contentScrollable;
311
312
		if (this.oContentVisible && this.oContentScrollable)
313
		{
314
			$(this.oContentVisible)
315
				.on('selectstart', (event) => {
316
					if (event && event.preventDefault)
317
					{
318
						event.preventDefault();
319
					}
320
				})
321
				.on('click', this.sItemSelector, (event) => {
322
					this.actionClick(ko.dataFor(event.currentTarget), event);
323
				})
324
				.on('click', this.sItemCheckedSelector, (event) => {
325
					const item = ko.dataFor(event.currentTarget);
326
					if (item)
327
					{
328
						if (event && event.shiftKey)
329
						{
330
							this.actionClick(item, event);
331
						}
332
						else
333
						{
334
							this.focusedItem(item);
335
							item.checked(!item.checked());
336
						}
337
					}
338
				});
339
340
			key('enter', keyScope, () => {
341
				if (this.focusedItem() && !this.focusedItem().selected())
342
				{
343
					this.actionClick(this.focusedItem());
344
					return false;
345
				}
346
347
				return true;
348
			});
349
350
			key('ctrl+up, command+up, ctrl+down, command+down', keyScope, () => false);
351
352
			key('up, shift+up, down, shift+down, home, end, pageup, pagedown, insert, space', keyScope, (event, handler) => {
353
				if (event && handler && handler.shortcut)
354
				{
355
					let eventKey = 0;
356
					switch (handler.shortcut)
357
					{
358
						case 'up':
359
						case 'shift+up':
360
							eventKey = EventKeyCode.Up;
361
							break;
362
						case 'down':
363
						case 'shift+down':
364
							eventKey = EventKeyCode.Down;
365
							break;
366
						case 'insert':
367
							eventKey = EventKeyCode.Insert;
368
							break;
369
						case 'space':
370
							eventKey = EventKeyCode.Space;
371
							break;
372
						case 'home':
373
							eventKey = EventKeyCode.Home;
374
							break;
375
						case 'end':
376
							eventKey = EventKeyCode.End;
377
							break;
378
						case 'pageup':
379
							eventKey = EventKeyCode.PageUp;
380
							break;
381
						case 'pagedown':
382
							eventKey = EventKeyCode.PageDown;
383
							break;
384
						// no default
385
					}
386
387
					if (0 < eventKey)
388
					{
389
						this.newSelectPosition(eventKey, key.shift);
390
						return false;
391
					}
392
				}
393
394
				return true;
395
			});
396
		}
397
	}
398
399
	/**
400
	 * @returns {boolean}
401
	 */
402
	autoSelect() {
403
		return !!(this.oCallbacks.onAutoSelect || noopTrue)();
404
	}
405
406
	/**
407
	 * @param {boolean} up
408
	 */
409
	doUpUpOrDownDown(up) {
410
		(this.oCallbacks.onUpUpOrDownDown || noopTrue)(!!up);
411
	}
412
413
	/**
414
	 * @param {Object} oItem
415
	 * @returns {string}
416
	 */
417
	getItemUid(item) {
418
419
		let uid = '';
420
421
		const getItemUidCallback = this.oCallbacks.onItemGetUid || null;
422
		if (getItemUidCallback && item)
423
		{
424
			uid = getItemUidCallback(item);
425
		}
426
427
		return uid.toString();
428
	}
429
430
	/**
431
	 * @param {number} iEventKeyCode
432
	 * @param {boolean} bShiftKey
433
	 * @param {boolean=} bForceSelect = false
434
	 */
435
	newSelectPosition(iEventKeyCode, bShiftKey, bForceSelect) {
436
437
		let
438
			index = 0,
439
			isNext = false,
440
			isStop = false,
441
			result = null;
442
443
		const
444
			pageStep = 10,
445
			list = this.list(),
446
			listLen = list ? list.length : 0,
447
			focused = this.focusedItem();
448
449
		if (0 < listLen)
450
		{
451
			if (!focused)
452
			{
453
				if (EventKeyCode.Down === iEventKeyCode || EventKeyCode.Insert === iEventKeyCode ||
454
					EventKeyCode.Space === iEventKeyCode || EventKeyCode.Home === iEventKeyCode ||
455
					EventKeyCode.PageUp === iEventKeyCode)
456
				{
457
					result = list[0];
458
				}
459
				else if (EventKeyCode.Up === iEventKeyCode || EventKeyCode.End === iEventKeyCode ||
460
					EventKeyCode.PageDown === iEventKeyCode)
461
				{
462
					result = list[list.length - 1];
463
				}
464
			}
465
			else if (focused)
466
			{
467
				if (EventKeyCode.Down === iEventKeyCode || EventKeyCode.Up === iEventKeyCode ||
468
					EventKeyCode.Insert === iEventKeyCode || EventKeyCode.Space === iEventKeyCode)
469
				{
470
					_.each(list, (item) => {
471
						if (!isStop)
472
						{
473
							switch (iEventKeyCode)
474
							{
475
								case EventKeyCode.Up:
476
									if (focused === item)
477
									{
478
										isStop = true;
479
									}
480
									else
481
									{
482
										result = item;
483
									}
484
									break;
485
								case EventKeyCode.Down:
486
								case EventKeyCode.Insert:
487
									if (isNext)
488
									{
489
										result = item;
490
										isStop = true;
491
									}
492
									else if (focused === item)
493
									{
494
										isNext = true;
495
									}
496
									break;
497
								// no default
498
							}
499
						}
500
					});
501
502
					if (!result && (EventKeyCode.Down === iEventKeyCode || EventKeyCode.Up === iEventKeyCode))
503
					{
504
						this.doUpUpOrDownDown(EventKeyCode.Up === iEventKeyCode);
505
					}
506
				}
507
				else if (EventKeyCode.Home === iEventKeyCode || EventKeyCode.End === iEventKeyCode)
508
				{
509
					if (EventKeyCode.Home === iEventKeyCode)
510
					{
511
						result = list[0];
512
					}
513
					else if (EventKeyCode.End === iEventKeyCode)
514
					{
515
						result = list[list.length - 1];
516
					}
517
				}
518
				else if (EventKeyCode.PageDown === iEventKeyCode)
519
				{
520
					for (; index < listLen; index++)
521
					{
522
						if (focused === list[index])
523
						{
524
							index += pageStep;
525
							index = listLen - 1 < index ? listLen - 1 : index;
526
							result = list[index];
527
							break;
528
						}
529
					}
530
				}
531
				else if (EventKeyCode.PageUp === iEventKeyCode)
532
				{
533
					for (index = listLen; 0 <= index; index--)
534
					{
535
						if (focused === list[index])
536
						{
537
							index -= pageStep;
538
							index = 0 > index ? 0 : index;
539
							result = list[index];
540
							break;
541
						}
542
					}
543
				}
544
			}
545
		}
546
547
		if (result)
548
		{
549
			this.focusedItem(result);
550
551
			if (focused)
552
			{
553
				if (bShiftKey)
554
				{
555
					if (EventKeyCode.Up === iEventKeyCode || EventKeyCode.Down === iEventKeyCode)
556
					{
557
						focused.checked(!focused.checked());
558
					}
559
				}
560
				else if (EventKeyCode.Insert === iEventKeyCode || EventKeyCode.Space === iEventKeyCode)
561
				{
562
					focused.checked(!focused.checked());
563
				}
564
			}
565
566
			if ((this.autoSelect() || !!bForceSelect) &&
567
				!this.isListChecked() && EventKeyCode.Space !== iEventKeyCode)
568
			{
569
				this.selectedItem(result);
570
			}
571
572
			this.scrollToFocused();
573
		}
574
		else if (focused)
575
		{
576
			if (bShiftKey && (EventKeyCode.Up === iEventKeyCode || EventKeyCode.Down === iEventKeyCode))
577
			{
578
				focused.checked(!focused.checked());
579
			}
580
			else if (EventKeyCode.Insert === iEventKeyCode || EventKeyCode.Space === iEventKeyCode)
581
			{
582
				focused.checked(!focused.checked());
583
			}
584
585
			this.focusedItem(focused);
586
		}
587
	}
588
589
	/**
590
	 * @returns {boolean}
591
	 */
592
	scrollToFocused() {
593
594
		if (!this.oContentVisible || !this.oContentScrollable)
595
		{
596
			return false;
597
		}
598
599
		const
600
			offset = 20,
601
			list = this.list(),
602
			$focused = $(this.sItemFocusedSelector, this.oContentScrollable),
603
			pos = $focused.position(),
604
			visibleHeight = this.oContentVisible.height(),
605
			focusedHeight = $focused.outerHeight();
606
607
		if (list && list[0] && list[0].focused())
608
		{
609
			this.oContentScrollable.scrollTop(0);
610
			return true;
611
		}
612
		else if (pos && (0 > pos.top || pos.top + focusedHeight > visibleHeight))
613
		{
614
			this.oContentScrollable.scrollTop(0 > pos.top ?
615
				this.oContentScrollable.scrollTop() + pos.top - offset :
616
				this.oContentScrollable.scrollTop() + pos.top - visibleHeight + focusedHeight + offset
617
			);
618
619
			return true;
620
		}
621
622
		return false;
623
	}
624
625
	/**
626
	 * @param {boolean=} fast = false
627
	 * @returns {boolean}
628
	 */
629
	scrollToTop(fast = false) {
630
631
		if (!this.oContentVisible || !this.oContentScrollable)
632
		{
633
			return false;
634
		}
635
636
		if (fast || 50 > this.oContentScrollable.scrollTop())
637
		{
638
			this.oContentScrollable.scrollTop(0);
639
		}
640
		else
641
		{
642
			this.oContentScrollable.stop().animate({scrollTop: 0}, 200);
643
		}
644
645
		return true;
646
	}
647
648
	eventClickFunction(item, event) {
649
650
		let
651
			index = 0,
652
			length = 0,
653
			changeRange = false,
654
			isInRange = false,
655
			list = [],
656
			checked = false,
657
			listItem = null,
658
			lineUid = '';
659
660
		const uid = this.getItemUid(item);
661
		if (event && event.shiftKey)
662
		{
663
			if ('' !== uid && '' !== this.sLastUid && uid !== this.sLastUid)
664
			{
665
				list = this.list();
666
				checked = item.checked();
667
668
				for (index = 0, length = list.length; index < length; index++)
669
				{
670
					listItem = list[index];
671
					lineUid = this.getItemUid(listItem);
672
673
					changeRange = false;
674
					if (lineUid === this.sLastUid || lineUid === uid)
675
					{
676
						changeRange = true;
677
					}
678
679
					if (changeRange)
680
					{
681
						isInRange = !isInRange;
682
					}
683
684
					if (isInRange || changeRange)
685
					{
686
						listItem.checked(checked);
687
					}
688
				}
689
			}
690
		}
691
692
		this.sLastUid = '' === uid ? '' : uid;
693
	}
694
695
	/**
696
	 * @param {Object} item
697
	 * @param {Object=} event
698
	 */
699
	actionClick(item, event = null) {
700
701
		if (item)
702
		{
703
			let click = true;
704
			if (event)
705
			{
706
				if (event.shiftKey && !(event.ctrlKey || event.metaKey) && !event.altKey)
707
				{
708
					click = false;
709
					if ('' === this.sLastUid)
710
					{
711
						this.sLastUid = this.getItemUid(item);
712
					}
713
714
					item.checked(!item.checked());
715
					this.eventClickFunction(item, event);
716
717
					this.focusedItem(item);
718
				}
719
				else if ((event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey)
720
				{
721
					click = false;
722
					this.focusedItem(item);
723
724
					if (this.selectedItem() && item !== this.selectedItem())
725
					{
726
						this.selectedItem().checked(true);
727
					}
728
729
					item.checked(!item.checked());
730
				}
731
			}
732
733
			if (click)
734
			{
735
				this.selectMessageItem(item);
736
			}
737
		}
738
	}
739
740
	on(eventName, callback) {
741
		this.oCallbacks[eventName] = callback;
742
	}
743
744
	selectMessageItem(messageItem) {
745
		this.focusedItem(messageItem);
746
		this.selectedItem(messageItem);
747
		this.scrollToFocused();
748
	}
749
}
750
751
export {Selector, Selector as default};
752