1
0
mirror of https://github.com/flarum/core.git synced 2025-07-31 21:50:50 +02:00

Upgrade to Ember 1.11-beta.1

HTMLBars goodness! Since there was some breakage and a lot of fiddling
around to get some things working, I took this opportunity to do a big
cleanup of the whole Ember app. I accidentally worked on some new
features too :3

Note that the app is still broken right now, pending on
https://github.com/emberjs/ember.js/issues/10401

Cleanup:
- Restructuring of components
- Consolidation of some stuff into mixins, cleanup of some APIs that
will be public
- Change all instances of .property() / .observes() / .on() to
Ember.computed() / Ember.observer() / Ember.on() respectively (I think
it is more readable)
- More comments
- Start conforming to a code style (2 spaces for indentation)

New features:
- Post hiding/restoring
- Mark individual discussions as read by clicking
- Clicking on a read discussion jumps to the end
- Mark all discussions as read
- Progressively mark the discussion as read as the page is scrolled
- Unordered list post formatting
- Post permalink popup

Demo once that Ember regression is fixed!
This commit is contained in:
Toby Zerner
2015-02-10 18:05:40 +10:30
parent cf88fda8c8
commit c28307903b
164 changed files with 4623 additions and 4587 deletions

View File

@@ -0,0 +1,29 @@
import Ember from 'ember';
var precompileTemplate = Ember.Handlebars.compile;
/**
Button which sends an action when clicked.
*/
export default Ember.Component.extend({
tagName: 'a',
attributeBindings: ['href', 'title'],
classNameBindings: ['className'],
href: '#',
layout: precompileTemplate('{{#if icon}}{{fa-icon icon class="fa-fw icon-glyph"}} {{/if}}<span>{{label}}</span>'),
label: '',
icon: '',
className: '',
action: null,
click: function(e) {
e.preventDefault();
var action = this.get('action');
if (typeof action === 'string') {
this.sendAction('action');
} else if (typeof action === 'function') {
action();
}
}
});

View File

@@ -0,0 +1,53 @@
import Ember from 'ember';
import HasItemLists from 'flarum/mixins/has-item-lists';
import ActionButton from 'flarum/components/ui/action-button';
/**
An alert message. Has a message, a `controls` item list, and a dismiss
button.
*/
export default Ember.Component.extend(HasItemLists, {
layoutName: 'components/ui/alert-message',
classNames: ['alert'],
classNameBindings: ['classForType'],
itemLists: ['controls'],
message: '',
type: '',
dismissable: true,
buttons: [],
classForType: Ember.computed('type', function() {
return 'alert-'+this.get('type');
}),
populateControls: function(controls) {
var component = this;
this.get('buttons').forEach(function(button) {
controls.pushObject(ActionButton.create({
label: button.label,
action: function() {
component.send('dismiss');
button.action();
}
}));
});
if (this.get('dismissable')) {
var dismiss = ActionButton.create({
icon: 'times',
className: 'btn btn-icon btn-link',
action: function() { component.send('dismiss'); }
});
controls.pushObjectWithTag(dismiss, 'dismiss');
}
},
actions: {
dismiss: function() {
this.sendAction('dismiss', this);
}
}
});

View File

@@ -1,28 +0,0 @@
import Ember from 'ember';
export default Ember.Component.extend({
label: '',
icon: '',
className: '',
action: null,
divider: false,
active: false,
classNames: [],
tagName: 'a',
attributeBindings: ['href', 'title'],
classNameBindings: ['className'],
href: '#',
layout: Ember.Handlebars.compile('{{#if icon}}{{fa-icon icon class="fa-fw icon-glyph"}} {{/if}}<span>{{label}}</span>'),
click: function(e) {
e.preventDefault();
var action = this.get('action');
if (typeof action === 'string') {
this.sendAction('action');
} else if (typeof action === 'function') {
action();
}
}
});

View File

@@ -1,27 +0,0 @@
import Ember from 'ember';
export default Ember.Component.extend({
items: null, // TaggedArray
layoutName: 'components/ui/controls/dropdown-button',
classNames: ['dropdown', 'btn-group'],
classNameBindings: ['itemCountClass', 'class'],
label: 'Controls',
icon: 'ellipsis-v',
buttonClass: 'btn btn-default',
menuClass: '',
dropdownMenuClass: function() {
return 'dropdown-menu '+this.get('menuClass');
}.property('menuClass'),
itemCountClass: function() {
return 'item-count-'+this.get('items.length');
}.property('items.length'),
actions: {
buttonClick: function() {
this.sendAction('buttonClick');
}
}
});

View File

@@ -1,28 +0,0 @@
import Ember from 'ember';
export default Ember.Component.extend({
items: [],
layoutName: 'components/ui/controls/dropdown-select',
classNames: ['dropdown', 'dropdown-select', 'btn-group'],
classNameBindings: ['itemCountClass', 'class'],
buttonClass: 'btn btn-default',
menuClass: '',
icon: 'ellipsis-v',
mainButtonClass: function() {
return 'btn '+this.get('buttonClass');
}.property('buttonClass'),
dropdownMenuClass: function() {
return 'dropdown-menu '+this.get('menuClass');
}.property('menuClass'),
itemCountClass: function() {
return 'item-count-'+this.get('items.length');
}.property('items.length'),
activeItem: function() {
return this.get('menu.childViews').findBy('active');
}.property('menu.childViews.@each.active')
});

View File

@@ -1,15 +0,0 @@
import DropdownButton from './dropdown-button';
export default DropdownButton.extend({
layoutName: 'components/ui/controls/dropdown-split',
classNames: ['dropdown', 'dropdown-split', 'btn-group'],
menuClass: 'pull-right',
mainButtonClass: function() {
return 'btn '+this.get('buttonClass');
}.property('buttonClass'),
firstItem: function() {
return this.get('items').objectAt(0);
}.property('items.[]')
});

View File

@@ -1,22 +0,0 @@
import Ember from 'ember';
import ComponentItem from '../items/component-item';
export default Ember.Component.extend({
tagName: 'ul',
layoutName: 'components/ui/controls/item-list',
listItems: function() {
if (!Ember.isArray(this.get('items'))) {
return [];
}
var listItems = [];
this.get('items').forEach(function(item) {
if (item.get('tagName') !== 'li') {
item = ComponentItem.extend({component: item});
}
listItems.push(item);
});
return listItems;
}.property('items.[]')
});

View File

@@ -1,14 +0,0 @@
import Ember from 'ember';
export default Ember.Component.extend({
classNames: ['loading-indicator'],
layout: Ember.Handlebars.compile('&nbsp;'),
size: 'small',
didInsertElement: function() {
var size = this.get('size');
Ember.$.fn.spin.presets[size].zIndex = 'auto';
this.$().spin(size);
}
});

View File

@@ -1,40 +0,0 @@
import Ember from 'ember';
export default Ember.Component.extend({
classNames: ['search-input'],
classNameBindings: ['active', 'value:clearable'],
layoutName: 'components/ui/controls/search-input',
didInsertElement: function() {
var self = this;
this.$().find('input').on('keydown', function(e) {
if (e.which === 27) {
self.clear();
}
});
this.$().find('.clear').on('mousedown', function(e) {
e.preventDefault();
}).on('click', function(e) {
e.preventDefault();
self.clear();
});
},
clear: function() {
this.set('value', '');
this.send('search');
this.$().find('input').focus();
},
willDestroyElement: function() {
this.$().find('input').off('keydown');
this.$().find('.clear').off('mousedown click');
},
actions: {
search: function() {
this.get('action')(this.get('value'));
}
}
});

View File

@@ -1,9 +0,0 @@
import Ember from 'ember';
export default Ember.Component.extend({
tagName: 'span',
classNames: ['select-input'],
optionValuePath: 'content',
optionLabelPath: 'content',
layout: Ember.Handlebars.compile('{{view "select" content=view.content optionValuePath=view.optionValuePath optionLabelPath=view.optionLabelPath value=view.value class="form-control"}} {{fa-icon "sort"}}')
});

View File

@@ -1,39 +0,0 @@
import Ember from 'ember';
import TaggedArray from '../../../utils/tagged-array';
import ActionButton from './action-button';
export default Ember.Component.extend({
disabled: false,
classNames: ['text-editor'],
didInsertElement: function() {
var controlItems = TaggedArray.create();
this.trigger('populateControls', controlItems);
this.set('controlItems', controlItems);
var component = this;
this.$('textarea').bind('keydown', 'meta+return', function() {
component.send('submit');
});
},
populateControls: function(controls) {
var component = this;
var submit = ActionButton.create({
label: this.get('submitLabel'),
className: 'btn btn-primary',
action: function() {
component.send('submit');
}
});
controls.pushObjectWithTag(submit, 'submit');
},
actions: {
submit: function() {
this.sendAction('submit', this.get('value'));
}
}
});

View File

@@ -1,18 +0,0 @@
import Ember from 'ember';
export default Ember.TextField.extend({
didInsertElement: function() {
var component = this;
this.$().on('input', function() {
var empty = !$(this).val();
if (empty) {
$(this).val(component.get('placeholder'));
}
$(this).css('width', 0);
$(this).width($(this)[0].scrollWidth);
if (empty) {
$(this).val('');
}
});
}
});

View File

@@ -0,0 +1,30 @@
import Ember from 'ember';
/**
Button which has an attached dropdown menu containing an item list.
*/
export default Ember.Component.extend({
layoutName: 'components/ui/dropdown-button',
classNames: ['dropdown', 'btn-group'],
classNameBindings: ['itemCountClass', 'class'],
label: 'Controls',
icon: 'ellipsis-v',
buttonClass: 'btn btn-default',
menuClass: '',
items: null,
dropdownMenuClass: Ember.computed('menuClass', function() {
return 'dropdown-menu '+this.get('menuClass');
}),
itemCountClass: Ember.computed('items.length', function() {
return 'item-count-'+this.get('items.length');
}),
actions: {
buttonClick: function() {
this.sendAction('buttonClick');
}
}
});

View File

@@ -0,0 +1,32 @@
import Ember from 'ember';
/**
Button which has an attached dropdown menu containing an item list. The
currently-active item's label is displayed as the label of the button.
*/
export default Ember.Component.extend({
layoutName: 'components/ui/dropdown-select',
classNames: ['dropdown', 'dropdown-select', 'btn-group'],
classNameBindings: ['itemCountClass', 'class'],
buttonClass: 'btn btn-default',
menuClass: '',
icon: 'ellipsis-v',
items: [],
mainButtonClass: Ember.computed('buttonClass', function() {
return 'btn '+this.get('buttonClass');
}),
dropdownMenuClass: Ember.computed('menuClass', function() {
return 'dropdown-menu '+this.get('menuClass');
}),
itemCountClass: Ember.computed('items.length', function() {
return 'item-count-'+this.get('items.length');
}),
activeItem: Ember.computed('menu.childViews.@each.active', function() {
return this.get('menu.childViews').findBy('active');
})
});

View File

@@ -0,0 +1,22 @@
import Ember from 'ember';
import DropdownButton from 'flarum/components/ui/dropdown-button';
/**
Given a list of items, this component displays a split button: the left side
is the first item in the list, while the right side is a dropdown-toggle
which shows a dropdown menu containing all of the items.
*/
export default DropdownButton.extend({
layoutName: 'components/ui/dropdown-split',
classNames: ['dropdown', 'dropdown-split', 'btn-group'],
menuClass: 'pull-right',
mainButtonClass: Ember.computed('buttonClass', function() {
return 'btn '+this.get('buttonClass');
}),
firstItem: Ember.computed('items.[]', function() {
return this.get('items').objectAt(0);
})
});

View File

@@ -0,0 +1,21 @@
import Ember from 'ember';
/**
Output a list of components within a <ul>, making sure each one is contained
in an <li> element.
*/
export default Ember.Component.extend({
layoutName: 'components/ui/item-list',
tagName: 'ul',
listItems: Ember.computed('items.[]', function() {
var items = this.get('items');
if (!Ember.isArray(items)) {
return [];
}
items.forEach(function(item) {
item.set('isListItem', item.get('tagName') === 'li');
});
return items;
})
});

View File

@@ -1,6 +0,0 @@
import Ember from 'ember';
export default Ember.Component.extend({
tagName: 'li',
layoutName: 'components/ui/items/component-item'
});

View File

@@ -1,37 +0,0 @@
import Ember from 'ember';
export default Ember.Component.extend({
icon: '',
label: '',
action: null,
badge: '',
tagName: 'li',
classNameBindings: ['active'],
active: function() {
return !! this.get('childViews').anyBy('active');
}.property('childViews.@each.active'),
// init: function() {
// var params = this.params;
// if (params[params.length - 1].queryParams) {
// this.queryParamsObject = {values: params.pop().queryParams};
// }
// this._super();
// },
layout: function() {
return Ember.Handlebars.compile('{{#link-to '+this.get('linkTo')+'}}'+this.get('iconTemplate')+' {{label}} <span class="count">{{badge}}</span>{{/link-to}}');
}.property('linkTo', 'iconTemplate'),
iconTemplate: function() {
return '{{fa-icon icon}}';
}.property(),
actions: {
main: function() {
this.get('action')();
}
}
});

View File

@@ -1,6 +0,0 @@
import Ember from 'ember';
export default Ember.Component.extend({
tagName: 'li',
classNames: ['divider']
});

View File

@@ -0,0 +1,19 @@
import Ember from 'ember';
var precompileTemplate = Ember.Handlebars.compile;
/**
Loading spinner.
*/
export default Ember.Component.extend({
classNames: ['loading-indicator'],
layout: precompileTemplate('&nbsp;'),
size: 'small',
didInsertElement: function() {
var size = this.get('size');
Ember.$.fn.spin.presets[size].zIndex = 'auto';
this.$().spin(size);
}
});

View File

@@ -0,0 +1,22 @@
import Ember from 'ember';
var precompileTemplate = Ember.Handlebars.compile;
/**
A list item which contains a navigation link. The list item's `active`
property reflects whether or not the link is active.
*/
export default Ember.Component.extend({
layout: precompileTemplate('{{#link-to routeName}}{{fa-icon icon}} {{label}} <span class="count">{{badge}}</span>{{/link-to}}'),
tagName: 'li',
classNameBindings: ['active'],
icon: '',
label: '',
badge: '',
routeName: '',
active: Ember.computed('childViews.@each.active', function() {
return !!this.get('childViews').anyBy('active');
})
});

View File

@@ -0,0 +1,36 @@
import Ember from 'ember';
/**
A basic search input. Comes with the ability to be cleared by pressing
escape or with a button. Sends an action when enter is pressed.
*/
export default Ember.Component.extend({
layoutName: 'components/ui/search-input',
classNames: ['search-input'],
classNameBindings: ['active', 'value:clearable'],
didInsertElement: function() {
this.$('input').on('keydown', 'esc', function(e) {
self.clear();
});
var self = this;
this.$('.clear').on('mousedown click', function(e) {
e.preventDefault();
}).on('click', function(e) {
self.clear();
});
},
clear: function() {
this.set('value', '');
this.send('search');
this.$().find('input').focus();
},
actions: {
search: function() {
this.get('action')(this.get('value'));
}
}
});

View File

@@ -0,0 +1,16 @@
import Ember from 'ember';
var precompileTemplate = Ember.Handlebars.compile;
/**
A basic select input. Wraps Ember's select component with a span/icon so
that we can style it more fancily.
*/
export default Ember.Component.extend({
layout: precompileTemplate('{{view "select" content=view.content optionValuePath=view.optionValuePath optionLabelPath=view.optionLabelPath value=view.value class="form-control"}} {{fa-icon "sort"}}'),
tagName: 'span',
classNames: ['select-input'],
optionValuePath: 'content',
optionLabelPath: 'content'
});

View File

@@ -0,0 +1,9 @@
import Ember from 'ember';
/**
A simple separator list item for use in menus.
*/
export default Ember.Component.extend({
tagName: 'li',
classNames: ['divider']
});

View File

@@ -0,0 +1,33 @@
import Ember from 'ember';
import HasItemLists from 'flarum/mixins/has-item-lists';
import ActionButton from 'flarum/components/ui/action-button';
/**
A text editor. Contains a textarea and an item list of `controls`, including
a submit button.
*/
export default Ember.Component.extend(HasItemLists, {
classNames: ['text-editor'],
itemLists: ['controls'],
value: '',
disabled: false,
didInsertElement: function() {
var component = this;
this.$('textarea').bind('keydown', 'meta+return', function() {
component.send('submit');
});
},
populateControls: function(items) {
this.addActionItem(items, 'submit', this.get('submitLabel')).set('className', 'btn btn-primary');
},
actions: {
submit: function() {
this.sendAction('submit', this.get('value'));
}
}
});

View File

@@ -0,0 +1,26 @@
import Ember from 'ember';
/**
An extension of Ember's text field with an option to set up an auto-growing
text input.
*/
export default Ember.TextField.extend({
autoGrow: false,
didInsertElement: function() {
if (this.get('autoGrow')) {
var component = this;
this.$().on('input', function() {
var empty = !$(this).val();
if (empty) {
$(this).val(component.get('placeholder'));
}
$(this).css('width', 0);
$(this).width($(this)[0].scrollWidth);
if (empty) {
$(this).val('');
}
});
}
}
});