define('application/components/search', [
'rofl/widgets/component',
'rofl/widgets/label',
'rofl/widgets/container',
'rofl/lib/l10n',
'antie/runtimecontext',
'application/widgets/search/input',
'application/widgets/search/decorator',
'application/managers/search',
'rofl/lib/utils',
'rofl/events/keyevent',
'application/widgets/search/contentList',
'rofl/widgets/horizontallist',
'application/widgets/infoblock',
'application/formatters/vodasset',
'application/formatters/homeepgasset',
'rofl/analytics/web/google',
'application/managers/halo'
], function (
Component,
Label,
Container,
L10N,
RuntimeContext,
Input,
Decorator,
SearchManager,
Utils,
KeyEvent,
ContentList,
HorizontalList,
InfoBlock,
VodFormatter,
EpgFormatter,
GoogleAnalytics,
HaloManager
) {
'use strict';
var l10n = L10N.getInstance(),
device = RuntimeContext.getDevice(),
application = RuntimeContext.getCurrentApplication(),
searchManager = SearchManager.getInstance(),
Search,
GA = GoogleAnalytics.getInstance(),
layout = application.getLayout(),
inputConfig = {
inputField: {
opts: {
placeholder: l10n.get('search.input.placeholder'),
validateLength: true
}
}
},
infoblockConfig = {
id: 'back-on-top-block',
text: l10n.get('infoblock'),
classname: ['icon', 'icon-back-v2'],
position: 'right'
},
EVENT = {
userSearched: 'UserSearched',
userClickedOnSearchResult: 'UserClickedOnSearchResult'
},
GALabels = {
liveTV: 'LiveTV'
},
CONTENT_TYPES = {
VOD: 'VOD',
SERIES: 'GROUP_OF_BUNDLES',
LIVE: 'PROGRAM'
};
Search = Component.extend({
/**
* Initialises the component.
*/
init: function init () {
init.base.call(this, 'search');
this._build();
this._onSelectBound = Utils.bind(this._onSelect, this);
this._onKeyDownBound = Utils.bind(this._onKeyDown, this);
this._onTextChangeBound = Utils.bind(this._onTextChange, this);
this._onSelectedItemChangeBound = Utils.bind(this._onSelectedItemChange, this);
this._lockAssetsBound = Utils.bind(this._lockAssets, this);
},
/**
* Builds the component.
*
* @private
*/
_build: function () {
this._buildPageHeader();
this._buildList();
this._buildInput();
this._buildDecorator();
this._buildContentList();
this._buildInfoBlock();
},
/**
* Handle KeyEvent.
*
* @param {KeyEvent} e - KeyEvent instance.
* @private
*/
_onKeyDown: function (e) {
var contentList = this._contentList,
activeRowNumber = contentList.getActiveChildWidgetIndex() + 1;
switch (e.keyCode) {
case KeyEvent.VK_LEFT:
e.stopPropagation();
application.focusMenu('main');
break;
case KeyEvent.VK_RIGHT:
e.stopPropagation();
break;
case KeyEvent.VK_BACK:
if (activeRowNumber >= 3 && contentList.isFocussed()) {
this._backToTop();
} else {
this._onClose();
}
break;
// no default
}
},
/**
* Builds the page header.
*
* @private
*/
_buildPageHeader: function () {
var headerContainer = new Container('top-header'),
branding = this._branding = new Container(),
pageTitle = new Label({ text: l10n.get('search.title'), classNames: ['top-title'] });
branding.addClass('page-branding');
branding.addClass('title');
branding.appendChildWidget(pageTitle);
headerContainer.appendChildWidget(branding);
this.appendChildWidget(headerContainer);
},
/**
* Builds the search list layout.
*
* @private
*/
_buildList: function () {
var list = this._list = new HorizontalList('search-layout'),
inputContent = this._inputContent = new Container('input-content'),
searchContent = this._searchContent = new Container('search-content');
list.appendChildWidget(inputContent);
list.appendChildWidget(searchContent);
this.appendChildWidget(list);
},
/**
* Builds the input widget.
*
* @private
*/
_buildInput: function () {
var input = this._input = new Input(inputConfig);
this._inputContent.appendChildWidget(input);
},
/**
* Builds the decorator widget.
*
* @private
*/
_buildDecorator: function () {
var decorator = this._decorator = new Decorator();
this._searchContent.appendChildWidget(decorator);
},
/**
* Builds the grid.
*
* @private
*/
_buildContentList: function () {
var contentList = this._contentList = new ContentList({
height: layout.search.contentList.height,
visibleItems: 3
});
this._searchContent.appendChildWidget(contentList);
},
/**
* Builds the infoblock.
*
* @private
*/
_buildInfoBlock: function () {
var infoblock = this._infoblock = new InfoBlock(infoblockConfig);
infoblock.addClass('info-block');
this.appendChildWidget(infoblock);
},
/**
* BeforeRender event.
*/
onBeforeRender: function () {
this._loadingTime = application.getDate();
},
/**
* BeforeShow event.
*
* @param {Event} e - Event.
*/
onBeforeShow: function (e) {
var contentList = this._contentList,
input = this._input,
keyboard = input.getKeyboard();
this._input.getInputField().showCursorBlink();
this._startSearchPage = application.getDate();
GA.onPageView('search');
this._infoblock.hide({skipAnim: true});
this._showLoader(true);
this._input.focus();
keyboard.setStyleTo('display', 'block');
this._infoblock.hide();
this._decorator.hide({
skipAnim: true
});
if (!e.fromBack) {
// Set input and title to their initial positions.
input.reset();
}
contentList.removeChildWidgets();
searchManager.searchPlaceholders()
.then(Utils.bind(this._setPlaceHolderCarousels, this));
HaloManager.getInstance()
.getServiceMessage()
.then(function (r) {
if (r) {
application.route('service');
}
});
},
/**
* Attempts to set the placeholder results.
*
* @param {Array} placeholderData - The placeholder results.
* @private
*/
_setPlaceHolderCarousels: function (placeholderData) {
var trending = placeholderData[0],
latest = placeholderData[1],
contentList = this._contentList;
if (trending.data.length) {
trending.carouselConfig = {
title: l10n.get('search.carousels.yesterday', {count: trending.data.length}),
visibleItems: 3,
layout: layout.search,
width: layout.search.carousel.width,
assetWidth: layout.asset.epg.width,
assetHeight: layout.asset.epg.height,
formatter: EpgFormatter,
pointerEnabled: true,
assetType: 'live'
};
}
if (latest.data.length) {
latest.carouselConfig = {
title: l10n.get('search.carousels.latest', {count: latest.data.length}),
visibleItems: 6,
layout: layout.search,
width: layout.search.carousel.width,
assetWidth: layout.asset.vod.width,
assetHeight: layout.asset.vod.height,
formatter: VodFormatter,
pointerEnabled: true,
assetType: 'vod'
};
}
// Filter out data.
placeholderData = Utils.filter(placeholderData, function (dataObj) {
return dataObj.data.length !== 0;
});
if (placeholderData.length) {
this.align(null);
contentList.setContent(placeholderData);
contentList.alignCarousels();
contentList.show({
duration: 300
});
} else {
this._decorator.show(this._decorator.DECORATOR_SEARCH);
}
this._showLoader(false);
},
/**
* AfterShow event.
*/
onAfterShow: function () {
var inputField = this._input.getInputField(),
value = ((application.getDate() - this._loadingTime) / 1000).toFixed(2);
this._setEventListeners();
inputField.animateFocusBar();
GA.onEvent('page', 'load', {
eventLabel: 'search',
eventValue: value
});
},
/**
* BeforeHide event.
*/
onBeforeHide: function () {
var inputField = this._input.getInputField(),
duration;
// Stop the current search query.
this._input.getInputField().hideCursor();
this._showLoader(false);
clearTimeout(this._searchTimeout);
this._removeEventListeners();
inputField.stopCursorBlink();
inputField.stopFocusbarAnimation();
duration = ((application.getDate() - this._startSearchPage) / 60000).toFixed(2);
GA.onEvent('page', 'time', {
eventLabel: 'search',
eventValue: duration
});
},
/**
* Sets the event listeners.
*
* @private
*/
_setEventListeners: function () {
this.addEventListener('textchange', this._onTextChangeBound);
this._contentList.addEventListener('selecteditemchange', this._onSelectedItemChangeBound);
application.addEventListener('$locked', this._lockAssetsBound);
this._setSelectListener();
},
/**
* Removes the select listener.
*
* @private
*/
_setSelectListener: function () {
if (this._listening) {
return;
}
this._listening = true;
this.addEventListener('select', this._onSelectBound);
this.addEventListener('keydown', this._onKeyDownBound);
},
/**
* Removes the select listener.
*
* @private
*/
_removeSelectListener: function () {
if (!this._listening) {
return;
}
this._listening = false;
this.removeEventListener('select', this._onSelectBound);
this.removeEventListener('keydown', this._onKeyDownBound);
},
/**
* Removes the event listeners.
*
* @private
*/
_removeEventListeners: function () {
this.removeEventListener('textchange', this._onTextChangeBound);
this._contentList.removeEventListener('selecteditemchange', this._onSelectedItemChangeBound);
application.removeEventListener('$locked', this._lockAssetsBound);
this._removeSelectListener();
},
/**
* Opens asset detail.
*
* @param {Object} data - Asset data.
* @private
*/
_openAssetDetail: function (data) {
var detailRoute;
switch (data.getContentType()) {
case CONTENT_TYPES.VOD:
detailRoute = 'voddetail';
break;
case CONTENT_TYPES.SERIES:
detailRoute = 'seriesdetail';
break;
case CONTENT_TYPES.LIVE:
detailRoute = 'epgdetail';
break;
}
application.route(detailRoute, {
data: data,
callback: Utils.bind(this.focus, this),
callingPage: 'search'
});
},
/**
* On select function.
*
* @param {event} e - Event passed on select.
*/
_onSelect: function (e) {
var target = e.target,
keyboard = this._input.getKeyboard(),
clearbutton = this._input.getClearButton(),
activeRowNumber = this._contentList.getActiveChildWidgetIndex() + 1,
innerKeyboard = this._input.getKeyboard().getKeyboard(),
dataItem;
switch (target.id) {
case 'clearbutton':
searchManager.clearLastSearchedItems();
this._input.updateLastSearchedList();
clearbutton.setStyleTo('display', 'none');
clearbutton.setDisabled(true);
keyboard.focus();
break;
case 'back-on-top-block':
if (activeRowNumber !== 1) {
innerKeyboard.setStyleTo('display', 'block');
innerKeyboard.focus();
} else {
this._onClose();
}
break;
}
if (target.hasClass('asset')) {
dataItem = target.getDataItem().item;
GA.onEvent('Action', EVENT.userClickedOnSearchResult, {eventLabel: GALabels.liveTV});
if (target.isLocked()) {
this._removeSelectListener();
application.route('parentalpin', {
successCallback:
Utils.bind(function () {
this._addLastSearchedItem(dataItem.getTitle());
this._openAssetDetail(dataItem);
this._unlockAssets();
this._setSelectListener();
}, this),
errorCallback: Utils.bind(this._setSelectListener, this),
escapeCallback: Utils.bind(this._setSelectListener, this)
});
} else {
this._addLastSearchedItem(dataItem.getTitle());
this._openAssetDetail(dataItem);
}
} else if (target.hasClass('last-searched')) {
clearTimeout(this._searchTimeout);
this._searchQuery = target.getLabel().getText();
keyboard.setText(this._searchQuery);
}
},
/**
* Save searchquery in searchmanager.
*
* @param {string} title - Title of the asset.
* @private
*/
_addLastSearchedItem: function (title) {
var input = this._input;
searchManager.addLastSearchedItem(title);
input.updateLastSearchedList();
},
/**
* Unlock the assets.
*
* @private
*/
_unlockAssets: function () {
var placeholderCarousels = this._contentList,
listContent = placeholderCarousels.getCarousels();
Utils.each(listContent, function (carousel) {
Utils.each(carousel.getChildWidgets(), function (asset) {
asset.unlock();
});
});
},
/**
* Lock the assets.
*
* @private
*/
_lockAssets: function () {
var placeholderCarousels = this._contentList,
listContent = placeholderCarousels.getCarousels();
Utils.each(listContent, function (carousel) {
Utils.each(carousel.getChildWidgets(), function (asset) {
asset.lock();
});
});
},
/**
* Gets executed when the text changes.
*
* @param {Object} e - The current text.
* @private
*/
_onTextChange: function (e) {
var text = e.text,
textLength = text.length;
clearTimeout(this._searchTimeout);
this._searchQuery = text.replace(/ /g, ' ');
if (textLength) {
if (textLength >= 3) {
// Execute search.
this._showLoader(true);
this._decorator.hide();
this._searchTimeout = setTimeout(Utils.bind(this._executeSearch, this, this._searchQuery), 2000);
} else {
// Hide contentlist carousels and show search placeholder screen
this._contentList.hide({
duration: 300,
onComplete: Utils.bind(this.removeChildWidgets, this._contentList)
});
this._decorator.show(this._decorator.DECORATOR_SEARCH);
}
} else {
this._showLoader(true);
this._contentList.removeChildWidgets();
searchManager.searchPlaceholders()
.then(Utils.bind(this._setPlaceHolderCarousels, this));
this._decorator.hide();
}
},
/**
* Executes a search request.
*
* @param {string} text - The search query.
* @private
*/
_executeSearch: function (text) {
var now = application.getDate(),
todayAndLaterStartTime = new Date(now), // +4 days.
todayAndLaterEndTime = new Date(now + (1000 * 60 * 60 * 24 * 4)), // +4 days.
catchupStartTime = new Date(now - (1000 * 60 * 60 * 24 * 6)), // -6 days.
searchParams = {
todayAndLater: {},
catchup: {
endTime: now - (1000 * 60 * 15) // 15 minutes
},
query: text
};
// Set today at 00:00:00.
todayAndLaterStartTime = this._modifyTime(todayAndLaterStartTime, 0, 0, 0, 0);
// Set +4 days at 23:59:59.
todayAndLaterEndTime = this._modifyTime(todayAndLaterEndTime, 23, 59, 59, 0);
// Set -6 days at 00:00.
catchupStartTime = this._modifyTime(catchupStartTime, 0, 0, 0, 0);
searchParams.todayAndLater.startTime = Date.parse(todayAndLaterStartTime);
searchParams.todayAndLater.endTime = Date.parse(todayAndLaterEndTime);
searchParams.catchup.startTime = Date.parse(catchupStartTime);
GA.onEvent('Action', EVENT.userSearched);
searchManager.searchContent(searchParams)
.then(Utils.bind(this._setSearchResults, this, text))
['catch'](function () {
application.route('error', {
type: 'fullscreen',
title: L10N.getInstance().get('errors.cannot_load_page'),
button: {id: 'error-close-button', label: L10N.getInstance().get('errors.close')},
imgUrl: 'src/assets/images/error-icon.png'
});
});
},
/**
* Modify time for the given date.
*
* @param {Date} time - Date time.
* @param {number} hh - Hours.
* @param {number} mm - Minutes.
* @param {number} ss - Seconds.
* @param {number} ms - Milliseconds.
* @returns {Date} - Modified time.
*/
_modifyTime: function (time, hh, mm, ss, ms) {
time.setHours(hh);
time.setMinutes(mm);
time.setSeconds(ss);
time.setMilliseconds(ms);
return time;
},
/**
* Attempts to set the search results.
*
* @param {string} text - The search query.
* @param {Array} result - The search results.
* @private
*/
_setSearchResults: function (text, result) {
var contentList = this._contentList,
liveAndLaterResult = result[0],
catchupResult = result[1],
vodResults = result[2];
if (text !== this._searchQuery) {
this._showLoader(false);
return;
}
if (liveAndLaterResult.data.length) {
liveAndLaterResult.carouselConfig = {
title: l10n.get('search.carousels.liveAndLater', {count: liveAndLaterResult.data.length}),
visibleItems: 3,
layout: layout.search,
width: layout.search.carousel.width,
assetWidth: layout.asset.epg.width,
assetHeight: layout.asset.epg.height,
formatter: EpgFormatter,
pointerEnabled: true,
assetType: 'live'
};
}
if (catchupResult.data.length) {
catchupResult.carouselConfig = {
title: l10n.get('search.carousels.catchup', {count: catchupResult.data.length}),
visibleItems: 3,
layout: layout.search,
width: layout.search.carousel.width,
assetWidth: layout.asset.epg.width,
assetHeight: layout.asset.epg.height,
formatter: EpgFormatter,
pointerEnabled: true,
assetType: 'live'
};
}
if (vodResults.data.length) {
vodResults.carouselConfig = {
title: l10n.get('search.carousels.vod', {count: vodResults.data.length}),
visibleItems: 6,
layout: layout.search,
width: layout.search.carousel.width,
assetWidth: layout.asset.vod.width,
assetHeight: layout.asset.vod.height,
formatter: VodFormatter,
pointerEnabled: true,
assetType: 'vod'
};
}
// Filter out data.
result = Utils.filter(result, function (dataObj) {
return dataObj.data.length !== 0;
});
contentList.removeChildWidgets();
this.align(null);
contentList.setContent(result);
contentList.alignCarousels();
if (result.length) {
contentList.show({
duration: 300
});
} else {
this._decorator.show(this._decorator.DECORATOR_NO_RESULT, {
query: text
});
}
this._showLoader(false);
},
/**
* Change elements positon on selected item change.
*
* @param {Object} e - The selected item change event.
* @private
*/
_onSelectedItemChange: function (e) {
var target = e.target;
if (target.hasClass('content-list') && target.getChildWidgetCount() > 2) {
if (e.index >= 2) {
this.align(target.getActiveChildWidget());
} else {
this.align(target.getActiveChildWidget());
}
}
},
/**
* Aligns the component based on the given item.
*
* @param {Object} item - The item.
*/
align: function (item) {
var listElement = this._contentList.outputElement,
listTop = listElement.getBoundingClientRect().top,
windowHeight = layout.requiredScreenSize.height,
scrollingLayout = layout.search.scrolling,
newTopToAdd = scrollingLayout.newTopToAdd,
left = scrollingLayout.left,
activeRowIndex = this._contentList.getActiveChildWidgetIndex(),
rowsAmount = this._contentList.getChildWidgetCount(),
dimensions,
perfectTop,
difference,
newTop;
if (item) {
dimensions = item.outputElement.getBoundingClientRect();
perfectTop = (windowHeight - dimensions.height) / 2;
difference = dimensions.top - perfectTop;
if (difference > 0) {
newTop = listTop - difference;
} else {
newTop = listTop + -difference;
}
if (newTop > 0) {
newTop = scrollingLayout.top;
}
newTop += newTopToAdd;
} else {
// Reset content.
newTop = 0;
}
if (activeRowIndex !== (rowsAmount - 1)) {
device.moveElementTo({
el: listElement,
skipAnim: true,
to: {
top: 0,
left: left
}
});
} else {
device.moveElementTo({
el: listElement,
skipAnim: true,
to: {
top: newTop,
left: left
}
});
}
},
/**
* Aligns the carousels back to the top to the first carousel.
*/
_backToTop: function () {
var contentList = this._contentList;
this.align(null);
contentList.setActiveChildIndex(0);
contentList.alignCarousels();
},
/**
* Executes the close functionality.
*/
_onClose: function () {
// Go back and focus the player.
application.focusMenu('main');
},
/**
* Shows or hides loader.
*
* @param {boolean} show - True to show loader.
*/
_showLoader: function (show) {
var loader = application.getComponent('loader'),
focusState = !show;
if (show) {
loader.setStylesTo({
'left': '26%'
});
application.showLoader(true);
if (this._contentList) {
this._contentList.setFocusable(focusState);
}
} else {
application.hideLoader(null, {
onComplete: function () {
loader.setStylesTo({
'left': '0'
});
}
});
}
}
});
return Search;
});