diff --git a/src/core/body/body.ctrl.js b/src/core/body/body.ctrl.js index 93e451289..613a7aefe 100644 --- a/src/core/body/body.ctrl.js +++ b/src/core/body/body.ctrl.js @@ -2,6 +2,7 @@ import { PathService } from '../path/path.service'; import { Fastdom } from '../services/fastdom'; import { GRID_PREFIX } from '../definition'; import { jobLine } from '../services/job.line'; +import { ScrollService } from '../scroll/scroll.service'; const MOUSE_LEFT_BUTTON = 1; const VERTICAL_SCROLL_CLASS = `${GRID_PREFIX}-scroll-vertical`; @@ -12,6 +13,7 @@ export class BodyCtrl { this.model = model; this.view = view; this.bag = bag; + this.scrollService = new ScrollService(model, table, bag, view); this.table = table; this.rangeStartCell = null; this.scrollingJob = jobLine(100); @@ -131,6 +133,7 @@ export class BodyCtrl { if (startCell && endCell) { this.navigate(endCell); this.view.selection.selectRange(startCell, endCell, 'body'); + this.scrollService.start(e, startCell); } } } @@ -147,8 +150,10 @@ export class BodyCtrl { } onMouseUp(e) { - const mode = this.selection.mode; - const edit = this.model.edit; + this.scrollService.stop(); + + const { mode } = this.selection; + const { edit } = this.model; if (e.which === MOUSE_LEFT_BUTTON) { const pathFinder = new PathService(this.bag.body); diff --git a/src/core/scroll/scroll.model.d.ts b/src/core/scroll/scroll.model.d.ts index 403b05f5e..6b8262f76 100644 --- a/src/core/scroll/scroll.model.d.ts +++ b/src/core/scroll/scroll.model.d.ts @@ -29,6 +29,16 @@ export declare interface ScrollModel { */ cursor?: number; + /** + * Scroll velocity. + */ + velocity?: number; + + /** + * Defines the distance from borders where scrolling should be initiated. + */ + offset?: number; + map: { rowToView: (index: number) => number, viewToRow: (index: number) => number, diff --git a/src/core/scroll/scroll.model.js b/src/core/scroll/scroll.model.js index 64269edba..00f0a0228 100644 --- a/src/core/scroll/scroll.model.js +++ b/src/core/scroll/scroll.model.js @@ -7,6 +7,8 @@ export class ScrollModel { this.top = 0; this.left = 0; this.cursor = 0; + this.velocity = 10; + this.offset = 50; this.map = { rowToView: identity, diff --git a/src/core/scroll/scroll.service.d.ts b/src/core/scroll/scroll.service.d.ts new file mode 100644 index 000000000..4df40df56 --- /dev/null +++ b/src/core/scroll/scroll.service.d.ts @@ -0,0 +1,10 @@ +import { Bag } from '../dom/bag'; +import { Model } from '../infrastructure/model'; +import { Table } from '../dom/table'; + +export declare class ScrollService { + constructor(model: Model, table: Table, bag: Bag, view: any); + + start(): void; + stop(): void; +} diff --git a/src/core/scroll/scroll.service.js b/src/core/scroll/scroll.service.js new file mode 100644 index 000000000..3f173dd79 --- /dev/null +++ b/src/core/scroll/scroll.service.js @@ -0,0 +1,185 @@ +import { AppError } from '../infrastructure/error'; +import { jobLine } from '../services/job.line'; +import { PathService } from '../path/path.service'; + +export class ScrollService { + constructor(model, table, bag, view) { + this.model = model; + this.table = table; + this.bag = bag; + this.view = view; + this.job = jobLine(50); + this.startCell = null; + this.mouseEvent = null; + this.inMotion = false; + this.allowScroll = false; + + const pathFinder = new PathService(bag.body); + model.scrollChanged.watch(e => { + const path = this.getPath(this.mouseEvent); + const td = pathFinder.cell(path); + + if (td) { + this.navigate(td); + this.view.selection.selectRange(this.startCell, td, 'body'); + } + }); + } + + canScroll(e) { + const rect = this.rect; + const offset = this.offset; + + const mouseOnTopSide = e.clientY < (rect.top + offset); + const mouseOnBottomSide = e.clientY > (rect.bottom - offset); + const mouseOnLeftSide = e.clientX < (rect.left + offset); + const mouseOnRightSide = e.clientX > (rect.right - offset); + + return mouseOnTopSide || mouseOnBottomSide || mouseOnLeftSide || mouseOnRightSide; + } + + start(e, startCell) { + this.mouseEvent = e; + this.startCell = startCell; + + const direction = this.onEdgeOf(e); + if (direction) { + this.allowScroll = true; + this.scroll(direction); + } + } + + scroll(direction) { + const { scroll } = this.model; + const scrollState = scroll(); + const { velocity } = scrollState; + const scrolledToEnd = () => this.isScrolledToEnd(direction); + + const timeout = () => setTimeout(() => { + if (this.allowScroll && this.canScroll(this.mouseEvent) && !scrolledToEnd()) { + this.inMotion = true; + switch (direction) { + case 'right': + case 'bottom': { + const course = direction === 'bottom' ? 'top' : 'left'; + const origin = scrollState[course]; + scroll({ [course]: origin + velocity }); + break; + } + case 'left': + case 'top': { + const course = direction === 'top' ? 'top' : 'left'; + const origin = scrollState[course]; + scroll({ [course]: origin - velocity }); + break; + } + default: { + throw new AppError('scroll.service', `doScroll: Wrong direction`); + } + } + } else { + this.inMotion = false; + return; + } + + timeout(); + + }, 50); + + if(!this.inMotion) { + timeout(); + } + } + + isScrolledToEnd(direction) { + const body = this.body; + + switch (direction) { + case 'top': { + return body.scrollTop === 0; + } + case 'bottom': { + return body.clientHeight === body.scrollHeight - body.scrollTop; + } + case 'left': { + return body.scrollLeft === 0; + } + case 'right': { + return body.scrollLeft === body.scrollWidth - body.clientWidth; + } + default: { + throw new AppError('scroll.service', `isScrolledToEnd: Wrong direction`); + } + } + } + + onEdgeOf(e) { + const rect = this.rect; + const offset = this.offset; + + if (e.clientY < (rect.top + offset) && + e.clientX > (rect.left + offset) && + e.clientX < (rect.right - offset)) { + return 'top'; + } + + if (e.clientY > (rect.bottom - offset) && + e.clientX > (rect.left + offset) && + e.clientX < (rect.right - offset)) { + return 'bottom'; + } + + if (e.clientX < (rect.left + offset) && + e.clientY > (rect.top + offset) && + e.clientY < (rect.bottom - offset)) { + return 'left'; + } + + if (e.clientX > (rect.right - offset) && + e.clientY > (rect.top + offset) && + e.clientY < (rect.bottom - offset)) { + return 'right'; + } + + return false; + } + + stop() { + this.allowScroll = false; + } + + clearInterval() { + clearInterval(this.interval); + this.interval = null; + } + + navigate(cell) { + const { focus } = this.view.nav; + if (focus.canExecute(cell)) { + focus.execute(cell); + } + } + + getPath(e) { + const path = []; + let element = document.elementFromPoint(e.clientX, e.clientY); + while (element) { + path.push(element); + element = element.parentElement; + } + + return path; + } + + get rect() { + return this.table.view.rect(this.body); + } + + get body() { + return this.table.view.markup.body; + } + + get offset() { + return this.model.scroll().offset; + } +} diff --git a/src/lib/main/core/body/body-core.component.ts b/src/lib/main/core/body/body-core.component.ts index 21b500814..ce20f55f2 100644 --- a/src/lib/main/core/body/body-core.component.ts +++ b/src/lib/main/core/body/body-core.component.ts @@ -1,4 +1,4 @@ -import { Component, ElementRef, OnInit, NgZone, Input, ChangeDetectorRef } from '@angular/core'; +import { Component, ElementRef, OnInit, NgZone, Input, ChangeDetectorRef, Inject } from '@angular/core'; import { EventListener } from 'ng2-qgrid/core/infrastructure/event.listener'; import { EventManager } from 'ng2-qgrid/core/infrastructure/event.manager'; import { ColumnView } from 'ng2-qgrid/core/scene/view/column.view'; @@ -12,7 +12,8 @@ import { TableCoreService } from '../table/table-core.service'; @Component({ selector: 'tbody[q-grid-core-body]', - templateUrl: './body-core.component.html' + templateUrl: './body-core.component.html', + providers: [{ provide: 'window', useValue: window }] }) export class BodyCoreComponent extends NgComponent implements OnInit { @Input() pin = 'body'; @@ -21,6 +22,7 @@ export class BodyCoreComponent extends NgComponent implements OnInit { rowId: (index: number, row: any) => any; constructor( + @Inject('window') private window: Window, private element: ElementRef, public $view: ViewCoreService, public $table: TableCoreService, @@ -39,6 +41,7 @@ export class BodyCoreComponent extends NgComponent implements OnInit { const table = this.$table; const ctrl = new BodyCtrl(model, view, this.root.table, this.root.bag); const listener = new EventListener(element, new EventManager(this)); + const windowListener = new EventListener(this.window, new EventManager(this)); this.zone.runOutsideAngular(() => { listener.on('wheel', e => ctrl.onWheel(e)); @@ -56,7 +59,13 @@ export class BodyCoreComponent extends NgComponent implements OnInit { }); listener.on('mousedown', ctrl.onMouseDown.bind(ctrl)); - listener.on('mouseup', ctrl.onMouseUp.bind(ctrl)); + + windowListener.on('mouseup', (e) => { + const isActive = model.focus().isActive; + if (isActive) { + ctrl.onMouseUp(e); + } + }); const { id } = model.data(); this.rowId = id.row; diff --git a/src/lib/main/core/cell/cell-handler.component.ts b/src/lib/main/core/cell/cell-handler.component.ts index 057860604..edf9505c5 100644 --- a/src/lib/main/core/cell/cell-handler.component.ts +++ b/src/lib/main/core/cell/cell-handler.component.ts @@ -175,14 +175,9 @@ export class CellHandlerComponent implements OnInit, AfterViewInit { } get isMarkerVisible() { - const model = this.root.model; - const { column } = model.navigation(); - - if (column) { - const type = column.type; - return model.edit().method === 'batch'; - } + const { model } = this.root; + const { method } = model.edit(); - return false; + return method === 'batch'; } }