UNPKG

blossom

Version:

Modern, Cross-Platform Application Framework

1,073 lines (863 loc) 36.2 kB
// ========================================================================== // 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/view'); SC.LOG_ILIST_UPDATES = false; var base03 = "#002b36"; var base02 = "#073642"; var base01 = "#586e75"; var base00 = "#657b83"; var base0 = "#839496"; var base1 = "#93a1a1"; var base2 = "#eee8d5"; var base3 = "#fdf6e3"; var yellow = "#b58900"; var orange = "#cb4b16"; var red = "#dc322f"; var magenta = "#d33682"; var violet = "#6c71c4"; var blue = "#268bd2"; var cyan = "#2aa198"; var green = "#859900"; var white = "white"; /** @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.IListView = SC.View.extend({ __tagName__: 'div', __useContentSize__: false, isCompositeSurface: true, // Walk like a duck. subsurfaces: function() { return [this._sc_scrollingSurface]; }.property(), /** true if the view should maintain a horizontal scroller. This property must be set when the view is created. @property {Boolean} */ hasHorizontalScroller: true, /** true if the view should maintain a horizontal scroller. This property must be set when the view is created. @property {Boolean} */ hasVerticalScroller: true, // .......................................................... // PSURFACE SUPPORT (Private) // updatePsurface: function(psurface, surfaces) { // console.log('SC.IListView#updatePsurface()'); sc_assert(this === SC.surfaces[this.__id__], "SC.Surface#updatePsurface() can only be called on active surfaces."); // Sanity check the Psurface. sc_assert(psurface); sc_assert(psurface instanceof SC.Psurface); sc_assert(psurface.__element__); sc_assert(psurface.__element__ === document.getElementById(this.__id__)); var surface = this._sc_scrollingSurface; if (surfaces) surfaces[surface.__id__] = surface; var cur = psurface.push(surface); surface.updatePsurface(cur, surfaces); cur.pop(); }, updateLayout: function() { // console.log('SC.IListView#updateLayout()'); arguments.callee.base.apply(this, arguments); this.adjustLayout(); }, content: null, contentBindingDefault: SC.Binding.multiple(), _sc_hasScrollListener: false, didCreateElement: function(div) { // We don't want SC.View's implementation; don't call it. div.style.overflowX = this.get('hasHorizontalScroller')? 'scroll' : 'hidden'; div.style.overflowY = this.get('hasVerticalScroller')? 'scroll' : 'hidden'; // FIXME: This should be done dynamically, per scrollview. I'm not doing // it now because the CSS has pseudo-selectors, so I have to generate // stylesheet code specially. (Here and a few other places, actually.) // // For now, I'll specially customize the CSS to work with Postbooks' UI // correctly. div.className = 'frame'; // This should probably only be set on mobile Safari/Google Chrome for // Android. // // See http://stackoverflow.com/questions/7763458/ios5-webkit-overflow-scrolling-causes-touch-events-stopping-work // for a fix I haven't yet implemented, too. div.style.webkitOverflowScrolling = 'touch'; // We have to establish this stuff every time we set up our DOM. SC.Event.add(div, 'scroll', this, this._sc_didScroll); this._sc_hasScrollListener = true; this._sc_scrollTopPrev = 0; this._sc_didNotRender = true; this._sc_scrollDelay = 0; }, _sc_scrollTopPrev: 0, _sc_didNotRender: true, _sc_scrollDelay: 0, _sc_rowIndex: 0, _sc_rowLength: 0, _sc_didScroll: function(evt) { // console.log('did scroll'); var now = new Date().getTime(), delay = this._sc_scrollDelay, height = this.get('rowHeight'), canvas = this._sc_context.__sc_canvas__, didNotDraw = true, rect, top, prev, div = SC.psurfaces[this.__id__].__element__; rect = SC.psurfaces[this._sc_scrollingSurface.__id__].__element__.getBoundingClientRect(); top = -rect.top; prev = this._sc_scrollTopPrev; if (this._sc_hasScrollListener) { SC.Event.remove(div, 'scroll', this, this._sc_didScroll); this._sc_hasScrollListener = false; } if (Math.abs(top - prev) < rect.height*0.10) { if (now - delay > 150) { // console.log('drawing'); didNotDraw = false; // var canvasTop = Math.min(Math.floor((top/(height*3)))*(height*3), 300000); var scrollframeHeight = this.get('frame').height, listHeight = this._sc_scrollingSurface.get('frame').height, canvasHeight = this._sc_scrollingSurface._sc_scrollingCanvas.get('frame').height, offset = rect.top - div.getBoundingClientRect().top, rowHeight = this.get('rowHeight'); // 1. var x = -offset + (scrollframeHeight/2) - (canvasHeight/2); // 2. var y = x - (x % rowHeight); // 3. var z = Math.min(y, listHeight - canvasHeight); // 4. offset = z < 0 ? 0 : z; // console.log(x, y, z, offset); // sc_assert(offset % rowHeight === 0); // sc_assert(offset >= 0 && offset <= listHeight - canvasHeight); if (offset === (listHeight - canvasHeight)) { console.log('requesting more records'); this._sc_tellSurrogateToFetchMoreRecords = true; } canvas.style.top = offset + 'px'; this.triggerRendering(); this._sc_rowIndex = offset/rowHeight; } } else { delay = new Date().getTime(); } if (didNotDraw || top !== prev) { // SC.requestAnimationFrame = true; var that = this; SC.RequestAnimationFrame(function() { SC.RunLoop.begin(); that._sc_didScroll(); SC.RunLoop.end(); }); this._sc_scrollTopPrev = top; } else { SC.Event.add(div, 'scroll', this, this._sc_didScroll); this._sc_hasScrollListener = true; } }, // .......................................................... // 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, isLast) { context.fillStyle = 'grey'; context.fillRect(0, 0, width, height); context.font = "12pt Helvetica"; context.fillStyle = 'black'; context.textAlign = 'center'; context.textBaseline = 'middle'; context.fillText(String(index), width/2, height/2); }, renderSurrogate: function(context, width, height, index, object, isSelected, isLast) { console.log('rendering surrogate', object.status); context.fillStyle = 'clear'; 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 = "10pt Helvetica"; context.fillStyle = 'black'; context.textAlign = 'center'; context.textBaseline = 'middle'; var status = object.get('status'); if (status === 'FULL') { context.fillText("All "+index+" records are loaded.", width/2, height/2+1); context.fillStyle = 'white'; context.fillText("All "+index+" records are loaded.", width/2, height/2); } else { context.fillText(object.get('status'), width/2, height/2+1); context.fillStyle = 'white'; context.fillText(object.get('status'), width/2, height/2); } }, hasHorizontalScroller: false, clearBackground: true, _sc_plistItems: null, __forceFullRender__: false, updateDisplay: function() { // console.log('SC.IListView#updateDisplay()', SC.guidFor(this)); var benchKey = 'SC.IListView#updateDisplay()'; SC.Benchmark.start(benchKey); var LOG = SC.LOG_ILIST_UPDATES; var ctx = this._sc_context; sc_assert(ctx); sc_assert(document.getElementById(ctx.__sc_canvas__.id)); // FIXME: Clear the background if requested. We don't really want to do this. // 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'), rowIndex = this._sc_rowIndex, len = this._sc_rowLength, w = ctx.w, h = this.get('rowHeight'), plistItems = this._sc_plistItems, newPlistItems = {}, forceFullRender = this.__forceFullRender__; this.__forceFullRender__ = false; // If we have fewer rows (no scrolling), don't try and render too much. len = Math.min(len, (content? content.get('length') : 0) - rowIndex); // console.log(idx, len); var changedStoreKeys = SC.changedStoreKeys; var needsRendering = false; // Create this here so we don't have to make so many. var processLayer = function(layer) { if (layer.__needsRendering__) needsRendering = true; else layer.get('sublayers').forEach(processLayer); }; if (content && len > 0) { for (var idx = 0; idx<len; ++idx) { var obj = content.objectAt(idx + rowIndex), storeKey = obj.storeKey, isSurrogate = obj.isIRecordArray; if (isSurrogate) storeKey = -1; // Won't collide with actual records. sc_assert(storeKey); var plistItem = plistItems[storeKey]; if (plistItem) { delete plistItems[storeKey]; // We've processed it. newPlistItems[storeKey] = plistItem; // We need to update this plist item and determine if it should be // re-rendered. First we check the properties dependent on the // list view itself. if (plistItem.index !== idx + rowIndex) { plistItem.index = idx + rowIndex; if (LOG) console.log(storeKey, 'needs rendering because', 'its index changed'); needsRendering = true; } if (plistItem.offset !== idx*h) { plistItem.offset = idx*h; if (LOG) console.log(storeKey, 'needs rendering because', 'its offset changed'); needsRendering = true; } var isSelected = selection.contains(obj); if (plistItem.isSelected !== isSelected) { plistItem.isSelected = isSelected; if (LOG) console.log(storeKey, 'needs rendering because', 'its isSelected value changed'); needsRendering = true; } var isLast = (idx + rowIndex) === len - 1; if (plistItem.isLast !== isLast) { plistItem.isLast = isSelected; if (LOG) console.log(storeKey, 'needs rendering because', 'its isLast value changed'); needsRendering = true; } if (isSurrogate) { var status = obj.status; if (plistItem.status !== obj.status) { plistItem.status = status; if (LOG) console.log(storeKey, 'needs rendering because', 'its status changed'); needsRendering = true; } if (this._sc_tellSurrogateToFetchMoreRecords) { this._sc_tellSurrogateToFetchMoreRecords = false; obj.fetchRecords(); } // Early exit for speed. if (needsRendering) plistItem.needsRendering = true; needsRendering = false; // Reset break; // We're done examining the surrogate. } // Next we check the storeKeys for changes *if* if (!needsRendering && !plistItem.needsRendering) { if (changedStoreKeys[storeKey]) { if (LOG) console.log(storeKey, 'needs rendering because', 'its storeKey change'); needsRendering = true; } // Okay, check our dependent keys. if (!needsRendering) { var dependentKeys = plistItem.dependentKeys; if (dependentKeys) { for (var dependentKey in dependentKey) { if (!dependentKeys.hasOwnProperty(dependentKey)) continue; if (changedStoreKeys[dependentKey]) { if (LOG) console.log(storeKey, 'needs rendering because', 'a dependent key changed', dependentKey); needsRendering = true; break; } } } } } // Check the layer tree, if it exists if (!needsRendering) { var layerTree = plistItem.editableLayerTree; if (!layerTree) layerTree = plistItem.mouseLayerTree; if (!layerTree) layerTree = plistItem.renderLayerTree; if (layerTree) { if (layerTree.__needsRendering__) { if (LOG) console.log(storeKey, 'needs rendering because', 'its layer tree needs rendering'); needsRendering = true; } else layerTree.get('sublayers').forEach(processLayer); } } if (needsRendering) plistItem.needsRendering = true; // Create a new plistItem } else { plistItem = newPlistItems[storeKey] = new SC.PListItem(idx + rowIndex, obj, idx*h); plistItem.isSelected = selection.contains(obj); plistItem.isLast = (idx + rowIndex) === len - 1; // item is already marked as needing rendering on init if (isSurrogate) { plistItem.isSurrogate = true; plistItem.status = obj.status; } } needsRendering = false; // Reset } } // Handle plist items that are no longer with us. var isInputSurface = SC.app.get('inputSurface') === this; for (var oldStoreKey in plistItems) { if (!plistItems.hasOwnProperty(oldStoreKey)) continue; plistItem = plistItems[oldStoreKey]; var editableLayerTree = plistItem.editableLayerTree; if (editableLayerTree && isInputSurface) SC.CloseFieldEditor(); // Release stuff. delete plistItem.object; delete plistItem.renderLayerTree; delete plistItem.mouseLayerTree; delete plistItem.editableLayerTree; } // Keep for next round. this._sc_plistItems = newPlistItems; if (LOG) console.log(newPlistItems); var clearBackground = this.get('clearBackground'), backgroundColor = this.get('backgroundColor'); // Draw plist items as needed. for (storeKey in newPlistItems) { if (!newPlistItems.hasOwnProperty(storeKey)) continue; plistItem = newPlistItems[storeKey]; if (forceFullRender || plistItem.needsRendering) { if (LOG) console.log('rendering storeKey', storeKey); plistItem.needsRendering = false; ctx.save(); ctx.translate(0, plistItem.offset); if (!plistItem.isSurrogate) { // We render the most complicated option. layerTree = plistItem.editableLayerTree; if (!layerTree) layerTree = plistItem.mouseLayerTree; if (!layerTree) layerTree = plistItem.renderLayerTree; var renderFunction = plistItem.renderFunction; if (!layerTree && !renderFunction) { if (plistItem.isSurrogate) { // We need to find or create the correct one. } else if (this.createRenderLayerTree) { // console.log('creating render tree'); layerTree = plistItem.renderLayerTree = this.createRenderLayerTree(); if (layerTree) { // sc_assert(layerTree); // sc_assert(layerTree.kindOf(SC.Layer)); layerTree.__forceWidthHeight__ = true; layerTree.set('width', w); layerTree.set('height', h); layerTree.__needsLayout__ = true; } } else { // FIXME: Seems like this could be done better... if (this.renderRow) { renderFunction = plistItem.renderFunction = this.renderRow; } } } } else { renderFunction = this.renderSurrogate; } // Render with either the layer tree or the render function. ctx.beginPath(); ctx.rect(0,0,w,h); ctx.clip(); if (layerTree) { // Set the properties for the layer tree. layerTree.set('rowIndex', plistItem.index); layerTree.set('content', plistItem.object); layerTree.set('isSelected', plistItem.isSelected); layerTree.set('isLast', plistItem.isLast); if (layerTree.__needsLayout__) { // Update the layout. var textLayersNeedingLayout = []; layerTree.updateLayout(textLayersNeedingLayout); // Update any text layouts. for (idx=0, len=textLayersNeedingLayout.length; idx<len; ++idx) { textLayersNeedingLayout[idx].updateTextLayout(ctx); } } if (clearBackground) ctx.clearRect(0, 0, w, h); else { // We need to draw the background color, even though it is also // applied with CSS, in order to get correct anti-aliasing. ctx.fillStyle = backgroundColor; ctx.fillRect(0, 0, w, h); } layerTree.renderIntoContext(ctx); } else if (renderFunction) { if (clearBackground) ctx.clearRect(0, 0, w, h); else { // We need to draw the background color, even though it is also // applied with CSS, in order to get correct anti-aliasing. ctx.fillStyle = backgroundColor; ctx.fillRect(0, 0, w, h); } renderFunction(ctx, w, h, plistItem.index, plistItem.object, plistItem.isSelected, plistItem.isLast); } ctx.restore(); } } // if (this.didRenderLayers) { // ctx.save(); // this.didRenderLayers(ctx); // ctx.restore(); // } SC.Benchmark.end(benchKey); }, /** Finds the layer that is hit by this event, and returns its behavior if it exists. Otherwise, returns the receiver. */ targetResponderForEvent: function(evt) { // console.log('ListDemo.LayerListView#targetResponderForEvent(', evt.type, ')'); var context = this._sc_hitTestCanvas.getContext('2d'), hitLayer = null, zIndex = -1, boundingRect, x, y; if (evt.pageX === undefined) return this; // debugger; boundingRect = evt.target.getBoundingClientRect(); x = evt.clientX - boundingRect.left + window.pageXOffset; y = evt.clientY - boundingRect.top + window.pageYOffset; // console.log('*****', evt.type, x, y, '*****'); // FIXME: calculate the correct rowIndex! var rowIndex = Math.floor((evt.pageY - boundingRect.top) / this.get('rowHeight')) + this._sc_rowIndex; sc_assert(!isNaN(rowIndex)); // console.log('rowIndex', rowIndex); function spaces(depth) { console.log(depth); var ret = "", idx, len; for (idx = 0, len = depth; idx<len; ++idx) ret += "--"; return ret; } function hitTestLayer(layer, point, depth) { // debugger; // console.log(spaces(depth), "on entry:", point.x, point.y); if (!layer.get('isVisible')) return; context.save(); // Prevent this layer and any sublayer from drawing paths outside our // bounds. layer.renderBoundsPath(context); context.clip(); // Make sure the layer's transform is current. if (layer._sc_transformFromSuperlayerToLayerIsDirty) { layer._sc_computeTransformFromSuperlayerToLayer(); } // Apply the sublayer's transform from our layer (it's superlayer). var t = layer._sc_transformFromSuperlayerToLayer; context.transform(t[0], t[1], t[2], t[3], t[4], t[5]); SC.PointApplyAffineTransformTo(point, t, point); var frame = layer.get('frame'); // console.log(spaces(depth), 'frame:', frame.x, frame.y, frame.width, frame.height); // console.log(spaces(depth), 'transformed:', point.x, point.y); // First, test our sublayers. var sublayers = layer.get('sublayers'), idx = sublayers.length; depth++; while (idx--) { hitTestLayer(sublayers[idx], SC.MakePoint(point), depth); } // Only test ourself if (a) no hit has been found, or (b) our zIndex is // higher than whatever hit has been found so far. var layerZ = layer.get('zIndex'); if (!hitLayer || zIndex < layerZ) { if (layer.hitsPointInContext(x, y, context)) { evt.hitPoint = SC.MakePoint(x - point[0]/*x*/, y - point[1]/*y*/); hitLayer = layer; zIndex = layerZ; } } context.restore(); } // Next, begin the hit testing process. When this completes, hitLayer // will contain the layer that was hit with the highest zIndex. var plistItems = this._sc_plistItems, rowHeight = this.get('rowHeight'), mouseY = evt.pageY - boundingRect.top, layerTree, offset; for (var storeKey in plistItems) { if (!plistItems.hasOwnProperty(storeKey)) continue; var plistItem = plistItems[storeKey]; offset = plistItem.offset; // this is relative to the canvas if (offset < mouseY && mouseY < (offset + rowHeight)) { layerTree = plistItem.editableLayerTree; if (!layerTree) layerTree = plistItem.mouseLayerTree; if (!layerTree) layerTree = plistItem.renderLayerTree; if (!layerTree) { return this; // row is using a render function } break; } } if (!layerTree) return this; context.save(); context.translate(0, (rowIndex - this._sc_rowIndex)*this.get('rowHeight')); hitTestLayer(layerTree, SC.MakePoint(), 0); context.restore(); // We don't need to test `layer`, because we already know it was hit when // this method is called by SC.RootResponder. if (!hitLayer) return this; else { // this.triggerRendering(); // if (evt.type === 'mousedown') console.log('rowIndex', rowIndex); // If we hit a layer, remember it so our view knows. evt.layer = hitLayer; evt.hitPoint.y = evt.hitPoint.y - offset; hitLayer.set('surface', this._sc_scrollingSurface._sc_scrollingCanvas); var content = this.get('content'); hitLayer.set('content', content.objectAt(rowIndex)); // if (evt.type === 'mousedown') console.log('content.objectAt(rowIndex).get(\'index\')', content.objectAt(rowIndex).get('index')); // Try and find the behavior attached to this layer. var behavior = hitLayer.get('behavior'); while (!behavior && hitLayer) { hitLayer = hitLayer.get('superlayer'); if (hitLayer) behavior = hitLayer.get('behavior'); } return behavior || this; } }, 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')) + this._sc_rowIndex; console.log('this._rowIndex', this._rowIndex); 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. } } }, // performLayoutIfNeeded: function(timestamp) { // console.log('SC.IListView#performLayoutIfNeeded()', SC.guidFor(this)); // arguments.callee.base.apply(this, arguments); // }, performRenderingIfNeeded: function(timestamp) { // console.log('SC.IListView#performRenderingIfNeeded()', SC.guidFor(this)); this.__needsRendering__ = true; // We do all our work in updateDisplay() arguments.callee.base.apply(this, arguments); }, adjustLayout: function() { console.log('SC.IListView#adjustLayout()', SC.guidFor(this)); var benchKey = 'SC.IListView#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_scrollingSurface.set('frame', frame); this._sc_scrollingSurface.__needsLayout__ = true; 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 ; } }, _sc_scrollingSurface: null, _sc_compositeIsPresentInViewportDidChange: function() { // console.log("SC.IListViewSurface#_sc_compositeIsPresentInViewportDidChange()"); var isPresentInViewport = this.get('isPresentInViewport'); this._sc_scrollingSurface.set('isPresentInViewport', isPresentInViewport); }.observes('isPresentInViewport'), init: function() { arguments.callee.base.apply(this, arguments); var scrollingSurface; scrollingSurface = this._sc_scrollingSurface = SC.InternalIListViewSurface.create({ supersurface: this, __scrollView__: this }); this._sc_scrollTranslation = SC.MakePoint(); this._sc_contentPropertyDidChange(); this._sc_selectionDidChange(); this._sc_plistItems = {}; }, rowOffsetForLayerTree: function(layerTree) { console.log("SC.IListViewSurface#rowOffsetForLayerTree()"); var plistItems = this._sc_plistItems; for (var storeKey in plistItems) { if (!plistItems.hasOwnProperty(storeKey)) continue; var plistItem = plistItems[storeKey]; var plistLayerTree = plistItem.editableLayerTree; if (!plistLayerTree) plistLayerTree = plistItem.mouseLayerTree; if (!plistLayerTree) plistLayerTree = plistItem.renderLayerTree; if (plistLayerTree === layerTree) { return plistItem.offset + parseInt(this._sc_context.__sc_canvas__.style.top.slice(0,-2), 10); } } debugger; console.log('Could not find row offset for layer tree. This is a bug.'); return 0; // Don't know! } }); /** @private */ SC.InternalIListViewSurface = SC.LeafSurface.extend({ __tagName__: 'div', isLeafSurface: false, isCompositeSurface: true, // Walk like a duck. subsurfaces: function() { return [this._sc_scrollingCanvas]; }.property(), rowOffsetForLayerTree: function(layerTree) { return this.__scrollView__.rowOffsetForLayerTree(layerTree); }, // .......................................................... // SURFACE TREE SUPPORT // _sc_compositeIsPresentInViewportDidChange: function() { // console.log("SC.InternalIListViewSurface#_sc_compositeIsPresentInViewportDidChange()"); var isPresentInViewport = this.get('isPresentInViewport'); this._sc_scrollingCanvas.set('isPresentInViewport', isPresentInViewport); }.observes('isPresentInViewport'), __scrollView__: null, surface: function() { // console.log('SC.InternalIListViewSurface@surface'); return this.__scrollView__; }.property().cacheable(), didCreateElement: function(div) { // console.log('SC.InternalIListViewSurface#didCreateElement()', SC.guidFor(this)); arguments.callee.base.apply(this, arguments); div.style.overflow = 'hidden'; div.style.backgroundImage = 'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAgCAMAAAARxWmYAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAEJQTFRF/////f39/v7++vr67u7u4+Pj9fX1vcC+0tPTvb6+09TT0tTT9fb14+Tj09TUvcC97e7uvb690tTU09PTvsC9vsC+wY1AyAAAAFpJREFUeNp8j0kOgDAMA8sSIKQp+/+/inqoqJHc3EaWxnG4FxmnoetDvrmGzVhyJK+SVRBigYcKFEpNiE0TS8D2/2AnPaC+vPFO/MDJuMYESE62BwRqRP0KMABnbwVKLpuQOQAAAABJRU5ErkJggg==)'; div.style.backgroundPosition = 'right top'; div.style.backgroundRepeat = 'repeat-y'; }, targetResponderForEvent: function(evt) { return this.get('surface').targetResponderForEvent(evt); }, // performLayoutIfNeeded: function(timestamp) { // console.log('SC.InternalIListViewSurface#performLayoutIfNeeded()', SC.guidFor(this)); // arguments.callee.base.apply(this, arguments); // // this._sc_scrollingCanvas.performLayoutIfNeeded(timestamp); // }, // .......................................................... // PSURFACE SUPPORT (Private) // updatePsurface: function(psurface, surfaces) { // console.log('SC.InternalIListViewSurface#updatePsurface()'); sc_assert(this === SC.surfaces[this.__id__], "SC.Surface#updatePsurface() can only be called on active surfaces."); // Sanity check the Psurface. sc_assert(psurface); sc_assert(psurface instanceof SC.Psurface); sc_assert(psurface.__element__); sc_assert(psurface.__element__ === document.getElementById(this.__id__)); var surface = this._sc_scrollingCanvas; if (surfaces) surfaces[surface.__id__] = surface; psurface.push(surface); }, updateLayout: function() { // console.log('SC.InternalIListViewSurface#updateLayout()', SC.guidFor(this)); arguments.callee.base.apply(this, arguments); this.adjustLayout(); }, _sc_scrollingCanvas: null, init: function() { arguments.callee.base.apply(this, arguments); var scrollingCanvas; scrollingCanvas = this._sc_scrollingCanvas = SC.InternalListViewCanvas.create({ supersurface: this, __scrollSurface__: this }); }, adjustLayout: function() { console.log('SC.InternalIListViewSurface#adjustLayout()', SC.guidFor(this)); var frame = SC.MakeRect(this.__scrollView__.get('frame')), myFrame = SC.MakeRect(this.get('frame')), rowHeight = this.__scrollView__.get('rowHeight'); frame[3] = frame[3] * 2; // We need to be even with the rowHeight frame[3] = frame[3] + (rowHeight - (frame[3] % rowHeight)); // Record how many rows we can render. this.__scrollView__._sc_rowLength = frame[3] / rowHeight; frame.x = myFrame.x-12; frame.y = myFrame.y; this._sc_scrollingCanvas.set('frame', frame); } }); /** @private */ SC.InternalListViewCanvas = SC.LeafSurface.extend({ __tagName__: 'canvas', __useContentSize__: true, // we need our width and height attributes set __neverAnimate__: true, __scrollSurface__: null, triggerContentSizeUpdate: function() { arguments.callee.base.apply(this, arguments); this.__scrollSurface__.__scrollView__.__forceFullRender__ = true; }, surface: function() { // console.log('SC.InternalListViewCanvas@surface'); return this.__scrollSurface__; }.property().cacheable(), didCreateElement: function(canvas) { // console.log('SC.InternalListViewCanvas#didCreateElement()', SC.guidFor(this)); arguments.callee.base.apply(this, arguments); var ctx = canvas.getContext('2d'); // Enables ctx.width and ctx.height to work. ctx.__sc_canvas__ = canvas; this.__scrollSurface__.__scrollView__._sc_context = ctx; this.__scrollSurface__.__scrollView__.__forceFullRender__ = true; this.__scrollSurface__.__scrollView__.triggerRendering(); }, rowOffsetForLayerTree: function(layerTree) { return this.__scrollSurface__.rowOffsetForLayerTree(layerTree); }, // performLayoutIfNeeded: function(timestamp) { // console.log('SC.InternalListViewCanvas#performLayoutIfNeeded()', SC.guidFor(this)); // arguments.callee.base.apply(this, arguments); // }, // updateLayout: function() { // console.log('SC.InternalListViewCanvas#updateLayout()', SC.guidFor(this)); // }, targetResponderForEvent: function(evt) { return this.get('surface').targetResponderForEvent(evt); } }); // Constructor SC.PListItem = function(index, object, offset) { // Set all properties for speed with hidden classes. this.index = index; this.object = object; this.storeKey = object.storeKey; this.offset = offset; this.isSelected = false; this.isLast = false; this.needsRendering = true; this.dependentStoreKeys = null; // A PListItem can only have one render function or layer tree, not both. this.renderFunction = null; // Shared with other PListItems this.renderLayerTree = null; // Shared with other PListItems // A PListItem can only have mouse or editable, not both. this.mouseLayerTree = null; // Exclusive to this PListItem this.editableLayerTree = null; // Exclusive to this PListItem this.status = null; this.isSurrogate = false; return this; };