/*
* Copyright (C) eZ Systems AS. All rights reserved.
* For full copyright and license information view LICENSE file distributed with this source code.
*/
YUI.add('ez-universaldiscoveryview', function (Y) {
"use strict";
/**
* Provides the Universal discovery View class
*
* @module ez-universaldiscoveryview
*/
Y.namespace('eZ');
var DISCOVERED = 'contentDiscovered',
CANCEL = 'cancelDiscover',
MULTIPLE_SELECTION_CLASS = 'is-multiple-selection-mode';
/**
* The universal discovery view is a widget to allow the user to pick one or
* several contents in the repository.
*
* @namespace eZ
* @class UniversalDiscoveryView
* @constructor
* @extends eZ.TemplateBasedView
*/
Y.eZ.UniversalDiscoveryView = Y.Base.create('universalDiscoveryView', Y.eZ.TemplateBasedView, [Y.eZ.Tabs], {
events: {
'.ez-universaldiscovery-confirm': {
'tap': '_confirmSelection'
},
'.ez-universaldiscovery-cancel': {
'tap': '_cancel',
},
},
initializer: function () {
this.on('changeTab', this._updateVisibleMethod);
this.after('multipleChange', this._uiMultipleMode);
this.after('visibleMethodChange', this._updateMethods);
this.after('*:selectContent', function (e) {
var isSelectable = this.get('isSelectable');
if (!this.get('multiple')) {
if (e.selection !== null && isSelectable(e.selection)) {
this._storeSelection(e.selection);
} else {
this._resetSelection();
}
}
});
this.after('*:unselectContent', function (e) {
this._unselectContent(e.contentId);
});
this.after('*:confirmSelectedContent', function (e) {
if ( !this._isAlreadySelected(e.selection) ) {
this._uiAnimateSelection(e.target);
this._storeSelection(e.selection);
}
});
this.after('selectionChange', function () {
this._uiSetConfirmButtonState();
if ( this.get('multiple') ) {
this.get('confirmedListView').set('confirmedList', this.get('selection'));
}
});
this.after('activeChange', function () {
if ( this.get('active') ) {
this._updateMethods();
this._uiUpdateTab();
this._uiUpdateTitle();
this._uiUpdateConfirmLabel();
}
});
this.on(['contentDiscoveredHandlerChange', 'cancelDiscoverHandlerChange'], function (e) {
this._syncEventHandler(e.attrName.replace(/Handler$/, ''), e.prevVal, e.newVal);
});
this._publishEvents();
},
/**
* Stores the given contentStruct in the selection. Depending on the
* `multiple` attribute value, the contentStruct is added to the
* selection or completely replaces it.
*
* @method _storeSelection
* @protected
* @param {Object|Null} contentStruct
*/
_storeSelection: function (contentStruct) {
if ( contentStruct === null ) {
this._resetSelection();
return;
}
if ( this.get('multiple') ) {
this._addToSelection(contentStruct);
} else {
this._set('selection', contentStruct);
}
},
/**
* Unselects the content from its content id.
*
* @method _unselectContent
* @param {String} contentId
*/
_unselectContent: function (contentId) {
var newSelection = [];
if ( this.get('multiple') ) {
newSelection = Y.Array.filter(this.get('selection'), function (struct) {
return struct.contentInfo.get('id') !== contentId;
});
}
this._notifyMethodsUnselectContent(contentId);
if ( newSelection.length === 0 ) {
this._resetSelection();
return;
}
this._set('selection', newSelection);
},
/**
* Notifies method views that a content is removed from the
* selection.
*
* @method _notifyMethodsUnselectContent
* @protected
* @param {String} contentId
*/
_notifyMethodsUnselectContent: function (contentId) {
Y.Array.each(this.get('methods'), function (method) {
method.onUnselectContent(contentId);
});
},
/**
* Checks whether the content is already selected
*
* @method _isAlreadySelected
* @protected
* @param {Object} contentStruct
* @return {Boolean}
*/
_isAlreadySelected: function (contentStruct) {
if ( !this.get('selection') ) {
return false;
}
return !!Y.Array.find(this.get('selection'), function (struct) {
return struct.contentInfo.get('id') === contentStruct.contentInfo.get('id');
});
},
/**
* Add a content to the selection
*
* @method _addToSelection
* @protected
* @param {Object} contentStruct
*/
_addToSelection: function (contentStruct) {
var sel = this.get('selection') || [];
sel.push(contentStruct);
this._set('selection', sel);
},
/**
* Resets the current selection
*
* @protected
* @method _resetSelection
*/
_resetSelection: function () {
this._set('selection', null);
},
/**
* Animates the selection done by the user with the given view. An
* animation is done only if the source view properly implements the
* `startAnimation` (see {{#crossLink
* "eZ.UniversalDiscoverySelectedView"}}Y.eZ.UniversalDiscoverySelectedView{{/crossLink}})
*
* @method _uiAnimateSelection
* @protected
* @param {Y.View} sourceView the view used by the user to select the
* content
*/
_uiAnimateSelection: function (sourceView) {
var elt, confirmNode;
confirmNode = this.get('confirmedListView').get('container');
if ( sourceView.startAnimation && (elt = sourceView.startAnimation()) ) {
elt.setX(confirmNode.getX());
elt.setY(confirmNode.getY() + confirmNode.get('offsetHeight') - elt.get('offsetHeight'));
}
},
/**
* Updates the tab to show the correct tab depending on the visible
* method
*
* @method _uiUpdateTab
* @protected
*/
_uiUpdateTab: function () {
var container = this.get('container'),
htmlId = '#' + this._visibleMethodView.getHTMLIdentifier();
this._selectTab(
this._getTabLabel(container.one('[href="' + htmlId + '"]')),
htmlId,
container
);
},
/**
* Adds or removes the multiple selection class on the container
* depending on the `multiple` attribute value.
*
* @method _uiMultipleMode
* @protected
*/
_uiMultipleMode: function () {
var container = this.get('container');
if ( this.get('multiple') ) {
container.addClass(MULTIPLE_SELECTION_CLASS);
} else {
container.removeClass(MULTIPLE_SELECTION_CLASS);
}
},
/**
* `selectionChange` event handler. It enables/disables the button
* depending on the selection
*
* @method _uiSetConfirmButtonState
* @protected
*/
_uiSetConfirmButtonState: function () {
var confirmButton = this.get('container').one('.ez-universaldiscovery-confirm');
confirmButton.set('disabled', !this.get('selection'));
},
/**
* Updates the title in the already rendered view
*
* @method _uiUpdateTitle
* @protected
*/
_uiUpdateTitle: function () {
this.get('container')
.one('.ez-universaldiscovery-title').setContent(this.get('title'));
},
/**
* Updates the label of the confirm button in the already rendered view
*
* @method _uiUpdateConfirmLabel
* @protected
*/
_uiUpdateConfirmLabel: function () {
this.get('container')
.one('.ez-universaldiscovery-confirm').setContent(this.get('confirmLabel'));
},
/**
* Updates the method views depending on the value so that their
* `visible` flag is consistent with the `visibleMethod` attribute value
* and so that they get the correct `multiple` and `loadContent` flag
* values as well. What's more the `isSelectable` function registered in
* UDW is passed to the method view.
*
* @method _updateMethods
* @protected
*/
_updateMethods: function () {
var visibleMethod = this.get('visibleMethod'),
startingLocationId = this.get('startingLocationId'),
startingLocation = this.get('startingLocation'),
minDiscoverDepth = this.get('minDiscoverDepth'),
virtualRootLocation = this.get('virtualRootLocation'),
defaultMethodView;
if ( !this.get('active') ) {
return;
}
/**
* Stores a reference to the visible method view
*
* @property _visibleMethodView
* @protected
* @type {eZ.UniversalDiscoveryMethodBaseView|Null}
*/
this._visibleMethodView = null;
Y.Array.each(this.get('methods'), function (method) {
var visible = (visibleMethod === method.get('identifier'));
if (!visible && (method.get('identifier') === Y.eZ.UniversalDiscoveryView.ATTRS.visibleMethod.value)) {
defaultMethodView = method;
}
method.setAttrs({
'virtualRootLocation': virtualRootLocation,
'multiple': this.get('multiple'),
'loadContent': true,
'minDiscoverDepth': minDiscoverDepth,
'startingLocationId': startingLocationId,
'startingLocation': startingLocation,
'visible': visible,
'isSelectable': Y.bind(this.get('isSelectable'), this),
'active': this.get('active'),
});
if ( visible ) {
this._visibleMethodView = method;
}
}, this);
if ( !this._visibleMethodView ) {
defaultMethodView.set('visible', true);
this._visibleMethodView = defaultMethodView;
}
},
/**
* tabChange event handler to update the `visibleMethod` attribute.
*
* @method _updateVisibleMethod
* @protected
* @param {EventFacade} e
*/
_updateVisibleMethod: function (e) {
var identifier = e.tabId.replace(/^#/, ''),
newlyVisibleMethod;
newlyVisibleMethod = Y.Array.find(this.get('methods'), function (method) {
return method.getHTMLIdentifier() === identifier;
});
this.set('visibleMethod', newlyVisibleMethod.get('identifier'));
},
/**
* Publishes the cancelDiscover and contentDiscovered events
*
* @method _publishEvents
* @protected
*/
_publishEvents: function () {
this.publish(DISCOVERED, {
bubbles: true,
emitFacade: true,
preventable: true,
defaultFn: this._resetState,
});
this.publish(CANCEL, {
bubbles: true,
emitFacade: true,
preventable: true,
defaultFn: this._resetState,
});
},
/**
* cancelDiscoverHandlerChange and contentDiscoveredHandlerChange event
* handler. It makes sure the potential previous event handler are
* removed and it adds the new handlers if any.
*
* @method _syncEventHandler
* @private
* @param {String} eventName event name
* @param {Function|Null} oldHandler the previous event handler
* @param {Function|Null} newHandler the new event handler
*/
_syncEventHandler: function (eventName, oldHandler, newHandler) {
if ( oldHandler ) {
this.detach(eventName, oldHandler);
}
if ( newHandler ) {
this.on(eventName, newHandler);
}
},
/**
* Resets the state of the view
*
* @method _resetState
* @protected
*/
_resetState: function () {
this.reset();
this._set('selection', null);
Y.Array.each(this.get('methods'), function (method) {
method.reset();
});
},
/**
* Custom reset implementation to make sure to reset the confirmed list
* sub view.
*
* @method reset
* @param {String} name
*/
reset: function (name) {
if ( name === 'confirmedListView' ) {
this.get('confirmedListView').reset();
return;
}
this.constructor.superclass.reset.apply(this, arguments);
},
/**
* Tap event handler on the cancel link(s).
*
* @method _cancel
* @param {EventFacade} the event facade of the tap event
* @protected
*/
_cancel: function (e) {
e.preventDefault();
/**
* Fired when the user cancel the selection. By default, the
* application will close the universal discovery view but this
* event can be prevented or stopped to avoid that.
*
* @event cancelDiscover
* @bubbles
*/
this.fire(CANCEL);
},
/**
* Tap event handler on the confirm button
*
* @method _confirmSelection
* @protected
*/
_confirmSelection: function () {
/**
* Fired when the user confirms the selection. By default, the
* application will close the universal discovery view but this
* event can be prevented and stopped so that it does not bubble to
* the app plugin responsible for that.
*
* @event contentDiscovered
* @bubbles
* @param {Null|Array|Object} selection the current selection of the
* discovery
*/
this.fire(DISCOVERED, {
selection: this.get('selection'),
});
},
render: function () {
var container = this.get('container');
this._uiMultipleMode();
container.setHTML(this.template({
title: this.get('title'),
multiple: this.get('multiple'),
methods: this._methodsList(),
confirmLabel: this.get('confirmLabel'),
}));
container.one('.ez-universaldiscovery-confirmed-list-container').append(
this.get('confirmedListView').render().get('container')
);
this._renderMethods();
return this;
},
/**
* Renders the available methods in a DOM element which id is the
* HTML identifier of the method.
*
* @method _renderMethods
* @protected
*/
_renderMethods: function () {
var container = this.get('container');
Y.Array.each(this.get('methods'), function (method) {
container.one('#' + method.getHTMLIdentifier()).append(method.render().get('container'));
});
},
/**
* Builds an array containing objects that describes the available
* methods.
*
* @protected
* @method _methodsList
* @return Array
*/
_methodsList: function () {
var res = [];
Y.Array.each(this.get('methods'), function (method) {
res.push({
title: method.get('title'),
identifier: method.getHTMLIdentifier(),
visible: method.get('visible'),
confirmLabel: method.get('confirmLabel'),
});
});
return res;
},
}, {
ATTRS: {
/**
* Title of the universal discovery view
*
* @attribute title
* @type {String}
* @default "Select your content"
*/
title: {
value: "Select your content",
},
/**
* Label of the 'confirm' button
*
* @attribute confirmLabel
* @type {String}
* @default Y.eZ.trans('universaldiscovery.confirm.selection', {}, 'universaldiscovery')
*/
confirmLabel: {
valueFn: function () {
return Y.eZ.trans('universaldiscovery.confirm.selection', {}, 'universaldiscovery');
},
},
/**
* Flag indicating whether the user is able to select several
* contents.
*
* @attribute multiple
* @type {Boolean}
* @default false
*/
multiple: {
value: false,
},
/**
* The id of a Location that should be considered as the starting
* point when discovering Content.
*
* @attribute startingLocationId
* @type {String}
* @default false if there is no starting location
*/
startingLocationId: {
value: false,
},
/**
* The depth of the root where we start discovering content.
* The UDW needs a starting location having a greater depth than the min discover depth to work.
*
* @attribute minDiscoverDepth
* @type {Number|false}
*/
minDiscoverDepth: {
value: false,
},
/**
* The Location that should be considered as the starting point when
* discovering Content.
*
* @attribute startingLocation
* @type {eZ.Location|false}
*/
startingLocation: {
value: false,
},
/**
* The virtual root Location object.
*
* @attribute virtualRootLocation
* @type {eZ.Location}
*/
virtualRootLocation: {},
/**
* Flag indicating whether the Content should be provided in the
* selection.
*
* @attribute loadContent
* @type {Boolean}
* @default false
*/
loadContent: {
value: false,
},
/**
* An event handler function for the `contentDiscovered` event.
*
* @attribute contentDiscoveredHandler
* @type {Function|null}
* @default null
*/
contentDiscoveredHandler: {
value: null,
},
/**
* An event handler function for the `cancelDiscover` event.
*
* @attribute cancelDiscoverHandler
* @type {Function|null}
* @default null
*/
cancelDiscoverHandler: {
value: null,
},
/**
* The available methods to discover content. Each element in the
* array should be an instance of a class extending
* Y.eZ.UniversalDiscoveryMethodBaseView.
*
* @attribute methods
* @type array
*/
methods: {
valueFn: function () {
return [
new Y.eZ.UniversalDiscoveryFinderView({
bubbleTargets: this,
priority: 100,
multiple: this.get('multiple'),
loadContent: true,
isAlreadySelected: Y.bind(this._isAlreadySelected, this),
minDiscoverDepth: this.get('minDiscoverDepth'),
startingLocationId: this.get('startingLocationId'),
startingLocation: this.get('startingLocation'),
virtualRootLocation: this.get('virtualRootLocation'),
}),
new Y.eZ.UniversalDiscoverySearchView({
bubbleTargets: this,
priority: 200,
multiple: this.get('multiple'),
loadContent: true,
isAlreadySelected: Y.bind(this._isAlreadySelected, this),
minDiscoverDepth: this.get('minDiscoverDepth'),
startingLocationId: this.get('startingLocationId'),
startingLocation: this.get('startingLocation'),
virtualRootLocation: this.get('virtualRootLocation'),
}),
];
},
},
/**
* The identifier of the visible method
*
* @attribute visibleMethod
* @type String
* @default 'finder'
*/
visibleMethod: {
value: 'finder',
},
/**
* The current selection of the discovery. This selection is
* provided to the contentDiscovered event handler in the event
* facade. Depending on the `multiple` flag and on the user action,
* the selection is either null or an object (`multiple` set to
* false) or an array (`multiple` set to true)
*
* @attribute selection
* @type {Null|Object|Array}
* @readOnly
* @default null
*/
selection: {
value: null,
readOnly: true,
},
/**
* The confirmed list view. It displays the user's current confirmed
* list content.
*
* @attribute @confirmedListView
* @type {eZ.UniversalDiscoveryConfirmedListView}
*/
confirmedListView: {
valueFn: function () {
return new Y.eZ.UniversalDiscoveryConfirmedListView({
bubbleTargets: this,
});
}
},
/**
* An arbitrary object the component triggering the universal
* discovery can set. It is useful to store some data the
* contentDiscovered and cancelDiscover handlers need.
*
* @attribute data
* @type {Object}
* @default {}
*/
data: {
value: {},
},
/**
* Checks wether the content is selectable. Function can be provided in the config
* when firing the `contentDiscover` event so it can check if content is selectable
* depending on the context where UDW is triggered.
*
* @attribute isSelectable
* @type {Function}
*/
isSelectable: {
validator: Y.Lang.isFunction,
value: function (contentStruct) {
return true;
}
},
}
});
});