Source: lists/AlfHashList.js

/**
 * 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/>.
 */

/**
 * This extends the [basic list]{@link module:alfresco/lists/AlfList} to provide support
 * for reloading data based on relevant changes to the browser URL hash. The hash parameters that
 * are relevant (i.e. that when changed should cause the data to be reloaded) should be defined 
 * in the [hashVarsForUpdate attribute]{@link module:alfresco/lists/AlfHashList#hashVarsForUpdate}.
 * 
 * @module alfresco/lists/AlfHashList
 * @extends module:alfresco/lists/AlfList
 * @mixes module:alfresco/documentlibrary/_AlfHashMixin
 * @author Dave Draper
 */
define(["dojo/_base/declare",
        "alfresco/lists/AlfList", 
        "alfresco/documentlibrary/_AlfHashMixin",
        "dojo/_base/array",
        "dojo/_base/lang",
        "alfresco/util/hashUtils",
        "dojo/io-query"], 
        function(declare, AlfList, _AlfHashMixin, array, lang, hashUtils, ioQuery) {
   
   return declare([AlfList, _AlfHashMixin], {
      
      /**
       * Indicates whether the location should be driven by changes to the browser URL hash
       *
       * @instance
       * @type {boolean}
       * @default
       */
      useHash: false,

      /**
       * An array of hash variables that when changed will trigger a reload
       *
       * @instance
       * @type {array}
       * @default []
       */
      hashVarsForUpdate: [],

      /**
       * Indicates whether or not the [hashVarsForUpdate]{@link module:alfresco/lists/AlfHashList#hashVarsForUpdate}
       * that are present in the current hash should be mapped directly into the 
       * [loadDataPublishPayload]{@link module:alfresco/lists/AlfList#loadDataPublishPayload}.
       *
       * @instance
       * @type {boolean}
       * @default
       */
      mapHashVarsToPayload: false,

      /**
       * Indicates whether or not changing URL hash parameters should be set as instance variables
       * on the list.
       *
       * @instance
       * @type {boolean}
       * @default
       */
      updateInstanceValues: false,

      /**
       * Determines whether or not a locally stored hash value should be maintained and re-used in the event of 
       * a hash not being found in the URL. This was added to support the scenario where a user might leave and 
       * then return to a page having lost the hash (e.g. actions on search).
       *
       * @instance
       * @type {boolean}
       * @default
       */
      useLocalStorageHashFallback: false,

      /**
       * The key to use for storing the hash when the [useLocalStorageHashFallback]{@link module:alfresco/lists/AlfHashList#useLocalStorageHashFallback}
       * is set to true.
       *
       * @instance
       * @type {string}
       * @default
       */
      useLocalStorageHashFallbackKey: "ALF_LOCAL_STORAGE_HASH",

      /**
       * This is used to keep track of any filters that are removed from the URL hash. It is reset in each call to
       * [onFiltersUpdated]{@link module:alfresco/lists/AlfHashList#onFiltersUpdated} and then incremented each time
       * a filter is removed (because it has become the empty string). This is then referenced in the
       * [onHashChange]{@link module:alfresco/lists/AlfHashList#onHashChange} in order to determine whether or not 
       * data reloading needs to occur as a result of filter removal.
       *
       * @instance
       * @type {number}
       * @default
       * @since 1.0.34
       */
      ___filtersRemoved: 0,

      /**
       * Extends the [inherited function]{@link module:alfresco/lists/AlfList#postCreate} to ensure that all views
       * have access to the [useHash]{@link module:alfresco/lists/AlfHashList#useHash} configuration. This is important 
       * because it may be necessary for them to update child widgets to reflect hash data in some way (this has been 
       * specifically added to address the issue of sort indication rendered by 
       * [HeaderCells]{@link module:alfresco/lists/views/layouts/HeaderCell}).
       * 
       * @instance
       * @since 1.0.56
       */
      postCreate: function alfresco_lists_AlfList__postCreate() {
         if (this.useHash)
         {
            if (this.widgets) {
               array.forEach(this.widgets, function(view) {
                  if (view)
                  {
                     if (!view.config)
                     {
                        view.config = {};
                     }
                     view.config.useHash = true;
                  }
               });
            }
         }
         this.inherited(arguments);
      },


      /**
       * The AlfHashList 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. This function extends the [inherited function]{@link module:alfresco/lists/AlfList#onPageWidgetsReady}
       * to honour the [useHash configuration]{@link module:alfresco/lists/AlfList#useHash} and load data
       * appropriately.
       *
       * @instance
       * @param {object} payload
       */
      onPageWidgetsReady: function alfresco_lists_AlfHashList__onPageWidgetsReady(/*jshint unused:false*/payload) {
         /*jshint maxcomplexity:false*/

         // Remove the subscription to ensure it's only processed once...
         this.alfUnsubscribe(this.pageWidgetsReadySubcription);

         this._readyToLoad = true;
         if (this.useHash)
         {
            // Only subscribe to filter changes if 'useHash' is set to true. This is because multiple DocLists might
            // be required on the same page and they can't all feed off the hash to drive the location.
            this.alfSubscribe(this.hashChangeTopic, lang.hitch(this, this.onHashChanged));

            var hashString = hashUtils.getHashString();
            if (hashString === "")
            {
               try 
               {
                  if (this.useLocalStorageHashFallback === true && 
                      ("localStorage" in window && window.localStorage !== null))
                  {
                     // No hash has been provided, check local storage for last hash...
                     var locallyStoredHash = localStorage.getItem(this.useLocalStorageHashFallbackKey);
                     hashString = (locallyStoredHash !== null) ? locallyStoredHash : "";
                     this.alfSubscribe(this.hashChangeTopic, lang.hitch(this, this.updateLocallyStoredHash));
                  }
               }
               catch(e)
               {
                  // No action when error occurs. The only reason that we're wrapping the local storage
                  // utilization in a try/catch block is to prevent failures when Firefox is used with
                  // SSO with cookies disabled. See MNT-16167 for details.
               }

               if (hashString)
               {
                  hashUtils.setHash(hashString);
               }
               else if (this.currentFilter)
               {
                  this.alfPublish("ALF_NAVIGATE_TO_PAGE", {
                     url: ioQuery.objectToQuery(this.currentFilter),
                     type: "HASH"
                  }, true);
               }
               else
               {
                  this.loadData();
               }
            }
            else 
            {
               try 
               {
                  if (this.useLocalStorageHashFallback === true && 
                      ("localStorage" in window && window.localStorage !== null))
                  {
                     // Store the initial hash...
                     localStorage.setItem(this.useLocalStorageHashFallbackKey, hashString);
                  }
               }
               catch(e)
               {
                  // No action when error occurs. The only reason that we're wrapping the local storage
                  // utilization in a try/catch block is to prevent failures when Firefox is used with
                  // SSO with cookies disabled. See MNT-16167 for details.
               }

               var currHash = ioQuery.queryToObject(hashString);
               this.doHashVarUpdate(currHash, this.updateInstanceValues);
               this._updateCoreHashVars(currHash);
               this.loadData();
            }
         }
         else
         {
            // When not using a URL hash (e.g. because this DocList is being used as a secondary item - 
            // maybe as part of a picker, etc) then we need to load the initial data set using the instance
            // variables provided. We also need to subscribe to topics that indicate that the location has 
            // changed. Each view renderer that registers a link will need to set a "linkClickTopic" and this
            // should be matched by the "linkClickTopic" of this instance)
            this.alfSubscribe(this.linkClickTopic, lang.hitch(this, this.onItemLinkClick));
            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);
            }
         }
      },

      /**
       * Extends the [inherited function]{@link module:alfresco/lists/AlfList#updateLoadDataPayload} to map
       * the [hashVarsForUpdate]{@link module:alfresco/lists/AlfHashList#hashVarsForUpdate} values into the
       * [loadDataPublishPayload]{@link module:alfresco/lists/AlfList#loadDataPublishPayload} if 
       * [mapHashVarsToPayload]{@link module:alfresco/lists/AlfHashList#mapHashVarsToPayload}
       * is set to true.
       *
       * @instance
       * @param {object} payload The payload to update
       */
      updateLoadDataPayload: function alfresco_lists_AlfHashList__updateLoadDataPayload(payload) {
         this.inherited(arguments);
         var currHash = hashUtils.getHash();
         if (this.mapHashVarsToPayload && this.doHashVarUpdate(currHash))
         {
            array.forEach(this.hashVarsForUpdate, function(hashName) {
               var hashValue;
               if(currHash.hasOwnProperty(hashName))
               {
                  hashValue = currHash[hashName];
                  if(hashValue !== null && typeof hashValue !== "undefined") {
                     payload[hashName] = hashValue;
                  }
                  else
                  {
                     delete payload[hashName];
                  }
               }
            }, this);
            this.alfLog("log", "LoadDataPayload updated", this);
         }
      },

      /**
       * Sets the current hash in the local storage.
       *
       * @instance
       * @param {object} payload
       */
      updateLocallyStoredHash: function alfresco_lists_AlfHashList__updateLocallyStoredHash(/*jshint unused:false*/payload) {
         // Save the hash to local storage if required...
         try
         {
            if(this.useLocalStorageHashFallback === true && 
               ("localStorage" in window && window.localStorage !== null))
            {
               localStorage.setItem(this.useLocalStorageHashFallbackKey, hashUtils.getHashString());
            }
         }
         catch(e)
         {
            // No action when error occurs. The only reason that we're wrapping the local storage
            // utilization in a try/catch block is to prevent failures when Firefox is used with
            // SSO with cookies disabled. See MNT-16167 for details.
         }
      },

      /**
       * This function is called whenever the browser URL hash fragment is changed.e end of the fragment
       * 
       * @instance
       * @param {object} payload
       * @returns {bool} true if data was loaded as a result of the hash-change
       */
      onHashChanged: function alfresco_lists_AlfHashList__onHashChanged(payload) {
         // Process the hash...
         var dataLoaded = false;
         if(this.doHashVarUpdate(payload, this.updateInstanceValues) || this.___filtersRemoved)
         {
            this._updateCoreHashVars(payload);
            if (this._readyToLoad)
            {
               if (this.useInfiniteScroll)
               {
                  this.clearViews();
               }
               this.loadData();
               dataLoaded = true;
            }
         }
         return dataLoaded;
      },

      /**
       * This is an extension point function that is provided to allow core URL hash parameters to be
       * addressed when the URL hash change triggers a reload. For example this allows the
       * [AlfSortablePaginatedList]{@link module:alfresco/lists/AlfSortablePaginatedList} to address
       * hash parameters relating to sorting and pagination and the 
       * [AlfDocumentList]{@link module:alfresco/documentlibrary/AlfDocumentList} to further address
       * hash parameters relating to filtering.
       *
       * @instance
       * @param {object} hashParameters An object containing the current hash parameters
       */
      _updateCoreHashVars: function alfresco_lists_AlfHashList___updateCoreHashVars(hashParameters) {
         // jshint unused:false
         // No action by default
      },

      /**
       * Extends the [inherited function]{@link module:alfresco/lists/AlfList#showView} to check
       * whether or not a URL hash parameter is set to indicate a particular item to bring into view.
       * This URL hash parameter checked is the "currentItem" and if this is found it will publish the
       * value on the "ALF_BRING_ITEM_INTO_VIEW" topic.
       *
       * @instance
       * @param  {object} payload The payload containing the loaded data.
       * @fires module:alfresco/lists/views/ListRenderer~event:ALF_BRING_ITEM_INTO_VIEW
       * @since 1.0.101
       */
      showView: function alfresco_lists_AlfHashList__showView(/*jshint unused:false*/ view) {
         this.inherited(arguments);
         var currHash = hashUtils.getHash();
         if (currHash.currentItem)
         {
            window.requestAnimationFrame(lang.hitch(this, function() {
               this.alfPublish("ALF_BRING_ITEM_INTO_VIEW", {
                  item: currHash.currentItem
               });
            }));
         }
      },

      /**
       * Handle filters being updated
       *
       * @instance
       */
      onFiltersUpdated: function alfresco_lists_AlfHashList__onFiltersUpdated() {
         // Reset the filtersRemoved count
         this.___filtersRemoved = 0;

         var filterValues = {};
         this.dataFilters = array.filter(this.dataFilters, function(dataFilter) {
            var filterValue = dataFilter.value;
            if(filterValue !== null && typeof filterValue !== "undefined") 
            {
               if(typeof filterValue === "string") 
               {
                  filterValue = lang.trim(filterValue);
                  if(!filterValue.length) 
                  {
                     // Remove empty strings from hash and update the count of filters removed
                     filterValue = null; 
                     this.___filtersRemoved++;
                  }
               }
            }
            filterValues[dataFilter.name] = filterValue;
            return !!filterValue;
         }, this);

         if (this.useHash)
         {
            hashUtils.updateHash(filterValues);
         }
         else 
         {
            this.clearViews();
            this.loadData();
         }
      }
   });
});