/**
* Copyright (C) 2005-2016 Alfresco Software Limited.
*
* This file is part of Alfresco
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* <p>This is the simplest widget for rendering lists of data. The data is retrieved by publishing a
* [loadDataPublishTopic]{@link module:alfresco/lists/AlfList#loadDataPublishTopic} that will
* need to be subscribed to by a service included on the page. Depending upon the service handling
* the request it may be necessary to configure a
* [loadDataPublishPayload]{@link module:alfresco/lists/AlfList#loadDataPublishPayload} to provide
* additional information on the data that should be loaded.</p>
* <p>Lists should be configured with at least one [AlfListView]{@link module:alfresco/lists/views/AlfListView}
* (or a widget that extends it) to render the loaded data. The most basic view that can be used
* would be the [HtmlListView]{@link module:alfresco/lists/views/HtmlListView} that renders a basic
* bullet list of the data</p>
* <p>If the list is not rendered on the page when first loaded (for example it might be displayed
* in a [dialog]{@link module:alfresco/dialogs/AlfDialog} or an initially hidden tab in an
* [AlfTabContainer]{@link module:alfresco/layouts/AlfTabContainer}) then it will be necessary to configure
* the [waitForPageWidgets]{@link module:alfresco/lists/AlfList#waitForPageWidgets} to be false to
* avoid the list waiting for an event that will never be fired before attempting to load data).</p>
* <p>If you need to use a list with greater capabilities then consider using the
* [AlfHashList]{@link module:alfresco/lists/AlfHashList},
* [AlfSortablePaginatedList]{@link module:alfresco/lists/AlfSortablePaginatedList} or
* [AlfFilteredList]{@link module:alfresco/lists/AlfFilteredList} as these incrementally provide
* additional capabilities for URL hashing, sorting and paginating and filtering respectively.</p>
* <p>Depending upon the payload of the data returned by the service it might be necessary to configure
* the [itemsProperty]{@link module:alfresco/lists/AlfList#itemsProperty} to identify the attribute
* containing the array of items to be rendered.<p>
* <p>It is also possible to explicitly define the data to be listed by setting the
* [currentData]{@link module:alfresco/lists/AlfList#currentData}. This should be configured to be an
* object containing an "items" attribute that is the data to be rendered.</p>
*
* @example <caption>Basic list with hard-coded data</caption>
* {
* name: "alfresco/lists/AlfList",
* config: {
* currentData: {
* items: [
* { value: "one"},
* { value: "two"}
* ]
* },
* widgets: [
* {
* name: "alfresco/lists/views/HtmlListView",
* config: {
* propertyToRender: "value"
* }
* }
* ]
* }
* }
*
* @example <caption>Basic list loading users from the Repository via the CrudService</caption>
* {
* name: "alfresco/lists/AlfList",
* config: {
* loadDataPublishTopic: "ALF_CRUD_GET_ALL",
* loadDataPublishPayload: {
* url: "api/people"
* },
* itemsProperty: "people",
* widgets: [
* {
* name: "alfresco/lists/views/HtmlListView",
* config: {
* propertyToRender: "userName"
* }
* }
* ]
* }
* }
*
* @module alfresco/lists/AlfList
* @extends external:dijit/_WidgetBase
* @mixes external:dijit/_TemplatedMixin
* @mixes module:alfresco/core/Core
* @mixes module:alfresco/core/CoreWidgetProcessing
* @mixes module:alfresco/lists/SelectedItemStateMixin
* @mixes module:alfresco/core/DynamicWidgetProcessingTopics
* @author Dave Draper
*/
define(["dojo/_base/declare",
"dijit/_WidgetBase",
"dijit/_TemplatedMixin",
"dojo/text!./templates/AlfList.html",
"alfresco/core/Core",
"alfresco/core/CoreWidgetProcessing",
"alfresco/core/topics",
"alfresco/core/WidgetsCreator",
"alfresco/lists/SelectedItemStateMixin",
"alfresco/core/DynamicWidgetProcessingTopics",
"alfresco/lists/views/AlfListView",
"alfresco/menus/AlfCheckableMenuItem",
"dojo/aspect",
"dojo/_base/array",
"dojo/_base/lang",
"dojo/dom-construct",
"dojo/dom-class",
"dojo/io-query",
"dojo/sniff"],
function(declare, _WidgetBase, _TemplatedMixin, template, AlfCore, CoreWidgetProcessing, topics, WidgetsCreator, SelectedItemStateMixin,
DynamicWidgetProcessingTopics, AlfListView, AlfCheckableMenuItem, aspect, array, lang, domConstruct,
domClass, ioQuery, sniff) {
return declare([_WidgetBase, _TemplatedMixin, AlfCore, CoreWidgetProcessing, SelectedItemStateMixin, DynamicWidgetProcessingTopics], {
/**
* An array of the i18n files to use with this widget.
*
* @instance
* @type {object[]}
* @default [{i18nFile: "./i18n/AlfList.properties"}]
*/
i18nRequirements: [{i18nFile: "./i18n/AlfList.properties"}],
/**
* An array of the CSS files to use with this widget.
*
* @instance cssRequirements {Array}
* @type {object[]}
* @default [{cssFile:"./css/AlfList.css"}]
*/
cssRequirements: [{cssFile:"./css/AlfList.css"}],
/**
* The HTML template to use for the widget.
* @instance
* @type {String}
*/
templateString: template,
/**
* This is the ID of the widget that should be targeted with adding additional view controls to
*
* @instance
* @type {string}
* @default
*/
additionalControlsTarget: "DOCLIB_TOOLBAR",
/**
* This is the dynacmic visibility configuration that should be applied
* to all additional controls added for a view.
*
* @instance
* @type {object}
* @default
*/
additionalViewControlVisibilityConfig: null,
/**
* Used to keep track of the current data for rendering by [views]{@link module:alfresco/lists/views/AlfListView}.
*
* @instance
* @type {object}
* @default
*/
currentData: null,
/**
* A property from the [currentItem]{@link module:alfresco/core/CoreWidgetProcessing#currentItem} that should be
* used as the list of items to be displayed. This is useful when rendering a list within a list.
*
* @instance
* @type {string}
* @default
* @since 1.0.85
*/
currentItemPropertyForDataItems: null,
/**
* An internally used attribute to hold a UUID for the any in-flight request. This allows the request to be
* cancelled if a subsequent request is issued before the in-flight request completes. This should not be
* configured.
*
* @instance
* @type {string}
* @default
* @since 1.0.75
*/
currentRequestId: null,
/**
* This is the message to display when data cannot be loaded Message keys will be localized
* where possible.
*
* @instance
* @type {string}
* @default
*/
dataFailureMessage: "alflist.data.failure.message",
/**
* An array of filters that should be included in data loading requests. The list itself will
* not perform any filtering it is up to the service (or API that the service calls) to filter
* the results based on the data provided.
*
* @instance
* @type {array}
* @default
*/
dataFilters: null,
/**
* This is the message to display when data is loading. Message keys will be localized
* where possible.
*
* @instance
* @type {string}
* @default
*/
fetchingDataMessage: "alflist.loading.data.message",
/**
* This is the message to display when fetching more data. Message keys will be localized
* where possible.
*
* @instance
* @type {string}
* @default
*/
fetchingMoreDataMessage: "alflist.loading.data.message",
/**
* An array of the topics to subscribe to that when published provide data that the indicates how the
* data requested should be filtered.
*
* @instance
* @type {array}
* @default
*/
filteringTopics: null,
/**
* Permit the loading indicators to remain on screen for a few milliseconds after the page loads
* in order to prevent "flashing" of the loading message.
*
* @instance
* @type {number}
* @default
*/
hideLoadingDelay: 250,
/**
* Specifies how long to wait (in ms) before forcing removal of the loading message
*
* @instance
* @type {number}
* @default
* @since 1.0.48
*/
hideLoadingTimeoutDuration: 10000,
/**
* The property in the data response that is the attribute of items to render
*
* @instance
* @type {string}
* @default
*/
itemsProperty: "items",
/**
* Indicates whether or not a request for data should be loaded as soon as the widget is created.
* This will have no effect when [currentData]{@link module:alfresco/listsl/AlfList#currentData}
* is configured.
*
* @instance
* @type {boolean}
* @default
*/
loadDataImmediately: true,
/**
* This is the payload to publish to make requests to retrieve data to populate the list
* with.
*
* @instance
* @type {object}
* @default
*/
loadDataPublishPayload: null,
/**
* This is the topic to publish to make requests to retrieve data to populate the list
* with. This can be overridden with alternative topics to obtain different data sets.
* Defaults to [GET_DOCUMENT_LIST]{@link module:alfresco/core/topics#GET_DOCUMENT_LIST}
*
* @instance
* @type {string}
* @default
*/
loadDataPublishTopic: topics.GET_DOCUMENT_LIST,
/**
* If not configured this will automatically be generated to be the
* [loadDataPublishTopic]{@link module:alfresco/lists/AlfList#loadDataPublishTopic}
* appended with "_FAILURE".
*
* @instance
* @type {string}
* @default
* @since 1.0.74
*/
loadDataFailureTopic: null,
/**
* If not configured this will automatically be generated to be the
* [loadDataPublishTopic]{@link module:alfresco/lists/AlfList#loadDataPublishTopic}
* appended with "_SUCCESS".
*
* @instance
* @type {string}
* @default
* @since 1.0.74
*/
loadDataSuccessTopic: null,
/**
* The property in the data response that is a metadata attribute containing additional information
* about the overall context of the list. This defaults to "metadata". If not attribute with the
* defined name is provided then no data will be assigned.
*
* @instance
* @type {string}
* @default
*/
metadataProperty: "metadata",
/**
* This is the message to display when no data is available. Message keys will be localized
* where possible.
*
* @instance
* @type {string}
* @default
*/
noDataMessage: "alflist.no.data.message",
/**
* This is the message to display whilst a view is being rendered. Message keys will be localized
* where possible.
*
* @instance
* @type {string}
* @default
*/
renderingViewMessage: "alflist.rendering.data.message",
/**
* Is there currently a request in progress?
*
* @instance
* @default
* @type {Boolean}
*/
requestInProgress: false,
/**
* An internally used attribute used to indicate whether or not a request is pending to be performed
* as soon as the current request completes.
*
* @instance
* @type {boolean}
* @default
*/
pendingLoadRequest: false,
/**
* The property in the response that indicates the starting index of overall data to request.
*
* @instance
* @type {string}
* @default
*/
startIndexProperty: "startIndex",
/**
* Indicates whether or not views should apply drag-and-drop highlighting. Each view used by the
* list will have this value applied (even if it overrides custom configuration) as it is up to
* the list to control whether or not it supported drag-and-drop behaviour.
*
* @instance
* @type {boolean}
* @default
* @since 1.0.39
*/
suppressDndUploading: true,
/**
* The property in the response that indicates the total number of results available.
*
* @instance
* @type {string}
* @default
*/
totalResultsProperty: "totalRecords",
/**
* Indicates whether Infinite Scroll should be used when requesting documents
*
* @instance
* @type {boolean}
* @default
*/
useInfiniteScroll: false,
/**
* A map of the additional controls that each view requires. This is map is populated as each view
* is selected (so that the controls are only loaded once) but are then loaded from the map. This
* allows the same controls to be added and removed as views are switched.
*
* @instance
* @type {object}
* @default
*/
viewControlsMap: null,
/**
* A map of views that the list can switch between.
*
* @instance
* @type {object}
* @default
*/
viewMap: null,
/**
* This can be configured to an array of modifying functions (typically provided by the
* [ObjectProcessingMixin]{@link module:alfresco/core/ObjectProcessingMixin} but can be provided
* by extending or mixed in modules) to process the [widget models]{@link module:alfresco/lists/AlfList#widgets}
* used to render views. A common example might be to use the
* [processInstanceTokens]{@link module:alfresco/core/ObjectProcessingMixin#processInstanceTokens} to pass on
* instance values of the list onto the views that are rendered.
*
* @instance
* @type {string[]}
* @default
* @since 1.0.65
*/
viewModifiers: null,
/**
* The preference property to use for saving the current view. Initially defaulted to
* the document library view preference but can be overridden if desired.
*
* @instance
* @type {string}
* @default
*/
viewPreferenceProperty: "org.alfresco.share.documentList.viewRendererName",
/**
* This is the string that is used to map the call to [processWidgets]{@link module:alfresco/core/Core#processWidgets}
* to create the views defined for the list to the resulting callback in
* [allWidgetsProcessed]{@link module:alfresco/core/Core#allWidgetsProcessed}
*
* @instance
* @type {string}
* @default
* @since 1.0.35
*/
viewWidgetsMappingId: "VIEWS",
/**
* This indicates that the instance should wait for all widgets on the page to finish rendering before
* making any attempt to load data. If this is set to true then loading can begin as soon as this instance
* has finished being created. This needs to be overridden in the case where the instance is created
* dynamically after the page has loaded.
*
* @instance
* @type {boolean}
* @default
*/
waitForPageWidgets: true,
/**
* The widgets processed by AlfDocumentList should all be instances of "alfresco/lists/views/AlfListView".
* Any widget that is instantiated that does not inherit from that class will not be included as a view.
*
* @instance
* @type {object[]}
* @default
*/
widgets: null,
/**
* An optional JSON model defining the widgets to display when an error occurs attempting to
* load data to display.
*
* @instance
* @type {object[]}
* @default
* @since 1.0.48
*/
widgetsForDataFailureDisplay: null,
/**
* Use to keeps track of the [view]{@link module:alfresco/lists/views/AlfListView} that is currently selected.
*
* @instance
* @type {string}
* @default
*/
_currentlySelectedView: null,
/**
* A flag to indicate whether or not the
* [widgetsForDataFailureDisplay]{@link module:alfresco/lists/AlfList#widgetsForDataFailureDisplay}
* have been rendered. This prevents them from being repeatedly set. It should not be explicitly
* configured in the list - it is for internal use only.
*
* @instance
* @type {boolean}
* @default
* @since 1.0.48
*/
_dataFailureWidgetsRendered: false,
/**
* @instance
* @type {number}
* @default
*/
_filterDelay: 1000,
/**
* The key of the item to attempt to focus on in the loaded results. This can be set via the
* payload handled by [onReloadData]{@link module:alfresco/lists/AlfList#onReloadData}.
*
* @instance
* @type {string}
* @default
* @since 1.0.77
*/
_focusItemKey: null,
/**
* This timeout pointer is used to ensure the loading message doesn't display forever
*
* @instance
* @type {object}
* @default
* @since 1.0.48
*/
_hideLoadingTimeoutPointer: null,
/**
* This is updated by the [onPageWidgetsReady]{@link module:alfresco/lists/AlfList#onPageWidgetsReady}
* function to be true when all widgets on the page have been loaded. It is used to block loading of
* data until the page is completely setup. This is done to avoid multiple data loads as other widgets
* on the page publish the details of their initial state (which would otherwise trigger a call to
* [loadData]{@link module:alfresco/lists/AlfList#onPageWidgetsReady})
*
* @instance
* @type {boolean}
* @default
*/
_readyToLoad: false,
/**
* Subscribe the document list topics.
*
* @instance
*/
postMixInProperties: function alfresco_lists_AlfList__postMixInProperties() {
if (!this.loadDataFailureTopic)
{
this.loadDataFailureTopic = this.loadDataPublishTopic + "_FAILURE";
}
if (!this.loadDataSuccessTopic)
{
this.loadDataSuccessTopic = this.loadDataPublishTopic + "_SUCCESS";
}
this.setupSubscriptions();
this.setDisplayMessages();
},
/**
* This function sets up the subscriptions that the Document List relies upon to manage its
* internal state and request documents.
*
* @instance
*/
setupSubscriptions: function alfresco_lists_AlfList__setupSubscriptions() {
this.alfSubscribe(this.reloadDataTopic, lang.hitch(this, this.onReloadData));
this.alfSubscribe(this.viewSelectionTopic, lang.hitch(this, this.onViewSelected));
this.alfSubscribe(this.loadDataSuccessTopic, lang.hitch(this, this.onDataLoadSuccess));
this.alfSubscribe(this.loadDataFailureTopic, lang.hitch(this, this.onDataLoadFailure));
this.alfSubscribe(this.requestInProgressTopic, lang.hitch(this, this.onRequestInProgress));
this.alfSubscribe(this.requestFinishedTopic, lang.hitch(this, this.onRequestFinished));
if (this.useInfiniteScroll)
{
this.alfSubscribe(this.scrollNearBottom, lang.hitch(this, this.onScrollNearBottom));
}
this.createSelectedItemSubscriptions();
},
/**
* The constructor
*
* @instance
*/
constructor: function alfresco_lists_AlfList__constructor(){
this.dataFilters = [];
},
/**
* This function should be overridden as necessary to change the messages displayed for various states
* of the list, e.g.
* <ul><li>When no view is configured</li>
* <li>When there is no data to render</li>
* <li>When data is being retrieved</li>
* <li>When the view is being rendered</li>
* <li>When the additional data is being retrieved (e.g. on infinite scroll)</li></ul>
*
* @instance
*/
setDisplayMessages: function alfresco_lists_AlfList__setDisplayMessages() {
this.noDataMessage = this.message(this.noDataMessage);
this.fetchingDataMessage = this.message(this.fetchingDataMessage);
this.renderingViewMessage = this.message(this.renderingViewMessage);
this.fetchingMoreDataMessage = this.message(this.fetchingMoreDataMessage);
this.dataFailureMessage = this.message(this.dataFailureMessage);
},
/**
* @instance
*/
postCreate: function alfresco_lists_AlfList__postCreate() {
// Process the array of widgets. Only views should be included as widgets of the DocumentList.
if (this.widgets)
{
// Iterate over all the configured views and apply the DND upload suppression
// configuration to each of them...
array.forEach(this.widgets, function(view) {
var viewConfig = lang.getObject("config", true, view); // NOTE: Create the config object if it doesn't exist
viewConfig.suppressDndUploading = this.suppressDndUploading;
}, this);
// Opting to NOT clone the widgets for performance here, but leaving the code commented out
// for hasty re-insertion if necessary. It is necessary to clone here because
// the views will clone as necessary...
var clonedWidgets = JSON.parse(JSON.stringify(this.widgets));
this.processWidgets(clonedWidgets, null, this.viewWidgetsMappingId);
}
},
/**
* Updates the list of filters that should currently be included when requesting data.
*
* @instance
* @param {object} payload The published payload containing the filter data.
*/
onFilterRequest: function alfresco_lists_AlfList__onFilterRequest(payload) {
if (payload && payload.name)
{
// Look to see if there is an existing filter that needs to be updated
var existingFilter = array.some(this.dataFilters, function(filter) {
var match = filter.name === payload.name;
if (match)
{
filter.value = payload.value;
}
return match;
});
// If there wasn't an existing filter then add the payload as a new one...
if (!existingFilter)
{
this.dataFilters.push({
name: payload.name,
value: payload.value
});
}
// Setup a new timeout (clearing the old one, just in case)
clearTimeout(this._filterTimeoutHandle);
this._filterTimeoutHandle = setTimeout(lang.hitch(this, function() {
if (this.requestInProgress)
{
this.pendingLoadRequest = true;
}
else
{
this.onFiltersUpdated();
}
}), this._filterDelay);
}
},
/**
* Handle filters being updated
*
* @instance
* @overrideable
*/
onFiltersUpdated: function alfresco_lists_AlfList__onFiltersUpdated() {
this.clearViews();
this.loadData();
},
/**
* The list is intended to work co-operatively with other widgets on a page to assist with
* setting the data that should be retrieved. As related widgets are created and publish their initial
* state they may trigger requests to load data. As such, data loading should not be started until
* all the widgets on the page are ready.
*
* @instance
* @param {object} payload
*/
onPageWidgetsReady: function alfresco_lists_AlfList__onPageWidgetsReady(/* jshint unused:false*/ payload) {
this.alfUnsubscribe(this.pageWidgetsReadySubcription);
this._readyToLoad = true;
if (this.currentItemPropertyForDataItems && this.currentItem)
{
this.currentData = {
items: lang.getObject(this.currentItemPropertyForDataItems, false, this.currentItem) || []
};
}
if (this.currentData)
{
this.processLoadedData(this.currentData);
this.renderView();
}
else if (this.loadDataImmediately)
{
this.loadData();
}
else
{
// No action required, wait for either a reload request or data to be published
this.alfLog("log", "This list configured to not load data immediately", this);
}
},
/**
* Iterates over the widgets processed and calls the [registerView]{@link module:alfresco/lists/AlfList#registerView}
* function with each one.
*
* @instance
* @param {object[]} The created widgets
* @param {string} processWidgetsId The ID that the call to the [processWidgets]{@link module:alfresco/core/Core#processWidgets} to
* create the views.
*/
allWidgetsProcessed: function alfresco_lists_AlfList__allWidgetsProcessed(widgets, /*jshint unused:false*/ processWidgetsId) {
if (processWidgetsId === "NEW_VIEW_INSTANCE")
{
this.handleNewViewInstances(widgets);
}
else
{
this.createFilterSubscriptions();
this.registerViews(widgets);
this.completeListSetup();
}
},
/**
*
* @instance
* @param {object[]} widgets The array of widgets created (this should just contain a single view instance)
* @since 1.0.48
*/
handleNewViewInstances: function alfresco_lists_AlfList__handleNewViewInstances(widgets) {
if (widgets.length === 1)
{
var oldView = this.viewMap[this._currentlySelectedView];
if (oldView)
{
// There should only be one view rendered...
var newView = widgets[0];
newView.noItemsMessage = this.noDataMessage;
// Pass useInfiniteScroll to the view
if (this.useInfiniteScroll) {
newView.useInfiniteScroll = true;
}
// Remove the old aspect handle for re-selecting items and apply the aspect to the new view...
var oldAspect = this.viewAspectHandles[this._currentlySelectedView];
oldAspect && oldAspect.remove();
var newAspect = aspect.after(newView, "renderView", lang.hitch(this, this.publishSelectedItems));
this.viewAspectHandles[this._currentlySelectedView] = newAspect;
this.copyViewData(oldView, newView);
// As part of the performance improvements (see AKU-1142) there is a switch
// to using native Promises in view rendering. The AlfListView renderView
// function has been adapted to return a promise but for backwards compatibility
// we need to retain the previous code path (which should be followed when
// rendering a view does not return a promise)...
var renderedView = newView.renderView(this.useInfiniteScroll);
if (renderedView && typeof renderedView.then === "function")
{
renderedView.then(lang.hitch(this, this.onNewViewRendered, oldView, newView));
}
else
{
this.onNewViewRendered(oldView, newView);
}
}
}
},
/**
*
* @param {object} oldView The previous view that is being replaced
* @param {object} newView The view that has now been rendered
* @param {object[]} renderedItems The items rendered (this should be returned)
* @return {Promise|object[]} The promise of the items rendered in the view
* @since 1.0.101
*/
onNewViewRendered: function alfresco_lists_AlfList__onViewRendered(oldView, newView, renderedItems) {
// Clear up the old view...
oldView.clearOldView();
oldView.destroy();
// Show the view...
this.viewMap[this._currentlySelectedView] = newView;
this.showView(newView);
return renderedItems;
},
/**
* This is called from [handleNewViewInstances]{@link module:alfresco/lists/AlfList#handleNewViewInstances}
* when a new view is created to replace the existing view. It allows all relevant data to be copied from
* the old view to the new view.
*
* @instance
* @extendable
* @since 1.0.51
*/
copyViewData: function alfresco_lists_AlfList__copyViewData(oldView, newView) {
// Set the current data...
newView.setData(this.currentData);
newView._currentNode = oldView._currentNode;
},
/**
* Create the subscriptions for the [filteringTopics]{@link module:alfresco/lists/AlfList#filteringTopics}. This is
* called from [allWidgetsProcessed]{@link module:alfresco/lists/AlfList#allWidgetsProcessed}.
*
* @instance
* @param {object[]} The created widgets
* @since 1.0.36.4
*/
createFilterSubscriptions: function alfresco_lists_AlfList__createFilterSubscriptions() {
if (this.filteringTopics)
{
array.forEach(this.filteringTopics, function(topic) {
this.alfSubscribe(topic, lang.hitch(this, this.onFilterRequest));
}, this);
}
},
/**
* This is called from [allWidgetsProcessed]{@link module:alfresco/lists/AlfList#allWidgetsProcessed} to
* determine whether or not to immediately load data or to wait for all of the widgets on the page to be
* created first.
*
* @instance
* @param {object[]} The created widgets
* @since 1.0.36.4
*/
completeListSetup: function alfresco_lists_AlfList__completeListSetup() {
if (this.waitForPageWidgets === true)
{
// Create a subscription to listen out for all widgets on the page being reported
// as ready (then we can start loading data)...
if (this.pageWidgetsReadySubcription)
{
// A subscription already exists, this can occur when an inheriting list module
// calls processWidgets more than once (which will result in allWidgetsProcessed
// being called more than once). We want to avoid multiple subscriptions - see AKU-508
}
else
{
this.pageWidgetsReadySubcription = this.alfSubscribe(topics.PAGE_WIDGETS_READY, lang.hitch(this, this.onPageWidgetsReady), true);
}
}
else
{
// Load data immediately...
this._readyToLoad = true;
this.onPageWidgetsReady();
}
},
/**
* Iterate of the supplied list of widgets (which should all be views) and calling the
* [registerView]{@link module:alfresco/lists/AlfList#registerView} function for each of them. Once
* the views are all registered ensure that the requested view to be initially displayed is rendered.
* This is called from [allWidgetsProcessed]{@link module:alfresco/lists/AlfList#allWidgetsProcessed}.
*
* @instance
* @param {object[]} The created widgets
* @since 1.0.36.4
*/
registerViews: function alfresco_lists_AlfList__registerViews(widgets) {
this.viewMap = {};
this.viewDefinitionMap = {};
this.viewAspectHandles = {};
array.forEach(widgets, lang.hitch(this, this.registerView));
// If no default view has been provided, then just use the first...
if (!this._currentlySelectedView)
{
for (var view in this.viewMap) {
if (this.viewMap.hasOwnProperty(view))
{
this._currentlySelectedView = view;
break;
}
}
}
this.alfPublish(this.viewSelectionTopic, {
value: this._currentlySelectedView,
preference: this.viewPreferenceProperty,
selected: true
});
},
/**
* This is called from [allWidgetsProcessed]{@link module:alfresco/lists/AlfList#allWidgetsProcessed} for
* each widget defined. Only widgets that inherit from [AlfListView]{@link module:alfresco/lists/views/AlfListView}
* will be successfully registered.
*
* @instance
* @param {object} view The view to register
* @param {number} index
*/
registerView: function alfresco_lists_AlfList__registerView(view, index) {
if (view.isInstanceOf(AlfListView))
{
this.alfLog("log", "Registering view", view);
var viewSelectionConfig = view.getViewSelectionConfig();
if (!viewSelectionConfig || !this.isValidViewSelectionConfig(viewSelectionConfig))
{
this.alfLog("error", "The following view does not provide a valid selection menu item upon request", viewSelectionConfig);
}
else
{
// Update the view message to be consistent with the configuration of the list...
view.noItemsMessage = this.noDataMessage;
this.processView(view, index);
}
}
else
{
this.alfLog("warn", "The following widget was provided as a view, but it does not inherit from 'alfresco/lists/views/AlfListView'", view);
}
},
/**
* Processes a registered view. This function essentially adds the view to the
* [viewMap]{@link module:alfresco/lists/AlfList#viewMap}.
*
* @instance
* @param {object} view The view to process
* @param {number} index The view index
*/
processView: function alfresco_lists_AlfList__processView(view, index) {
var viewName = view.getViewName();
if (!viewName)
{
viewName = index;
}
// Pass useInfiniteScroll to the view
if (this.useInfiniteScroll) {
view.useInfiniteScroll = true;
}
// Create a new new menu item using the supplied configuration...
var viewSelectionConfig = view.getViewSelectionConfig();
viewSelectionConfig.value = viewName;
// Check if this is the initially requested view...
if (viewName === this.view)
{
this._currentlySelectedView = viewName;
viewSelectionConfig.checked = true;
}
// Attempt to get a localized version of the label...
viewSelectionConfig.label = this.message(viewSelectionConfig.label);
// After a view has been rendered publish the selected items to ensure
// that selection consistency has been maintained. This approach also ensures
// that where views re-render themselves (e.g. resizing a gallery view)
// that selection will be maintained even if the underlying renderer is destroyed
// and recreated...
var aspectHandle = aspect.after(view, "renderView", lang.hitch(this, this.publishSelectedItems));
this.viewAspectHandles[viewName] = aspectHandle;
// Publish the additional controls...
this.publishAdditionalControls(viewName, view);
// Set the value of the publish topic...
viewSelectionConfig.publishTopic = this.viewSelectionTopic;
viewSelectionConfig.publishPayload = {
preference: this.viewPreferenceProperty
};
// Set a common group for the menu item...
viewSelectionConfig.group = this.viewSelectionMenuItemGroup;
// Make sure to inherit pubSubScope:
viewSelectionConfig.pubSubScope = this.pubSubScope;
// Create a new AlfCheckableMenuItem for selecting the view. This will then be published and any menus that have subscribed
// to the topic defined by "selectionMenuItemTopic" should add the menu item. When the menu item is clicked it will publish
// the selection on the topic defined by the "viewSelectionTopic" (to which this DocumentList instance subscribes) and the
// new view will be rendered...
var selectionMenuItem = new AlfCheckableMenuItem(viewSelectionConfig);
// If the view meets all the required criteria then we can add it for selection...
this.alfPublish(this.selectionMenuItemTopic, {
menuItem: selectionMenuItem
});
this.viewMap[viewName] = view;
this.viewDefinitionMap[viewName] = index;
},
/**
* Gets the additional controls for a view and publishes them.
*
* @instance
* @param {string} viewName The name of the view
* @param {object} view The view to get the controls for.
*/
publishAdditionalControls: function alfresco_lists_AlfList__publishAdditionalControls(viewName, view) {
if (!this.viewControlsMap)
{
this.viewControlsMap = {};
}
// Get any new additional controls (check the map first)
var newAdditionalControls = this.viewControlsMap[viewName];
if (!newAdditionalControls)
{
newAdditionalControls = view.getAdditionalControls();
if (this.additionalViewControlVisibilityConfig)
{
array.forEach(newAdditionalControls, function(control) {
control.visibilityConfig = this.additionalViewControlVisibilityConfig;
this.setupVisibilityConfigProcessing(control);
}, this);
}
this.viewControlsMap[viewName] = newAdditionalControls;
}
// Publish the new additional controls for anyone wishing to display them...
if (newAdditionalControls)
{
this.alfPublish(this.dynamicallyAddWidgetTopic, {
targetId: this.additionalControlsTarget,
targetPosition: 0,
widgets: newAdditionalControls
});
}
},
/**
* By default this just ensures that a label has been provided. However this function could be overridden to provide
* more complete validation if there are specific requirements for view selection configuration.
*
* @instance isValidViewSelectionConfig
* @param {object} viewSelectionConfig The configuration to validate
* @return {boolean} Either true or false depending upon the validity of the supplied configuration.
*/
isValidViewSelectionConfig: function alfresco_lists_AlfList__isValidViewSelectionConfig(viewSelectionConfig) {
return (viewSelectionConfig.label && viewSelectionConfig.label !== "");
},
/**
* Handles requests to switch views. This is called whenever the [viewSelectionTopic]{@link module:alfresco/documentlibrary/_AlfDocumentListTopicMixin#viewSelectionTopic}
* topic is published on and expects a payload containing an attribute "value" which should map to a registered
* [view]{@link module:alfresco/lists/views/AlfListView}. The views are mapped against the index they were configured
* with so the value is expected to be an integer.
*
* @instance
* @param {object} payload The payload published on the view selection topic.
*/
onViewSelected: function alfresco_lists_AlfList__onViewSelected(payload) {
if (!this.currentData)
{
this.alfLog("warn", "There is no data to render a view with");
this.showNoDataMessage();
}
else if (!payload || !payload.value)
{
this.alfLog("warn", "A request was made to select a view, but not enough information was provided", payload);
}
else if (this._currentlySelectedView === payload.value)
{
// The requested view is the current view. No action required.
}
else if (!this.viewMap[payload.value])
{
// An invalid view was requested. Each view will have been mapped to the order in which it occurred in the
// the "widgets" array provided for the DocumentList. The payload value attribute should correspond to an
// index from that array...
this.alfLog("error", "A request was made to select a non-existent view. Requested view: ", payload.value, " from: ", this.viewMap);
}
else
{
// Just do some double-checking to be sure...
if (typeof this.viewMap[payload.value].renderView === "function")
{
// Render the selected view...
this._currentlySelectedView = payload.value;
var newView = this.viewMap[payload.value];
this.showRenderingMessage();
// See AKU-792 - We need to use setTimeout in order to ensure that the rendering message is displayed. The browser needs
// to have a change to draw the message before starting the rendering process. But Firefox (poor old Firefox)
// needs to have a "proper" break to give it a chance...
var delay = sniff("ff") ? 250 : 0;
setTimeout(lang.hitch(this, function() {
newView.setData(this.currentData);
newView.currentData.previousItemCount = 0;
// As part of the performance improvements (see AKU-1142) there is a switch
// to using native Promises in view rendering. The AlfListView renderView
// function has been adapted to return a promise but for backwards compatibility
// we need to retain the previous code path (which should be followed when
// rendering a view does not return a promise)...
var renderedView = newView.renderView(false);
if (renderedView && typeof renderedView.then === "function")
{
renderedView.then(lang.hitch(this, this.showView, newView));
}
else
{
this.showView(newView);
}
}), delay);
}
else
{
this.alfLog("error", "A view was requested that does not define a 'renderView' function", this.viewMap[payload.value]);
}
}
},
/**
* Iterates over all the views and calls their
* [clearOldView]{@link module:alfresco/lists/views/AlfListView#clearOldView} function.
*
* @instance
*/
clearViews: function alfresco_lists_AlfList__clearViews() {
// Publish the clear selected items topic to ensure that any selected items menus
// don't retain stale data
this.alfPublish(topics.CLEAR_SELECTED_ITEMS);
for (var viewName in this.viewMap)
{
if (this.viewMap.hasOwnProperty(viewName))
{
var view = this.viewMap[viewName];
if (typeof view.clearOldView === "function")
{
view.clearOldView();
}
}
}
},
/**
* Hides all the children of the supplied DOM node by applying the "share-hidden" CSS class to them.
*
* @instance
* @param {Element} targetNode The DOM node to hide the children of.
*/
hideChildren: function alfresco_lists_AlfList__hideChildren(targetNode) {
this.hideLoadingMessage();
array.forEach(targetNode.children, function(node) {
domClass.add(node, "share-hidden");
});
},
/**
* If there is no data to render a view with then this function will be called to update the DocumentList
* view node with a message explaining the situation.
*
* @instance
*/
showNoDataMessage: function alfresco_lists_AlfList__showNoDataMessage() {
this.hideChildren(this.domNode);
if (!this.requestInProgress)
{
domClass.remove(this.noDataNode, "share-hidden");
}
},
/**
* If there is no data to render a view with then this function will be called to update the DocumentList
* view node with a message explaining the situation.
*
* @instance
*/
showDataLoadFailure: function alfresco_lists_AlfList__showDataLoadFailure() {
this.hideChildren(this.domNode);
domClass.remove(this.dataFailureNode, "share-hidden");
if (this.widgetsForDataFailureDisplay && !this._dataFailureWidgetsRendered)
{
var wc = new WidgetsCreator({
widgets: this.widgetsForDataFailureDisplay
});
domConstruct.empty(this.dataFailureNode);
wc.buildWidgets(this.dataFailureNode);
this._dataFailureWidgetsRendered = true;
}
},
/**
* This is called before a request to load more data is made so that the user is aware that data
* is being asynchronously loaded.
*
* @instance
*/
showLoadingMessage: function alfresco_lists_AlfList__showLoadingMessage() {
if (!this.useInfiniteScroll)
{
clearTimeout(this._hideLoadingTimeoutPointer);
(requestAnimationFrame || setTimeout)(lang.hitch(this, function() {
domClass.add(this.domNode, "alfresco-lists-AlfList--loading");
this._hideLoadingTimeoutPointer = setTimeout(lang.hitch(this, this.hideLoadingMessage), this.hideLoadingTimeoutDuration);
}));
}
else
{
domClass.add(this.dataFailureNode, "share-hidden");
domClass.remove(this.dataLoadingMoreNode, "share-hidden");
}
},
/**
* Remove any loading displays.
*
* @instance
* @since 1.0.48
*/
hideLoadingMessage: function alfresco_lists_AlfList__hideLoadingMessage() {
setTimeout(lang.hitch(this, function() {
// AKU-875: Test domNode exists before using it.
if (this.domNode) {
domClass.remove(this.domNode, ["alfresco-lists-AlfList--loading", "alfresco-lists-AlfList--rendering"]);
}
}), this.hideLoadingDelay);
},
/**
* This is called once data has been loaded but before the view rendering begins. This can be useful
* when there is a lot of data and the view is complex to render so may not be instantaneous.
*
* @instance
*/
showRenderingMessage: function alfresco_lists_AlfList__showRenderingMessage() {
if (!this.useInfiniteScroll)
{
// See AKU-792 - we definitely don't want to use requestAnimationFrame here - we want the rendering message
// to appear immediately. With requestAnimationFrame we won't see the message until *after*
// the view has been rendered.
domClass.replace(this.domNode, "alfresco-lists-AlfList--rendering","alfresco-lists-AlfList--loading");
}
},
/**
* @instance
* @param {object} view The view to show
* @param {object[]} renderedItems The items rendered (this should be returned)
* @return {Promise|object[]} The promise of the items rendered in the view
* @fires module:alfresco/core/topics#VIEW_RENDERING_COMPLETE
*/
showView: function alfresco_lists_AlfList__showView(view, renderedItems) {
this.hideChildren(this.domNode);
if (this.viewsNode.children.length > 0)
{
this.viewsNode.removeChild(this.viewsNode.children[0]);
}
// Add the new view...
domConstruct.place(view.domNode, this.viewsNode);
domClass.remove(this.viewsNode, "share-hidden");
// Tell the view that it's now on display...
view.onViewShown();
// Attempt to focus a specific item if requested...
if (this._focusItemKey)
{
view.focusOnItem(this._focusItemKey);
}
// After a view has been rendered publish the selected items to ensure
// that selection consistency has been maintained. This approach also ensures
// that where views re-render themselves (e.g. resizing a gallery view)
// that selection will be maintained even if the underlying renderer is destroyed
// and recreated...
this.publishSelectedItems();
// Wait for rendering to complete before publishing that it has completed...
window.requestAnimationFrame(lang.hitch(this, this.alfPublish, topics.VIEW_RENDERING_COMPLETE));
return renderedItems;
},
/**
* Handles requests to reload the current list data by clearing all the previous data and then calling
* [loadData]{@link module:alfresco/lists/AlfList#loadData}.
*
* @instance
* @param {object} [payload] The payload supplied when making the reload request.
* @param {string} [payload.focusItemKey] An item to focus on if it is in the data that is reloaded
*/
onReloadData: function alfresco_lists_AlfList__onReloadData(payload) {
this.hideChildren(this.domNode);
this.clearViews();
// See AKU-1020 - Support the ability to load data with an item to focus on...
var parameters = {};
if (payload && payload.focusItemKey)
{
parameters.focusItemKey = payload.focusItemKey;
}
this.loadData(parameters);
},
/**
* Makes a request to load data from the repository. If the request is successful then then the
* [onDataLoadSuccess]{@link module:alfresco/lists/AlfList#onDataLoadSuccess}
* function will be called. If the request fails then the
* [onDataLoadFailure]{@link module:alfresco/lists/AlfList#onDataLoadFailure}
* function will be called.
*
* @instance
* @param {object} [parameters] An optional parameters object providing information about the data to load
* @param {string} [parameters.focusItemKey] An item to focus on if it is in the data that is reloaded
* @fires module:alfresco/core/topics#STOP_XHR_REQUEST
*/
loadData: function alfresco_lists_AlfList__loadData(parameters) {
if (!this.requestInProgress)
{
// Ensure any no data node is hidden...
domClass.add(this.noDataNode, "share-hidden");
this.showLoadingMessage();
var payload;
if (this.loadDataPublishPayload)
{
payload = lang.clone(this.loadDataPublishPayload);
}
else
{
payload = {
alfSuccessTopic: this.pubSubScope + this.loadDataSuccessTopic,
alfFailureTopic: this.pubSubScope + this.loadDataFailureTopic
};
}
!payload.alfSuccessTopic && (payload.alfSuccessTopic = this.pubSubScope + this.loadDataSuccessTopic);
!payload.alfFailureTopic && (payload.alfFailureTopic = this.pubSubScope + this.loadDataFailureTopic);
// Generate and set a requestId. If the service supports passing this in the XHR request
// (which is not guaranteed) then this allows for the opportunity to cancel the request
// if a second request is made before the first completes...
this.currentRequestId = this.generateUuid();
payload.requestId = this.currentRequestId;
if (!payload.alfResponseTopic)
{
payload.alfResponseTopic = this.pubSubScope + this.loadDataPublishTopic;
}
else
{
payload.alfResponseTopic = this.pubSubScope + payload.alfResponseTopic;
}
if (this.dataFilters)
{
payload.dataFilters = this.dataFilters;
}
this.updateLoadDataPayload(payload);
this.requestInProgress = true;
// Locally register an item key to find and focus on when the data is loaded (the
// key may not match an item in the loaded data of course)...
if (parameters && parameters.focusItemKey)
{
this._focusItemKey = parameters.focusItemKey;
}
else
{
this._focusItemKey = null;
}
setTimeout(lang.hitch(this, this.alfPublish, this.loadDataPublishTopic, payload, true));
}
else
{
this.pendingLoadRequest = true;
if (this.currentRequestId)
{
this.alfPublish(topics.STOP_XHR_REQUEST, {
requestId: this.currentRequestId
}, true);
}
}
},
/**
* This is an extension point for extending modules to use. By default it does nothing to
* the supplied payload.
*
* @instance
* @param {object} payload The payload object to update
*/
updateLoadDataPayload: function alfresco_lists_AlfList__updateLoadDataPayload(payload) {
// jshint unused:false
// Does nothing by default.
},
/**
* Handles successful calls to get data from the repository.
*
* @instance
* @param {object} response The response object
* @param {object} originalRequestConfig The configuration that was passed to the [serviceXhr]{@link module:alfresco/core/CoreXhr#serviceXhr} function
*/
onDataLoadSuccess: function alfresco_lists_AlfList__onDataLoadSuccess(payload) {
// There is a pending load request, this will typically be the case when a new filter has been
// applied before the last request has returned. By requesting another data load the latest
// filters will be requested...
if (this.pendingLoadRequest === true)
{
this.alfLog("log", "Found pending request, loading data...");
this.requestInProgress = false;
this.pendingLoadRequest = false;
this.loadData();
}
else
{
this.alfLog("log", "Data Loaded", payload, this);
var foundItems = false;
if (!this.itemsProperty)
{
this.currentData = {};
this.currentData.items = payload.response;
foundItems = true;
}
else
{
var items = lang.getObject(this.itemsProperty, false, payload.response);
if (!items)
{
// As a fallback we're going to check the actual payload object...
// It would be reasonable to ask why we don't just look in payload initially and
// expect the "itemsProperty" to include "response", however that is not the most common
// scenario and this approach catches the edge cases...
items = lang.getObject(this.itemsProperty, false, payload);
}
if (items)
{
this.currentData = {};
this.currentData.items = items;
foundItems = true;
// We lose metaData unless we store that as well.
var metadata = lang.getObject(this.metadataProperty, false, payload.response);
if (metadata)
{
this.currentData.metadata = metadata;
}
}
else
{
this.alfLog("warn", "Failure to retrieve items with given itemsProperty: " + this.itemsProperty, this);
this.showDataLoadFailure();
}
}
if (foundItems)
{
this.currentData.filters = payload.requestConfig && this._extractFilters(payload.requestConfig);
this.processLoadedData(payload.response || this.currentData);
this.renderView();
this.retainPreviousItemSelectionState(items);
}
// This request has finished, allow another one to be triggered.
this.alfPublish(this.requestFinishedTopic, {});
}
},
/**
* This function renders the view with the current data.
*
* @instance
*/
renderView: function alfresco_lists_AlfList__renderView() {
// Re-render the current view with the new data...
var view = this.viewMap[this._currentlySelectedView];
if (view)
{
if (this.useInfiniteScroll)
{
view.augmentData(this.currentData);
this.currentData = view.getData();
var renderedView = view.renderView(this.useInfiniteScroll);
if (renderedView && typeof renderedView.then === "function")
{
renderedView.then(lang.hitch(this, this.showView, view));
}
else
{
this.showView(view);
}
}
else
{
// We need to clone the widget configuration that was used to populate the original
// view. It needs to be cloned so that the original preferred ID of the widget is preferred
// (otherwise it will be set in the original model and re-used)...
var index = this.viewDefinitionMap[this._currentlySelectedView];
var clonedWidgets = [JSON.parse(JSON.stringify(this.widgets[index]))];
if (this.viewModifiers)
{
this.processObject(this.viewModifiers, clonedWidgets);
}
this.processWidgets(clonedWidgets, null, "NEW_VIEW_INSTANCE");
}
}
// Hide any messages
this.hideLoadingMessage();
},
/**
* Publishes the details of the documents that have been loaded (primarily for multi-selection purposes)
* and stores any requested starting index and total records data.
*
* @instance
* @param {object} response The original response.
*/
processLoadedData: function alfresco_lists_AlfList__processLoadedData(response) {
// Publish the details of the loaded documents. The initial use case for this was to allow
// the selected items menu to know how many items were available for selection but it
// clearly has many other uses...
this.totalRecords = this.currentData.items ? this.currentData.items.length : 0;
this.startIndex = 0;
if (response !== null)
{
var tmp = lang.getObject(this.totalResultsProperty, false, response);
if (tmp)
{
this.totalRecords = tmp;
}
tmp = lang.getObject(this.startIndexProperty, false, response);
if (tmp)
{
this.startIndex = tmp;
}
}
this.currentData.totalRecords = this.totalRecords;
this.currentData.startIndex = this.startIndex;
this.alfPublish(this.documentsLoadedTopic, {
documents: this.currentData.items,
totalRecords: this.totalRecords,
startIndex: this.startIndex
});
},
/**
* Handles failed calls to get data from the repository.
*
* @instance
* @param {object} response The response object
* @param {object} originalRequestConfig The configuration that was passed to the [serviceXhr]{@link module:alfresco/core/CoreXhr#serviceXhr} function
*/
onDataLoadFailure: function alfresco_lists_AlfList__onDataLoadFailure(response, originalRequestConfig) {
this.alfLog("error", "Data Load Failed", response, originalRequestConfig);
this.currentData = null;
// Only show a failure if there is not another request pending...
if (!this.pendingLoadRequest)
{
this.showDataLoadFailure();
}
this.alfPublish(this.documentLoadFailedTopic, {});
this.alfPublish(this.requestFinishedTopic, {});
},
/**
* This is an extension point function that performs no action but can be overridden by
* extending modules.
*
* @instance
* @param payload
*/
onScrollNearBottom: function alfresco_lists_AlfList__onScrollNearBottom(payload) {
// jshint unused:false
// No action by default - this is an extension point only.
},
/**
* Triggered when a request is in progress to prevent multiple submissions.
*
* @instance
*/
onRequestInProgress: function alfresco_lists_AlfList__onRequestInProgress() {
this.requestInProgress = true;
},
/**
* Triggered when a request has finished to allow another submission.
*
* @instance
*/
onRequestFinished: function alfresco_lists_AlfList__onRequestFinished() {
this.requestInProgress = false;
if (this.pendingLoadRequest)
{
this.pendingLoadRequest = false;
this.loadData();
}
},
/**
* Extract the filter objects from the supplied successful requestConfig
*
* @instance
* @param {Object} requestConfig The request config from a successful request
* @returns {Object[]} The filters in the request (empty if none found)
*/
_extractFilters: function alfresco_lists_AlfList___extractFilters(requestConfig) {
var filters = [];
if (requestConfig.url) {
var paramString = requestConfig.url.split("?")[1],
paramObj = (paramString && ioQuery.queryToObject(paramString)) || {},
filterStrings = paramObj.filters ? paramObj.filters.split(",") : [];
filters = array.map(filterStrings, function(filter) {
var filterParts = filter.split("|");
return {
name: filterParts[0],
value: filterParts[1]
};
});
}
return filters;
}
});
});