Source: search/FacetFilters.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/>.
 */

/**
 * @module alfresco/search/FacetFilters
 * @extends alfresco/documentlibrary/AlfDocumentFilters
 * @author Dave Draper
 */
define(["dojo/_base/declare",
        "alfresco/documentlibrary/AlfDocumentFilters",
        "alfresco/core/topics",
        "alfresco/search/FacetFilter",
        "dojo/_base/lang",
        "dojo/_base/array",
        "dojo/dom-class",
        "dojo/on",
        "dijit/registry"], 
        function(declare, AlfDocumentFilters, topics, FacetFilter, lang, array, domClass, on, registry) {

   return declare([AlfDocumentFilters], {
      
      /**
       * @instance
       * @type {string}
       * @default
       */
      filterPrefsName: "docListFilterPref",
      
      /**
       * This should be configured to be a QName for use as a facet
       *
       * @instance
       * @type {string}
       * @default
       */
      facetQName: null,

      /**
       * Indicates whether or not the full width of the [child filters]{@link module:alfresco/search/FacetFilter}
       * (including the count and the white space) can be clicked to toggle the filter. The corresponding 
       * [attribute]{@link module:alfresco/search/FacetFilter#fullWidthClick} will be set on each filter.
       * 
       * @instance
       * @type {boolean}
       * @default
       * @since 1.0.47
       */
      fullWidthClick: false,

      /**
       * The number of filters that should be shown for each facet. If this is null then there is no maximum
       *
       * @instance
       * @type {number}
       * @default
       */
      maxFilters: null,

      /**
       * The number hits that a filter should have in order for it to be shown.
       *
       * @instance
       * @type {number}
       * @default
       */
      hitThreshold: null,

      /**
       * The length (in number of characters) that a filter should have in order for it to be shown. This is particularly
       * relevant for facets similar to description where lots of short meaningless filters might be returned.
       *
       * @instance
       * @type {number}
       * @default
       */
      minFilterValueLength: null,

      /**
       * How the filters are ordered. The options are:
       * <ul><li>ALPHABETICALLY (sort alphabetically by label)</li>
       * <li>ASCENDING (ascending number of hits)</li>
       * <li>DESCENDING (descending number of hits)</li></ul>
       *
       * @instance
       * @type {string}
       * @default
       */
      sortBy: "ALPHABETICALLY",

      /**
       * Pass through setting for useHash on initialised FacetFilter widgets
       *
       * @instance
       * @type {boolean}
       * @default
       */
      useHash: false,

      /**
       * Processes any widgets defined in the configuration for this instance.
       * 
       * @instance
       */
      postCreate: function alfresco_search_FacetFilters__postCreate() {
         this.inherited(arguments);

         // PLEASE NOTE: It's possible that this subscription is no longer required, since publications
         //              are blocked until page loading completes anyway. However, this code has been
         //              left as is until such time that we can be sure we want to just call
         //              publishFacets as soon as the widget is created (see AKU-477)
         this.alfSubscribe(topics.PAGE_WIDGETS_READY, lang.hitch(this, this.publishFacets), true);
      },

      /**
       * This has been added to support the initial implementation of date and size faceting against
       * Solr 1.4. The search service will always include modification/creation date and file size
       * filter data in all search responses as these are hard-coded into the Search API as facet queries
       * and so requesting those as filters will generate unnecessary and confusing response data
       *
       * @instance
       * @type {boolean}
       * @default
       */
      blockIncludeFacetRequest: false,

      /**
       * 
       * @instance
       */
      publishFacets: function alfresco_search_FacetFilters__publishFacets() {
         if (this.facetQName)
         {
            // Publish the details of the facet for the SearchService to log for inclusion in search requests
            this.alfPublish("ALF_INCLUDE_FACET", {
               qname: this.facetQName,
               blockIncludeFacetRequest: this.blockIncludeFacetRequest
            }, true);

            // Subscribe to facet property results to add facet properties...
            this.alfSubscribe("ALF_FACET_RESULTS_" + this.facetQName, lang.hitch(this, this.processFacetFilters));
         }
         else
         {
            this.alfLog("warn", "No facet QNname was provided for a filter", this);
         }
      },

      /**
       * This function is expectd to handle generation of filters relevant to the current search
       * results. It clears out the previous filters and adds the new ones
       *
       * @instance
       * @param {object} payload The details of the filters to generate
       */
      processFacetFilters: function alfresco_search_FacetFilters__processFacetFilters(payload) {
         this.alfLog("log", "Facet Results received!", payload, this);

         // Reset the more/less options...
         this.moreFiltersList = null;
         domClass.add(this.showMoreNode, "hidden");
         domClass.add(this.showLessNode, "hidden");

         // Remove all previous filters...
         var widgets = registry.findWidgets(this.contentNode);
         array.forEach(widgets, function(widget) {
            widget.destroy();
         });

         // Put all the active filters into a list...
         var activeFilters = [];
         if (payload.activeFilters)
         {
            var encodedActiveFilters = payload.activeFilters.split(",");
            for(var i = 0; i < encodedActiveFilters.length; i++)
            {
               activeFilters.push(decodeURIComponent(encodedActiveFilters[i]));
            }
         }

         // Create a new array and populate with the facet filters...
         var filters = [];
         for (var key in payload.facetResults)
         {
            if (payload.facetResults.hasOwnProperty(key))
            {
               var currFilter = payload.facetResults[key];
               filters.push({
                  label: currFilter.label,
                  value: currFilter.value,
                  hits: currFilter.hits,
                  index: currFilter.index
               });
            }
         }

         // Sort the filters...
         filters = this.sortFacetFilters(filters);
         filters = array.filter(filters, lang.hitch(this, this.filterFacetFilters));

         // Set a count of the filters to add...
         var filtersToAdd = this.maxFilters;

         // Create widgets for each filter...
         array.forEach(filters, function(filter) {
            if (this.minFilterValueLength && filter.value.length < this.minFilterValueLength)
            {
               // Don't add filters that have a value shorter than the minimum specified length
            }
            else if (!this.hitThreshold || this.hitThreshold <= filter.hits)
            {
               // Check to see if this is an active filter...
               var applied = array.some(activeFilters, function(activeFilter) {
                  return activeFilter === this.facetQName.replace(/\.__.u/g, "").replace(/\.__/g, "") + "|" + filter.value;
               }, this);

               // Keeping adding (visible) filters until we've hit the maximum number...
               var filterWidget = new FacetFilter({
                  label: filter.label,
                  hits: filter.hits,
                  filter: filter.value,
                  facet: this.facetQName,
                  applied: applied,
                  useHash: this.useHash,
                  pubSubScope: this.pubSubScope,
                  fullWidthClick: this.fullWidthClick
               });

               if ((filtersToAdd || filtersToAdd === 0) && filtersToAdd <= 0 && !applied)
               {
                  this.addMoreFilter(filterWidget);
               }

               filterWidget.placeAt(this.showMoreNode, "before");

               // Decrement the filter count...
               if (filtersToAdd) 
               {
                  filtersToAdd--;
               }
            }

         }, this);

         if (this.moreFiltersList)
         {
            domClass.remove(this.showMoreNode, "hidden");
         }

         if (filters.length === 0)
         {
            domClass.add(this.domNode, "hidden");
         }
         else
         {
            domClass.remove(this.domNode, "hidden");
         }
      },

      /**
       * Filters out any filter values that do not meet the hits or value length requirements.
       *
       * @instance
       * @param {object} filter The filter to test
       * @param {number} index The index of the filter
       */
      filterFacetFilters: function alfresco_search_FacetFilters__filterFacetFilters(filter, /*jshint unused:false*/ index) {
         return ((!this.hitThreshold || this.hitThreshold <= filter.hits) &&
                 (!this.minFilterValueLength || filter.value.length >= this.minFilterValueLength));
      },

      /**
       * Sorts the filter values based on the sortBy attribute.
       *
       * @instance
       * @param {array} filters The processed filters to sort
       * @returns {array} The sorted filters
       */
      sortFacetFilters: function alfresco_search_FacetFilters__sortFacetFilters(filters) {
         if (!this.sortBy || lang.trim(this.sortBy) === "ALPHABETICALLY")
         {
            return filters.sort(this._alphaSort);
         }
         else if (lang.trim(this.sortBy) === "REVERSE_ALPHABETICALLY")
         {
            return filters.sort(this._reverseAlphaSort);
         }
         else if (lang.trim(this.sortBy) === "ASCENDING")
         {
            return filters.sort(this._ascSort);
         }
         else if (lang.trim(this.sortBy) === "DESCENDING")
         {
            return filters.sort(this._descSort);
         }
         else if (lang.trim(this.sortBy) === "INDEX")
         {
            return filters.sort(this._indexSort);
         }
         else
         {
            return filters;
         }
      },

      /**
       * A function for sorting the facet filter values alphabetically (from a-b)
       *
       * @instance
       * @param {object} a The first filter value object
       * @param {object} b The second filter value object
       * @returns {number} -1, 0 or 1 according to standard array sorting conventions
       */
      _alphaSort: function alfresco_search_FacetFilters___alphaSort(a,b) {
         var result = 0;
         var alc = a.label.toLowerCase();
         var blc = b.label.toLowerCase();
         if(alc < blc)
         {
            result = -1;
         }
         if(alc > blc)
         {
            result = 1;
         }
         return result;
      },

      /**
       * A function for sorting the facet filter values into reverse alphabetical order (from z-a)
       * 
       * @instance
       * @param {object} a The first filter value object
       * @param {object} b The second filter value object
       * @returns {number} -1, 0 or 1 according to standard array sorting conventions
       */
      _reverseAlphaSort: function alfresco_search_FacetFilters___reverseAlphaSort(a, b) {
         var result = 0;
         var alc = a.label.toLowerCase();
         var blc = b.label.toLowerCase();
         if(alc > blc)
         {
            result = -1;
         }
         if(alc < blc)
         {
            result = 1;
         }
         return result;
      },

      /**
       * A function for sorting the facet filter values hits from low to high
       *
       * @instance
       * @param {object} a The first filter value object
       * @param {object} b The second filter value object
       * @returns {number} -1, 0 or 1 according to standard array sorting conventions
       */
      _ascSort: function alfresco_search_FacetFilters___ascSort(a, b) {
         return b.hits - a.hits;
      },

      /**
       * A function for sorting the facet filter values hits from high to low
       *
       * @instance
       * @param {object} a The first filter value object
       * @param {object} b The second filter value object
       * @returns {number} -1, 0 or 1 according to standard array sorting conventions
       */
      _descSort: function alfresco_search_FacetFilter___descSort(a, b) {
         return a.hits - b.hits;
      },

      /**
       * A function for sorting the facet filter value index. The index is a special
       * attribute that is only included on custom facet query fields. These are
       * the created, modified and size properties.
       *
       * @instance
       * @param {object} a The first filter value object
       * @param {object} b The second filter value object
       * @returns {number} -1, 0 or 1 according to standard array sorting conventions
       */
      _indexSort: function alfresco_search_FacetFilter___indexSort(a, b) {
         if ((a.index || a.index === 0) && (b.index || b.index === 0))
         {
            return a.index - b.index;
         }
         else
         {
            return 0;
         }
      }
   });
});