FIX: List views in admin were broken

This commit is contained in:
Robin Ward 2015-05-11 13:16:44 -04:00
parent a74689932e
commit 0d51c1f0a0
21 changed files with 1474 additions and 1942 deletions

View file

@ -3,8 +3,8 @@ export default Ember.ObjectController.extend({
savedIpAddress: null, savedIpAddress: null,
isRange: function() { isRange: function() {
return this.get("ip_address").indexOf("/") > 0; return this.get("model.ip_address").indexOf("/") > 0;
}.property("ip_address"), }.property("model.ip_address"),
actions: { actions: {
allow: function(record) { allow: function(record) {
@ -19,14 +19,14 @@ export default Ember.ObjectController.extend({
edit: function() { edit: function() {
if (!this.get('editing')) { if (!this.get('editing')) {
this.savedIpAddress = this.get('ip_address'); this.savedIpAddress = this.get('model.ip_address');
} }
this.set('editing', true); this.set('editing', true);
}, },
cancel: function() { cancel: function() {
if (this.get('savedIpAddress') && this.get('editing')) { if (this.get('savedIpAddress') && this.get('editing')) {
this.set('ip_address', this.get('savedIpAddress')); this.set('model.ip_address', this.get('savedIpAddress'));
} }
this.set('editing', false); this.set('editing', false);
}, },

View file

@ -1,11 +1,6 @@
/** /**
Represents an IP address that is watched for during account registration Represents an IP address that is watched for during account registration
(and possibly other times), and an action is taken. (and possibly other times), and an action is taken.
@class ScreenedIpAddress
@extends Discourse.Model
@namespace Discourse
@module Discourse
**/ **/
Discourse.ScreenedIpAddress = Discourse.Model.extend({ Discourse.ScreenedIpAddress = Discourse.Model.extend({
actionName: function() { actionName: function() {
@ -17,21 +12,9 @@ Discourse.ScreenedIpAddress = Discourse.Model.extend({
}.property('action_name'), }.property('action_name'),
actionIcon: function() { actionIcon: function() {
if (this.get('action_name') === 'block') { return (this.get('action_name') === 'block') ? 'ban' : 'check';
return this.get('blockIcon');
} else {
return this.get('doNothingIcon');
}
}.property('action_name'), }.property('action_name'),
blockIcon: function() {
return 'fa-ban';
}.property(),
doNothingIcon: function() {
return 'fa-check';
}.property(),
save: function() { save: function() {
return Discourse.ajax("/admin/logs/screened_ip_addresses" + (this.id ? '/' + this.id : '') + ".json", { return Discourse.ajax("/admin/logs/screened_ip_addresses" + (this.id ? '/' + this.id : '') + ".json", {
type: this.id ? 'PUT' : 'POST', type: this.id ? 'PUT' : 'POST',

View file

@ -1,35 +1,35 @@
<div class="col first ip_address"> <div class="col first ip_address">
{{#if editing}} {{#if editing}}
{{text-field value=ip_address autofocus="autofocus"}} {{text-field value=model.ip_address autofocus="autofocus"}}
{{else}} {{else}}
<span {{action "edit" this}}> <span {{action "edit" this}}>
{{#if isRange}} {{#if isRange}}
<strong>{{ip_address}}</strong> <strong>{{model.ip_address}}</strong>
{{else}} {{else}}
{{ip_address}} {{model.ip_address}}
{{/if}} {{/if}}
</span> </span>
{{/if}} {{/if}}
</div> </div>
<div class="col action"> <div class="col action">
<i {{bind-attr class=":fa actionIcon"}}></i> {{fa-icon model.actionIcon}}
{{actionName}} {{model.actionName}}
</div> </div>
<div class="col match_count">{{match_count}}</div> <div class="col match_count">{{model.match_count}}</div>
<div class="col last_match_at"> <div class="col last_match_at">
{{#if last_match_at}} {{#if model.last_match_at}}
{{age-with-tooltip last_match_at}} {{age-with-tooltip model.last_match_at}}
{{/if}} {{/if}}
</div> </div>
<div class="col created_at">{{age-with-tooltip created_at}}</div> <div class="col created_at">{{age-with-tooltip model.created_at}}</div>
<div class="col actions"> <div class="col actions">
{{#unless editing}} {{#unless editing}}
<button class="btn btn-danger" {{action "destroy" this}}><i class="fa fa-trash-o"></i></button> <button class="btn btn-danger" {{action "destroy" this}}><i class="fa fa-trash-o"></i></button>
<button class="btn" {{action "edit" this}}><i class="fa fa-pencil"></i></button> <button class="btn" {{action "edit" this}}><i class="fa fa-pencil"></i></button>
{{#if isBlocked}} {{#if model.isBlocked}}
<button class="btn" {{action "allow" this}}><i {{bind-attr class=":fa doNothingIcon"}}></i> {{i18n 'admin.logs.screened_ips.actions.do_nothing'}}</button> <button class="btn" {{action "allow" this}}>{{fa-icon "check"}} {{i18n 'admin.logs.screened_ips.actions.do_nothing'}}</button>
{{else}} {{else}}
<button class="btn" {{action "block" this}}><i {{bind-attr class=":fa blockIcon"}}></i> {{i18n 'admin.logs.screened_ips.actions.block'}}</button> <button class="btn" {{action "block" this}}>{{fa-icon "ban"}} {{i18n 'admin.logs.screened_ips.actions.block'}}</button>
{{/if}} {{/if}}
{{else}} {{else}}
<button class="btn" {{action "save" this}}>{{i18n 'admin.logs.save'}}</button> <button class="btn" {{action "save" this}}>{{i18n 'admin.logs.save'}}</button>

View file

@ -1,5 +0,0 @@
Discourse.ScreenedEmailsListView = Ember.ListView.extend({
height: 700,
rowHeight: 32,
itemViewClass: Ember.ListItemView.extend({templateName: "admin/templates/logs/screened_emails_list_item"})
});

View file

@ -1,5 +0,0 @@
Discourse.ScreenedIpAddressesListView = Ember.ListView.extend({
height: 700,
rowHeight: 32,
itemViewClass: Ember.ListItemView.extend({templateName: "admin/templates/logs/screened_ip_addresses_list_item"})
});

View file

@ -1,5 +0,0 @@
Discourse.ScreenedUrlsListView = Ember.ListView.extend({
height: 700,
rowHeight: 32,
itemViewClass: Ember.ListItemView.extend({templateName: "admin/templates/logs/screened_urls_list_item"})
});

View file

@ -1,5 +0,0 @@
Discourse.StaffActionLogsListView = Ember.ListView.extend({
height: 700,
rowHeight: 75,
itemViewClass: Ember.ListItemView.extend({templateName: "admin/templates/logs/staff_action_logs_list_item"})
});

View file

@ -0,0 +1,8 @@
import ListView from 'ember-addons/list-view';
import ListItemView from 'ember-addons/list-item-view';
export default ListView.extend({
height: 700,
rowHeight: 32,
itemViewClass: ListItemView.extend({templateName: "admin/templates/logs/screened_emails_list_item"})
});

View file

@ -0,0 +1,8 @@
import ListView from 'ember-addons/list-view';
import ListItemView from 'ember-addons/list-item-view';
export default ListView.extend({
height: 700,
rowHeight: 32,
itemViewClass: ListItemView.extend({templateName: "admin/templates/logs/screened_ip_addresses_list_item"})
});

View file

@ -0,0 +1,8 @@
import ListView from 'ember-addons/list-view';
import ListItemView from 'ember-addons/list-item-view';
export default ListView.extend({
height: 700,
rowHeight: 32,
itemViewClass: ListItemView.extend({templateName: "admin/templates/logs/screened_urls_list_item"})
});

View file

@ -0,0 +1,8 @@
import ListView from 'ember-addons/list-view';
import ListItemView from 'ember-addons/list-item-view';
export default ListView.extend({
height: 700,
rowHeight: 75,
itemViewClass: ListItemView.extend({templateName: "admin/templates/logs/staff_action_logs_list_item"})
});

View file

@ -1,6 +1,11 @@
/*global Favcount:true*/ /*global Favcount:true*/
var DiscourseResolver = require('discourse/ember/resolver').default; var DiscourseResolver = require('discourse/ember/resolver').default;
// Allow us to import Ember
define('ember', ['exports'], function(__exports__) {
__exports__["default"] = Ember;
});
window.Discourse = Ember.Application.createWithMixins(Discourse.Ajax, { window.Discourse = Ember.Application.createWithMixins(Discourse.Ajax, {
rootElement: '#main', rootElement: '#main',
_docTitle: document.title, _docTitle: document.title,

View file

@ -0,0 +1,45 @@
import Ember from 'ember';
function samePosition(a, b) {
return a && b && a.x === b.x && a.y === b.y;
}
function positionElement() {
var element, position, _position;
Ember.instrument('view.updateContext.positionElement', this, function() {
element = this.element;
position = this.position;
_position = this._position;
if (!position || !element) {
return;
}
// // TODO: avoid needing this by avoiding unnecessary
// // calls to this method in the first place
if (samePosition(position, _position)) {
return;
}
Ember.run.schedule('render', this, this._parentView.applyTransform, this, position.x, position.y);
this._position = position;
}, this);
}
export default Ember.Mixin.create({
classNames: ['ember-list-item-view'],
style: Ember.String.htmlSafe(''),
attributeBindings: ['style'],
_position: null,
_positionElement: positionElement,
positionElementWhenInserted: Ember.on('init', function(){
this.one('didInsertElement', positionElement);
}),
updatePosition: function(position) {
this.position = position;
this._positionElement();
}
});

View file

@ -0,0 +1,57 @@
import Ember from 'ember';
import ListItemViewMixin from './list-item-view-mixin';
var get = Ember.get, set = Ember.set;
/**
The `Ember.ListItemView` view class renders a
[div](https://developer.mozilla.org/en/HTML/Element/div) HTML element
with `ember-list-item-view` class. It allows you to specify a custom item
handlebars template for `Ember.ListView`.
Example:
```handlebars
<script type="text/x-handlebars" data-template-name="row_item">
{{name}}
</script>
```
```javascript
App.ListView = Ember.ListView.extend({
height: 500,
rowHeight: 20,
itemViewClass: Ember.ListItemView.extend({templateName: "row_item"})
});
```
@extends Ember.View
@class ListItemView
@namespace Ember
*/
export default Ember.View.extend(ListItemViewMixin, {
updateContext: function(newContext) {
var context = get(this, 'context');
Ember.instrument('view.updateContext.render', this, function() {
if (context !== newContext) {
set(this, 'context', newContext);
if (newContext && newContext.isController) {
set(this, 'controller', newContext);
}
}
}, this);
},
rerender: function () {
if (this.isDestroying || this.isDestroyed) {
return;
}
return this._super.apply(this, arguments);
},
_contextDidChange: Ember.observer(function () {
Ember.run.once(this, this.rerender);
}, 'context', 'controller')
});

View file

@ -0,0 +1,94 @@
import Ember from 'ember';
// TODO - remove this!
var el = document.body || document.createElement('div');
var style = el.style;
var set = Ember.set;
function getElementStyle (prop) {
var uppercaseProp = prop.charAt(0).toUpperCase() + prop.slice(1);
var props = [
prop,
'webkit' + prop,
'webkit' + uppercaseProp,
'Moz' + uppercaseProp,
'moz' + uppercaseProp,
'ms' + uppercaseProp,
'ms' + prop
];
for (var i=0; i < props.length; i++) {
var property = props[i];
if (property in style) {
return property;
}
}
return null;
}
function getCSSStyle (attr) {
var styleName = getElementStyle(attr);
var prefix = styleName.toLowerCase().replace(attr, '');
var dic = {
webkit: '-webkit-' + attr,
moz: '-moz-' + attr,
ms: '-ms-' + attr
};
if (prefix && dic[prefix]) {
return dic[prefix];
}
return styleName;
}
var styleAttributeName = getElementStyle('transform');
var transformProp = getCSSStyle('transform');
var perspectiveProp = getElementStyle('perspective');
var supports2D = !!transformProp;
var supports3D = !!perspectiveProp;
function setStyle (optionalStyleString) {
return function (obj, x, y) {
var isElement = obj instanceof Element;
if (optionalStyleString && (supports2D || supports3D)) {
var style = Ember.String.fmt(optionalStyleString, x, y);
if (isElement) {
obj.style[styleAttributeName] = Ember.String.htmlSafe(style);
} else {
set(obj, 'style', Ember.String.htmlSafe(transformProp + ': ' + style));
}
} else {
if (isElement) {
obj.style.top = y;
obj.style.left = x;
}
}
};
}
export default {
transformProp: transformProp,
applyTransform: (function () {
if (supports2D) {
return setStyle('translate(%@px, %@px)');
}
return setStyle();
})(),
apply3DTransform: (function () {
if (supports3D) {
return setStyle('translate3d(%@px, %@px, 0)');
} else if (supports2D) {
return setStyle('translate(%@px, %@px)');
}
return setStyle();
})()
};

View file

@ -0,0 +1,886 @@
// TODO: remove unused: false
/* jshint unused: false*/
import Ember from 'ember';
import ReusableListItemView from './reusable-list-item-view';
var get = Ember.get;
var set = Ember.set;
var min = Math.min;
var max = Math.max;
var floor = Math.floor;
var ceil = Math.ceil;
var forEach = Ember.ArrayPolyfills.forEach;
function addContentArrayObserver() {
var content = get(this, 'content');
if (content) {
content.addArrayObserver(this);
}
}
function removeAndDestroy(object) {
this.removeObject(object);
object.destroy();
}
function syncChildViews() {
Ember.run.once(this, '_syncChildViews');
}
function sortByContentIndex (viewOne, viewTwo) {
return get(viewOne, 'contentIndex') - get(viewTwo, 'contentIndex');
}
function removeEmptyView() {
var emptyView = get(this, 'emptyView');
if (emptyView && emptyView instanceof Ember.View) {
emptyView.removeFromParent();
if (this.totalHeightDidChange !== undefined) {
this.totalHeightDidChange();
}
}
}
function addEmptyView() {
var emptyView = get(this, 'emptyView');
if (!emptyView) {
return;
}
if ('string' === typeof emptyView) {
emptyView = get(emptyView) || emptyView;
}
emptyView = this.createChildView(emptyView);
set(this, 'emptyView', emptyView);
if (Ember.CoreView.detect(emptyView)) {
this._createdEmptyView = emptyView;
}
this.unshiftObject(emptyView);
}
function enableProfilingOutput() {
function before(name, time/*, payload*/) {
console.time(name);
}
function after (name, time/*, payload*/) {
console.timeEnd(name);
}
if (Ember.ENABLE_PROFILING) {
Ember.subscribe('view._scrollContentTo', {
before: before,
after: after
});
Ember.subscribe('view.updateContext', {
before: before,
after: after
});
}
}
/**
@class Ember.ListViewMixin
@namespace Ember
*/
export default Ember.Mixin.create({
itemViewClass: ReusableListItemView,
emptyViewClass: Ember.View,
classNames: ['ember-list-view'],
attributeBindings: ['style'],
classNameBindings: ['_isGrid:ember-list-view-grid:ember-list-view-list'],
scrollTop: 0,
bottomPadding: 0, // TODO: maybe this can go away
_lastEndingIndex: 0,
paddingCount: 1,
_cachedPos: 0,
_isGrid: Ember.computed.gt('columnCount', 1).readOnly(),
/**
@private
Setup a mixin.
- adding observer to content array
- creating child views based on height and length of the content array
@method init
*/
init: function() {
this._super();
this._cachedHeights = [0];
this.on('didInsertElement', this._syncListContainerWidth);
this.columnCountDidChange();
this._syncChildViews();
this._addContentArrayObserver();
},
_addContentArrayObserver: Ember.beforeObserver(function() {
addContentArrayObserver.call(this);
}, 'content'),
/**
Called on your view when it should push strings of HTML into a
`Ember.RenderBuffer`.
Adds a [div](https://developer.mozilla.org/en-US/docs/HTML/Element/div)
with a required `ember-list-container` class.
@method render
@param {Ember.RenderBuffer} buffer The render buffer
*/
render: function (buffer) {
var element = buffer.element();
var dom = buffer.dom;
var container = dom.createElement('div');
container.className = 'ember-list-container';
element.appendChild(container);
this._childViewsMorph = dom.appendMorph(container, container, null);
return container;
},
createChildViewsMorph: function (element) {
this._childViewsMorph = this._renderer._dom.createMorph(element.lastChild, element.lastChild, null);
return element;
},
willInsertElement: function() {
if (!this.get('height') || !this.get('rowHeight')) {
throw new Error('A ListView must be created with a height and a rowHeight.');
}
this._super();
},
/**
@private
Sets inline styles of the view:
- height
- width
- position
- overflow
- -webkit-overflow
- overflow-scrolling
Called while attributes binding.
@property {Ember.ComputedProperty} style
*/
style: Ember.computed('height', 'width', function() {
var height, width, style, css;
height = get(this, 'height');
width = get(this, 'width');
css = get(this, 'css');
style = '';
if (height) {
style += 'height:' + height + 'px;';
}
if (width) {
style += 'width:' + width + 'px;';
}
for ( var rule in css ) {
if (css.hasOwnProperty(rule)) {
style += rule + ':' + css[rule] + ';';
}
}
return Ember.String.htmlSafe(style);
}),
/**
@private
Performs visual scrolling. Is overridden in Ember.ListView.
@method scrollTo
*/
scrollTo: function(y) {
throw new Error('must override to perform the visual scroll and effectively delegate to _scrollContentTo');
},
/**
@private
Internal method used to force scroll position
@method scrollTo
*/
_scrollTo: Ember.K,
/**
@private
@method _scrollContentTo
*/
_scrollContentTo: function(y) {
var startingIndex, endingIndex,
contentIndex, visibleEndingIndex, maxContentIndex,
contentIndexEnd, contentLength, scrollTop, content;
scrollTop = max(0, y);
if (this.scrollTop === scrollTop) {
return;
}
// allow a visual overscroll, but don't scroll the content. As we are doing needless
// recycyling, and adding unexpected nodes to the DOM.
var maxScrollTop = max(0, get(this, 'totalHeight') - get(this, 'height'));
scrollTop = min(scrollTop, maxScrollTop);
content = get(this, 'content');
contentLength = get(content, 'length');
startingIndex = this._startingIndex(contentLength);
Ember.instrument('view._scrollContentTo', {
scrollTop: scrollTop,
content: content,
startingIndex: startingIndex,
endingIndex: min(max(contentLength - 1, 0), startingIndex + this._numChildViewsForViewport())
}, function () {
this.scrollTop = scrollTop;
maxContentIndex = max(contentLength - 1, 0);
startingIndex = this._startingIndex();
visibleEndingIndex = startingIndex + this._numChildViewsForViewport();
endingIndex = min(maxContentIndex, visibleEndingIndex);
if (startingIndex === this._lastStartingIndex &&
endingIndex === this._lastEndingIndex) {
this.trigger('scrollYChanged', y);
return;
} else {
Ember.run(this, function() {
this._reuseChildren();
this._lastStartingIndex = startingIndex;
this._lastEndingIndex = endingIndex;
this.trigger('scrollYChanged', y);
});
}
}, this);
},
/**
@private
Computes the height for a `Ember.ListView` scrollable container div.
You must specify `rowHeight` parameter for the height to be computed properly.
@property {Ember.ComputedProperty} totalHeight
*/
totalHeight: Ember.computed('content.length',
'rowHeight',
'columnCount',
'bottomPadding', function() {
if (typeof this.heightForIndex === 'function') {
return this._totalHeightWithHeightForIndex();
} else {
return this._totalHeightWithStaticRowHeight();
}
}),
_doRowHeightDidChange: function() {
this._cachedHeights = [0];
this._cachedPos = 0;
this._syncChildViews();
},
_rowHeightDidChange: Ember.observer('rowHeight', function() {
Ember.run.once(this, this._doRowHeightDidChange);
}),
_totalHeightWithHeightForIndex: function() {
var length = this.get('content.length');
return this._cachedHeightLookup(length);
},
_totalHeightWithStaticRowHeight: function() {
var contentLength, rowHeight, columnCount, bottomPadding;
contentLength = get(this, 'content.length');
rowHeight = get(this, 'rowHeight');
columnCount = get(this, 'columnCount');
bottomPadding = get(this, 'bottomPadding');
return ((ceil(contentLength / columnCount)) * rowHeight) + bottomPadding;
},
/**
@private
@method _prepareChildForReuse
*/
_prepareChildForReuse: function(childView) {
childView.prepareForReuse();
},
createChildView: function (_view) {
return this._super(_view, this._itemViewProps || {});
},
/**
@private
@method _reuseChildForContentIndex
*/
_reuseChildForContentIndex: function(childView, contentIndex) {
var content, context, newContext, childsCurrentContentIndex, position, enableProfiling, oldChildView;
var contentViewClass = this.itemViewForIndex(contentIndex);
if (childView.constructor !== contentViewClass) {
// rather then associative arrays, lets move childView + contentEntry maping to a Map
var i = this._childViews.indexOf(childView);
childView.destroy();
childView = this.createChildView(contentViewClass);
this.insertAt(i, childView);
}
content = get(this, 'content');
enableProfiling = get(this, 'enableProfiling');
position = this.positionForIndex(contentIndex);
childView.updatePosition(position);
set(childView, 'contentIndex', contentIndex);
if (enableProfiling) {
Ember.instrument('view._reuseChildForContentIndex', position, function() {
}, this);
}
newContext = content.objectAt(contentIndex);
childView.updateContext(newContext);
},
/**
@private
@method positionForIndex
*/
positionForIndex: function(index) {
if (typeof this.heightForIndex !== 'function') {
return this._singleHeightPosForIndex(index);
}
else {
return this._multiHeightPosForIndex(index);
}
},
_singleHeightPosForIndex: function(index) {
var elementWidth, width, columnCount, rowHeight, y, x;
elementWidth = get(this, 'elementWidth') || 1;
width = get(this, 'width') || 1;
columnCount = get(this, 'columnCount');
rowHeight = get(this, 'rowHeight');
y = (rowHeight * floor(index/columnCount));
x = (index % columnCount) * elementWidth;
return {
y: y,
x: x
};
},
// 0 maps to 0, 1 maps to heightForIndex(i)
_multiHeightPosForIndex: function(index) {
var elementWidth, width, columnCount, rowHeight, y, x;
elementWidth = get(this, 'elementWidth') || 1;
width = get(this, 'width') || 1;
columnCount = get(this, 'columnCount');
x = (index % columnCount) * elementWidth;
y = this._cachedHeightLookup(index);
return {
x: x,
y: y
};
},
_cachedHeightLookup: function(index) {
for (var i = this._cachedPos; i < index; i++) {
this._cachedHeights[i + 1] = this._cachedHeights[i] + this.heightForIndex(i);
}
this._cachedPos = i;
return this._cachedHeights[index];
},
/**
@private
@method _childViewCount
*/
_childViewCount: function() {
var contentLength, childViewCountForHeight;
contentLength = get(this, 'content.length');
childViewCountForHeight = this._numChildViewsForViewport();
return min(contentLength, childViewCountForHeight);
},
/**
@private
Returns a number of columns in the Ember.ListView (for grid layout).
If you want to have a multi column layout, you need to specify both
`width` and `elementWidth`.
If no `elementWidth` is specified, it returns `1`. Otherwise, it will
try to fit as many columns as possible for a given `width`.
@property {Ember.ComputedProperty} columnCount
*/
columnCount: Ember.computed('width', 'elementWidth', function() {
var elementWidth, width, count;
elementWidth = get(this, 'elementWidth');
width = get(this, 'width');
if (elementWidth && width > elementWidth) {
count = floor(width / elementWidth);
} else {
count = 1;
}
return count;
}),
/**
@private
Fires every time column count is changed.
@event columnCountDidChange
*/
columnCountDidChange: Ember.observer(function() {
var ratio, currentScrollTop, proposedScrollTop, maxScrollTop,
scrollTop, lastColumnCount, newColumnCount, element;
lastColumnCount = this._lastColumnCount;
currentScrollTop = this.scrollTop;
newColumnCount = get(this, 'columnCount');
maxScrollTop = get(this, 'maxScrollTop');
element = this.element;
this._lastColumnCount = newColumnCount;
if (lastColumnCount) {
ratio = (lastColumnCount / newColumnCount);
proposedScrollTop = currentScrollTop * ratio;
scrollTop = min(maxScrollTop, proposedScrollTop);
this._scrollTo(scrollTop);
this.scrollTop = scrollTop;
}
if (arguments.length > 0) {
// invoked by observer
Ember.run.schedule('afterRender', this, this._syncListContainerWidth);
}
}, 'columnCount'),
/**
@private
Computes max possible scrollTop value given the visible viewport
and scrollable container div height.
@property {Ember.ComputedProperty} maxScrollTop
*/
maxScrollTop: Ember.computed('height', 'totalHeight', function(){
var totalHeight, viewportHeight;
totalHeight = get(this, 'totalHeight');
viewportHeight = get(this, 'height');
return max(0, totalHeight - viewportHeight);
}),
/**
@private
Determines whether the emptyView is the current childView.
@method _isChildEmptyView
*/
_isChildEmptyView: function() {
var emptyView = get(this, 'emptyView');
return emptyView && emptyView instanceof Ember.View &&
this._childViews.length === 1 && this._childViews.indexOf(emptyView) === 0;
},
/**
@private
Computes the number of views that would fit in the viewport area.
You must specify `height` and `rowHeight` parameters for the number of
views to be computed properly.
@method _numChildViewsForViewport
*/
_numChildViewsForViewport: function() {
if (this.heightForIndex) {
return this._numChildViewsForViewportWithMultiHeight();
} else {
return this._numChildViewsForViewportWithoutMultiHeight();
}
},
_numChildViewsForViewportWithoutMultiHeight: function() {
var height, rowHeight, paddingCount, columnCount;
height = get(this, 'height');
rowHeight = get(this, 'rowHeight');
paddingCount = get(this, 'paddingCount');
columnCount = get(this, 'columnCount');
return (ceil(height / rowHeight) * columnCount) + (paddingCount * columnCount);
},
_numChildViewsForViewportWithMultiHeight: function() {
var rowHeight, paddingCount, columnCount;
var scrollTop = this.scrollTop;
var viewportHeight = this.get('height');
var length = this.get('content.length');
var heightfromTop = 0;
var padding = get(this, 'paddingCount');
var startingIndex = this._calculatedStartingIndex();
var currentHeight = 0;
var offsetHeight = this._cachedHeightLookup(startingIndex);
for (var i = 0; i < length; i++) {
if (this._cachedHeightLookup(startingIndex + i + 1) - offsetHeight > viewportHeight) {
break;
}
}
return i + padding + 1;
},
/**
@private
Computes the starting index of the item views array.
Takes `scrollTop` property of the element into account.
Is used in `_syncChildViews`.
@method _startingIndex
*/
_startingIndex: function(_contentLength) {
var scrollTop, rowHeight, columnCount, calculatedStartingIndex,
contentLength;
if (_contentLength === undefined) {
contentLength = get(this, 'content.length');
} else {
contentLength = _contentLength;
}
scrollTop = this.scrollTop;
rowHeight = get(this, 'rowHeight');
columnCount = get(this, 'columnCount');
if (this.heightForIndex) {
calculatedStartingIndex = this._calculatedStartingIndex();
} else {
calculatedStartingIndex = floor(scrollTop / rowHeight) * columnCount;
}
var viewsNeededForViewport = this._numChildViewsForViewport();
var paddingCount = (1 * columnCount);
var largestStartingIndex = max(contentLength - viewsNeededForViewport, 0);
return min(calculatedStartingIndex, largestStartingIndex);
},
_calculatedStartingIndex: function() {
var rowHeight, paddingCount, columnCount;
var scrollTop = this.scrollTop;
var viewportHeight = this.get('height');
var length = this.get('content.length');
var heightfromTop = 0;
var padding = get(this, 'paddingCount');
for (var i = 0; i < length; i++) {
if (this._cachedHeightLookup(i + 1) >= scrollTop) {
break;
}
}
return i;
},
/**
@private
@event contentWillChange
*/
contentWillChange: Ember.beforeObserver(function() {
var content = get(this, 'content');
if (content) {
content.removeArrayObserver(this);
}
}, 'content'),
/**),
@private
@event contentDidChange
*/
contentDidChange: Ember.observer(function() {
addContentArrayObserver.call(this);
syncChildViews.call(this);
}, 'content'),
/**
@private
@property {Function} needsSyncChildViews
*/
needsSyncChildViews: Ember.observer(syncChildViews, 'height', 'width', 'columnCount'),
/**
@private
Returns a new item view. Takes `contentIndex` to set the context
of the returned view properly.
@param {Number} contentIndex item index in the content array
@method _addItemView
*/
_addItemView: function (contentIndex) {
var itemViewClass, childView;
itemViewClass = this.itemViewForIndex(contentIndex);
childView = this.createChildView(itemViewClass);
this.pushObject(childView);
},
/**
@public
Returns a view class for the provided contentIndex. If the view is
different then the one currently present it will remove the existing view
and replace it with an instance of the class provided
@param {Number} contentIndex item index in the content array
@method _addItemView
@returns {Ember.View} ember view class for this index
*/
itemViewForIndex: function(contentIndex) {
return get(this, 'itemViewClass');
},
/**
@public
Returns a view class for the provided contentIndex. If the view is
different then the one currently present it will remove the existing view
and replace it with an instance of the class provided
@param {Number} contentIndex item index in the content array
@method _addItemView
@returns {Ember.View} ember view class for this index
*/
heightForIndex: null,
/**
@private
Intelligently manages the number of childviews.
@method _syncChildViews
**/
_syncChildViews: function () {
var childViews, childViewCount,
numberOfChildViews, numberOfChildViewsNeeded,
contentIndex, startingIndex, endingIndex,
contentLength, emptyView, count, delta;
if (this.isDestroyed || this.isDestroying) {
return;
}
contentLength = get(this, 'content.length');
emptyView = get(this, 'emptyView');
childViewCount = this._childViewCount();
childViews = this.positionOrderedChildViews();
if (this._isChildEmptyView()) {
removeEmptyView.call(this);
}
startingIndex = this._startingIndex();
endingIndex = startingIndex + childViewCount;
numberOfChildViewsNeeded = childViewCount;
numberOfChildViews = childViews.length;
delta = numberOfChildViewsNeeded - numberOfChildViews;
if (delta === 0) {
// no change
} else if (delta > 0) {
// more views are needed
contentIndex = this._lastEndingIndex;
for (count = 0; count < delta; count++, contentIndex++) {
this._addItemView(contentIndex);
}
} else {
// less views are needed
forEach.call(
childViews.splice(numberOfChildViewsNeeded, numberOfChildViews),
removeAndDestroy,
this
);
}
this._reuseChildren();
this._lastStartingIndex = startingIndex;
this._lastEndingIndex = this._lastEndingIndex + delta;
if (contentLength === 0 || contentLength === undefined) {
addEmptyView.call(this);
}
},
/**
@private
Applies an inline width style to the list container.
@method _syncListContainerWidth
**/
_syncListContainerWidth: function() {
var elementWidth, columnCount, containerWidth, element;
elementWidth = get(this, 'elementWidth');
columnCount = get(this, 'columnCount');
containerWidth = elementWidth * columnCount;
element = this.$('.ember-list-container');
if (containerWidth && element) {
element.css('width', containerWidth);
}
},
/**
@private
@method _reuseChildren
*/
_reuseChildren: function(){
var contentLength, childViews, childViewsLength,
startingIndex, endingIndex, childView, attrs,
contentIndex, visibleEndingIndex, maxContentIndex,
contentIndexEnd, scrollTop;
scrollTop = this.scrollTop;
contentLength = get(this, 'content.length');
maxContentIndex = max(contentLength - 1, 0);
childViews = this.getReusableChildViews();
childViewsLength = childViews.length;
startingIndex = this._startingIndex();
visibleEndingIndex = startingIndex + this._numChildViewsForViewport();
endingIndex = min(maxContentIndex, visibleEndingIndex);
contentIndexEnd = min(visibleEndingIndex, startingIndex + childViewsLength);
for (contentIndex = startingIndex; contentIndex < contentIndexEnd; contentIndex++) {
childView = childViews[contentIndex % childViewsLength];
this._reuseChildForContentIndex(childView, contentIndex);
}
},
/**
@private
@method getReusableChildViews
*/
getReusableChildViews: function() {
return this._childViews;
},
/**
@private
@method positionOrderedChildViews
*/
positionOrderedChildViews: function() {
return this.getReusableChildViews().sort(sortByContentIndex);
},
arrayWillChange: Ember.K,
/**
@private
@event arrayDidChange
*/
// TODO: refactor
arrayDidChange: function(content, start, removedCount, addedCount) {
var index, contentIndex, state;
if (this._isChildEmptyView()) {
removeEmptyView.call(this);
}
// Support old and new Ember versions
state = this._state || this.state;
if (state === 'inDOM') {
// ignore if all changes are out of the visible change
if (start >= this._lastStartingIndex || start < this._lastEndingIndex) {
index = 0;
// ignore all changes not in the visible range
// this can re-position many, rather then causing a cascade of re-renders
forEach.call(
this.positionOrderedChildViews(),
function(childView) {
contentIndex = this._lastStartingIndex + index;
this._reuseChildForContentIndex(childView, contentIndex);
index++;
},
this
);
}
syncChildViews.call(this);
}
},
destroy: function () {
if (!this._super()) {
return;
}
if (this._createdEmptyView) {
this._createdEmptyView.destroy();
}
return this;
}
});

View file

@ -0,0 +1,167 @@
import Ember from 'ember';
import ListViewHelper from './list-view-helper';
import ListViewMixin from './list-view-mixin';
var get = Ember.get;
/**
The `Ember.ListView` view class renders a
[div](https://developer.mozilla.org/en/HTML/Element/div) HTML element,
with `ember-list-view` class.
The context of each item element within the `Ember.ListView` are populated
from the objects in the `ListView`'s `content` property.
### `content` as an Array of Objects
The simplest version of an `Ember.ListView` takes an array of object as its
`content` property. The object will be used as the `context` each item element
inside the rendered `div`.
Example:
```javascript
App.ContributorsRoute = Ember.Route.extend({
model: function () {
return [
{ name: 'Stefan Penner' },
{ name: 'Alex Navasardyan' },
{ name: 'Ray Cohen'}
];
}
});
```
```handlebars
{{#ember-list items=contributors height=500 rowHeight=50}}
{{name}}
{{/ember-list}}
```
Would result in the following HTML:
```html
<div id="ember181" class="ember-view ember-list-view" style="height:500px;width:500px;position:relative;overflow:scroll;-webkit-overflow-scrolling:touch;overflow-scrolling:touch;">
<div class="ember-list-container">
<div id="ember186" class="ember-view ember-list-item-view" style="transform: translate(0px, 0px)">
Stefan Penner
</div>
<div id="ember187" class="ember-view ember-list-item-view" style="transform: translate(0px, 50px">
Alex Navasardyan
</div>
<div id="ember188" class="ember-view ember-list-item-view" style="transform: translate(0px, 100px)">
Ray Cohen
</div>
</div>
</div>
```
By default `Ember.ListView` provides support for `height`,
`rowHeight`, `width`, `elementWidth`, `scrollTop` parameters.
Note, that `height` and `rowHeight` are required parameters.
```handlebars
{{#ember-list items=this height=500 rowHeight=50}}
{{name}}
{{/ember-list}}
```
If you would like to have multiple columns in your view layout, you can
set `width` and `elementWidth` parameters respectively.
```handlebars
{{#ember-list items=this height=500 rowHeight=50 width=500 elementWidth=80}}
{{name}}
{{/ember-list}}
```
### Extending `Ember.ListView`
Example:
```handlebars
{{view 'list-view' content=content}}
<script type="text/x-handlebars" data-template-name="row_item">
{{name}}
</script>
```
```javascript
App.ListView = Ember.ListView.extend({
height: 500,
width: 500,
elementWidth: 80,
rowHeight: 20,
itemViewClass: Ember.ListItemView.extend({templateName: "row_item"})
});
```
@extends Ember.ContainerView
@class ListView
@namespace Ember
*/
export default Ember.ContainerView.extend(ListViewMixin, {
css: {
position: 'relative',
overflow: 'auto',
'-webkit-overflow-scrolling': 'touch',
'overflow-scrolling': 'touch'
},
applyTransform: ListViewHelper.applyTransform,
_scrollTo: function(scrollTop) {
var element = this.element;
if (element) { element.scrollTop = scrollTop; }
},
didInsertElement: function() {
var that = this;
this._updateScrollableHeight();
this._scroll = function(e) { that.scroll(e); };
Ember.$(this.element).on('scroll', this._scroll);
},
willDestroyElement: function() {
Ember.$(this.element).off('scroll', this._scroll);
},
scroll: function(e) {
this.scrollTo(e.target.scrollTop);
},
scrollTo: function(y) {
this._scrollTo(y);
this._scrollContentTo(y);
},
totalHeightDidChange: Ember.observer(function () {
Ember.run.scheduleOnce('afterRender', this, this._updateScrollableHeight);
}, 'totalHeight'),
_updateScrollableHeight: function () {
var height, state;
// Support old and new Ember versions
state = this._state || this.state;
if (state === 'inDOM') {
// if the list is currently displaying the emptyView, remove the height
if (this._isChildEmptyView()) {
height = '';
} else {
height = get(this, 'totalHeight');
}
this.$('.ember-list-container').css({
height: height
});
}
}
});

View file

@ -0,0 +1,38 @@
import Ember from 'ember';
import ListItemViewMixin from './list-item-view-mixin';
var get = Ember.get, set = Ember.set;
export default Ember.View.extend(ListItemViewMixin, {
prepareForReuse: Ember.K,
init: function () {
this._super();
var context = Ember.ObjectProxy.create();
this.set('context', context);
this._proxyContext = context;
},
isVisible: Ember.computed('context.content', function () {
return !!this.get('context.content');
}),
updateContext: function (newContext) {
var context = get(this._proxyContext, 'content');
// Support old and new Ember versions
var state = this._state || this.state;
if (context !== newContext) {
if (state === 'inDOM') {
this.prepareForReuse(newContext);
}
set(this._proxyContext, 'content', newContext);
if (newContext && newContext.isController) {
set(this, 'controller', newContext);
}
}
}
});

View file

@ -1,4 +1,4 @@
//= require list-view //= require_tree ./ember-addons
//= require admin/models/user-field //= require admin/models/user-field
//= require admin/models/site-setting //= require admin/models/site-setting
//= require admin/controllers/admin-email-skipped //= require admin/controllers/admin-email-skipped

File diff suppressed because it is too large Load diff

View file

@ -1,69 +1,166 @@
var define, requireModule, require, requirejs, hasModule; var define, requireModule, require, requirejs;
(function() { (function() {
var registry = {}, seen = {}, state = {};
var _isArray;
if (!Array.isArray) {
_isArray = function (x) {
return Object.prototype.toString.call(x) === "[object Array]";
};
} else {
_isArray = Array.isArray;
}
var registry = {};
var seen = {};
var FAILED = false; var FAILED = false;
var uuid = 0;
function tryFinally(tryable, finalizer) {
try {
return tryable();
} finally {
finalizer();
}
}
function unsupportedModule(length) {
throw new Error("an unsupported module was defined, expected `define(name, deps, module)` instead got: `" + length + "` arguments to define`");
}
var defaultDeps = ['require', 'exports', 'module'];
function Module(name, deps, callback, exports) {
this.id = uuid++;
this.name = name;
this.deps = !deps.length && callback.length ? defaultDeps : deps;
this.exports = exports || { };
this.callback = callback;
this.state = undefined;
this._require = undefined;
}
Module.prototype.makeRequire = function() {
var name = this.name;
return this._require || (this._require = function(dep) {
return require(resolve(dep, name));
});
}
define = function(name, deps, callback) { define = function(name, deps, callback) {
registry[name] = { if (arguments.length < 2) {
deps: deps, unsupportedModule(arguments.length);
callback: callback }
};
if (!_isArray(deps)) {
callback = deps;
deps = [];
}
registry[name] = new Module(name, deps, callback);
}; };
function reify(deps, name, seen) { // we don't support all of AMD
// define.amd = {};
// we will support petals...
define.petal = { };
function Alias(path) {
this.name = path;
}
define.alias = function(path) {
return new Alias(path);
};
function reify(mod, name, seen) {
var deps = mod.deps;
var length = deps.length; var length = deps.length;
var reified = new Array(length); var reified = new Array(length);
var dep; var dep;
var exports; // TODO: new Module
// TODO: seen refactor
var module = { };
for (var i = 0, l = length; i < l; i++) { for (var i = 0, l = length; i < l; i++) {
dep = deps[i]; dep = deps[i];
if (dep === 'exports') { if (dep === 'exports') {
exports = reified[i] = seen; module.exports = reified[i] = seen;
} else if (dep === 'require') {
reified[i] = mod.makeRequire();
} else if (dep === 'module') {
mod.exports = seen;
module = reified[i] = mod;
} else { } else {
reified[i] = require(resolve(dep, name)); reified[i] = requireFrom(resolve(dep, name), name);
} }
} }
return { return {
deps: reified, deps: reified,
exports: exports module: module
}; };
} }
hasModule = function(name){ function requireFrom(name, origin) {
return !!registry[name]; var mod = registry[name];
}; if (!mod) {
throw new Error('Could not find module `' + name + '` imported from `' + origin + '`');
}
return require(name);
}
function missingModule(name) {
throw new Error('Could not find module ' + name);
}
requirejs = require = requireModule = function(name) { requirejs = require = requireModule = function(name) {
if (state[name] !== FAILED && var mod = registry[name];
if (mod && mod.callback instanceof Alias) {
mod = registry[mod.callback.name];
}
if (!mod) { missingModule(name); }
if (mod.state !== FAILED &&
seen.hasOwnProperty(name)) { seen.hasOwnProperty(name)) {
return seen[name]; return seen[name];
} }
if (!registry[name]) {
throw new Error('Could not find module ' + name);
}
var mod = registry[name];
var reified; var reified;
var module; var module;
var loaded = false; var loaded = false;
seen[name] = { }; // placeholder for run-time cycles seen[name] = { }; // placeholder for run-time cycles
try { tryFinally(function() {
reified = reify(mod.deps, name, seen[name]); reified = reify(mod, name, seen[name]);
module = mod.callback.apply(this, reified.deps); module = mod.callback.apply(this, reified.deps);
loaded = true; loaded = true;
} finally { }, function() {
if (!loaded) { if (!loaded) {
state[name] = FAILED; mod.state = FAILED;
} }
});
var obj;
if (module === undefined && reified.module.exports) {
obj = reified.module.exports;
} else {
obj = seen[name] = module;
} }
return reified.exports ? seen[name] : (seen[name] = module); if (obj !== null &&
(typeof obj === 'object' || typeof obj === 'function') &&
obj['default'] === undefined) {
obj['default'] = obj;
}
return (seen[name] = obj);
}; };
function resolve(child, name) { function resolve(child, name) {
@ -71,20 +168,19 @@ var define, requireModule, require, requirejs, hasModule;
var parts = child.split('/'); var parts = child.split('/');
var nameParts = name.split('/'); var nameParts = name.split('/');
var parentBase; var parentBase = nameParts.slice(0, -1);
if (nameParts.length === 1) {
parentBase = nameParts;
} else {
parentBase = nameParts.slice(0, -1);
}
for (var i = 0, l = parts.length; i < l; i++) { for (var i = 0, l = parts.length; i < l; i++) {
var part = parts[i]; var part = parts[i];
if (part === '..') { parentBase.pop(); } if (part === '..') {
else if (part === '.') { continue; } if (parentBase.length === 0) {
else { parentBase.push(part); } throw new Error('Cannot access parent module of root');
}
parentBase.pop();
} else if (part === '.') {
continue;
} else { parentBase.push(part); }
} }
return parentBase.join('/'); return parentBase.join('/');