Quick-Search: Added Quick-search in the topbar
Changed how and what we store in elastic to unify it with how we store things in mongodb so we can have more generic javascript code to render the data. Elastic changes: Added: Node.project.url Altered to store id instead of url Node.picture Made Post searchable ./manage.py elastic reset_index ./manage.py elastic reindex Thanks to Pablo and Sybren
This commit is contained in:
58
src/scripts/js/es6/common/quicksearch/MultiSearch.js
Normal file
58
src/scripts/js/es6/common/quicksearch/MultiSearch.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import {SearchParams} from './SearchParams';
|
||||
|
||||
export class MultiSearch {
|
||||
constructor(kwargs) {
|
||||
this.uiUrl = kwargs['uiUrl']; // Url for advanced search
|
||||
this.apiUrl = kwargs['apiUrl']; // Url for api calls
|
||||
this.searchParams = MultiSearch.createMultiSearchParams(kwargs['searchParams']);
|
||||
this.q = '';
|
||||
}
|
||||
|
||||
setSearchWord(q) {
|
||||
this.q = q;
|
||||
this.searchParams.forEach((qsParam) => {
|
||||
qsParam.setSearchWord(q);
|
||||
});
|
||||
}
|
||||
|
||||
getSearchUrl() {
|
||||
return this.uiUrl + '?q=' + this.q;
|
||||
}
|
||||
|
||||
getAllParams() {
|
||||
let retval = $.map(this.searchParams, (msParams) => {
|
||||
return msParams.params;
|
||||
});
|
||||
return retval;
|
||||
}
|
||||
|
||||
parseResult(rawResult) {
|
||||
return $.map(rawResult, (subResult, index) => {
|
||||
let name = this.searchParams[index].name;
|
||||
let pStr = this.searchParams[index].getParamStr();
|
||||
let result = $.map(subResult.hits.hits, (hit) => {
|
||||
return hit._source;
|
||||
});
|
||||
return {
|
||||
name: name,
|
||||
url: this.uiUrl + '?' + pStr,
|
||||
result: result,
|
||||
hasResults: !!result.length
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
thenExecute() {
|
||||
let data = JSON.stringify(this.getAllParams());
|
||||
let rawAjax = $.getJSON(this.apiUrl, data);
|
||||
let prettyPromise = rawAjax.then(this.parseResult.bind(this));
|
||||
prettyPromise['abort'] = rawAjax.abort.bind(rawAjax); // Hack to be able to abort the promise down the road
|
||||
return prettyPromise;
|
||||
}
|
||||
|
||||
static createMultiSearchParams(argsList) {
|
||||
return $.map(argsList, (args) => {
|
||||
return new SearchParams(args);
|
||||
});
|
||||
}
|
||||
}
|
204
src/scripts/js/es6/common/quicksearch/QuickSearch.js
Normal file
204
src/scripts/js/es6/common/quicksearch/QuickSearch.js
Normal file
@@ -0,0 +1,204 @@
|
||||
import { create$noHits, create$results, create$input } from './templates'
|
||||
import {SearchFacade} from './SearchFacade';
|
||||
/**
|
||||
* QuickSearch : Interacts with the dom document
|
||||
* 1-SearchFacade : Controls which multisearch is active
|
||||
* *-MultiSearch : One multi search is typically Project or Cloud
|
||||
* *-SearchParams : The search params for the individual searches
|
||||
*/
|
||||
|
||||
export class QuickSearch {
|
||||
/**
|
||||
* Interacts with the dom document and deligates the input down to the SearchFacade
|
||||
* @param {selector string} searchToggle The quick-search toggle
|
||||
* @param {*} kwargs
|
||||
*/
|
||||
constructor(searchToggle, kwargs) {
|
||||
this.$body = $('body');
|
||||
this.$quickSearch = $('.quick-search');
|
||||
this.$inputComponent = $(kwargs['inputTarget']);
|
||||
this.$inputComponent.empty();
|
||||
this.$inputComponent.append(create$input(kwargs['searches']));
|
||||
this.$searchInput = this.$inputComponent.find('input');
|
||||
this.$searchSelect = this.$inputComponent.find('select');
|
||||
this.$resultTarget = $(kwargs['resultTarget']);
|
||||
this.$searchSymbol = this.$inputComponent.find('.qs-busy-symbol');
|
||||
this.searchFacade = new SearchFacade(kwargs['searches'] || {});
|
||||
this.$searchToggle = $(searchToggle);
|
||||
this.isBusy = false;
|
||||
this.attach();
|
||||
}
|
||||
|
||||
attach() {
|
||||
if (this.$searchSelect.length) {
|
||||
this.$searchSelect
|
||||
.change(this.execute.bind(this))
|
||||
.change(() => this.$searchInput.focus());
|
||||
this.$searchInput.addClass('multi-scope');
|
||||
}
|
||||
|
||||
this.$searchInput
|
||||
.keyup(this.onInputKeyUp.bind(this));
|
||||
|
||||
this.$inputComponent
|
||||
.on('pillar:workStart', () => {
|
||||
this.$searchSymbol.addClass('spinner')
|
||||
this.$searchSymbol.toggleClass('pi-spin pi-cancel')
|
||||
})
|
||||
.on('pillar:workStop', () => {
|
||||
this.$searchSymbol.removeClass('spinner')
|
||||
this.$searchSymbol.toggleClass('pi-spin pi-cancel')
|
||||
});
|
||||
|
||||
this.searchFacade.setOnResultCB(this.renderResult.bind(this));
|
||||
this.searchFacade.setOnFailureCB(this.onSearchFailed.bind(this));
|
||||
this.$searchToggle
|
||||
.one('click', this.execute.bind(this)); // Initial search executed once
|
||||
|
||||
this.registerShowGui();
|
||||
this.registerHideGui();
|
||||
}
|
||||
|
||||
registerShowGui() {
|
||||
this.$searchToggle
|
||||
.click((e) => {
|
||||
this.showGUI();
|
||||
e.stopPropagation();
|
||||
});
|
||||
}
|
||||
|
||||
registerHideGui() {
|
||||
this.$searchSymbol
|
||||
.click(() => {
|
||||
this.hideGUI();
|
||||
});
|
||||
this.$body.click((e) => {
|
||||
let $target = $(e.target);
|
||||
let isClickInResult = $target.hasClass('.qs-result') || !!$target.parents('.qs-result').length;
|
||||
let isClickInInput = $target.hasClass('.qs-input') || !!$target.parents('.qs-input').length;
|
||||
if (!isClickInResult && !isClickInInput) {
|
||||
this.hideGUI();
|
||||
}
|
||||
});
|
||||
$(document).keyup((e) => {
|
||||
if (e.key === 'Escape') {
|
||||
this.hideGUI();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
showGUI() {
|
||||
this.$body.addClass('has-overlay');
|
||||
this.$quickSearch.trigger('pillar:searchShow');
|
||||
this.$quickSearch.addClass('show');
|
||||
if (!this.$searchInput.is(':focus')) {
|
||||
this.$searchInput.focus();
|
||||
}
|
||||
}
|
||||
|
||||
hideGUI() {
|
||||
this.$body.removeClass('has-overlay');
|
||||
this.$searchToggle.addClass('pi-search');
|
||||
this.$searchInput.blur();
|
||||
this.$quickSearch.removeClass('show');
|
||||
this.$quickSearch.trigger('pillar:searchHidden');
|
||||
}
|
||||
|
||||
onInputKeyUp(e) {
|
||||
let newQ = this.$searchInput.val();
|
||||
let currQ = this.searchFacade.getSearchWord();
|
||||
this.searchFacade.setSearchWord(newQ);
|
||||
let searchUrl = this.searchFacade.getSearchUrl();
|
||||
if (e.key === 'Enter') {
|
||||
window.location.href = searchUrl;
|
||||
return;
|
||||
}
|
||||
if (newQ !== currQ) {
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
|
||||
execute() {
|
||||
this.busy(true);
|
||||
let scope = this.getScope();
|
||||
this.searchFacade.setCurrentScope(scope);
|
||||
let q = this.$searchInput.val();
|
||||
this.searchFacade.setSearchWord(q);
|
||||
this.searchFacade.execute();
|
||||
}
|
||||
|
||||
renderResult(results) {
|
||||
this.$resultTarget.empty();
|
||||
this.$resultTarget.append(this.create$result(results));
|
||||
this.busy(false);
|
||||
}
|
||||
|
||||
create$result(results) {
|
||||
let withHits = results.reduce((aggr, subResult) => {
|
||||
if (subResult.hasResults) {
|
||||
aggr.push(subResult);
|
||||
}
|
||||
return aggr;
|
||||
}, []);
|
||||
|
||||
if (!withHits.length) {
|
||||
return create$noHits(this.searchFacade.getSearchUrl());
|
||||
}
|
||||
return create$results(results, this.searchFacade.getSearchUrl());
|
||||
}
|
||||
|
||||
onSearchFailed(err) {
|
||||
toastr.error(xhrErrorResponseMessage(err), 'Unable to perform search:');
|
||||
this.busy(false);
|
||||
this.$inputComponent.trigger('pillar:failed', err);
|
||||
}
|
||||
|
||||
getScope() {
|
||||
return !!this.$searchSelect.length ? this.$searchSelect.val() : 'cloud';
|
||||
}
|
||||
|
||||
busy(val) {
|
||||
if (val !== this.isBusy) {
|
||||
var eventType = val ? 'pillar:workStart' : 'pillar:workStop';
|
||||
this.$inputComponent.trigger(eventType);
|
||||
}
|
||||
this.isBusy = val;
|
||||
}
|
||||
}
|
||||
|
||||
$.fn.extend({
|
||||
/**
|
||||
* $('#qs-toggle').quickSearch({
|
||||
* resultTarget: '#search-overlay',
|
||||
* inputTarget: '#qs-input',
|
||||
* searches: {
|
||||
* project: {
|
||||
* name: 'Project',
|
||||
* uiUrl: '{{ url_for("projects.search", project_url=project.url)}}',
|
||||
* apiUrl: '/api/newsearch/multisearch',
|
||||
* searchParams: [
|
||||
* {name: 'Assets', params: {project: '{{ project._id }}', node_type: 'asset'}},
|
||||
* {name: 'Blog', params: {project: '{{ project._id }}', node_type: 'post'}},
|
||||
* {name: 'Groups', params: {project: '{{ project._id }}', node_type: 'group'}},
|
||||
* ]
|
||||
* },
|
||||
* cloud: {
|
||||
* name: 'Cloud',
|
||||
* uiUrl: '/search',
|
||||
* apiUrl: '/api/newsearch/multisearch',
|
||||
* searchParams: [
|
||||
* {name: 'Assets', params: {node_type: 'asset'}},
|
||||
* {name: 'Blog', params: {node_type: 'post'}},
|
||||
* {name: 'Groups', params: {node_type: 'group'}},
|
||||
* ]
|
||||
* },
|
||||
* },
|
||||
* });
|
||||
* @param {*} kwargs
|
||||
*/
|
||||
quickSearch: function (kwargs) {
|
||||
$(this).each((i, qsElem) => {
|
||||
new QuickSearch(qsElem, kwargs);
|
||||
});
|
||||
}
|
||||
})
|
68
src/scripts/js/es6/common/quicksearch/SearchFacade.js
Normal file
68
src/scripts/js/es6/common/quicksearch/SearchFacade.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import {MultiSearch} from './MultiSearch';
|
||||
|
||||
export class SearchFacade {
|
||||
/**
|
||||
* One SearchFacade holds n-number of MultiSearch objects, and delegates search requests to the active mutlisearch
|
||||
* @param {*} kwargs
|
||||
*/
|
||||
constructor(kwargs) {
|
||||
this.searches = SearchFacade.createMultiSearches(kwargs);
|
||||
this.currentScope = 'cloud'; // which multisearch to use
|
||||
this.currRequest;
|
||||
this.resultCB;
|
||||
this.failureCB;
|
||||
this.q = '';
|
||||
}
|
||||
|
||||
setSearchWord(q) {
|
||||
this.q = q;
|
||||
$.each(this.searches, (k, mSearch) => {
|
||||
mSearch.setSearchWord(q);
|
||||
});
|
||||
}
|
||||
|
||||
getSearchWord() {
|
||||
return this.q;
|
||||
}
|
||||
|
||||
getSearchUrl() {
|
||||
return this.searches[this.currentScope].getSearchUrl();
|
||||
}
|
||||
|
||||
setCurrentScope(scope) {
|
||||
this.currentScope = scope;
|
||||
}
|
||||
|
||||
execute() {
|
||||
if (this.currRequest) {
|
||||
this.currRequest.abort();
|
||||
}
|
||||
this.currRequest = this.searches[this.currentScope].thenExecute();
|
||||
this.currRequest
|
||||
.then((results) => {
|
||||
this.resultCB(results);
|
||||
})
|
||||
.fail((err, reason) => {
|
||||
if (reason == 'abort') {
|
||||
return;
|
||||
}
|
||||
this.failureCB(err);
|
||||
});
|
||||
}
|
||||
|
||||
setOnResultCB(cb) {
|
||||
this.resultCB = cb;
|
||||
}
|
||||
|
||||
setOnFailureCB(cb) {
|
||||
this.failureCB = cb;
|
||||
}
|
||||
|
||||
static createMultiSearches(kwargs) {
|
||||
var searches = {};
|
||||
$.each(kwargs, (key, value) => {
|
||||
searches[key] = new MultiSearch(value);
|
||||
});
|
||||
return searches;
|
||||
}
|
||||
}
|
14
src/scripts/js/es6/common/quicksearch/SearchParams.js
Normal file
14
src/scripts/js/es6/common/quicksearch/SearchParams.js
Normal file
@@ -0,0 +1,14 @@
|
||||
export class SearchParams {
|
||||
constructor(kwargs) {
|
||||
this.name = kwargs['name'] || '';
|
||||
this.params = kwargs['params'] || {};
|
||||
}
|
||||
|
||||
setSearchWord(q) {
|
||||
this.params['q'] = q || '';
|
||||
}
|
||||
|
||||
getParamStr() {
|
||||
return jQuery.param(this.params);
|
||||
}
|
||||
}
|
1
src/scripts/js/es6/common/quicksearch/init.js
Normal file
1
src/scripts/js/es6/common/quicksearch/init.js
Normal file
@@ -0,0 +1 @@
|
||||
export { QuickSearch } from './QuickSearch';
|
93
src/scripts/js/es6/common/quicksearch/templates.js
Normal file
93
src/scripts/js/es6/common/quicksearch/templates.js
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Creates the jQuery object that is rendered when nothing is found
|
||||
* @param {String} advancedUrl Url to the advanced search with the current query
|
||||
* @returns {$element} The jQuery element that is rendered wher there are no hits
|
||||
*/
|
||||
function create$noHits(advancedUrl) {
|
||||
return $('<div>')
|
||||
.addClass('qs-msg text-center p-3')
|
||||
.append(
|
||||
$('<div>')
|
||||
.addClass('h1 pi-displeased'),
|
||||
$('<div>')
|
||||
.addClass('h2')
|
||||
.append(
|
||||
$('<a>')
|
||||
.attr('href', advancedUrl)
|
||||
.text('Advanced search')
|
||||
)
|
||||
)
|
||||
}
|
||||
/**
|
||||
* Creates the jQuery object that is rendered as the search input
|
||||
* @param {Dict} searches The searches dict that is passed in on construction of the Quick-Search
|
||||
* @returns {$element} The jQuery object that renders the search input components.
|
||||
*/
|
||||
function create$input(searches) {
|
||||
let input = $('<input>')
|
||||
.addClass('qs-input')
|
||||
.attr('type', 'search')
|
||||
.attr('autocomplete', 'off')
|
||||
.attr('spellcheck', 'false')
|
||||
.attr('autocorrect', 'false')
|
||||
.attr('placeholder', 'Search...');
|
||||
let workingSymbol = $('<i>')
|
||||
.addClass('pi-cancel qs-busy-symbol');
|
||||
let inputComponent = [input, workingSymbol];
|
||||
if (Object.keys(searches).length > 1) {
|
||||
let i = 0;
|
||||
let select = $('<select>')
|
||||
.append(
|
||||
$.map(searches, (it, value) => {
|
||||
let option = $('<option>')
|
||||
.attr('value', value)
|
||||
.text(it['name']);
|
||||
if (i === 0) {
|
||||
option.attr('selected', 'selected');
|
||||
}
|
||||
i += 1;
|
||||
return option;
|
||||
})
|
||||
);
|
||||
inputComponent.push(select);
|
||||
}
|
||||
return inputComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the search result
|
||||
* @param {List} results
|
||||
* @param {String} advancedUrl
|
||||
* @returns {$element} The jQuery object that is rendered as the result
|
||||
*/
|
||||
function create$results(results, advancedUrl) {
|
||||
let $results = results.reduce((agg, res)=> {
|
||||
if(res['result'].length) {
|
||||
agg.push(
|
||||
$('<a>')
|
||||
.addClass('h4 mt-4 d-flex')
|
||||
.attr('href', res['url'])
|
||||
.text(res['name'])
|
||||
)
|
||||
agg.push(
|
||||
$('<div>')
|
||||
.addClass('card-deck card-deck-responsive card-padless js-asset-list p-3')
|
||||
.append(
|
||||
...pillar.templates.Nodes.createListOf$nodeItems(res['result'], 10, 0)
|
||||
)
|
||||
)
|
||||
}
|
||||
return agg;
|
||||
}, [])
|
||||
$results.push(
|
||||
$('<a>')
|
||||
.attr('href', advancedUrl)
|
||||
.text('Advanced search...')
|
||||
)
|
||||
|
||||
return $('<div>')
|
||||
.addClass('m-auto qs-result')
|
||||
.append(...$results)
|
||||
}
|
||||
|
||||
export { create$noHits, create$results, create$input }
|
@@ -1 +0,0 @@
|
||||
export { Nodes } from './templates/templates'
|
@@ -1,5 +1,4 @@
|
||||
import { Assets } from '../assets'
|
||||
import {} from ''
|
||||
import { Assets } from '../nodes/Assets'
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
@@ -8,11 +7,16 @@ describe('Assets', () => {
|
||||
let nodeDoc;
|
||||
let spyGet;
|
||||
beforeEach(()=>{
|
||||
// mock now to get a stable pretty printed created
|
||||
Date.now = jest.fn(() => new Date(Date.UTC(2018,
|
||||
10, //November! zero based month!
|
||||
28, 11, 46, 30)).valueOf()); // A Tuesday
|
||||
|
||||
nodeDoc = {
|
||||
_id: 'my-asset-id',
|
||||
name: 'My Asset',
|
||||
pretty_created: '2 hours ago',
|
||||
node_type: 'asset',
|
||||
_created: "Wed, 07 Nov 2018 16:35:09 GMT",
|
||||
project: {
|
||||
name: 'My Project',
|
||||
url: 'url-to-project'
|
||||
@@ -52,8 +56,9 @@ describe('Assets', () => {
|
||||
let $card = Assets.create$listItem(nodeDoc);
|
||||
jest.runAllTimers();
|
||||
expect($card.length).toEqual(1);
|
||||
expect($card.prop('tagName')).toEqual('A');
|
||||
expect($card.hasClass('card asset')).toBeTruthy();
|
||||
expect($card.prop('tagName')).toEqual('A'); // <a>
|
||||
expect($card.hasClass('asset')).toBeTruthy();
|
||||
expect($card.hasClass('card')).toBeTruthy();
|
||||
expect($card.attr('href')).toEqual('/nodes/my-asset-id/redir');
|
||||
expect($card.attr('title')).toEqual('My Asset');
|
||||
|
||||
@@ -77,14 +82,17 @@ describe('Assets', () => {
|
||||
|
||||
let $watched = $card.find('.card-label');
|
||||
expect($watched.length).toEqual(0);
|
||||
|
||||
expect($card.find(':contains(3 weeks ago)').length).toBeTruthy();
|
||||
done();
|
||||
});
|
||||
|
||||
test('node without picture', done => {
|
||||
let $card = Assets.create$listItem(nodeDoc);
|
||||
expect($card.length).toEqual(1);
|
||||
expect($card.prop('tagName')).toEqual('A');
|
||||
expect($card.hasClass('card asset')).toBeTruthy();
|
||||
expect($card.prop('tagName')).toEqual('A'); // <a>
|
||||
expect($card.hasClass('asset')).toBeTruthy();
|
||||
expect($card.hasClass('card')).toBeTruthy();
|
||||
expect($card.attr('href')).toEqual('/nodes/my-asset-id/redir');
|
||||
expect($card.attr('title')).toEqual('My Asset');
|
||||
|
||||
@@ -107,9 +115,10 @@ describe('Assets', () => {
|
||||
|
||||
let $watched = $card.find('.card-label');
|
||||
expect($watched.length).toEqual(0);
|
||||
|
||||
expect($card.find(':contains(3 weeks ago)').length).toBeTruthy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
})
|
||||
});
|
||||
|
@@ -0,0 +1,48 @@
|
||||
import { Assets } from '../nodes/Assets'
|
||||
import { Users } from '../users/Users'
|
||||
import { Component } from '../init' // Component is initialized in init
|
||||
|
||||
describe('Component', () => {
|
||||
test('can create Users listItem', () => {
|
||||
let userDoc = {
|
||||
_id: 'my-user-id',
|
||||
username: 'My User Name',
|
||||
full_name: 'My full name',
|
||||
roles: ['admin', 'subscriber']
|
||||
};
|
||||
|
||||
let $user_actual = Component.create$listItem(userDoc);
|
||||
expect($user_actual.length).toBe(1);
|
||||
|
||||
let $user_reference = Users.create$listItem(userDoc);
|
||||
expect($user_actual).toEqual($user_reference);
|
||||
});
|
||||
|
||||
test('can create Asset listItem', () => {
|
||||
let nodeDoc = {
|
||||
_id: 'my-asset-id',
|
||||
name: 'My Asset',
|
||||
node_type: 'asset',
|
||||
project: {
|
||||
name: 'My Project',
|
||||
url: 'url-to-project'
|
||||
},
|
||||
properties: {
|
||||
content_type: 'image'
|
||||
}
|
||||
};
|
||||
|
||||
let $asset_actual = Component.create$listItem(nodeDoc);
|
||||
expect($asset_actual.length).toBe(1);
|
||||
|
||||
let $asset_reference = Assets.create$listItem(nodeDoc);
|
||||
expect($asset_actual).toEqual($asset_reference);
|
||||
});
|
||||
|
||||
test('fail to create unknown', () => {
|
||||
expect(()=>Component.create$listItem({})).toThrow('Can not create component using: {}')
|
||||
expect(()=>Component.create$listItem()).toThrow('Can not create component using: undefined')
|
||||
expect(()=>Component.create$listItem({strange: 'value'}))
|
||||
.toThrow('Can not create component using: {"strange":"value"}')
|
||||
});
|
||||
});
|
67
src/scripts/js/es6/common/templates/__tests__/utils.test.js
Normal file
67
src/scripts/js/es6/common/templates/__tests__/utils.test.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import { prettyDate } from '../utils'
|
||||
|
||||
describe('prettydate', () => {
|
||||
beforeEach(() => {
|
||||
Date.now = jest.fn(() => new Date(Date.UTC(2016,
|
||||
10, //November! zero based month!
|
||||
8, 11, 46, 30)).valueOf()); // A Tuesday
|
||||
});
|
||||
|
||||
test('bad input', () => {
|
||||
expect(prettyDate(undefined)).toBeUndefined();
|
||||
expect(prettyDate(null)).toBeUndefined();
|
||||
expect(prettyDate('my birthday')).toBeUndefined();
|
||||
});
|
||||
|
||||
test('past dates',() => {
|
||||
expect(pd({seconds: -5})).toBe('just now');
|
||||
expect(pd({minutes: -5})).toBe('5m ago')
|
||||
expect(pd({days: -7})).toBe('last Tuesday')
|
||||
expect(pd({days: -8})).toBe('1 week ago')
|
||||
expect(pd({days: -14})).toBe('2 weeks ago')
|
||||
expect(pd({days: -31})).toBe('8 Oct')
|
||||
expect(pd({days: -(31 + 366)})).toBe('8 Oct 2015')
|
||||
});
|
||||
|
||||
test('past dates with time',() => {
|
||||
expect(pd({seconds: -5, detailed: true})).toBe('just now');
|
||||
expect(pd({minutes: -5, detailed: true})).toBe('5m ago')
|
||||
expect(pd({days: -7, detailed: true})).toBe('last Tuesday at 11:46')
|
||||
expect(pd({days: -8, detailed: true})).toBe('1 week ago at 11:46')
|
||||
// summer time bellow
|
||||
expect(pd({days: -14, detailed: true})).toBe('2 weeks ago at 10:46')
|
||||
expect(pd({days: -31, detailed: true})).toBe('8 Oct at 10:46')
|
||||
expect(pd({days: -(31 + 366), detailed: true})).toBe('8 Oct 2015 at 10:46')
|
||||
});
|
||||
|
||||
test('future dates',() => {
|
||||
expect(pd({seconds: 5})).toBe('just now')
|
||||
expect(pd({minutes: 5})).toBe('in 5m')
|
||||
expect(pd({days: 7})).toBe('next Tuesday')
|
||||
expect(pd({days: 8})).toBe('in 1 week')
|
||||
expect(pd({days: 14})).toBe('in 2 weeks')
|
||||
expect(pd({days: 30})).toBe('8 Dec')
|
||||
expect(pd({days: 30 + 365})).toBe('8 Dec 2017')
|
||||
});
|
||||
|
||||
test('future dates',() => {
|
||||
expect(pd({seconds: 5, detailed: true})).toBe('just now')
|
||||
expect(pd({minutes: 5, detailed: true})).toBe('in 5m')
|
||||
expect(pd({days: 7, detailed: true})).toBe('next Tuesday at 11:46')
|
||||
expect(pd({days: 8, detailed: true})).toBe('in 1 week at 11:46')
|
||||
expect(pd({days: 14, detailed: true})).toBe('in 2 weeks at 11:46')
|
||||
expect(pd({days: 30, detailed: true})).toBe('8 Dec at 11:46')
|
||||
expect(pd({days: 30 + 365, detailed: true})).toBe('8 Dec 2017 at 11:46')
|
||||
});
|
||||
|
||||
function pd(params) {
|
||||
let theDate = new Date(Date.now());
|
||||
theDate.setFullYear(theDate.getFullYear() + (params['years'] || 0));
|
||||
theDate.setMonth(theDate.getMonth() + (params['months'] || 0));
|
||||
theDate.setDate(theDate.getDate() + (params['days'] || 0));
|
||||
theDate.setHours(theDate.getHours() + (params['hours'] || 0));
|
||||
theDate.setMinutes(theDate.getMinutes() + (params['minutes'] || 0));
|
||||
theDate.setSeconds(theDate.getSeconds() + (params['seconds'] || 0));
|
||||
return prettyDate(theDate, (params['detailed'] || false))
|
||||
}
|
||||
});
|
@@ -1,97 +0,0 @@
|
||||
import { NodesFactoryInterface } from './nodes'
|
||||
import { thenLoadImage, thenLoadVideoProgress } from './utils';
|
||||
|
||||
class Assets extends NodesFactoryInterface{
|
||||
static create$listItem(node) {
|
||||
var markIfPublic = true;
|
||||
let $card = $('<a class="card asset card-image-fade pr-0 mx-0 mb-2">')
|
||||
.addClass('js-tagged-asset')
|
||||
.attr('href', '/nodes/' + node._id + '/redir')
|
||||
.attr('title', node.name);
|
||||
|
||||
let $thumbnailContainer = $('<div class="embed-responsive embed-responsive-16by9">');
|
||||
|
||||
function warnNoPicture() {
|
||||
let $cardIcon = $('<div class="card-img-top card-icon embed-responsive-item">');
|
||||
$cardIcon.html('<i class="pi-' + node.node_type + '">');
|
||||
$thumbnailContainer.append($cardIcon);
|
||||
}
|
||||
|
||||
if (!node.picture) {
|
||||
warnNoPicture();
|
||||
} else {
|
||||
$(window).trigger('pillar:workStart');
|
||||
|
||||
thenLoadImage(node.picture)
|
||||
.fail(warnNoPicture)
|
||||
.then((imgVariation)=>{
|
||||
let img = $('<img class="card-img-top embed-responsive-item">')
|
||||
.attr('alt', node.name)
|
||||
.attr('src', imgVariation.link)
|
||||
.attr('width', imgVariation.width)
|
||||
.attr('height', imgVariation.height);
|
||||
$thumbnailContainer.append(img);
|
||||
})
|
||||
.always(function(){
|
||||
$(window).trigger('pillar:workStop');
|
||||
});
|
||||
}
|
||||
|
||||
$card.append($thumbnailContainer);
|
||||
|
||||
/* Card body for title and meta info. */
|
||||
let $cardBody = $('<div class="card-body py-2 d-flex flex-column">');
|
||||
let $cardTitle = $('<div class="card-title mb-1 font-weight-bold">');
|
||||
$cardTitle.text(node.name);
|
||||
$cardBody.append($cardTitle);
|
||||
|
||||
let $cardMeta = $('<ul class="card-text list-unstyled d-flex text-black-50 mt-auto">');
|
||||
let $cardProject = $('<a class="font-weight-bold pr-2">')
|
||||
.attr('href', '/p/' + node.project.url)
|
||||
.attr('title', node.project.name)
|
||||
.text(node.project.name);
|
||||
|
||||
$cardMeta.append($cardProject);
|
||||
$cardMeta.append('<li>' + node.pretty_created + '</li>');
|
||||
$cardBody.append($cardMeta);
|
||||
|
||||
if (node.properties.duration){
|
||||
let $cardDuration = $('<div class="card-label right">' + node.properties.duration + '</div>');
|
||||
$thumbnailContainer.append($cardDuration);
|
||||
|
||||
/* Video progress and 'watched' label. */
|
||||
$(window).trigger('pillar:workStart');
|
||||
thenLoadVideoProgress(node._id)
|
||||
.fail(console.log)
|
||||
.then((view_progress)=>{
|
||||
if (!view_progress) return
|
||||
|
||||
let $cardProgress = $('<div class="progress rounded-0">');
|
||||
let $cardProgressBar = $('<div class="progress-bar">');
|
||||
$cardProgressBar.css('width', view_progress.progress_in_percent + '%');
|
||||
$cardProgress.append($cardProgressBar);
|
||||
$thumbnailContainer.append($cardProgress);
|
||||
|
||||
if (view_progress.done){
|
||||
let card_progress_done = $('<div class="card-label">WATCHED</div>');
|
||||
$thumbnailContainer.append(card_progress_done);
|
||||
}
|
||||
})
|
||||
.always(function() {
|
||||
$(window).trigger('pillar:workStop');
|
||||
});
|
||||
}
|
||||
|
||||
/* 'Free' ribbon for public assets. */
|
||||
if (markIfPublic && node.permissions && node.permissions.world){
|
||||
$card.addClass('free');
|
||||
}
|
||||
|
||||
$card.append($cardBody);
|
||||
|
||||
return $card;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export { Assets };
|
34
src/scripts/js/es6/common/templates/component/Component.js
Normal file
34
src/scripts/js/es6/common/templates/component/Component.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import { ComponentCreatorInterface } from './ComponentCreatorInterface'
|
||||
|
||||
const REGISTERED_CREATORS = []
|
||||
|
||||
export class Component extends ComponentCreatorInterface {
|
||||
static create$listItem(doc) {
|
||||
let creator = Component.getCreator(doc);
|
||||
return creator.create$listItem(doc);
|
||||
}
|
||||
|
||||
static create$item(doc) {
|
||||
let creator = Component.getCreator(doc);
|
||||
return creator.create$item(doc);
|
||||
}
|
||||
|
||||
static canCreate(candidate) {
|
||||
return !!Component.getCreator(candidate);
|
||||
}
|
||||
|
||||
static regiseterCreator(creator) {
|
||||
REGISTERED_CREATORS.push(creator);
|
||||
}
|
||||
|
||||
static getCreator(doc) {
|
||||
if (doc) {
|
||||
for (let candidate of REGISTERED_CREATORS) {
|
||||
if (candidate.canCreate(doc)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw 'Can not create component using: ' + JSON.stringify(doc);
|
||||
}
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
export class ComponentCreatorInterface {
|
||||
/**
|
||||
* @param {JSON} doc
|
||||
* @returns {$element}
|
||||
*/
|
||||
static create$listItem(doc) {
|
||||
throw 'Not Implemented';
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {JSON} doc
|
||||
* @returns {$element}
|
||||
*/
|
||||
static create$item(doc) {
|
||||
throw 'Not Implemented';
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {JSON} candidate
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static canCreate(candidate) {
|
||||
throw 'Not Implemented';
|
||||
}
|
||||
}
|
18
src/scripts/js/es6/common/templates/init.js
Normal file
18
src/scripts/js/es6/common/templates/init.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Nodes } from './nodes/Nodes';
|
||||
import { Assets } from './nodes/Assets';
|
||||
import { Posts } from './nodes/Posts';
|
||||
|
||||
import { Users } from './users/Users';
|
||||
import { Component } from './component/Component';
|
||||
|
||||
Nodes.registerTemplate('asset', Assets);
|
||||
Nodes.registerTemplate('post', Posts);
|
||||
|
||||
Component.regiseterCreator(Nodes);
|
||||
Component.regiseterCreator(Users);
|
||||
|
||||
export {
|
||||
Nodes,
|
||||
Users,
|
||||
Component
|
||||
};
|
@@ -1,48 +0,0 @@
|
||||
|
||||
let CREATE_NODE_ITEM_MAP = {}
|
||||
|
||||
class Nodes {
|
||||
static create$listItem(node) {
|
||||
return CREATE_NODE_ITEM_MAP[node.node_type].create$listItem(node);
|
||||
}
|
||||
|
||||
static create$item(node) {
|
||||
return CREATE_NODE_ITEM_MAP[node.node_type].create$item(node);
|
||||
}
|
||||
|
||||
static createListOf$nodeItems(nodes, initial=8, loadNext=8) {
|
||||
let nodesLeftToRender = nodes.slice();
|
||||
let nodesToCreate = nodesLeftToRender.splice(0, initial);
|
||||
let listOf$items = nodesToCreate.map(Nodes.create$listItem);
|
||||
|
||||
if (loadNext > 0 && nodesLeftToRender.length) {
|
||||
let $link = $('<a>')
|
||||
.addClass('btn btn-outline-primary px-5 mb-auto btn-block js-load-next')
|
||||
.attr('href', 'javascript:void(0);')
|
||||
.click((e)=> {
|
||||
let $target = $(e.target);
|
||||
$target.replaceWith(Nodes.createListOf$nodeItems(nodesLeftToRender, loadNext, loadNext));
|
||||
})
|
||||
.text('Load More');
|
||||
|
||||
listOf$items.push($link);
|
||||
}
|
||||
return listOf$items;
|
||||
}
|
||||
|
||||
static registerTemplate(key, klass) {
|
||||
CREATE_NODE_ITEM_MAP[key] = klass;
|
||||
}
|
||||
}
|
||||
|
||||
class NodesFactoryInterface{
|
||||
static create$listItem(node) {
|
||||
throw 'Not Implemented'
|
||||
}
|
||||
|
||||
static create$item(node) {
|
||||
throw 'Not Implemented'
|
||||
}
|
||||
}
|
||||
|
||||
export { Nodes, NodesFactoryInterface };
|
45
src/scripts/js/es6/common/templates/nodes/Assets.js
Normal file
45
src/scripts/js/es6/common/templates/nodes/Assets.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import { NodesBase } from "./NodesBase";
|
||||
import { thenLoadVideoProgress } from '../utils';
|
||||
|
||||
export class Assets extends NodesBase{
|
||||
static create$listItem(node) {
|
||||
var markIfPublic = true;
|
||||
let $card = super.create$listItem(node);
|
||||
$card.addClass('asset');
|
||||
|
||||
if (node.properties && node.properties.duration){
|
||||
let $thumbnailContainer = $card.find('.js-thumbnail-container')
|
||||
let $cardDuration = $('<div class="card-label right">' + node.properties.duration + '</div>');
|
||||
$thumbnailContainer.append($cardDuration);
|
||||
|
||||
/* Video progress and 'watched' label. */
|
||||
$(window).trigger('pillar:workStart');
|
||||
thenLoadVideoProgress(node._id)
|
||||
.fail(console.log)
|
||||
.then((view_progress)=>{
|
||||
if (!view_progress) return
|
||||
|
||||
let $cardProgress = $('<div class="progress rounded-0">');
|
||||
let $cardProgressBar = $('<div class="progress-bar">');
|
||||
$cardProgressBar.css('width', view_progress.progress_in_percent + '%');
|
||||
$cardProgress.append($cardProgressBar);
|
||||
$thumbnailContainer.append($cardProgress);
|
||||
|
||||
if (view_progress.done){
|
||||
let card_progress_done = $('<div class="card-label">WATCHED</div>');
|
||||
$thumbnailContainer.append(card_progress_done);
|
||||
}
|
||||
})
|
||||
.always(function() {
|
||||
$(window).trigger('pillar:workStop');
|
||||
});
|
||||
}
|
||||
|
||||
/* 'Free' ribbon for public assets. */
|
||||
if (markIfPublic && node.permissions && node.permissions.world){
|
||||
$card.addClass('free');
|
||||
}
|
||||
|
||||
return $card;
|
||||
}
|
||||
}
|
63
src/scripts/js/es6/common/templates/nodes/Nodes.js
Normal file
63
src/scripts/js/es6/common/templates/nodes/Nodes.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import { NodesBase } from './NodesBase';
|
||||
import { ComponentCreatorInterface } from '../component/ComponentCreatorInterface'
|
||||
|
||||
let CREATE_NODE_ITEM_MAP = {}
|
||||
|
||||
export class Nodes extends ComponentCreatorInterface {
|
||||
/**
|
||||
* Creates a small list item out of a node document
|
||||
* @param {NodeDoc} node mongodb or elastic node document
|
||||
*/
|
||||
static create$listItem(node) {
|
||||
let factory = CREATE_NODE_ITEM_MAP[node.node_type] || NodesBase;
|
||||
return factory.create$listItem(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a full view out of a node document
|
||||
* @param {NodeDoc} node mongodb or elastic node document
|
||||
*/
|
||||
static create$item(node) {
|
||||
let factory = CREATE_NODE_ITEM_MAP[node.node_type] || NodesBase;
|
||||
return factory.create$item(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a list of items and a 'Load More' button
|
||||
* @param {List} nodes A list of nodes to be created
|
||||
* @param {Int} initial Number of nodes to show initially
|
||||
* @param {Int} loadNext Number of nodes to show when clicking 'Load More'. If 0, no load more button will be shown
|
||||
*/
|
||||
static createListOf$nodeItems(nodes, initial=8, loadNext=8) {
|
||||
let nodesLeftToRender = nodes.slice();
|
||||
let nodesToCreate = nodesLeftToRender.splice(0, initial);
|
||||
let listOf$items = nodesToCreate.map(Nodes.create$listItem);
|
||||
|
||||
if (loadNext > 0 && nodesLeftToRender.length) {
|
||||
let $link = $('<a>')
|
||||
.addClass('btn btn-outline-primary px-5 mb-auto btn-block js-load-next')
|
||||
.attr('href', 'javascript:void(0);')
|
||||
.click((e)=> {
|
||||
let $target = $(e.target);
|
||||
$target.replaceWith(Nodes.createListOf$nodeItems(nodesLeftToRender, loadNext, loadNext));
|
||||
})
|
||||
.text('Load More');
|
||||
|
||||
listOf$items.push($link);
|
||||
}
|
||||
return listOf$items;
|
||||
}
|
||||
|
||||
static canCreate(candidate) {
|
||||
return !!candidate.node_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register template classes to handle the cunstruction of diffrent node types
|
||||
* @param { String } node_type The node type whose template that is registered
|
||||
* @param { NodesBase } klass The class to handle the creation of jQuery objects
|
||||
*/
|
||||
static registerTemplate(node_type, klass) {
|
||||
CREATE_NODE_ITEM_MAP[node_type] = klass;
|
||||
}
|
||||
}
|
58
src/scripts/js/es6/common/templates/nodes/NodesBase.js
Normal file
58
src/scripts/js/es6/common/templates/nodes/NodesBase.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import { thenLoadImage, prettyDate } from '../utils';
|
||||
import { ComponentCreatorInterface } from '../component/ComponentCreatorInterface'
|
||||
|
||||
export class NodesBase extends ComponentCreatorInterface {
|
||||
static create$listItem(node) {
|
||||
let nid = (node._id || node.objectID); // To support both mongo and elastic nodes
|
||||
let $card = $('<a class="card node card-image-fade asset">')
|
||||
.attr('data-node-id', nid)
|
||||
.attr('href', '/nodes/' + nid + '/redir')
|
||||
.attr('title', node.name);
|
||||
let $thumbnailContainer = $('<div class="card-thumbnail js-thumbnail-container">');
|
||||
function warnNoPicture() {
|
||||
let $cardIcon = $('<div class="card-img-top card-icon">');
|
||||
$cardIcon.html('<i class="pi-' + node.node_type + '">');
|
||||
$thumbnailContainer.append($cardIcon);
|
||||
}
|
||||
if (!node.picture) {
|
||||
warnNoPicture();
|
||||
}
|
||||
else {
|
||||
$(window).trigger('pillar:workStart');
|
||||
thenLoadImage(node.picture)
|
||||
.fail(warnNoPicture)
|
||||
.then((imgVariation) => {
|
||||
let img = $('<img class="card-img-top">')
|
||||
.attr('alt', node.name)
|
||||
.attr('src', imgVariation.link)
|
||||
.attr('width', imgVariation.width)
|
||||
.attr('height', imgVariation.height);
|
||||
$thumbnailContainer.append(img);
|
||||
})
|
||||
.always(function () {
|
||||
$(window).trigger('pillar:workStop');
|
||||
});
|
||||
}
|
||||
$card.append($thumbnailContainer);
|
||||
/* Card body for title and meta info. */
|
||||
let $cardBody = $('<div class="card-body p-2 d-flex flex-column">');
|
||||
let $cardTitle = $('<div class="card-title px-2 mb-2 font-weight-bold">');
|
||||
$cardTitle.text(node.name);
|
||||
$cardBody.append($cardTitle);
|
||||
let $cardMeta = $('<ul class="card-text px-2 list-unstyled d-flex text-black-50 mt-auto">');
|
||||
let $cardProject = $('<a class="font-weight-bold pr-2">')
|
||||
.attr('href', '/p/' + node.project.url)
|
||||
.attr('title', node.project.name)
|
||||
.text(node.project.name);
|
||||
$cardMeta.append($cardProject);
|
||||
let created = node._created || node.created_at; // mongodb + elastic
|
||||
$cardMeta.append('<li>' + prettyDate(created) + '</li>');
|
||||
$cardBody.append($cardMeta);
|
||||
$card.append($cardBody);
|
||||
return $card;
|
||||
}
|
||||
|
||||
static canCreate(candidate) {
|
||||
return !!candidate.node_type;
|
||||
}
|
||||
}
|
@@ -1,11 +1,10 @@
|
||||
import { NodesFactoryInterface } from './nodes'
|
||||
import { NodesBase } from "./NodesBase";
|
||||
|
||||
class Posts extends NodesFactoryInterface {
|
||||
export class Posts extends NodesBase {
|
||||
static create$item(post) {
|
||||
let content = [];
|
||||
let $title = $('<a>')
|
||||
.attr('href', '/nodes/' + post._id + '/redir')
|
||||
.addClass('h2 text-uppercase font-weight-bold d-block pb-3')
|
||||
let $title = $('<div>')
|
||||
.addClass('display-4 text-uppercase font-weight-bold')
|
||||
.text(post.name);
|
||||
content.push($title);
|
||||
let $post = $('<div>')
|
||||
@@ -20,5 +19,3 @@ class Posts extends NodesFactoryInterface {
|
||||
return $post;
|
||||
}
|
||||
}
|
||||
|
||||
export { Posts };
|
@@ -1,8 +0,0 @@
|
||||
import { Nodes } from './nodes';
|
||||
import { Assets } from './assets';
|
||||
import { Posts } from './posts';
|
||||
|
||||
Nodes.registerTemplate('asset', Assets);
|
||||
Nodes.registerTemplate('post', Posts);
|
||||
|
||||
export { Nodes };
|
23
src/scripts/js/es6/common/templates/users/Users.js
Normal file
23
src/scripts/js/es6/common/templates/users/Users.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ComponentCreatorInterface } from '../component/ComponentCreatorInterface'
|
||||
|
||||
export class Users extends ComponentCreatorInterface {
|
||||
static create$listItem(userDoc) {
|
||||
return $('<div>')
|
||||
.addClass('users p-2 border-bottom')
|
||||
.attr('data-user-id', userDoc._id || userDoc.objectID )
|
||||
.append(
|
||||
$('<h6>')
|
||||
.addClass('mb-0 font-weight-bold')
|
||||
.text(userDoc.full_name),
|
||||
$('<small>')
|
||||
.text(userDoc.username),
|
||||
$('<small>')
|
||||
.addClass('d-block roles text-info')
|
||||
.text(userDoc.roles.join(', '))
|
||||
)
|
||||
}
|
||||
|
||||
static canCreate(candidate) {
|
||||
return !!candidate.username;
|
||||
}
|
||||
}
|
@@ -0,0 +1,46 @@
|
||||
import { Users } from '../Users'
|
||||
|
||||
describe('Users', () => {
|
||||
let userDoc;
|
||||
describe('create$listItem', () => {
|
||||
beforeEach(()=>{
|
||||
userDoc = {
|
||||
_id: 'my-user-id',
|
||||
username: 'My User Name',
|
||||
full_name: 'My full name',
|
||||
roles: ['admin', 'subscriber']
|
||||
};
|
||||
});
|
||||
test('happy case', () => {
|
||||
let $user = Users.create$listItem(userDoc);
|
||||
expect($user.length).toBe(1);
|
||||
expect($user.hasClass('users')).toBeTruthy();
|
||||
expect($user.data('user-id')).toBe('my-user-id');
|
||||
|
||||
let $username = $user.find(':contains(My User Name)');
|
||||
expect($username.length).toBe(1);
|
||||
|
||||
let $fullName = $user.find(':contains(My full name)');
|
||||
expect($fullName.length).toBe(1);
|
||||
|
||||
let $roles = $user.find('.roles');
|
||||
expect($roles.length).toBe(1);
|
||||
expect($roles.text()).toBe('admin, subscriber')
|
||||
});
|
||||
})
|
||||
|
||||
describe('create$item', () => {
|
||||
beforeEach(()=>{
|
||||
userDoc = {
|
||||
_id: 'my-user-id',
|
||||
username: 'My User Name',
|
||||
full_name: 'My full name',
|
||||
roles: ['admin', 'subscriber']
|
||||
};
|
||||
});
|
||||
test('Not Implemented', () => {
|
||||
// Replace with proper test once implemented
|
||||
expect(()=>Users.create$item(userDoc)).toThrow('Not Implemented');
|
||||
});
|
||||
})
|
||||
});
|
@@ -21,4 +21,102 @@ function thenLoadVideoProgress(nodeId) {
|
||||
return $.get('/api/users/video/' + nodeId + '/progress')
|
||||
}
|
||||
|
||||
export { thenLoadImage, thenLoadVideoProgress };
|
||||
function prettyDate(time, detail=false) {
|
||||
/**
|
||||
* time is anything Date can parse, and we return a
|
||||
pretty string like 'an hour ago', 'Yesterday', '3 months ago',
|
||||
'just now', etc
|
||||
*/
|
||||
let theDate = new Date(time);
|
||||
if (!time || isNaN(theDate)) {
|
||||
return
|
||||
}
|
||||
let pretty = '';
|
||||
let now = new Date(Date.now()); // Easier to mock Date.now() in tests
|
||||
let second_diff = Math.round((now - theDate) / 1000);
|
||||
|
||||
let day_diff = Math.round(second_diff / 86400); // seconds per day (60*60*24)
|
||||
|
||||
if ((day_diff < 0) && (theDate.getFullYear() !== now.getFullYear())) {
|
||||
// "Jul 16, 2018"
|
||||
pretty = theDate.toLocaleDateString('en-NL',{day: 'numeric', month: 'short', year: 'numeric'});
|
||||
}
|
||||
else if ((day_diff < -21) && (theDate.getFullYear() == now.getFullYear())) {
|
||||
// "Jul 16"
|
||||
pretty = theDate.toLocaleDateString('en-NL',{day: 'numeric', month: 'short'});
|
||||
}
|
||||
else if (day_diff < -7){
|
||||
let week_count = Math.round(-day_diff / 7);
|
||||
if (week_count == 1)
|
||||
pretty = "in 1 week";
|
||||
else
|
||||
pretty = "in " + week_count +" weeks";
|
||||
}
|
||||
else if (day_diff < -1)
|
||||
// "next Tuesday"
|
||||
pretty = 'next ' + theDate.toLocaleDateString('en-NL',{weekday: 'long'});
|
||||
else if (day_diff === 0) {
|
||||
if (second_diff < 0) {
|
||||
let seconds = Math.abs(second_diff);
|
||||
if (seconds < 10)
|
||||
return 'just now';
|
||||
if (seconds < 60)
|
||||
return 'in ' + seconds +'s';
|
||||
if (seconds < 120)
|
||||
return 'in a minute';
|
||||
if (seconds < 3600)
|
||||
return 'in ' + Math.round(seconds / 60) + 'm';
|
||||
if (seconds < 7200)
|
||||
return 'in an hour';
|
||||
if (seconds < 86400)
|
||||
return 'in ' + Math.round(seconds / 3600) + 'h';
|
||||
} else {
|
||||
let seconds = second_diff;
|
||||
if (seconds < 10)
|
||||
return "just now";
|
||||
if (seconds < 60)
|
||||
return seconds + "s ago";
|
||||
if (seconds < 120)
|
||||
return "a minute ago";
|
||||
if (seconds < 3600)
|
||||
return Math.round(seconds / 60) + "m ago";
|
||||
if (seconds < 7200)
|
||||
return "an hour ago";
|
||||
if (seconds < 86400)
|
||||
return Math.round(seconds / 3600) + "h ago";
|
||||
}
|
||||
|
||||
}
|
||||
else if (day_diff == 1)
|
||||
pretty = "yesterday";
|
||||
|
||||
else if (day_diff <= 7)
|
||||
// "last Tuesday"
|
||||
pretty = 'last ' + theDate.toLocaleDateString('en-NL',{weekday: 'long'});
|
||||
|
||||
else if (day_diff <= 22) {
|
||||
let week_count = Math.round(day_diff / 7);
|
||||
if (week_count == 1)
|
||||
pretty = "1 week ago";
|
||||
else
|
||||
pretty = week_count + " weeks ago";
|
||||
}
|
||||
else if (theDate.getFullYear() === now.getFullYear())
|
||||
// "Jul 16"
|
||||
pretty = theDate.toLocaleDateString('en-NL',{day: 'numeric', month: 'short'});
|
||||
|
||||
else
|
||||
// "Jul 16", 2009
|
||||
pretty = theDate.toLocaleDateString('en-NL',{day: 'numeric', month: 'short', year: 'numeric'});
|
||||
|
||||
if (detail){
|
||||
// "Tuesday at 04:20"
|
||||
let paddedHour = ('00' + theDate.getUTCHours()).substr(-2);
|
||||
let paddedMin = ('00' + theDate.getUTCMinutes()).substr(-2);
|
||||
return pretty + ' at ' + paddedHour + ':' + paddedMin;
|
||||
}
|
||||
|
||||
return pretty;
|
||||
}
|
||||
|
||||
export { thenLoadImage, thenLoadVideoProgress, prettyDate };
|
@@ -1 +0,0 @@
|
||||
export { transformPlaceholder } from './utils/placeholder'
|
1
src/scripts/js/es6/common/utils/init.js
Normal file
1
src/scripts/js/es6/common/utils/init.js
Normal file
@@ -0,0 +1 @@
|
||||
export { transformPlaceholder } from './placeholder'
|
@@ -20,7 +20,7 @@
|
||||
const DEFAULT_URL = '/api/timeline';
|
||||
const transformPlaceholder = pillar.utils.transformPlaceholder;
|
||||
|
||||
class Timeline {
|
||||
export class Timeline {
|
||||
constructor(target, builder) {
|
||||
this._$targetDom = $(target);
|
||||
this._url;
|
||||
@@ -197,5 +197,3 @@ $.fn.extend({
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
export { Timeline };
|
@@ -1,4 +1,4 @@
|
||||
export { Timeline } from './timeline/timeline';
|
||||
export { Timeline } from './Timeline';
|
||||
|
||||
// Init timelines on document ready
|
||||
$(function() {
|
Reference in New Issue
Block a user