blossom
Version:
Modern, Cross-Platform Application Framework
253 lines (197 loc) • 7.44 kB
JavaScript
// ==========================================================================
// Project: Blossom - Modern, Cross-Platform Application Framework
// Copyright: ©2012 Fohr Motion Picture Studios. All rights reserved.
// License: Licensed under the GPLv3 license (see BLOSSOM-LICENSE).
// ==========================================================================
/*globals sc_assert */
sc_require('surfaces/scroll_view');
/** @class
`SC.ListView` implements a scrollable view. You can use it
interchangeably with `SC.View`, the only difference is the scrolling.
Setting the bounds of the scroll is important.
@extends SC.ScrollView
@since Blossom 1.0
*/
SC.ListView = SC.ScrollView.extend({
content: null,
contentBindingDefault: SC.Binding.multiple(),
// ..........................................................
// SELECTION SUPPORT
//
selection: null,
_sc_selection: null,
_sc_selectionDidChange: function() {
var old = this._sc_selection,
cur = this.get('selection'),
func = this.triggerRendering;
if (old === cur) return; // nothing to do
if (old) old.removeObserver('[]', this, func);
this._sc_selection = cur;
if (cur) cur.addObserver('[]', this, func);
this.triggerRendering();
}.observes('selection'),
rowHeight: 30,
renderRow: function(context, width, height, index, object, isSelected) {
context.fillStyle = isSelected? '#99CCFF' : 'white';
context.fillRect(0, 0, width, height);
context.strokeStyle = 'grey';
context.lineWidth = 1;
context.beginPath();
context.moveTo(0, height - 0.5);
context.lineTo(width, height - 0.5);
context.stroke();
context.font = "12pt Helvetica";
context.fillStyle = 'black';
context.textAlign = 'center';
context.textBaseline = 'middle';
context.fillText(String(index), width/2, height/2);
},
hasHorizontalScroller: false,
clearBackground: true,
updateDisplay: function() {
// console.log('SC.ListView#updateDisplay()', SC.guidFor(this));
var benchKey = 'SC.ListView#updateDisplay()';
SC.Benchmark.start(benchKey);
var ctx = this._sc_context;
sc_assert(ctx);
sc_assert(document.getElementById(ctx.__sc_canvas__.id));
// Clear the background if requested.
if (this.get('clearBackground')) ctx.clearRect(0, 0, ctx.w, ctx.h);
if (this.willRenderLayers) {
ctx.save();
this.willRenderLayers(ctx);
ctx.restore();
}
var content = this.get('content'),
selection = this.get('selection'),
idx, len, w = ctx.w, h = this.get('rowHeight');
sc_assert(selection && selection.contains, "ListView must have a selection and it must respond to `contains()`");
if (content && (len = content.get('length')) > 0) {
for (idx=0; idx<len; ++idx) {
var obj = content.objectAt(idx);
ctx.save();
ctx.translate(0, idx*h);
if (this.renderRow) {
this.renderRow(ctx, w, h, idx, obj, selection.contains(obj),
idx===0? true : false,
idx===(len-1)? true : false);
}
ctx.restore();
}
}
if (this.didRenderLayers) {
ctx.save();
this.didRenderLayers(ctx);
ctx.restore();
}
SC.Benchmark.end(benchKey);
},
mouseDown: function(evt) {
// console.log('SC.ListView#mouseDown()', SC.guidFor(this));
var top = evt.target.getBoundingClientRect().top;
this._scrollTop = top;
this._scrollTarget = evt.target;
this._rowIndex = Math.floor((evt.pageY - top) / this.get('rowHeight'));
evt.allowDefault();
return true;
},
// FIXME: This behavior is only needed on touch devices!
mouseUp: function(evt) {
var idx = this._rowIndex,
content = this._sc_content,
selection = this._sc_selection,
scrollTarget = this._scrollTarget;
this._scrollTarget = null;
if (Math.abs(this._scrollTop - scrollTarget.getBoundingClientRect().top) > 15) {
return; // We're scrolling...
}
if (content && selection) {
var obj = content.objectAt(idx);
if (obj && !selection.contains(obj)) {
var sel = SC.SelectionSet.create();
sel.addObject(obj);
this.set('selection', sel.freeze());
var action = this.get('action');
if (action && typeof action === 'function') {
action.call(this, obj, idx);
}
// TODO: Support the usual target/action paradigm.
}
}
},
adjustLayout: function() {
// console.log('SC.ListView#adjustLayout()', SC.guidFor(this));
var benchKey = 'SC.ListView#adjustLayout()';
SC.Benchmark.start(benchKey);
var frame = SC.MakeRect(this.get('frame')),
content = this.get('content'),
rowHeight = this.get('rowHeight');
var rows = content? content.get('length') : 0;
frame[0]/*x*/ = 0;
frame[1]/*y*/ = 0;
frame[2]/*w*/ = frame[2]/*w*/ ; // - 15; // account for scroller
frame[3]/*h*/ = Math.max(rows*rowHeight, frame[3]/*h*/);
// We never have to offset in this manner.
var scrollTranslation = this._sc_scrollTranslation;
scrollTranslation[0]/*x*/ = 0;
scrollTranslation[1]/*y*/ = 0;
this._sc_scrollingCanvas.set('frame', frame);
SC.Benchmark.end(benchKey);
},
_sc_content: null,
_sc_contentPropertyDidChange: function() {
// console.log('SC.ListView#_sc_contentPropertyDidChange()', SC.guidFor(this));
var func = this._sc_contentLengthDidChange,
old = this._sc_content,
cur = this.get('content');
if (old === cur) return;
if (old) {
this._sc_removeContentRangeObserver();
old.removeObserver('length', this, func);
}
this._sc_content = cur;
if (cur) {
cur.addObserver('length', this, func);
this._sc_updateContentRangeObserver();
}
this._sc_contentLengthDidChange();
}.observes('content'),
_sc_contentLengthDidChange: function() {
// console.log('SC.ListView#_sc_contentLengthDidChange()', SC.guidFor(this));
this._sc_updateContentRangeObserver();
this.triggerLayoutAndRendering();
},
_sc_contentRangeObserver: null,
_sc_updateContentRangeObserver: function() {
// console.log('SC.ListView#_sc_updateContentRangeObserver()', SC.guidFor(this));
var observer = this._sc_contentRangeObserver,
content = this.get('content');
if (!content) return ; // nothing to do
var nowShowing = SC.IndexSet.create(0, content.get('length'));
if (observer) {
content.updateRangeObserver(observer, nowShowing);
} else {
var func = this._sc_contentRangeDidChange;
observer = content.addRangeObserver(nowShowing, this, func, null, true);
this._sc_contentRangeObserver = observer;
}
},
_sc_contentRangeDidChange: function() {
// console.log('SC.ListView#_sc_contentRangeDidChange()', SC.guidFor(this));
this.triggerRendering();
},
_sc_removeContentRangeObserver: function() {
// console.log('SC.ListView#_sc_removeContentRangeObserver()', SC.guidFor(this));
var content = this.get('content'),
observer = this._sc_contentRangeObserver ;
if (observer) {
if (content) content.removeRangeObserver(observer);
this._sc_contentRangeObserver = null ;
}
},
init: function() {
arguments.callee.base.apply(this, arguments);
this._sc_contentPropertyDidChange();
this._sc_selectionDidChange();
}
});