API Docs for: 1.0.0
Show:

File: Resources/public/js/views/services/plugins/ez-searchplugin.js

/*
 * 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-searchplugin', function (Y) {
    "use strict";
    /**
     * Provides the search plugin
     *
     * @module ez-searchplugin
     */
    Y.namespace('eZ.Plugin');

    /**
     * Search plugin. It listens to `locationSearch` and `contentSearch` events
     * and execute the corresponding search.
     *
     * @namespace eZ.Plugin
     * @class Search
     * @constructor
     * @extends eZ.Plugin.ViewServiceBase
     * @since 1.2
     */
    Y.eZ.Plugin.Search = Y.Base.create('searchPlugin', Y.eZ.Plugin.ViewServiceBase, [], {
        initializer: function () {
            this.onHostEvent('*:locationSearch', this._doLocationSearch);
            this.onHostEvent('*:contentSearch', this._doContentSearch);
        },

        /**
         * Returns the content service
         *
         * @method _getContentService
         * @private
         * @return Object
         */
        _getContentService: function () {
            return this.get('host').get('capi').getContentService();
        },

        /**
         * Returns a ViewCreateStruct
         *
         * @method _createNewCreateViewStruct
         * @private
         * @param {String} name
         * @param {String} type
         * @param {Object} search
         * @param {Object} [search.criteria]
         * @param {Object} [search.query]
         * @param {Object} [search.filter]
         * @param {Object} search.sortClauses
         * @param {Number} [search.limit]
         * @param {Number} [search.offset]
         * @return {Object}
         */
        _createNewCreateViewStruct: function (name, type, search) {
            var query,
                contentService = this._getContentService();

            query = contentService.newViewCreateStruct(name, type);
            // TODO ViewCreateStruct should expose an API
            // see https://jira.ez.no/browse/EZP-24808
            if (search.query) {
                query.setQuery(search.query);
            }
            if (search.filter) {
                query.setFilter(search.filter);
            }
            if (search.criteria) {
                query.setCriteria(search.criteria);
                console.log('[DEPRECATED] Criteria property is deprecated');
                console.log('[DEPRECATED] it will be removed from PlatformUI 2.0');
                console.log('[DEPRECATED] Please use Query or Filter instead');
            }

            query.setLimitAndOffset(search.limit, search.offset);
            if ( search.sortClauses ) {
                query.setSortClauses(search.sortClauses);
            } else if (search.sortCondition) {
                query.setSortClauses(this._getSortClause(
                    search.sortCondition.sortField,
                    search.sortCondition.sortOrder
                ));
            } else if ( search.sortLocation ) {
                query.setSortClauses(this._getSortClause(
                    search.sortLocation.get('sortField'),
                    search.sortLocation.get('sortOrder')
                ));
            }

            return query;
        },

        /**
         * `contentSearch` event handler. It executes a content search and calls
         * the callback provided in the event parameters when done.
         *
         * @method _doContentSearch
         * @protected
         * @param {EventFacade} e
         * @param {Object} e.search
         * @param {Object} e.search.criteria the search criteria used as Criteria in ContentQuery
         * @param {Object} e.search.sortClauses the search sort clauses
         * @param {Integer} e.search.offset the offset for the search result
         * @param {Integer} e.search.limit number of records returned
         * @param {Bool} e.loadContentType the flag indicating whether the eZ.ContentType should be loaded for all
         * of the locations searched. If it is set to *TRUE* then `contentType` field will be present inside
         * every search result (struct)
         * @param {Function} e.callback the callback to execute when the search
         * is done
         * @param {false|Error} e.callback.error
         * @param {Array} e.callback.results array of struct objects containing a `content` property
         * and optionally a `contentType` (depending on `loadContentType`)
         * @param {Number} e.callback.count the total number of search result
         */
        _doContentSearch: function (e) {
            var search = Y.merge(e.search);

            search.viewName = e.viewName;
            search.loadContentType = e.loadContentType;
            search.loadLocation = e.loadLocation;
            delete search.callback;
            this.findContent(search, e.callback);
        },

        /**
         * Executes a Content search based on the provided `search` object.
         *
         * @method findContent
         * @param {Object} search
         * @param {String} search.viewName the name of the REST view to use
         * @param {Object} search.criteria (deprecated) the search criteria used as Criteria in ContentQuery
         * @param {Object} search.query  the search query used as Query in ContentQuery
         * @param {Object} search.filter the search filter used as Filter in ContentQuery
         * @param {Object} [search.sortClauses] the sort clauses
         * @param {Number} [search.offset]
         * @param {Number} [search.limit]
         * @param {Boolean} [search.loadContentType] flag indicating whether the
         * Content Type of each result has to be loaded in addition
         * @param {Function} callback
         * @param {Error|null} callback.error
         * @param {Response|Array} callback.result the Response object in case
         * of error or an array of Content struct. A Content struct is object
         * containing the Content item and the Content
         * Type depending on the `loadContentType` flag.
         * @param {Number} callback.resultCount the total result number of the
         * search
         */
        findContent: function (search, callback) {
            var query = this._createNewCreateViewStruct(search.viewName, 'ContentQuery', search),
                contentService = this._getContentService();

            contentService.createView(query, Y.bind(function (error, result) {
                var parsedResult;

                if ( error ) {
                    return callback(error);
                }
                parsedResult = this._parseSearchResult(result, 'content', '_createContent');
                if (parsedResult.length && (search.loadLocation || search.loadContentType)) {
                    this._loadContentResources(
                        search.viewName,
                        search.loadContentType,
                        search.loadLocation,
                        parsedResult,
                        function (error, structs) {
                            callback(error, structs, result.document.View.Result.count);
                        }
                    );
                } else {
                    callback(error, parsedResult, result.document.View.Result.count);
                }
            }, this));
        },

        /**
         * Returns the REST API sort order based on the sortOrder attribute
         *
         * @protected
         * @method _getSortDirection
         * @param {String} sortOrder
         * @return 'ascending' or 'descending'
         */
        _getSortDirection: function (sortOrder) {
            return sortOrder === 'ASC' ? 'ascending' : 'descending';
        },

        /**
         * Returns the REST API sort clause identifier based on the sortField
         * attribute.
         *
         * @protected
         * @method _getSortClauseIdentifier
         * @param {String} sortField
         * @return {String|Null}
         */
        _getSortClauseIdentifier: function (sortField) {
            switch(sortField) {
                case 'MODIFIED':
                    return 'DateModified';
                case 'PUBLISHED':
                    return 'DatePublished';
                case 'PATH':
                    return 'LocationPath';
                case 'SECTION':
                    return 'SectionIdentifier';
                case 'DEPTH':
                    return 'LocationDepth';
                case 'CLASS_IDENTIFIER':
                case 'CLASS_NAME':
                    console.warn(sortField + ' sort order is unsupported');
                    return null;
                case 'PRIORITY':
                    return 'LocationPriority';
                case 'NAME':
                    return 'ContentName';
                default:
                    console.warn("Unknow sortField '" + this.get('sortField') + "'");
                    return 'DateModified';
            }
        },

        /**
         * Returns the sort clause suitable for the Search Plugin based on the
         * Location's sortOrder and sortField attribute.
         *
         * @method getSortClause
         * @param {String} sortField
         * @param {String} sortOrder
         * @return {Object}
         */
        _getSortClause: function (sortField, sortOrder) {
            var clause = {},
                identifier = this._getSortClauseIdentifier(sortField);

            if ( identifier ) {
                clause[identifier] = this._getSortDirection(sortOrder);
            }
            return clause;
        },

        /**
         * Executes a location search based on the provide `search` object.
         *
         * @method findLocations
         * @param {Object} search
         * @param {String} search.viewName the name of the REST view to use
         * @param {Object} search.criteria (deprecated) the search criteria used as Criteria in LocationQuery
         * @param {Object} search.query  the search query used as Query in LocationQuery
         * @param {Object} search.filter the search filter used as Filter in LocationQuery
         * @param {Object} [search.sortClauses] the sort clauses
         * @param {eZ.Location} [search.sortLocation] the Location to use to
         * generate the sort clauses
         * @param {Number} [search.offset]
         * @param {Number} [search.limit]
         * @param {Boolean} [search.loadContent] flag indicating whether the
         * Content item of each result has to be loaded in addition
         * @param {Boolean} [search.loadContentType] flag indicating whether the
         * Content Type of each result has to be loaded in addition
         * @param {Function} callback
         * @param {Error|null} callback.error
         * @param {Response|Array} callback.result the Response object in case
         * of error or an array of Location struct. A Location struct is object
         * containing the Location and/or the Content item and/or the Content
         * Type depending on the `loadContent` and `loadContentType` flags.
         * @param {Number} callback.resultCount the total result number of the
         * search
         */
        findLocations: function (search, callback) {
            var contentService = this._getContentService(),
                query;

            query = this._createNewCreateViewStruct(search.viewName, 'LocationQuery', search);
            contentService.createView(query, Y.bind(function (error, result) {
                var parsedResult = [];

                if ( error ) {
                    return callback(error, result, 0);
                }
                parsedResult = this._parseSearchResult(result, 'location', '_createLocation');
                if (parsedResult.length && (search.loadContentType || search.loadContent)) {
                    this._loadLocationResources(
                        search.viewName,
                        search.loadContentType,
                        search.loadContent,
                        parsedResult,
                        function (error, structs) {
                            callback(error, structs, result.document.View.Result.count);
                        }
                    );
                } else {
                    callback(error, parsedResult, result.document.View.Result.count);
                }
            }, this));
        },

        /**
         * `locationSearch` event handler. It executes the location search and
         * set the result on the target of the event.
         *
         * @method _doLocationSearch
         * @protected
         * @param {EventFacade} e
         * @param {Object} e.search
         * @param {Object} e.search.criteria (deprecated) the search criteria used as Criteria in LocationQuery
         * @param {Object} e.search.query  the search query used as Query in LocationQuery
         * @param {Object} e.search.filter the search filter used as Filter in LocationQuery
         * @param {Object} [e.search.sortClauses] the search sort clauses
         * @param {eZ.Location} [search.sortLocation] the Location to use to
         * generate the sort clauses
         * @param {Integer} e.search.offset the offset for the search result
         * @param {Integer} e.search.limit number of records returned
         * @param {Function} [e.callback] callback to call when search is done
         * @param {String} [e.resultAttribute] the name of attribute that will by updated with search results
         * @param {String} [e.resultTotalCountAttribute] the name of attribute that will be updated with total
         * number of records matching search criteria
         * @param {Bool} [e.loadContent] the flag indicating whether the eZ.Content should be loaded for all
         * of the locations searched. If it is set to *TRUE* then `content` field will be present inside
         * every search result (locationStruct)
         * @param {Bool} [e.loadContentType] the flag indicating whether the eZ.ContentType should be loaded for all
         * of the locations searched. If it is set to *TRUE* then `contentType` field will be present inside
         * every search result (locationStruct)
         */
        _doLocationSearch: function (e) {
            var search = Y.merge(e.search),
                listView = e.target,
                attrs = {'loadingError': false},
                callback = function (error, result, resultCount) {
                    attrs.loadingError = error ? true : false;
                    if ( !error ) {
                        attrs[e.resultAttribute] = result;
                        if ( e.resultTotalCountAttribute ) {
                            attrs[e.resultTotalCountAttribute] = resultCount;
                        }
                    }
                    listView.setAttrs(attrs);
                };

            if ( e.callback ) {
                callback = e.callback;
            }

            search.viewName = e.viewName;
            search.loadContent = e.loadContent;
            search.loadContentType = e.loadContentType;
            this.findLocations(search, callback);
        },

        /**
         * Parses the search result to create a model and return the array of struct
         *
         * @method _parseSearchResult
         * @protected
         * @param {Response} result the CAPI Response
         * @param {String} property the property to fill in the struct
         * @param {String} methodName the name of the method to get the model
         * @return {Array} ret array of stuct
         */
        _parseSearchResult: function (result, property, methodName) {
            var ret = [];

            Y.Array.each(result.document.View.Result.searchHits.searchHit, function (hit) {
                var struct = {};

                struct[property] = this[methodName](hit);
                ret.push(struct);
            }, this);

            return ret;
        },

        /**
         * Creates a Location for the searchHit
         *
         * @method _createLocation
         * @param {Object} searchHit
         * @protected
         * @return {eZ.Location}
         */
        _createLocation: function (searchHit) {
            var Location = this.get('locationModelConstructor'),
                location;

            location = new Location();
            location.loadFromHash(searchHit.value.Location);

            return location;
        },

        /**
         *
         * @method _createContent
         * @param {Object} searchHit
         * @protected
         * @return {eZ.Location}
         */
        _createContent: function (searchHit) {
            var Content= this.get('contentModelConstructor'),
                content;

            content = new Content();
            content.loadFromHash(searchHit.value.Content);

            return content;
        },

        /**
         * Loads resources for the given array of location structs. Depending on given
         * `loadContentType` and `loadContent` bool parameters it loads Content and ContentType
         * and includes them into the location structs.
         *
         * @method _loadResources
         * @protected
         * @deprecated
         * @param {Bool|undefined} loadContentType
         * @param {Bool|undefined} loadContent
         * @param {Array} locationStructArr
         * @param {Function} callback
         */
        _loadResources: function (viewName, loadContentType, loadContent, locationStructArr, callback) {
            console.log('[DEPRECATED] _loadResources is deprecated, it will be removed from PlatformUI 2.0');
            console.log('[DEPRECATED] Use _loadLocationResources instead');
            this._loadLocationResources(viewName, loadContentType, loadContentType, locationStructArr, callback);
        },

        /**
         * Loads resources for the given array of location structs. Depending on
         * given `loadContentType` and `loadContent` bool parameters it loads
         * Content and ContentType and includes them into the location structs.
         *
         * @method _loadLocationResources
         * @protected
         * @param {Bool|undefined} loadContentType
         * @param {Bool|undefined} loadContent
         * @param {Array} locationStructArr
         * @param {Function} callback
         */
        _loadLocationResources: function (viewName, loadContentType, loadContent, locationStructArr, callback) {
            var tasks = new Y.Parallel(),
                loadResourcesError = false;

            if (loadContentType) {
                var endContentTypeLoad = tasks.add(function (error, response) {
                        if (error) {
                            loadResourcesError = true;
                            return;
                        }
                    });

                this._loadContentTypeForStruct(locationStructArr, function (struct) {
                    return struct.location.get('contentInfo').get('resources').ContentType;
                }, endContentTypeLoad);
            }

            if (loadContent) {
                var endContentLoad = tasks.add(function (error, response) {
                        if (error) {
                            loadResourcesError = true;
                            return;
                        }
                    });

                this._loadContentForLocations(viewName, locationStructArr, endContentLoad);
            }

            tasks.done(function () {
                callback(loadResourcesError, locationStructArr);
            });
        },

        /**
         * Loads resources for the given array of content structs. Depending on
         * given `loadContentType` and `loadLocation` bool parameters it loads
         * ContentType and Location and includes them into the content structs.
         *
         * @method _loadContentResources
         * @protected
         * @param {Bool|undefined} loadContentType
         * @param {Bool|undefined} loadLocation
         * @param {Array} contentStructArr
         * @param {Function} callback
         */
        _loadContentResources: function (viewName, loadContentType, loadLocation, contentStructArr, callback) {
            var tasks = new Y.Parallel(),
                loadResourcesError = false;

            if (loadContentType) {
                var endContentTypeLoad = tasks.add(function (error) {
                        if (error) {
                            loadResourcesError = error;
                            return;
                        }
                    });

                this._loadContentTypeForStruct(contentStructArr, function (struct) {
                    return struct.content.get('resources').ContentType;
                }, endContentTypeLoad);
            }

            if (loadLocation) {
                var endContentLoad = tasks.add(function (error) {
                        if (error) {
                            loadResourcesError = error;
                            return;
                        }
                    });

                this._loadLocationForContent(viewName, contentStructArr, endContentLoad);
            }

            tasks.done(function () {
                callback(loadResourcesError, contentStructArr);
            });
        },

        /**
         * Loads ContentType for each location struct in given array and puts it in the new `contentType`
         * field for the location struct.
         *
         * @method _loadContentTypeForLocations
         * @protected
         * @param {Array|Null} structArray
         * @param {Function} getContentTypeIdFunc
         * @param {Function} callback
         */
        _loadContentTypeForStruct: function (structArray, getContentTypeIdFunc, callback) {
            var tasks = new Y.Parallel(),
                loadContentTypeError = false,
                contentTypList = [],
                contentTypeIds = [];

            // Get list of content types ids
            contentTypeIds = Y.Array.map(structArray, function (struct, i) {
                return getContentTypeIdFunc(struct);
            });

            // Get rid of duplicates to avoid loading same item several times
            contentTypeIds = Y.Array.dedupe(contentTypeIds);

            // Load the unique content types
            Y.Array.each(contentTypeIds, function (contentTypeId) {
                var contentType = new Y.eZ.ContentType(
                        {id: contentTypeId}
                    ),
                    capi = this.get('host').get('capi'),
                    options = {api: capi},
                    endContentTypeLoad = tasks.add(function (error, response) {
                        if (error) {
                            loadContentTypeError = error;
                            contentTypList[contentTypeId] = null;
                            return;
                        }
                        contentTypList[contentTypeId] = contentType;
                    });

                contentType.load(options, endContentTypeLoad);
            }, this);

            tasks.done(function () {
                // Add the loaded content types on the structArray
                Y.Array.each(structArray, function (struct, i) {
                    structArray[i].contentType = contentTypList[getContentTypeIdFunc(struct)];
                });

                callback(loadContentTypeError, structArray);
            });
        },

        /**
         * Loads Content for each location struct in given array and puts it in the new `content`
         * field for the location struct.
         *
         * @method _loadContentForLocations
         * @protected
         * @param {Array|Null} locationStructArr
         * @param {Function} callback
         */
        _loadContentForLocations: function (viewName, locationStructArr, callback) {
            var contentService = this._getContentService(),
                contentIds,
                contentIdsLocationIndexMap = {},
                query;

            contentIds = Y.Array.reduce(locationStructArr, '', function (previousId, struct, index) {
                var contentId = struct.location.get('contentInfo').get('contentId');

                if (!contentIdsLocationIndexMap[contentId]) {
                    contentIdsLocationIndexMap[contentId] = [index];
                } else {
                    contentIdsLocationIndexMap[contentId].push(index);
                }

                previousId = previousId ? previousId + ',' : previousId;
                return previousId + contentId;
            });

            query = this._createNewCreateViewStruct('contents-loading-' + viewName, 'ContentQuery', {
                filter: {
                    "ContentIdCriterion": contentIds
                },
                // In case we are asking for more then 25 items which is default limit, specify limit
                limit: Object.keys(contentIdsLocationIndexMap).length
            });

            contentService.createView(query, Y.bind(function (err, response) {
                if (err) {
                    callback(err, locationStructArr);
                    return;
                }
                Y.Array.each(response.document.View.Result.searchHits.searchHit, function (hit) {
                    var content = this._createContent(hit),
                        locationIndexes = contentIdsLocationIndexMap[content.get('contentId')];

                    locationIndexes.forEach(function(index) {
                        locationStructArr[index].content = content;
                    });
                }, this);
                callback(err, locationStructArr);
            }, this));
        },

        /**
         * Loads Location for each content struct in given array and puts it in the new `location`
         * field for the content struct.
         *
         * @method _loadLocationForContent
         * @protected
         * @param {Array|Null} contentStructArr
         * @param {Function} callback
         */
        _loadLocationForContent: function (viewName, contentStructArr, callback) {
            var contentService = this._getContentService(),
                locationsIds,
                query;

            locationsIds = Y.Array.reduce(contentStructArr, '', function (previousId, struct) {
                var locationId;

                if (struct.content.get('resources').MainLocation) {
                    locationId = struct.content.get('resources').MainLocation.split('/').pop();

                    previousId = previousId ? previousId + ',' : previousId;
                    return previousId + locationId;
                }
                return;
            });

            if (locationsIds) {
                query = this._createNewCreateViewStruct('locations-loading-' + viewName, 'LocationQuery', {
                    filter: {
                        "LocationIdCriterion": locationsIds,
                    },
                    limit: contentStructArr.length
                });

                contentService.createView(query, Y.bind(function (err, response) {
                    if (err) {
                        callback(err, contentStructArr);
                        return;
                    }
                    Y.Array.each(response.document.View.Result.searchHits.searchHit, function (hit) {
                        var location = this._createLocation(hit);

                        Y.Array.some(contentStructArr, function (struct) {
                            if ( struct.content.get('resources').MainLocation === location.get('id') ) {
                                struct.location = location;
                                return true;
                            }
                            return false;
                        });
                    }, this);
                    callback(err, contentStructArr);
                }, this));
            } else {
                callback(false, contentStructArr);
            }
        },
    }, {
        NS: 'search',

        ATTRS: {
            /**
             * Holds the eZ.Location constructor function
             *
             * @attribute locationModelConstructor
             * @type {Function}
             * @default Y.eZ.Location
             */
            locationModelConstructor: {
                value: Y.eZ.Location
            },

            /**
             * Holds the eZ.Content constructor function
             *
             * @attribute contentModelConstructor
             * @type {Function}
             * @default Y.eZ.Content
             */
            contentModelConstructor: {
                value: Y.eZ.Content
            },
        },
    });

    Y.eZ.PluginRegistry.registerPlugin(
        Y.eZ.Plugin.Search, [
            'locationViewViewService', 'universalDiscoveryViewService',
            'contentEditViewService', 'contentCreateViewService',
            'dashboardBlocksViewService', 'contentPeekViewService',
            'subitemBoxViewService',
        ]
    );
});