comindware.ui
Version:
Comindware Core UI provides the basic components like editors, lists, dropdowns, popups that we so desperately need while creating Marionette-based single-page applications.
349 lines (287 loc) • 10.5 kB
JavaScript
/**
* Developer: Stepan Burguchev
* Date: 7/7/2014
* Copyright: 2009-2016 Comindware®
* All Rights Reserved
* Published under the MIT license
*/
'use strict';
import { Handlebars } from 'lib';
import template from '../templates/scrollbar.hbs';
/*
Public interface:
This view produce:
trigger: positionChanged (sender, { oldPosition, position })
This view react on:
collection change (via Backbone.Collection events)
position change (when we scroll content view somehow without this scrollbar): updatePosition(newPosition)
viewportHeight change (when we resize content views attached to this scrollbar): updateViewportHeight(newViewportHeight)
*/
/**
* Some description for initializer
* @name ScrollBarView
* @memberof module:core.list.views
* @class ScrollBarView
* @extends Marionette.ItemView
* @constructor
* @description View Scrollbar'а
* @param {Object} options Constructor options
* */
const ScrollbarView = Marionette.ItemView.extend({
initialize() {
if (this.collection === undefined) {
throw 'You must provide a collection to display.';
}
_.bindAll(this, '__documentMouseUp', '__documentMouseMove');
this.$document = $(document);
this.state = {
position: 0,
viewportHeight: 25,
count: 0
};
this.__updateCount(this.collection.length);
},
className: 'scrollbar',
template: Handlebars.compile(template),
model: null,
state: null,
dragContext: null,
ui: {
dragger: '.dragger'
},
constants: {
minDraggerHeight: 25 // in pixels
},
collectionEvents: {
add: '__handleCollectionAdd',
remove: '__handleCollectionRemove',
reset: '__handleCollectionReset'
},
events: {
mousewheel: '__mousewheel',
mousedown: '__mousedown',
mouseenter: '__mouseenter',
mouseleave: '__mouseleave',
'mousedown @ui.dragger': '__draggerMousedown'
},
onShow() {
this.rendered = true;
this.__updateScrollbarVisibility();
this.__updateScrollbarVisibility();
this.__updateDraggerPosition();
this.__updateDraggerHeight();
},
onRender() {
function stopAndPreventDefault(e) {
e.preventDefault();
e.stopPropagation();
}
this.el.onselectstart = stopAndPreventDefault;
this.el.ondragstart = stopAndPreventDefault;
},
updateViewportHeight(newViewportHeight) {
if (newViewportHeight === undefined) {
throw 'newViewportHeight is undefined';
}
if (newViewportHeight < 1) {
throw 'newViewportHeight is invalid';
}
if (!this.rendered) {
this.state.viewportHeight = newViewportHeight;
return;
}
if (this.state.viewportHeight !== newViewportHeight) {
this.state.viewportHeight = newViewportHeight;
this.__updateScrollbarVisibility();
this.__updateDraggerHeight();
const maxPos = this.__getMaxPosition();
if (this.state.position > maxPos) {
this.__updatePositionState(maxPos, true);
this.__updateDraggerPosition();
}
}
},
updatePosition(newPosition) {
this.__updatePositionInternal(newPosition, false);
},
__updateCount(newCount) {
if (newCount === undefined) {
throw 'newCount is undefined';
}
if (newCount < 0) {
throw 'newCount is invalid';
}
if (!this.rendered) {
this.state.count = newCount;
return;
}
if (this.state.count !== newCount) {
const maxPos = this.__getMaxPosition();
const newMaxPos = Math.max(0, maxPos - (this.state.count - newCount));
if (this.state.position > newMaxPos) {
this.__updatePositionState(newMaxPos, true);
}
this.state.count = newCount;
this.__updateScrollbarVisibility();
this.__updateDraggerHeight();
this.__updateDraggerPosition();
}
},
// normalizes new position into [min,max] and updates view+state
__updatePositionInternal(newPosition, triggerEvents) {
if (newPosition === undefined) {
throw 'newPosition is undefined';
}
if (!this.rendered) {
this.state.position = newPosition;
return;
}
newPosition = Math.max(0, Math.min(this.__getMaxPosition(), newPosition));
if (this.state.position !== newPosition) {
this.__updatePositionState(newPosition, triggerEvents);
this.__updateDraggerPosition();
}
return newPosition;
},
__handleCollectionAdd(model, collection) {
this.__updateCount(collection.length);
},
__handleCollectionRemove(model, collection) {
this.__updateCount(collection.length);
},
__handleCollectionReset(collection) {
this.__updateCount(collection.length);
},
__mouseenter() {
this.$el.addClass('hover');
},
__mouseleave() {
this.$el.removeClass('hover');
},
__draggerMousedown(e) {
this.__stopDrag();
this.__startDrag(e);
return false;
},
__documentMouseMove(e) {
if (!this.dragContext) {
return;
}
const ctx = this.dragContext;
if (e.pageY !== ctx.pageOffsetY) {
const availableHeight = ctx.scrollbarHeight - ctx.draggerHeight;
const currentPosition = e.pageY - ctx.mouseOffsetY - ctx.scrollbarPositionY;
const newDraggerTop = Math.min(Math.max(currentPosition, 0), availableHeight);
const devicePercents = newDraggerTop / ctx.scrollbarHeight * 100;
this.ui.dragger.css({ top: `${devicePercents}%` });
// updating scrollbar state, sending positionChanged event if needed
const maxPos = this.__getMaxPosition();
const newPosition = availableHeight !== 0 ? Math.min(maxPos, Math.floor((maxPos + 1) * (newDraggerTop / availableHeight))) : 0;
this.__updatePositionState(newPosition, true);
}
return false;
},
__documentMouseUp() {
this.__stopDrag();
return false;
},
__mousedown(e) {
if (e.target !== e.currentTarget) {
return false;
}
const draggerY = this.__getPosition(this.ui.dragger).y;
let sign = e.pageY - draggerY;
sign /= Math.abs(sign);
const delta = this.state.viewportHeight;
const newPosition = this.state.position + sign * delta;
this.__updatePositionInternal(newPosition, true);
return false;
},
__mousewheel(e) {
const delta = this.state.viewportHeight;
const newPosition = this.state.position - e.deltaY * Math.max(1, Math.floor(delta / 6));
this.__updatePositionInternal(newPosition, true);
return false;
},
__updatePositionState(newPosition, triggerEvents) {
if (this.state.position === newPosition) {
return;
}
const oldPosition = this.state.position;
this.state.position = newPosition;
if (triggerEvents) {
this.trigger('positionChanged', this, {
oldPosition,
position: newPosition
});
}
},
__updateScrollbarVisibility() {
if (this.state.count > this.state.viewportHeight) {
this.$el.parent().removeClass('dev-scrollbar__hidden');
} else {
this.$el.parent().addClass('dev-scrollbar__hidden');
}
},
__updateDraggerHeight() {
const minHeight = Math.min(1, this.constants.minDraggerHeight / this.$el.height());
const heightPc = Math.max(minHeight, Math.min(1, this.state.viewportHeight / this.state.count)) * 100;
this.ui.dragger.css({ height: `${heightPc}%` });
},
__updateDraggerPosition() {
let newTopPc;
const maxPos = this.__getMaxPosition();
if (maxPos > 0) {
const h = this.$el.height();
const dh = this.ui.dragger.height();
const availableHeight = h - dh;
const newTop = this.state.position / this.__getMaxPosition() * availableHeight;
newTopPc = newTop / h * 100;
} else {
newTopPc = 0;
}
this.ui.dragger.css({ top: `${newTopPc}%` });
},
__getMaxPosition() {
return Math.max(0, (this.state.count - 1) - this.state.viewportHeight + 1);
},
__startDrag(event) {
this.dragContext = {
scrollbarHeight: this.$el.height(),
draggerHeight: this.ui.dragger.height(),
pageOffsetY: event.pageY,
scrollbarPositionY: this.__getPosition(this.el).y,
mouseOffsetY: event.pageY - this.__getPosition(this.ui.dragger).y
};
this.ui.dragger.addClass('active');
$(document).mousemove(this.__documentMouseMove).mouseup(this.__documentMouseUp);
},
__stopDrag() {
if (!this.dragContext) {
return;
}
this.dragContext = null;
this.$document.unbind('mousemove', this.__documentMouseMove);
this.$document.unbind('mouseup', this.__documentMouseUp);
this.ui.dragger.removeClass('active');
},
// returns DOM element position relatively to the document
__getPosition(domElement) {
if (domElement instanceof jQuery) {
domElement = domElement[0];
}
let left = 0;
let top = 0;
do {
if (!isNaN(domElement.offsetLeft)) {
left += domElement.offsetLeft;
}
if (!isNaN(domElement.offsetTop)) {
top += domElement.offsetTop;
}
domElement = domElement.offsetParent;
} while (domElement);
return { x: left, y: top };
}
});
export default ScrollbarView;