Replace Ember app with Mithril app

This commit is contained in:
Toby Zerner 2015-04-25 22:28:39 +09:30
parent cceb2b1249
commit b5c95a222f
377 changed files with 5641 additions and 7330 deletions

View File

@ -1,4 +0,0 @@
{
"directory": "bower_components",
"analytics": false
}

View File

@ -1,33 +0,0 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
indent_style = space
indent_size = 2
[*.js]
indent_style = space
indent_size = 2
[*.hbs]
indent_style = space
indent_size = 2
[*.css]
indent_style = space
indent_size = 2
[*.html]
indent_style = space
indent_size = 2
[*.{diff,md}]
trim_trailing_whitespace = false

View File

@ -1,17 +0,0 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/tmp
# dependencies
/node_modules
/bower_components
# misc
/.sass-cache
/connect.lock
/coverage/*
/libpeerconnection.log
npm-debug.log
testem.log

View File

@ -1,33 +0,0 @@
{
"predef": [
"document",
"window",
"-Promise",
"moment"
],
"browser": true,
"boss": true,
"curly": true,
"debug": false,
"devel": true,
"eqeqeq": true,
"evil": true,
"forin": false,
"immed": false,
"laxbreak": false,
"newcap": true,
"noarg": true,
"noempty": false,
"nonew": false,
"nomen": false,
"onevar": false,
"plusplus": false,
"regexp": false,
"undef": true,
"sub": true,
"strict": false,
"white": false,
"eqnull": true,
"esnext": true,
"unused": true
}

View File

@ -1,20 +0,0 @@
---
language: node_js
sudo: false
cache:
directories:
- node_modules
before_install:
- "npm config set spin false"
- "npm install -g npm@^2"
install:
- npm install -g bower
- npm install
- bower install
script:
- npm test

View File

@ -1,20 +0,0 @@
/* global require, module */
var EmberApp = require('ember-cli/lib/broccoli/ember-app');
var app = new EmberApp();
app.import('bower_components/bootstrap/dist/js/bootstrap.js');
app.import('bower_components/spin.js/spin.js');
app.import('bower_components/spin.js/jquery.spin.js');
app.import('bower_components/moment/moment.js');
app.import('bower_components/jquery.hotkeys/jquery.hotkeys.js');
app.import('bower_components/blurjs/dist/jquery.blur.js');
app.import('bower_components/font-awesome/fonts/fontawesome-webfont.eot');
app.import('bower_components/font-awesome/fonts/fontawesome-webfont.svg');
app.import('bower_components/font-awesome/fonts/fontawesome-webfont.ttf');
app.import('bower_components/font-awesome/fonts/fontawesome-webfont.woff');
app.import('bower_components/font-awesome/fonts/FontAwesome.otf');
module.exports = app.toTree();

View File

@ -1,18 +0,0 @@
import Ember from 'ember';
import Resolver from 'ember/resolver';
import loadInitializers from 'ember/load-initializers';
import config from './config/environment';
Ember.MODEL_FACTORY_INJECTIONS = true;
var App = Ember.Application.extend({
modulePrefix: config.modulePrefix,
podModulePrefix: config.podModulePrefix,
Resolver: Resolver
});
loadInitializers(App, config.modulePrefix);
Ember.$('#assets-loading').remove();
export default App;

View File

@ -1,8 +0,0 @@
import Ember from 'ember';
import NavItem from './nav-item';
var precompileTemplate = Ember.Handlebars.compile;
export default NavItem.extend({
layout: precompileTemplate('{{#link-to routeName}}{{fa-icon icon class="icon"}} <span class="label">{{label}}</span> <div class="description">{{description}}</div>{{/link-to}}')
});

View File

@ -1,23 +0,0 @@
import Ember from 'ember';
import HasItemLists from '../mixins/has-item-lists';
import DropdownButton from './ui/dropdown-button';
var precompileTemplate = Ember.Handlebars.compile;
export default DropdownButton.extend(HasItemLists, {
layoutName: 'components/application/user-dropdown',
itemLists: ['items'],
buttonClass: 'btn btn-default btn-naked btn-rounded btn-user',
menuClass: 'pull-right',
label: Ember.computed.alias('user.username'),
populateItems: function(items) {
var self = this;
this.addActionItem(items, 'logout', 'Log Out', 'sign-out', null, function() {
self.get('parentController').send('invalidateSession');
});
}
})

View File

@ -1,9 +0,0 @@
import Ember from 'ember';
export default Ember.Controller.extend({
actions: {
toggleDrawer: function() {
this.toggleProperty('drawerShowing');
}
}
});

View File

@ -1,25 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Flarum</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
{{content-for 'head'}}
<link rel="stylesheet" href="assets/vendor.css">
<link rel="stylesheet" href="assets/flarum.css">
{{content-for 'head-footer'}}
</head>
<body>
{{content-for 'body'}}
<script src="assets/vendor.js"></script>
<script src="assets/flarum.js"></script>
{{content-for 'body-footer'}}
</body>
</html>

View File

@ -1,16 +0,0 @@
import Ember from 'ember';
import config from './config/environment';
var Router = Ember.Router.extend({
location: config.locationType
});
Router.map(function() {
this.resource('dashboard', {path: '/'});
this.resource('basics');
this.resource('permissions');
this.resource('appearance');
this.resource('extensions');
});
export default Router;

View File

@ -1,2 +0,0 @@
// Flarum styles are stored in the top-level `less` directory. This remains
// here as a placeholder file to prevent ember-cli from crashing.

View File

@ -1 +0,0 @@
Appearance

View File

@ -1,43 +0,0 @@
<div id="page" class="global-page with-pane">
{{application/back-button className="back-control" toggleDrawer="toggleDrawer" goBack="goBack" canGoBack=false}}
<div id="drawer" class="global-drawer">
<header id="header" class="global-header">
{{application/back-button goBack="goBack" canGoBack=true}}
<div class="container">
<div class="header-primary">
<h1 class="header-title">
Administration
</h1>
{{ui/item-list items=view.headerPrimary class="header-controls"}}
</div>
<div class="header-secondary">
{{ui/item-list items=view.headerSecondary class="header-controls"}}
</div>
</div>
</header>
</div>
<main id="content" class="global-content">
<div class="container">
<div class="side-nav admin-nav title-control">
{{ui/dropdown-select items=view.adminNav}}
</div>
<div class="admin-content">
{{outlet}}
</div>
</div>
</main>
</div>
<div id="modal" class="modal fade">
{{outlet "modal"}}
</div>
{{render "alerts"}}

View File

@ -1 +0,0 @@
Basics

View File

@ -1 +0,0 @@
Dashboard

View File

@ -1 +0,0 @@
Extensions

View File

@ -1 +0,0 @@
{{ui/loading-indicator class="loading-indicator-block"}}

View File

@ -1 +0,0 @@
Permissions

View File

@ -1,79 +0,0 @@
import Ember from 'ember';
import HasItemLists from '../mixins/has-item-lists';
import AdminNavItem from '../components/ui/admin-nav-item';
import SearchInput from '../components/ui/search-input';
import UserDropdown from '../components/user-dropdown';
export default Ember.View.extend(HasItemLists, {
itemLists: ['headerPrimary', 'headerSecondary', 'adminNav'],
drawerShowingChanged: Ember.observer('controller.drawerShowing', function() {
Ember.run.scheduleOnce('afterRender', this, function() {
$('body').toggleClass('drawer-open', this.get('controller.drawerShowing'));
});
}),
didInsertElement: function() {
this.$('.global-content').click(function(e) {
if (view.get('controller.drawerShowing')) {
e.preventDefault();
view.set('controller.drawerShowing', false);
}
});
},
populateHeaderSecondary: function(items) {
var controller = this.get('controller');
items.pushObjectWithTag(SearchInput.extend({
placeholder: 'Search Forum',
controller: controller,
valueBinding: Ember.Binding.oneWay('controller.searchQuery'),
activeBinding: Ember.Binding.oneWay('controller.searchActive'),
action: function(value) { controller.send('search', value); }
}), 'search');
items.pushObjectWithTag(UserDropdown.extend({
user: this.get('controller.session.user'),
parentController: controller
}), 'user');
},
populateAdminNav: function(items) {
items.pushObjectWithTag(AdminNavItem.extend({
routeName: 'dashboard',
icon: 'bar-chart',
label: 'Dashboard',
description: 'Your forum at a glance.'
}), 'dashboard');
items.pushObjectWithTag(AdminNavItem.extend({
routeName: 'basics',
icon: 'pencil',
label: 'Basics',
description: 'Set your forum title, language, and other basic settings.'
}), 'basics');
items.pushObjectWithTag(AdminNavItem.extend({
routeName: 'permissions',
icon: 'key',
label: 'Permissions',
description: 'Configure who can see and do what.'
}), 'permissions');
items.pushObjectWithTag(AdminNavItem.extend({
routeName: 'appearance',
icon: 'paint-brush',
label: 'Appearance',
description: 'Customize your forum\'s colors, logos, and other variables.'
}), 'appearance');
items.pushObjectWithTag(AdminNavItem.extend({
routeName: 'extensions',
icon: 'puzzle-piece',
label: 'Extensions',
description: 'Add extra functionality to your forum and make it your own.'
}), 'extensions');
}
});

View File

@ -1,28 +0,0 @@
{
"name": "flarum-admin",
"dependencies": {
"jquery": "2.1.3",
"ember": "1.11.0-beta.3",
"ember-data": "1.0.0-beta.16.1",
"ember-resolver": "~0.1.11",
"loader.js": "ember-cli/loader.js#1.0.1",
"ember-cli-shims": "ember-cli/ember-cli-shims#0.0.3",
"ember-cli-test-loader": "0.1.3",
"ember-load-initializers": "ember-cli/ember-load-initializers#0.0.2",
"ember-qunit": "0.2.8",
"ember-qunit-notifications": "0.0.7",
"qunit": "~1.17.1",
"bootstrap": "~3.3.2",
"font-awesome": "~4",
"spin.js": "~2.0.1",
"moment": "~2.8.4",
"ember-simple-auth": "0.7.2",
"jquery.hotkeys": "jeresig/jquery.hotkeys#0.2.0",
"blurjs": ""
},
"resolutions": {
"ember-cli-test-loader": "0.1.3",
"ember-qunit": "0.2.8",
"ember-qunit-notifications": "0.0.7"
}
}

View File

@ -1,53 +0,0 @@
/* jshint node: true */
module.exports = function(environment) {
var ENV = {
modulePrefix: 'flarum-admin',
environment: environment,
baseURL: '/',
apiURL: '/api',
locationType: 'hash',
EmberENV: {
FEATURES: {
// Here you can enable experimental features on an ember canary build
// e.g. 'with-controller': true
}
},
APP: {
// Here you can pass flags/options to your application instance
// when it is created
}
};
ENV['simple-auth'] = {
authorizer: 'authorizer:flarum'
};
if (environment === 'development') {
// ENV.APP.LOG_RESOLVER = true;
// ENV.APP.LOG_ACTIVE_GENERATION = true;
// ENV.APP.LOG_TRANSITIONS = true;
// ENV.APP.LOG_TRANSITIONS_INTERNAL = true;
// ENV.APP.LOG_VIEW_LOOKUPS = true;
}
if (environment === 'test') {
// Testem prefers this...
ENV.baseURL = '/';
ENV.locationType = 'none';
ENV.apiURL = 'http://flarum.dev/api',
// keep test console output quieter
ENV.APP.LOG_ACTIVE_GENERATION = false;
ENV.APP.LOG_VIEW_LOOKUPS = false;
ENV.APP.rootElement = '#ember-testing';
}
if (environment === 'production') {
}
return ENV;
};

View File

@ -1,41 +0,0 @@
{
"name": "flarum-admin",
"version": "0.0.0",
"private": true,
"directories": {
"doc": "doc",
"test": "tests"
},
"scripts": {
"start": "ember server",
"build": "ember build",
"test": "ember test",
"preinstall": "sudo npm link ../common"
},
"repository": "",
"engines": {
"node": ">= 0.10.0"
},
"author": "",
"license": "MIT",
"devDependencies": {
"ember-cli": "^0.2.0-beta.1",
"ember-cli-app-version": "0.3.1",
"ember-cli-babel": "^4.1.0",
"ember-cli-content-security-policy": "0.3.0",
"ember-cli-dependency-checker": "0.0.7",
"ember-cli-htmlbars": "^0.7.4",
"ember-cli-ic-ajax": "0.1.1",
"ember-cli-inject-live-reload": "^1.3.0",
"ember-cli-qunit": "^0.3.8",
"ember-cli-simple-auth": "^0.7.2",
"ember-cli-uglify": "1.0.1",
"ember-data": "1.0.0-beta.16.1",
"ember-export-application-global": "^1.0.2",
"ember-json-api": "eneuhauser/ember-json-api",
"broccoli-ember-inline-template-compiler": "tobscure/broccoli-ember-inline-template-compiler#f884d11",
"express": "^4.8.5",
"glob": "^4.0.5",
"flarum-common": "*"
}
}

View File

@ -1,11 +0,0 @@
{
"framework": "qunit",
"test_page": "tests/index.html?hidepassed",
"launch_in_ci": [
"PhantomJS"
],
"launch_in_dev": [
"PhantomJS",
"Chrome"
]
}

View File

@ -1,74 +0,0 @@
{
"predef": [
"document",
"window",
"location",
"setTimeout",
"$",
"-Promise",
"QUnit",
"define",
"console",
"equal",
"notEqual",
"notStrictEqual",
"test",
"asyncTest",
"testBoth",
"testWithDefault",
"raises",
"throws",
"deepEqual",
"start",
"stop",
"ok",
"strictEqual",
"module",
"moduleFor",
"moduleForComponent",
"moduleForModel",
"process",
"expect",
"visit",
"exists",
"fillIn",
"click",
"keyEvent",
"triggerEvent",
"find",
"findWithAssert",
"wait",
"DS",
"isolatedContainer",
"startApp",
"andThen",
"currentURL",
"currentPath",
"currentRouteName"
],
"node": false,
"browser": false,
"boss": true,
"curly": false,
"debug": false,
"devel": false,
"eqeqeq": true,
"evil": true,
"forin": false,
"immed": false,
"laxbreak": false,
"newcap": true,
"noarg": true,
"noempty": false,
"nonew": false,
"nomen": false,
"onevar": false,
"plusplus": false,
"regexp": false,
"undef": true,
"sub": true,
"strict": false,
"white": false,
"eqnull": true,
"esnext": true
}

View File

@ -1,11 +0,0 @@
import Resolver from 'ember/resolver';
import config from '../../config/environment';
var resolver = Resolver.create();
resolver.namespace = {
modulePrefix: config.modulePrefix,
podModulePrefix: config.podModulePrefix
};
export default resolver;

View File

@ -1,19 +0,0 @@
import Ember from 'ember';
import Application from '../../app';
import Router from '../../router';
import config from '../../config/environment';
export default function startApp(attrs) {
var application;
var attributes = Ember.merge({}, config.APP);
attributes = Ember.merge(attributes, attrs); // use defaults, but you can override;
Ember.run(function() {
application = Application.create(attributes);
application.setupForTesting();
application.injectTestHelpers();
});
return application;
}

View File

@ -1,33 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Flarum Tests</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
{{content-for 'head'}}
{{content-for 'test-head'}}
<link rel="stylesheet" href="assets/vendor.css">
<link rel="stylesheet" href="assets/flarum-admin.css">
<link rel="stylesheet" href="assets/test-support.css">
{{content-for 'head-footer'}}
{{content-for 'test-head-footer'}}
</head>
<body>
{{content-for 'body'}}
{{content-for 'test-body'}}
<script src="assets/vendor.js"></script>
<script src="assets/test-support.js"></script>
<script src="assets/flarum-admin.js"></script>
<script src="testem.js"></script>
<script src="assets/test-loader.js"></script>
{{content-for 'body-footer'}}
{{content-for 'test-body-footer'}}
</body>
</html>

View File

@ -1,48 +0,0 @@
import Ember from "ember";
import { test } from 'ember-qunit';
import startApp from '../helpers/start-app';
var App;
module('Index', {
setup: function() {
App = startApp();
},
teardown: function() {
Ember.run(App, App.destroy);
}
});
test('Discussion list loading', function() {
expect(3);
visit('/').then(function() {
equal(find('.discussions-list').length, 1, 'Page contains list of discussions');
equal(find('.discussions-list li').length, 20, 'There are 20 discussions in the list');
click('.control-loadMore').then(function() {
equal(find('.discussions-list li').length, 40, 'There are 40 discussions in the list');
});
});
});
test('Discussion list sorting', function() {
expect(1);
visit('/').then(function() {
fillIn('.control-sort select', 'replies').then(function() {
var discussions = find('.discussions-list li');
var good = true;
var getCount = function(item) {
return parseInt(item.find('.count strong').text());
};
var previousCount = getCount(discussions.eq(0));
for (var i = 1; i < discussions.length; i++) {
var count = getCount(discussions.eq(i));
if (count > previousCount) {
good = false;
break;
}
previousCount = count;
}
ok(good, 'Discussions are listed in order of reply count');
});
});
});

View File

@ -1,6 +0,0 @@
import resolver from './helpers/resolver';
import {
setResolver
} from 'ember-qunit';
setResolver(resolver);

View File

@ -1,4 +0,0 @@
{
"directory": "bower_components",
"analytics": false
}

View File

@ -1,33 +0,0 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
indent_style = space
indent_size = 2
[*.js]
indent_style = space
indent_size = 2
[*.hbs]
indent_style = space
indent_size = 2
[*.css]
indent_style = space
indent_size = 2
[*.html]
indent_style = space
indent_size = 2
[*.{diff,md}]
trim_trailing_whitespace = false

View File

@ -1,17 +0,0 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/tmp
# dependencies
/node_modules
/bower_components
# misc
/.sass-cache
/connect.lock
/coverage/*
/libpeerconnection.log
npm-debug.log
testem.log

View File

@ -1,32 +0,0 @@
{
"predef": [
"document",
"window",
"-Promise"
],
"browser": true,
"boss": true,
"curly": true,
"debug": false,
"devel": true,
"eqeqeq": true,
"evil": true,
"forin": false,
"immed": false,
"laxbreak": false,
"newcap": true,
"noarg": true,
"noempty": false,
"nonew": false,
"nomen": false,
"onevar": false,
"plusplus": false,
"regexp": false,
"undef": true,
"sub": true,
"strict": false,
"white": false,
"eqnull": true,
"esnext": true,
"unused": true
}

View File

@ -1,12 +0,0 @@
bower_components/
tests/
.bowerrc
.editorconfig
.ember-cli
.travis.yml
.npmignore
**/.gitkeep
bower.json
Brocfile.js
testem.json

View File

@ -1,20 +0,0 @@
---
language: node_js
sudo: false
cache:
directories:
- node_modules
before_install:
- "npm config set spin false"
- "npm install -g npm@^2"
install:
- npm install -g bower
- npm install
- bower install
script:
- npm test

View File

@ -1,21 +0,0 @@
/* jshint node: true */
/* global require, module */
var EmberAddon = require('ember-cli/lib/broccoli/ember-addon');
var app = new EmberAddon();
// Use `app.import` to add additional libraries to the generated
// output files.
//
// If you need to use different assets in different
// environments, specify an object as the first parameter. That
// object's keys should be the environment name and the values
// should be the asset to use in that environment.
//
// If the library that you are including contains AMD or ES6
// modules that you would like to import into your application
// please specify an object with the list of modules as keys
// along with the exports of each module as its value.
module.exports = app.toTree();

View File

@ -1,50 +0,0 @@
import DS from 'ember-data';
import JsonApiAdapter from 'ember-json-api/json-api-adapter';
import config from '../config/environment';
import AlertMessage from '../components/ui/alert-message';
export default JsonApiAdapter.extend({
host: config.apiURL,
pathForType: function(type) {
if (type == 'activity') {
return type;
}
return this._super(type);
},
ajaxError: function(jqXHR) {
var errors = this._super(jqXHR);
// Reparse the errors in accordance with the JSON-API spec to fit with
// Ember Data style. Hopefully something like this will eventually be a
// part of the JsonApiAdapter.
if (errors instanceof DS.InvalidError) {
var newErrors = {};
for (var i in errors.errors) {
var error = errors.errors[i];
newErrors[error.path] = error.detail;
}
return new DS.InvalidError(newErrors);
}
// If it's a server error, show an alert message. The alerts controller
// has been injected into this adapter.
if (errors instanceof JsonApiAdapter.ServerError) {
var message;
if (errors.status === 401) {
message = 'You don\'t have permission to do this.';
} else {
message = errors.message;
}
var alert = AlertMessage.extend({
type: 'warning',
message: message
});
this.get('alerts').send('alert', alert);
}
return errors;
}
});

View File

@ -1,28 +0,0 @@
import Base from 'simple-auth/authenticators/base';
import config from '../config/environment';
export default Base.extend({
authenticate: function(credentials) {
var container = this.container;
return new Ember.RSVP.Promise(function(resolve, reject) {
Ember.$.ajax({
url: config.baseURL+'login',
type: 'POST',
data: { identification: credentials.identification, password: credentials.password }
}).then(function(response) {
container.lookup('store:main').find('user', response.userId).then(function(user) {
resolve({ token: response.token, userId: response.userId, user: user });
});
}, function(xhr, status, error) {
reject(xhr.responseJSON.errors);
});
});
},
invalidate: function(data) {
return new Ember.RSVP.Promise(function() {
window.location = config.baseURL+'logout';
});
}
});

View File

@ -1,10 +0,0 @@
import Base from 'simple-auth/authorizers/base';
export default Base.extend({
authorize: function(jqXHR, requestOptions) {
var token = this.get('session.token');
if (this.get('session.isAuthenticated') && !Ember.isEmpty(token)) {
jqXHR.setRequestHeader('Authorization', 'Token ' + token);
}
}
});

View File

@ -1,43 +0,0 @@
import Ember from 'ember';
/**
The back/pin button group in the top-left corner of Flarum's interface.
*/
export default Ember.Component.extend({
classNames: ['back-button'],
classNameBindings: ['active', 'className'],
active: Ember.computed.or('target.paneIsShowing', 'target.paneIsPinned'),
mouseEnter: function() {
var target = this.get('target');
if (target) {
target.send('showPane');
}
},
mouseLeave: function() {
var target = this.get('target');
if (target) {
target.send('hidePane');
}
},
actions: {
// WE HAVE TO GO BACK. WAAAAAALLLLLLTTTTT
back: function() {
this.sendAction('goBack');
},
togglePinned: function() {
var target = this.get('target');
if (target) {
target.send('togglePinned');
}
},
toggleDrawer: function() {
this.sendAction('toggleDrawer');
}
}
});

View File

@ -1,29 +0,0 @@
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 class="label">{{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.call(this);
}
}
});

View File

@ -1,53 +0,0 @@
import Ember from 'ember';
import HasItemLists from '../../mixins/has-item-lists';
import ActionButton from './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.extend({
label: button.label,
action: function() {
component.send('dismiss');
button.action();
}
}));
});
if (this.get('dismissable')) {
var dismiss = ActionButton.extend({
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,11 +0,0 @@
import ActionButton from './action-button';
export default ActionButton.extend({
tagName: 'span',
classNames: ['badge'],
title: Ember.computed.alias('label'),
didInsertElement: function() {
this.$().tooltip();
}
});

View File

@ -1,31 +0,0 @@
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() {
var count = this.get('items.length');
return count ? 'item-count-'+this.get('items.length') : '';
}),
actions: {
buttonClick: function() {
this.sendAction('buttonClick');
}
}
});

View File

@ -1,32 +0,0 @@
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', 'className'],
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

@ -1,22 +0,0 @@
import Ember from 'ember';
import DropdownButton from './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

@ -1,13 +0,0 @@
import Ember from 'ember';
/**
A set of fields with a heading.
*/
export default Ember.Component.extend({
layoutName: 'components/ui/field-set',
tagName: 'fieldset',
classNameBindings: ['className'],
label: '',
fields: []
});

View File

@ -1,24 +0,0 @@
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 [];
}
var instances = [];
items.forEach(function(item) {
item = item.create();
item.set('isListItem', item.constructor.proto().tagName === 'li');
instances.pushObject(item);
});
return instances;
})
});

View File

@ -1,19 +0,0 @@
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

@ -1,22 +0,0 @@
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

@ -1,36 +0,0 @@
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

@ -1,16 +0,0 @@
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

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

View File

@ -1,19 +0,0 @@
import Ember from 'ember';
/**
A toggle switch.
*/
export default Ember.Component.extend({
layoutName: 'components/ui/switch-input',
classNames: ['checkbox', 'checkbox-switch'],
label: '',
toggleState: true,
didInsertElement: function() {
var component = this;
this.$('input').on('change', function() {
component.get('changed')($(this).prop('checked'), component);
});
}
});

View File

@ -1,33 +0,0 @@
import Ember from 'ember';
import HasItemLists from '../../mixins/has-item-lists';
import ActionButton from './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'), 'check').reopen({className: 'btn btn-primary', listItemClass: 'primary-control'});
},
actions: {
submit: function() {
this.sendAction('submit', this.get('value'));
}
}
});

View File

@ -1,26 +0,0 @@
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('');
}
});
}
}
});

View File

@ -1,20 +0,0 @@
import Ember from 'ember';
/**
A toggle switch.
*/
export default Ember.Component.extend({
layoutName: 'components/ui/yesno-input',
tagName: 'label',
classNames: ['yesno-control'],
toggleState: true,
disabled: false,
didInsertElement: function() {
var component = this;
this.$('input').on('change', function() {
component.get('changed')($(this).prop('checked'), component);
});
}
});

View File

@ -1,17 +0,0 @@
import Ember from 'ember';
export default Ember.Controller.extend({
alerts: [],
actions: {
alert: function(message) {
this.get('alerts').pushObject(message);
},
dismissAlert: function(message) {
this.get('alerts').removeObject(message.constructor);
},
clearAlerts: function() {
this.get('alerts').clear();
}
}
});

View File

@ -1,6 +0,0 @@
import Ember from 'ember';
export default Ember.Handlebars.makeBoundHelper(function(number) {
return new Ember.Handlebars.SafeString(''+number);
});

View File

@ -1,6 +0,0 @@
import Ember from 'ember';
export default Ember.Handlebars.makeBoundHelper(function(icon, options) {
return new Ember.Handlebars.SafeString('<i class="fa fa-fw fa-'+icon+' '+(options.hash.class || '')+'"></i>');
});

View File

@ -1,9 +0,0 @@
import Ember from 'ember';
export default Ember.Handlebars.makeBoundHelper(function(time) {
var m = moment(time);
var datetime = m.format();
var full = m.format('LLLL');
return new Ember.Handlebars.SafeString('<time pubdate datetime="'+datetime+'">'+full+'</time>');
});

View File

@ -1,17 +0,0 @@
import Ember from 'ember';
export default Ember.Handlebars.makeBoundHelper(function(text, phrase) {
if (phrase) {
var words = phrase.split(' ');
var replacement = function(matched) {
return '<span class="highlight-keyword">'+matched+'</span>';
};
words.forEach(function(word) {
text = text.replace(
new RegExp("\\b"+word+"\\b", 'gi'),
replacement
);
});
}
return new Ember.Handlebars.SafeString(text);
});

View File

@ -1,13 +0,0 @@
import Ember from 'ember';
import humanTime from '../utils/human-time';
export default Ember.Handlebars.makeBoundHelper(function(time) {
var m = moment(time);
var datetime = m.format();
var full = m.format('LLLL');
var ago = humanTime(m);
return new Ember.Handlebars.SafeString('<time pubdate datetime="'+datetime+'" title="'+full+'" data-humantime>'+ago+'</time>');
});

View File

@ -1,26 +0,0 @@
import Ember from 'ember';
export default Ember.Handlebars.makeBoundHelper(function(user, options) {
var attributes = 'class="avatar '+(options.hash.class || '')+'"';
var content = '';
if (user) {
var username = user.get('username') || '?';
if (typeof options.hash.title === 'undefined') {
options.hash.title = Ember.Handlebars.Utils.escapeExpression(username);
}
attributes += ' title="'+options.hash.title+'"';
var avatarUrl = user.get('avatarUrl');
if (avatarUrl) {
return new Ember.Handlebars.SafeString('<img src="'+avatarUrl+'" '+attributes+'>');
}
content = username.charAt(0).toUpperCase();
attributes += ' style="background:'+user.get('color')+'"';
}
return new Ember.Handlebars.SafeString('<span '+attributes+'>'+content+'</span>');
}, 'avatarUrl', 'username', 'color');

View File

@ -1,12 +0,0 @@
import Ember from 'ember';
export default Ember.Handlebars.makeBoundHelper(function(user, options) {
var username;
if (user) {
username = user.get('username');
}
username = username || '[deleted]';
return new Ember.Handlebars.SafeString('<span class="username">'+Ember.Handlebars.Utils.escapeExpression(username)+'</span>');
});

View File

@ -1,11 +0,0 @@
import FlarumAuthorizer from '../authorizers/flarum';
import Config from '../config/environment';
export default {
name: 'authentication',
before: 'simple-auth',
initialize: function(container) {
container.register('authorizer:flarum', FlarumAuthorizer);
Config['simple-auth'] = {authorizer: 'authorizer:flarum'};
}
};

View File

@ -1,25 +0,0 @@
import DS from 'ember-data';
// This can be removed when
// https://github.com/emberjs/data/pull/2584 is implemented.
export default {
name: 'find-query-one',
initialize: function(container) {
DS.Store.reopen({
findQueryOne: function(type, id, query) {
var store = this;
var typeClass = store.modelFor(type);
var adapter = store.adapterFor(typeClass);
var serializer = store.serializerFor(typeClass);
var url = adapter.buildURL(type, id);
var ajaxPromise = adapter.ajax(url, 'GET', { data: query });
return ajaxPromise.then(function(rawPayload) {
var extractedPayload = serializer.extract(store, typeClass, rawPayload, id, 'find');
return store.push(typeClass, extractedPayload);
});
}
});
}
};

View File

@ -1,147 +0,0 @@
import Ember from 'ember';
import humanTime from '../utils/human-time';
var $ = Ember.$;
export default {
name: 'human-time-updater',
initialize: function(container) {
// Livestamp.js / v1.1.2 / (c) 2012 Matt Bradley / MIT License
// @todo rewrite this to be simpler and cleaner
(function($, moment) {
var updateInterval = 1e3,
paused = false,
$livestamps = $([]),
init = function() {
livestampGlobal.resume();
},
prep = function($el, timestamp) {
var oldData = $el.data('livestampdata');
if (typeof timestamp == 'number')
timestamp *= 1e3;
$el.removeAttr('data-livestamp')
.removeData('livestamp');
timestamp = moment(timestamp);
if (timestamp.diff(moment(new Date())) < 60 * 60) {
return;
}
if (moment.isMoment(timestamp) && !isNaN(+timestamp)) {
var newData = $.extend({ }, { 'original': $el.contents() }, oldData);
newData.moment = moment(timestamp);
$el.data('livestampdata', newData).empty();
$livestamps.push($el[0]);
}
},
run = function() {
if (paused) return;
livestampGlobal.update();
setTimeout(run, updateInterval);
},
livestampGlobal = {
update: function() {
$('[data-humantime]').each(function() {
var $this = $(this);
prep($this, $this.attr('datetime'));
});
var toRemove = [];
$livestamps.each(function() {
var $this = $(this),
data = $this.data('livestampdata');
if (data === undefined)
toRemove.push(this);
else if (moment.isMoment(data.moment)) {
var from = $this.html(),
to = humanTime(data.moment);
// to = data.moment.fromNow();
if (from != to) {
var e = $.Event('change.livestamp');
$this.trigger(e, [from, to]);
if (!e.isDefaultPrevented())
$this.html(to);
}
}
});
$livestamps = $livestamps.not(toRemove);
},
pause: function() {
paused = true;
},
resume: function() {
paused = false;
run();
},
interval: function(interval) {
if (interval === undefined)
return updateInterval;
updateInterval = interval;
}
},
livestampLocal = {
add: function($el, timestamp) {
if (typeof timestamp == 'number')
timestamp *= 1e3;
timestamp = moment(timestamp);
if (moment.isMoment(timestamp) && !isNaN(+timestamp)) {
$el.each(function() {
prep($(this), timestamp);
});
livestampGlobal.update();
}
return $el;
},
destroy: function($el) {
$livestamps = $livestamps.not($el);
$el.each(function() {
var $this = $(this),
data = $this.data('livestampdata');
if (data === undefined)
return $el;
$this
.html(data.original ? data.original : '')
.removeData('livestampdata');
});
return $el;
},
isLivestamp: function($el) {
return $el.data('livestampdata') !== undefined;
}
};
$.livestamp = livestampGlobal;
$(init);
$.fn.livestamp = function(method, options) {
if (!livestampLocal[method]) {
options = method;
method = 'add';
}
return livestampLocal[method](this, options);
};
})(jQuery, moment);
}
};

View File

@ -1,10 +0,0 @@
export default {
name: 'inject-components',
initialize: function(container, application) {
application.inject('adapter', 'alerts', 'controller:alerts')
application.inject('component', 'alerts', 'controller:alerts')
application.inject('model', 'session', 'simple-auth-session:main')
application.inject('component', 'session', 'simple-auth-session:main')
application.inject('component', 'store', 'store:main')
}
};

View File

@ -1,20 +0,0 @@
import Ember from 'ember';
export default {
name: 'preload-data',
after: 'ember-data',
initialize: function(container) {
var store = container.lookup('store:main');
if (!Ember.isEmpty(FLARUM_DATA)) {
store.pushPayload({included: FLARUM_DATA});
}
if (!Ember.isEmpty(FLARUM_SESSION)) {
FLARUM_SESSION.user = store.getById('user', FLARUM_SESSION.userId);
container.lookup('simple-auth-session:main').setProperties({
isAuthenticated: true,
authenticator: 'authenticator:flarum',
content: FLARUM_SESSION
});
}
}
};

View File

@ -1,16 +0,0 @@
import Ember from 'ember';
export default Ember.Mixin.create({
activate: function() {
var cssClass = this.toCssClass();
Ember.$('body').addClass(cssClass);
},
deactivate: function() {
Ember.$('body').removeClass(this.toCssClass());
},
toCssClass: function() {
return this.routeName.replace(/\./g, '-').dasherize();
}
});

View File

@ -1,14 +0,0 @@
import Ember from 'ember';
export default Ember.Mixin.create({
fadeIn: Ember.on('didInsertElement', function() {
var $this = this.$();
var targetOpacity = $this.css('opacity');
$this.css('opacity', 0);
setTimeout(function() {
$this.animate({opacity: targetOpacity}, 'fast', function() {
$this.css('opacity', '');
});
}, 100);
})
});

View File

@ -1,62 +0,0 @@
import Ember from 'ember';
import TaggedArray from '../utils/tagged-array';
import ActionButton from '../components/ui/action-button';
import SeparatorItem from '../components/ui/separator-item';
export default Ember.Mixin.create({
itemLists: [],
initItemLists: Ember.on('init', function() {
var self = this;
this.get('itemLists').forEach(function(name) {
self.initItemList(name);
});
}),
initItemList: function(name) {
this.set(name, this.populateItemList(name));
},
populateItemList: function(name) {
var items = TaggedArray.create();
this.trigger('populate'+name.charAt(0).toUpperCase()+name.slice(1), items);
this.removeUnneededSeparatorItems(items);
return items;
},
addActionItem: function(items, tag, label, icon, conditionProperty, action) {
if (conditionProperty && !this.get(conditionProperty)) { return; }
var self = this;
var item = ActionButton.extend({
label: label,
icon: icon,
action: action || function() {
self.get('controller').send(tag);
}
});
items.pushObjectWithTag(item, tag);
return item;
},
addSeparatorItem: function(items) {
items.pushObject(SeparatorItem);
},
removeUnneededSeparatorItems: function(items) {
var prevItem = null;
items.forEach(function(item) {
if (prevItem === SeparatorItem && item === SeparatorItem) {
items.removeObject(item);
return;
}
prevItem = item;
});
if (prevItem === SeparatorItem) {
items.removeObject(prevItem);
}
}
});

View File

@ -1,9 +0,0 @@
import Ember from 'ember';
export default Ember.Mixin.create(Ember.Evented, {
actions: {
focus: function() {
this.trigger('focus');
}
}
});

View File

@ -1,15 +0,0 @@
import Ember from 'ember';
export default Ember.Mixin.create({
focusEventOn: Ember.on('didInsertElement', function() {
this.get('controller').on('focus', this, this.focus);
}),
focusEventOff: Ember.on('willDestroyElement', function() {
this.get('controller').off('focus', this, this.focus);
}),
focus: Ember.on('didInsertElement', function() {
this.$('input:first:visible:enabled').focus();
})
});

View File

@ -1,11 +0,0 @@
import DS from 'ember-data';
export default DS.Model.extend({
contentType: DS.attr('string'),
content: DS.attr(),
time: DS.attr('date'),
user: DS.belongsTo('user'),
sender: DS.belongsTo('user'),
post: DS.belongsTo('post')
});

View File

@ -1,7 +0,0 @@
import Ember from 'ember';
export default Ember.ObjectProxy.extend({
relevantPosts: null,
startPost: null,
lastPost: null
});

View File

@ -1,73 +0,0 @@
import Ember from 'ember';
import DS from 'ember-data';
import HasItemLists from '../mixins/has-item-lists';
import Subject from './subject';
export default Subject.extend(HasItemLists, {
/**
Define a "badges" item list. Example usage:
```
populateBadges: function(items) {
items.pushObjectWithTag(BadgeButton.extend({
label: 'Sticky',
icon: 'thumb-tack',
className: 'badge-sticky',
discussion: this,
isHiddenInList: Ember.computed.not('discussion.sticky')
}), 'sticky');
}
```
*/
itemLists: ['badges'],
title: DS.attr('string'),
slug: Ember.computed('title', function() {
return this.get('title').toLowerCase().replace(/[^a-z0-9]/gi, '-').replace(/-+/g, '-').replace(/-$|^-/g, '');
}),
startTime: DS.attr('date'),
startUser: DS.belongsTo('user'),
startPost: DS.belongsTo('post'),
lastTime: DS.attr('date'),
lastUser: DS.belongsTo('user'),
lastPost: DS.belongsTo('post'),
lastPostNumber: DS.attr('number'),
canReply: DS.attr('boolean'),
canEdit: DS.attr('boolean'),
canDelete: DS.attr('boolean'),
commentsCount: DS.attr('number'),
repliesCount: Ember.computed('commentsCount', function() {
return Math.max(0, this.get('commentsCount') - 1);
}),
// The API returns the `posts` relationship as a list of IDs. To hydrate a
// post-stream object, we're only interested in obtaining a list of IDs, so
// we make it a string and then split it by comma. Instead, we'll put a
// relationship on `loadedPosts`.
// posts: DS.attr('string'),
posts: DS.hasMany('post', {async: true}),
postIds: Ember.computed(function() {
var ids = [];
this.get('data.posts').forEach(function(post) {
ids.push(post.id);
});
return ids;
}),
loadedPosts: DS.hasMany('post'),
relevantPosts: DS.hasMany('post'),
addedPosts: DS.hasMany('post'),
readTime: DS.attr('date'),
readNumber: DS.attr('number'),
unreadCount: Ember.computed('lastPostNumber', 'readNumber', 'session.user.readTime', function() {
return this.get('session.user.readTime') < this.get('lastTime') ? Math.max(0, this.get('lastPostNumber') - (this.get('readNumber') || 0)) : 0;
}),
isUnread: Ember.computed.bool('unreadCount'),
// Only used to save a new discussion
content: DS.attr('string')
});

View File

@ -1,6 +0,0 @@
import DS from 'ember-data';
export default DS.Model.extend({
name: DS.attr('string'),
users: DS.hasMany('group'),
});

View File

@ -1,21 +0,0 @@
import DS from 'ember-data';
export default DS.Model.extend({
contentType: DS.attr('string'),
subjectId: DS.attr('number'),
content: DS.attr(),
time: DS.attr('date'),
isRead: DS.attr('boolean'),
unreadCount: DS.attr('number'),
additionalUnreadCount: Ember.computed('unreadCount', function() {
return Math.max(0, this.get('unreadCount') - 1);
}),
decodedContent: Ember.computed('content', function() {
return JSON.parse(this.get('content'));
}),
user: DS.belongsTo('user'),
sender: DS.belongsTo('user'),
subject: DS.belongsTo('subject', {polymorphic: true})
});

View File

@ -1,22 +0,0 @@
import Ember from 'ember';
var PostResult = Ember.ObjectProxy.extend({
relevantContent: ''
});
PostResult.reopenClass({
create: function(post) {
if (!post) {
return null;
}
var result = this._super();
result.set('content', post);
result.set('relevantContent', post.get('content'));
return result;
}
});
export default PostResult;

View File

@ -1,209 +0,0 @@
import Ember from 'ember';
/**
The post stream is an object which represents the posts in a discussion as
they are displayed on the discussion page, from top to bottom. ...
*/
export default Ember.ArrayProxy.extend(Ember.Evented, {
// An array of all of the post IDs, in chronological order, in the discussion.
ids: null,
content: null,
store: null,
discussion: null,
postLoadCount: 20,
count: Ember.computed.alias('ids.length'),
loadedCount: Ember.computed('content.@each', function() {
return this.get('content').filterBy('content').length;
}),
firstLoaded: Ember.computed('content.@each', function() {
var first = this.objectAt(0);
return first && first.content;
}),
lastLoaded: Ember.computed('content.@each', function() {
var last = this.objectAt(this.get('length') - 1);
return last && last.content;
}),
init: function() {
this._super();
this.set('ids', Ember.A());
this.clear();
},
setup: function(ids) {
// Set our ids to the array provided and reset the content of the
// stream to a big gap that covers the amount of posts we now have.
this.set('ids', ids);
this.clear();
},
// Clear the contents of the post stream, resetting it to one big gap.
clear: function() {
var content = Ember.A();
content.clear().pushObject(this.makeItem(0, this.get('count') - 1).set('loading', true));
this.set('content', content);
},
loadRange: function(start, end, backwards) {
var limit = this.get('postLoadCount');
// Find the appropriate gap objects in the post stream. When we find
// one, we will turn on its loading flag.
this.get('content').forEach(function(item) {
if (!item.content && ((item.indexStart >= start && item.indexStart <= end) || (item.indexEnd >= start && item.indexEnd <= end))) {
item.set('loading', true);
item.set('direction', backwards ? 'up' : 'down');
}
});
// Get a list of post numbers that we'll want to retrieve. If there are
// more post IDs than the number of posts we want to load, then take a
// slice of the array in the appropriate direction.
var ids = this.get('ids').slice(start, end + 1);
ids = backwards ? ids.slice(-limit) : ids.slice(0, limit);
return this.loadPosts(ids);
},
loadPosts: function(ids) {
if (!ids.length) {
return Ember.RSVP.resolve();
}
var stream = this;
return this.store.find('post', {ids: ids}).then(function(posts) {
stream.addPosts(posts);
});
},
loadNearNumber: function(number) {
// Find the item in the post stream which is nearest to this number. If
// it turns out the be the actual post we're trying to load, then we can
// return a resolved promise (i.e. we don't need to make an API
// request.) Or, if it's a gap, we'll switch on its loading flag.
var item = this.findNearestToNumber(number);
if (item) {
if (item.get('content.number') == number) {
return Ember.RSVP.resolve([item.get('content')]);
} else if (! item.content) {
item.set('direction', 'down').set('loading', true);
}
}
var stream = this;
return this.store.find('post', {
discussions: this.get('discussion.id'),
near: number,
count: this.get('postLoadCount')
}).then(function(posts) {
stream.addPosts(posts);
});
},
loadNearIndex: function(index, backwards) {
// Find the item in the post stream which is nearest to this index. If
// it turns out the be the actual post we're trying to load, then we can
// return a resolved promise (i.e. we don't need to make an API
// request.) Or, if it's a gap, we'll switch on its loading flag.
var item = this.findNearestToIndex(index);
if (item) {
if (item.content) {
return Ember.RSVP.resolve([item.get('content')]);
}
return this.loadRange(Math.max(item.indexStart, index - this.get('postLoadCount') / 2), item.indexEnd, backwards);
}
return Ember.RSVP.reject();
},
addPosts: function(posts) {
this.trigger('postsLoaded', posts);
var stream = this;
var content = this.get('content');
content.beginPropertyChanges();
posts.forEach(function(post) {
stream.addPost(post);
});
content.endPropertyChanges();
this.trigger('postsAdded');
},
addPost: function(post) {
var index = this.get('ids').indexOf(post.get('id'));
var content = this.get('content');
var makeItem = this.makeItem;
// Here we loop through each item in the post stream, and find the gap
// in which this post should be situated. When we find it, we can replace
// it with the post, and new gaps either side if appropriate.
content.some(function(item, i) {
if (item.indexStart <= index && item.indexEnd >= index) {
var newItems = [];
if (item.indexStart < index) {
newItems.push(makeItem(item.indexStart, index - 1));
}
newItems.push(makeItem(index, index, post));
if (item.indexEnd > index) {
newItems.push(makeItem(index + 1, item.indexEnd));
}
content.replace(i, 1, newItems);
return true;
}
});
},
addPostToEnd: function(post) {
this.get('ids').pushObject(post.get('id'));
var index = this.get('count') - 1;
this.get('content').pushObject(this.makeItem(index, index, post));
},
removePost: function(post) {
this.get('ids').removeObject(post.get('id'));
var content = this.get('content');
content.removeObject(content.findBy('content', post));
},
makeItem: function(indexStart, indexEnd, post) {
var item = Ember.Object.create({
indexStart: indexStart,
indexEnd: indexEnd
});
if (post) {
item.setProperties({
content: post,
component: 'discussion/post-'+post.get('contentType')
});
}
return item;
},
findNearestTo: function(index, property) {
var nearestItem;
this.get('content').some(function(item) {
if (item.get(property) > index) {
return true;
}
nearestItem = item;
});
return nearestItem;
},
findNearestToNumber: function(number) {
return this.findNearestTo(number, 'content.number');
},
findNearestToIndex: function(index) {
return this.findNearestTo(index, 'indexStart');
}
});

View File

@ -1,25 +0,0 @@
import Ember from 'ember';
import DS from 'ember-data';
import Subject from './subject';
export default Subject.extend({
discussion: DS.belongsTo('discussion', {inverse: 'loadedPosts'}),
number: DS.attr('number'),
time: DS.attr('date'),
user: DS.belongsTo('user'),
contentType: DS.attr('string'),
content: DS.attr(),
contentHtml: DS.attr('string'),
editTime: DS.attr('date'),
editUser: DS.belongsTo('user'),
isEdited: Ember.computed.notEmpty('editTime'),
hideTime: DS.attr('date'),
hideUser: DS.belongsTo('user'),
isHidden: DS.attr('boolean'),
canEdit: DS.attr('boolean'),
canDelete: DS.attr('boolean')
});

View File

@ -1,5 +0,0 @@
import DS from 'ember-data';
export default DS.Model.extend({
notification: DS.belongsTo('notification')
});

View File

@ -1,36 +0,0 @@
import DS from 'ember-data';
import HasItemLists from '../mixins/has-item-lists';
import stringToColor from '../utils/string-to-color';
export default DS.Model.extend(HasItemLists, {
itemLists: ['badges'],
username: DS.attr('string'),
email: DS.attr('string'),
password: DS.attr('string'),
avatarUrl: DS.attr('string'),
bio: DS.attr('string'),
bioHtml: DS.attr('string'),
preferences: DS.attr(),
groups: DS.hasMany('group'),
joinTime: DS.attr('date'),
lastSeenTime: DS.attr('date'),
online: Ember.computed('lastSeenTime', function() {
return this.get('lastSeenTime') > moment().subtract(5, 'minutes').toDate();
}),
readTime: DS.attr('date'),
unreadNotificationsCount: DS.attr('number'),
discussionsCount: DS.attr('number'),
commentsCount: DS.attr('number'),
canEdit: DS.attr('boolean'),
canDelete: DS.attr('boolean'),
color: Ember.computed('username', function() {
return '#'+stringToColor(this.get('username'));
})
});

View File

@ -1,5 +0,0 @@
import JsonApiSerializer from 'ember-json-api/json-api-serializer';
export default JsonApiSerializer.extend({
store: Ember.inject.service()
});

View File

@ -1,16 +0,0 @@
import ApplicationSerializer from '../serializers/application';
export default ApplicationSerializer.extend({
attrs: {
number: {serialize: false},
time: {serialize: false},
type: {serialize: false},
contentHtml: {serialize: false},
editTime: {serialize: false},
editUser: {serialize: false},
hideTime: {serialize: false},
hideUser: {serialize: false},
canEdit: {serialize: false},
canDelete: {serialize: false}
}
});

View File

@ -1,7 +0,0 @@
<div class="alerts">
{{#each alert in alerts}}
<div class="alert-wrapper">
{{view alert dismiss="dismissAlert"}}
</div>
{{/each}}
</div>

Some files were not shown because too many files have changed in this diff Show More