diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d31019 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.sass-cache/ diff --git a/config.rb b/config.rb new file mode 100644 index 0000000..8a28a4e --- /dev/null +++ b/config.rb @@ -0,0 +1,23 @@ +# Require any additional compass plugins here. + +# Set this to the root of your project when deployed: +http_path = "/" +css_dir = "css" +sass_dir = "sass" +images_dir = "images" +javascripts_dir = "js" + +output_style = :expanded +environment = :production + +relative_assets = true + +line_comments = false +color_output = false + + +# If you prefer the indented syntax, you might want to regenerate this +# project again passing --syntax sass, or you can uncomment this: +# preferred_syntax = :sass +# and then run: +# sass-convert -R --from scss --to sass sass scss && rm -rf sass && mv scss sass diff --git a/css/ting_search_carousel.css b/css/ting_search_carousel.css index 60aa3fa..9fd8040 100644 --- a/css/ting_search_carousel.css +++ b/css/ting_search_carousel.css @@ -1,195 +1,121 @@ -.ting-search-carousel { - width: 100%; -} - -.ting-search-carousel .search-controller { - height: 20px; - list-style: none outside none; - margin: 0; - padding: 0; - width: 100%; -} - -.ting-search-carousel .search-controller li.active { - background: none repeat scroll 0 0 #918F91; -} - -.ting-search-carousel .search-controller li { - background: none repeat scroll 0 0 #D1D9D7; - float: left; - font-size: 11px; - height: 18px; - line-height: 15px; - margin: -1px 2px 0 0; - padding: 0 17px; -} - -.ting-search-carousel .search-controller li a { - font-size: 11px; - line-height: 15px; -} - -.ting-search-carousel .ting-search-results { +/* ========================================================================== + ting search carousel styling + ========================================================================== */ +.rs-carousel, +.rs-carousel-mask, +.rs-carousel-runner { position: relative; - height: 200px; } -.ting-search-carousel .ting-search-results .subtitle { - font-size: 42px; - font-weight: normal; - line-height: 0.9; - margin: 10px 0 0 40px; +.rs-carousel-mask, +.rs-carousel-runner { overflow: hidden; - padding: 10px 0 10px 10px; - text-transform: uppercase; - width: 250px; } -.ting-search-carousel .ting-search-results .info { - height: 39px; - margin-top: -3px; +.rs-carousel-inner { overflow: hidden; - width: 100px; -} - -.ting-search-carousel .ting-search-results .info span { - display: block; - font-size: 10px; - line-height: 1.25; - text-align: center; } -.ting-search-carousel .ting-rs-carousel, .subtitle { - float: left; +rs-carousel-runner { + -moz-transform: translate3d(0, 0, 0); + -webkit-transform: translate3d(0, 0, 0); + -o-transform: translate3d(0, 0, 0); + -ms-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); } -/* carousel */ -.ting-search-carousel .rs-carousel { - opacity: 1 !important; - overflow: hidden; - width: 580px; +.rs-carousel-transition .rs-carousel-runner { + -moz-transition: -moz-transform .400s ease; + -webkit-transition: -webkit-transform .400s ease; + -o-transition: -o-transform .400s ease; + -ms-transition: -ms-transform .400s ease; + transition: transform .400s ease; } -.ting-search-carousel .rs-carousel .rs-carousel-mask { - position:relative; - height: 200px; -} - -.ting-search-carousel .rs-carousel .rs-carousel-runner { - left: 0; - list-style: none; - margin: 0; +/* Pagination and previous and next buttons + ========================================================================== */ +.rs-carousel-action-prev, +.rs-carousel-action-next { + background-image: url('../images/arrows.png'); + height: 55px; overflow: hidden; - padding: 0; + margin-top: -27px; position: absolute; - width: 580px; -} - -.ting-search-carousel .rs-carousel .rs-carousel-item img { - max-width: 112px; - height: 140px; -} -.ting-search-carousel .no-js .rs-carousel .rs-carousel-runner { - position: static; -} - -.ting-search-carousel .rs-carousel .rs-carousel-item { - float: left; - height: 180px; - overflow: hidden; - padding-top: 20px; - text-align: center; - padding-right: 16px; - width: 100px; -} - -.ting-search-carousel .rs-carousel.rs-carousel-vertical .rs-carousel-item { - float: none; + text-indent: -1000px; + top: 50%; + width: 55px; } -/* pagination */ -.ting-search-carousel .rs-carousel-pagination { - list-style: none; - margin: 0; - padding: 0; +.rs-carousel-action-next { + background-position: right 0; + right: 0; } -.ting-search-carousel .rs-carousel-disabled .rs-carousel-pagination { +.rs-carousel-disabled .rs-carousel-action-prev, +.rs-carousel-disabled .rs-carousel-action-next { display: none; } -.ting-search-carousel .rs-carousel-pagination-link { - display: inline; -} - -.ting-search-carousel .rs-carousel-pagination-link a { - padding: 2px 6px; -} -.ting-search-carousel .rs-carousel-pagination-link a:hover { - text-decoration: none; -} - -.ting-search-carousel .rs-carousel-pagination-link-active a { - background: #444; -} - -/* next & prev actions */ -.ting-search-carousel .rs-carousel-disabled .rs-carousel-action-next, -.ting-search-carousel .rs-carousel-disabled .rs-carousel-action-prev { - display: none; -} - -.ting-search-carousel .rs-carousel-action-next, -.ting-search-carousel .rs-carousel-action-prev { - background-image: url('../images/buttons.png'); +/* Carousel content, mask, title etc. + ========================================================================== */ +.ajax-loader { + background: transparent url('../images/ajax-loader.gif') no-repeat center center; + left: 50%; + height: 20px; + margin-top: -10px; + margin-left: -110px; position: absolute; - top: 86px; - height: 35px; - outline: none; - text-indent: -9999px; - width: 35px; + top: 50%; + width: 220px; } -.ting-search-carousel .rs-carousel-action-prev { - left: -15px; +.rs-carousel-mask, +.rs-carousel-mask ul { + display: inline-block; + vertical-align: top; } -.ting-search-carousel .rs-carousel-action-next { - right: -15px; - background-position: 0 -35px; +.rs-carousel-mask { + width: 100%; } -.ting-search-carousel .rs-carousel-action-prev:hover { - background-position: 70px 0; +.rs-carousel-mask ul { + margin: 0; + list-style: none; } -.ting-search-carousel .rs-carousel-action-next:hover { - background-position: 70px -35px; +.rs-carousel-mask li { + float: left; + width: 100px; + text-align: center; } -.ting-search-carousel .rs-carousel-action-disabled { - background-position: 35px 0 !important; +.rs-carousel-item-image { + display: block; } -.ting-search-carousel .rs-carousel-action-prev + .rs-carousel-action-disabled { - background-position: 35px -35px !important; +.rs-carousel-item-image img { + display: block; + margin-left: auto; + margin-right: auto; } -.ting-search-carousel .rs-carousel-action-disabled { - color: gray; - cursor: default; -} -.ting-search-carousel .rs-carousel-pagination { - display: none; +/* Carousel tabs + ========================================================================== */ +.rs-carousel-tabs ul { + margin: 0; + position: relative; + text-align: center; } -.search-carousel-query { - float: left; +.rs-carousel-tabs ul li { + display: table-cell; + vertical-align: top; + text-align: center; } -.sort-hidden, .search-query-hidden { - display: none; +.rs-carousel-tabs ul li.active a { + position: relative; } - diff --git a/images/arrows.png b/images/arrows.png new file mode 100644 index 0000000..0fa4570 Binary files /dev/null and b/images/arrows.png differ diff --git a/images/buttons.png b/images/buttons.png deleted file mode 100644 index e2b5ab6..0000000 Binary files a/images/buttons.png and /dev/null differ diff --git a/js/jquery.rs.carousel.js b/js/jquery.rs.carousel.js index 93e8b29..146ec2c 100644 --- a/js/jquery.rs.carousel.js +++ b/js/jquery.rs.carousel.js @@ -1,7 +1,13 @@ +/*global jQuery */ +/*jshint bitwise: true, camelcase: true, curly: true, eqeqeq: true, forin: true, +immed: true, indent: 4, latedef: true, newcap: true, nonew: true, quotmark: single, +undef: true, unused: true, strict: true, trailing: true, browser: true */ + /* - * jquery.rs.carousel.js v0.8.1 + * jquery.rs.carousel.js 1.0.1 + * https://github.com/richardscarrott/jquery-ui-carousel * - * Copyright (c) 2011 Richard Scarrott + * Copyright (c) 2013 Richard Scarrott * http://www.richardscarrott.co.uk * * Dual licensed under the MIT and GPL licenses: @@ -9,56 +15,65 @@ * http://www.gnu.org/licenses/gpl.html * * Depends: - * jquery.js v1.4+ + * jquery.js v1.8+ * jquery.ui.widget.js v1.8+ - * */ - + (function ($, undefined) { - var _super = $.Widget.prototype, - horizontal = { - pos: 'left', - pos2: 'right', - dim: 'width' - }, - vertical = { - pos: 'top', - pos2: 'bottom', - dim: 'height' - }; - + 'use strict'; + + var _super = $.Widget.prototype; + $.widget('rs.carousel', { + version: '1.0.1', + options: { - itemsPerPage: 'auto', + // selectors + mask: '> div', + runner: '> ul', + items: '> li', + + // options itemsPerTransition: 'auto', orientation: 'horizontal', - pagination: true, - insertPagination: null, + loop: false, + whitespace: false, nextPrevActions: true, - insertNextAction: null, - insertPrevAction: null, + insertPrevAction: function () { + return $('Prev').appendTo(this); + }, + insertNextAction: function () { + return $('Next').appendTo(this); + }, + pagination: true, + insertPagination: function (pagination) { + return $(pagination).insertAfter($(this).find('.rs-carousel-mask')); + }, speed: 'normal', easing: 'swing', - startAt: null, - nextText: 'Next', - prevText: 'Previous', - create_: null, // widget factory uses create, but doesn't provide any useful data... - beforeAnimate: null, - afterAnimate: null + fx: 'slide', + translate3d: false, + + // callbacks + create: null, + before: null, + after: null }, _create: function () { - this.page = 0; + // widget factory 1.8.* backwards compat + this.widgetFullName = this.widgetFullName || this.widgetBaseClass; + this.eventNamespace = this.eventNamespace || '.' + this.widgetName; + + this.index = 0; this._elements(); - this._defineOrientation(); + this._setIsHorizontal(); this._addMask(); this._addNextPrevActions(); - // pass in false to avoid re-caching items again this.refresh(false); - this._trigger('create_', null, this._getData()); return; }, @@ -67,71 +82,43 @@ _elements: function () { var elems = this.elements = {}, - baseClass = '.' + this.widgetBaseClass; - - elems.mask = this.element.children(baseClass + '-mask'); - elems.runner = this.element.find(baseClass + '-runner').first(); - elems.items = elems.runner.children(baseClass + '-item'); - elems.pagination = undefined; - elems.nextAction = undefined; - elems.prevAction = undefined; - - return; - }, + fullName = this.widgetFullName; - _addClasses: function () { + this.element.addClass(fullName); - if (!this.oldClass) { - this.oldClass = this.element[0].className; - } - - this._removeClasses(); - - var baseClass = this.widgetBaseClass, - classes = []; + elems.mask = this.element + .find(this.options.mask) + .addClass(fullName + '-mask'); - classes.push(baseClass); - classes.push(baseClass + '-' + this.options.orientation); - classes.push(baseClass + '-items-' + this.options.itemsPerPage); + elems.runner = (elems.mask.length ? elems.mask : this.element) + .find(this.options.runner) + .addClass(fullName + '-runner'); - this.element.addClass(classes.join(' ')); + elems.items = elems.runner + .find(this.options.items) + .addClass(fullName + '-item'); return; }, - // removes rs-carousel* classes - _removeClasses: function () { - - var self = this, - widgetClasses = []; + _setIsHorizontal: function () { - this.element.removeClass(function (i, classes) { - - $.each(classes.split(' '), function (i, value) { - - if (value.indexOf(self.widgetBaseClass) !== -1) { - widgetClasses.push(value); - } - - }); - - return widgetClasses.join(' '); - - }); - - return; - }, + var elems = this.elements, + fullName = this.widgetFullName; - // defines obj to hold strings based on orientation for dynamic method calls - _defineOrientation: function () { + this.element + .removeClass(fullName + '-horizontal') + .removeClass(fullName + '-vertical'); if (this.options.orientation === 'horizontal') { this.isHorizontal = true; - this.helperStr = horizontal; + this.element.addClass(fullName + '-horizontal'); + elems.runner.css('top', ''); } else { this.isHorizontal = false; - this.helperStr = vertical; + this.element.addClass(fullName + '-vertical'); + elems.runner.css('left', ''); } return; @@ -148,7 +135,7 @@ } elems.mask = elems.runner - .wrap('
') + .wrap('
') .parent(); // indicates whether mask was dynamically added or already existed in mark-up @@ -157,37 +144,6 @@ return; }, - // sets runners width - _setRunnerWidth: function () { - - if (!this.isHorizontal) { - return; - } - - var self = this; - - this.elements.runner.width(function () { - return self._getItemDim() * self.getNoOfItems(); - }); - - return; - }, - - // sets itemDim to the dimension of first item incl. margin - _getItemDim: function () { - - // is this ridiculous?? - return this.elements.items - ['outer' + this.helperStr.dim.charAt(0).toUpperCase() + this.helperStr.dim.slice(1)](true); - - }, - - getNoOfItems: function () { - - return this.elements.items.length; - - }, - // adds next and prev links _addNextPrevActions: function () { @@ -198,45 +154,156 @@ var self = this, elems = this.elements, opts = this.options, - baseClass = this.widgetBaseClass; - + eventNamespace = this.eventNamespace; + this._removeNextPrevActions(); - elems.prevAction = $('' + opts.prevText + '') - .bind('click.' + this.widgetName, function (e) { + elems.prevAction = opts.insertPrevAction.apply(this.element[0]) + .bind('click' + eventNamespace, function (e) { e.preventDefault(); self.prev(); - });; + }); - elems.nextAction = $('' + opts.nextText + '') - .bind('click.' + this.widgetName, function (e) { + elems.nextAction = opts.insertNextAction.apply(this.element[0]) + .bind('click' + eventNamespace, function (e) { e.preventDefault(); self.next(); }); - $.isFunction(opts.insertPrevAction) ? - opts.insertPrevAction.apply(elems.prevAction[0]) : elems.prevAction.appendTo(this.element); - - $.isFunction(opts.insertNextAction) ? - opts.insertNextAction.apply(elems.nextAction[0]) : elems.nextAction.appendTo(this.element); - + return; }, _removeNextPrevActions: function () { - + var elems = this.elements; - + if (elems.nextAction) { elems.nextAction.remove(); elems.nextAction = undefined; - } - + } + if (elems.prevAction) { elems.prevAction.remove(); elems.prevAction = undefined; } - - return; + + return; + }, + + // refresh carousel + refresh: function (recache) { + + // undefined should pass condition + if (recache !== false) { + this._recacheItems(); + } + + this._setPages(); + this._addPagination(); + this._setRunnerWidth(); + this.index = this._makeValid(this.index); + this.goToPage(this.index, false, undefined, true); + this._checkDisabled(); + + return; + }, + + // re-cache items in case new items have been added, + // moved to own method so continuous can easily override + // to avoid clones + _recacheItems: function () { + + this.elements.items = this.elements.runner + .find(this.options.items) + .addClass(this.widgetFullName + '-item'); + + return; + }, + + // sets pages array - [jQuery(li, li, li), jQuery(li, li, li, li), jQuery(li, li, li), jQuery(li, li)] + _setPages: function () { + + var self = this, + itemIndex = 0, + lastItemIndex = isNaN(this.options.itemsPerTransition) ? undefined : this._getLastItemIndex(), + start; + + this.pages = []; + + while (itemIndex < this.getNoOfItems()) { + + // if itemsPerTransition isn't a number we need to get the visible + // items at each item index + if (isNaN(this.options.itemsPerTransition)) { + this.pages.push(self._getVisibleItems(itemIndex)); + itemIndex += this.pages[this.pages.length - 1].length; + } + // otherwise simply slice up the items based on itemsPerTransition + else { + // making sure we don't go past the lastItemIndex + if (itemIndex >= lastItemIndex) { + this.pages.push(this.elements.items.slice(itemIndex)); + break; + } + start = itemIndex; + // #37 - allow `itemsPerTransition` to be passed in as string + itemIndex += parseInt(this.options.itemsPerTransition, 10); + this.pages.push(this.elements.items.slice(start, itemIndex)); + } + } + + return; + }, + + // returns last logical item index + _getLastItemIndex: function () { + + if (this.options.whitespace) { + return; + } + + return this.elements.items.index(this._getVisibleItems(0, true).last()); + }, + + // returns a jquery object containing the visible items where the `itemIndex` + // is considered to be the first visible item. Passing in reverse as true allows + // us to, for example, return the visible items from the last item backwards. + _getVisibleItems: function (itemIndex, reverse) { + + var self = this, + page = [], + items = !reverse ? this.elements.items.slice(itemIndex) : [].reverse.apply($(this.elements.items)).slice(itemIndex), + maskDim = this._getMaskDim(), + dim = 0; + + items + .each(function () { + dim += self.isHorizontal ? $(this).outerWidth(true) : $(this).outerHeight(true); + if (dim > maskDim) { + // if no items have been pushed to page then it means the + // first item is larger than the mask so we still need to push before + // breaking. + if (page.length === 0) { + page.push(this); + } + return false; + } + page.push(this); + }); + + return $(page); + }, + + _getMaskDim: function () { + + return this.elements.mask[this.isHorizontal ? 'width' : 'height'](); + + }, + + getNoOfItems: function () { + + return this.elements.items.length; + }, // adds pagination links and binds associated events @@ -247,217 +314,317 @@ } var self = this, - elems = this.elements, - opts = this.options, - baseClass = this.widgetBaseClass, + fullName = this.widgetFullName, + pagination = $('
    '), links = [], - noOfPages = this._getNoOfPages(), + noOfPages = this.getNoOfPages(), i; - + this._removePagination(); - for (i = 1; i <= noOfPages; i++) { - links[i] = ''; + for (i = 0; i < noOfPages; i++) { + links[i] = ''; } - elems.pagination = $('
      ') + pagination .append(links.join('')) - .delegate('a', 'click.' + this.widgetName, function (e) { + .delegate('a', 'click' + this.eventNamespace, function (e) { e.preventDefault(); - self.goToPage(parseInt(this.hash.split('-')[1], 10)); - }); - $.isFunction(opts.insertPagination) ? - opts.insertPagination.apply(elems.pagination[0]) : elems.pagination.insertAfter(elems.mask); - + this.elements.pagination = this.options.insertPagination.call(this.element[0], pagination); + return; }, _removePagination: function () { - + if (this.elements.pagination) { this.elements.pagination.remove(); this.elements.pagination = undefined; } - - return; - }, - - // sets array of pages - _setPages: function () { - - var index = 1, - page = 0; - - this.pages = []; - - while (page < this._getNoOfPages()) { - - // if index is greater than total number of items just go to last - if (index > this.getNoOfItems()) { - index = this.getNoOfItems(); - } - - this.pages[page] = index; - index += this._getItemsPerTransition(); // this._getItemsPerPage(index); - page++; - } return; }, - // gets noOfPages - _getNoOfPages: function () { + // returns noOfPages + getNoOfPages: function () { - return Math.ceil((this.getNoOfItems() - this._getItemsPerPage()) / this._getItemsPerTransition()) + 1; + return this.pages.length; }, - // gets options.itemsPerPage. If set to not number it's calculated based on maskdim - _getItemsPerPage: function () { + // if no of items is less than items on first page then the + // carousel should be disabled. + _checkDisabled: function () { - // if itemsPerPage of type number don't dynamically calculate - if (typeof this.options.itemsPerPage === 'number') { - return this.options.itemsPerPage; + if (this.getNoOfPages() <= 1) { + this.disable(); + this._disabled = true; + } + // only enable if carousel was disabled internally. + else if (this._disabled) { + this.enable(); + this._disabled = false; } - - return Math.floor(this._getMaskDim() / this._getItemDim()); + return; }, - _getItemsPerTransition: function () { + _setRunnerWidth: function () { - if (typeof this.options.itemsPerTransition === 'number') { - return this.options.itemsPerTransition; + var elems = this.elements, + width = 0; + + // reset width in case orientation has been changed + elems.runner.width(''); + + if (!this.isHorizontal) { + return; } - return this._getItemsPerPage(); - - }, + elems.runner + .width(function () { + elems.items + .each(function () { + width += $(this).outerWidth(true); + }); - _getMaskDim: function () { - - return this.elements.mask[this.helperStr.dim](); + return width; + }); + return; }, next: function (animate) { - - this.goToPage(this.page + 1, animate); + + var index = this.index + 1; + + if (this.options.loop && index >= this.getNoOfPages()) { + index = 0; + } + + this.goToPage(index, animate, 'carousel:next'); return; }, prev: function (animate) { - - this.goToPage(this.page - 1, animate); + + var index = this.index - 1; + + if (this.options.loop && index < 0) { + index = this.getNoOfPages() - 1; + } + + this.goToPage(index, animate, 'carousel:prev'); return; }, - // shows specific page (one based) - goToPage: function (page, animate) { + goToPage: function (index, animate, /* INTERNAL */ eventName, /* INTERNAL */ silent) { + + // undefined should pass + animate = animate === false ? false : true; - if (!this.options.disabled && this._isValid(page)) { - this.oldPage = this.page; - this.page = page; - this._go(animate); + if (!this.options.disabled && this._isValid(index)) { + this.prevIndex = this.index; + this.index = index; + // calling `this._slide` using `options.fx` to easily override within extensions + this['_' + this.options.fx]($.Event(eventName ? eventName : 'carousel:gotopage', { + animate: animate, + speed: animate ? this.options.speed : 0 + }), silent); } - + + // make sure updateUi is called even when disabled + this._updateUi(); + return; }, - // returns true if page index is valid, false if not - _isValid: function (page) { - - if (page <= this._getNoOfPages() && page >= 1) { - return true; + // `index` can be $obj, element or 0 based index + goToItem: function (index, animate) { + + var page, + pageLength, + item, + itemLength; + + // if a number get the element + if (!isNaN(index)) { + index = this.elements.items.eq(index); } - - return false; - }, - // returns valid page index - _makeValid: function (page) { - - if (page < 1) { - page = 1; + if (index.jquery) { + // unwrap from jquery object + index = index[0]; } - else if (page > this._getNoOfPages()) { - page = this._getNoOfPages(); + + // find item in pages array + pages: + for (page = 0, pageLength = this.getNoOfPages(); page < pageLength; page++) { + for (item = 0, itemLength = this.getPage(page).length; item < itemLength; item++) { + if (this.getPage(page)[item] === index) { + break pages; + } + } } - return page; + this.goToPage(page, animate); + + // return item as jquery object + return $(index); }, - // returns obj with useful data to be passed into callback events - _getData: function () { - - return { - page: this.page, - oldPage: this.oldPage, - noOfItems: this.getNoOfItems(), - noOfPages: this._getNoOfPages(), - elements: this.elements - }; - + // returns true if index is valid, false if not + _isValid: function (index) { + + if (index < this.getNoOfPages() && index >= 0) { + return true; + } + + return false; }, - // abstract _slide to easily override within extensions - _go: function (animate) { - - this._slide(animate); + // returns valid page index + _makeValid: function (index) { - return; + if (index < 0) { + index = 0; + } + else if (index >= this.getNoOfPages()) { + index = this.getNoOfPages() - 1; + } + + return index; }, - _slide: function (animate) { - - this._trigger('beforeAnimate', null, this._getData()); + // if silent is true callbacks won't be fired + _slide: function (e, silent) { var self = this, - speed = animate === false ? 0 : this.options.speed, // default to animate animateProps = {}, lastPos = this._getAbsoluteLastPos(), - - pos = this.elements.items - .eq(this.pages[this.page - 1] - 1) // arrays and .eq() are zero based, carousel is 1 based - .position()[this.helperStr.pos]; + page = this.getPage(), + pos = page.first().position()[this.isHorizontal ? 'left' : 'top'], + eventNamespace = this.eventNamespace, + fullName = this.widgetFullName, + transitionEndEvent; + + // if before returns false return and revert index back to prevIndex + if (!silent && !this._trigger('before', e, this._getEventData())) { + this.index = this.prevIndex; + return; + } // check pos doesn't go past last posible pos if (pos > lastPos) { pos = lastPos; } - animateProps[this.helperStr.pos] = -pos; - this.elements.runner - .stop() - .animate(animateProps, speed, this.options.easing, function () { + if (this.options.translate3d) { - self._trigger('afterAnimate', null, self._getData()); + transitionEndEvent = [ + 'transitionend' + eventNamespace, + 'webkitTransitionEnd' + eventNamespace, + 'oTransitionEnd' + eventNamespace + ]; - }); - - this._updateUi(); + if (e.animate) { + this.element.addClass(fullName + '-transition'); + } + + this.elements.runner + .unbind(transitionEndEvent.join(' ')) + .on(transitionEndEvent.join(' '), function (e) { + self.element.removeClass(fullName + '-transition'); + if (!silent) { + self._trigger('after', e, self._getEventData()); + } + }) + .css('transform', 'translate3d(' + (this.isHorizontal ? -pos + 'px, 0, 0' : '0, ' + -pos + 'px, 0') + ')'); + + // if we're not animating the after callback should still be called + if (!e.animate) { + self.element.removeClass(fullName + '-transition'); + if (!silent) { + self._trigger('after', e, self._getEventData()); + } + } + + } + else { + + animateProps[this.isHorizontal ? 'left' : 'top'] = -pos; + this.elements.runner + .stop() + .animate(animateProps, e.speed, this.options.easing, function () { + if (!silent) { + self._trigger('after', e, self._getEventData()); + } + }); + + } return; }, - // gets lastPos to ensure runner doesn't move beyond mask (allowing mask to be any width and the use of margins) + // gets lastPos to ensure runner doesn't move beyond mask _getAbsoluteLastPos: function () { - - var lastItem = this.elements.items.eq(this.getNoOfItems() - 1); - - return lastItem.position()[this.helperStr.pos] + this._getItemDim() - - this._getMaskDim() - parseInt(lastItem.css('margin-' + this.helperStr.pos2), 10); + + if (this.options.whitespace) { + return; + } + + var lastPos, + lastItem = this.elements.items.eq(this.getNoOfItems() - 1), + lastItemPos = lastItem.position()[this.isHorizontal ? 'left' : 'top'], + lastItemDim = lastItem[this.isHorizontal ? 'outerWidth' : 'outerHeight'](true); + + lastPos = lastItemPos + lastItemDim - this._getMaskDim(); + + // if lastPos is less than 0 it means there aren't enough items to fill the entire mask + return lastPos < 0 ? undefined : lastPos; + }, + + // returns jQuery object of items on page + getPage: function (index) { + + return this.pages[(typeof index !== 'undefined' ? index : this.index)] || $([]); }, - // updates pagination, next and prev link status classes + // returns pages array + getPages: function () { + + return this.pages; + + }, + + _getEventData: function () { + + return { + page: this.getPage(), + prevPage: this.getPage(this.prevIndex), + elements: this.elements + }; + + }, + + _getCreateEventData: function () { + + return this._getEventData(); + + }, + + + // updates pagination, next and prev link state classes _updateUi: function () { + this._updateActiveItems(); + if (this.options.pagination) { this._updatePagination(); } @@ -469,84 +636,85 @@ return; }, - _updatePagination: function () { - - var baseClass = this.widgetBaseClass, - activeClass = baseClass + '-pagination-link-active'; + _updateActiveItems: function () { - this.elements.pagination - .children('.' + baseClass + '-pagination-link') - .removeClass(activeClass) - .eq(this.page - 1) - .addClass(activeClass); + var fullName = this.widgetFullName, + activeClass = fullName + '-item-active'; + + this.elements.items + .removeClass(activeClass); + + this.getPage() + .addClass(activeClass); return; }, - _updateNextPrevActions: function () { - - var elems = this.elements, - page = this.page, - disabledClass = this.widgetBaseClass + '-action-disabled'; + _updatePagination: function () { - elems.nextAction - .add(elems.prevAction) + var fullName = this.widgetFullName, + activeClass = fullName + '-pagination-link-active', + disabledClass = fullName + '-pagination-disabled', + pagination = this.elements.pagination .removeClass(disabledClass); - - if (page === this._getNoOfPages()) { - elems.nextAction.addClass(disabledClass); - } - else if (page === 1) { - elems.prevAction.addClass(disabledClass); + + if (this.options.disabled) { + pagination.addClass(disabledClass); } + pagination + .children('.' + fullName + '-pagination-link') + .removeClass(activeClass) + .eq(this.index) + .addClass(activeClass); + return; }, - // formalise appending items as continuous adding complexity by inserting - // cloned items - add: function (items) { + _updateNextPrevActions: function () { - this.elements.runner.append(items); - this.refresh(); + var elems = this.elements, + actions = elems.nextAction.add(elems.prevAction), + index = this.index, + fullName = this.widgetFullName, + activeClass = fullName + '-action-active', + disabledClass = fullName + '-action-disabled'; + + actions + .addClass(activeClass) + .removeClass(disabledClass); + + if (this.options.disabled) { + actions.addClass(disabledClass); + } - return; - }, + if (!this.options.loop) { - remove: function (selector) { - - if (this.getNoOfItems() > 0) { + if (index === this.getNoOfPages() - 1) { + elems.nextAction + .removeClass(activeClass); + } - this.elements.items - .filter(selector) - .remove(); + if (index === 0) { + elems.prevAction + .removeClass(activeClass); + } - this.refresh(); } return; }, - // handles option updates _setOption: function (option, value) { - var requiresRefresh = [ - 'itemsPerPage', - 'itemsPerTransition', - 'orientation' - ]; - _super._setOption.apply(this, arguments); switch (option) { case 'orientation': - - this.elements.runner - .css(this.helperStr.pos, '') - .width(''); - this._defineOrientation(); + this._setIsHorizontal(); + this.refresh(); break; @@ -573,88 +741,102 @@ } break; - } - if ($.inArray(option, requiresRefresh) !== -1) { + case 'loop': + case 'disabled': + + this._updateUi(); + + break; + + case 'itemsPerTransition': + case 'whitespace': + this.refresh(); + + break; } return; }, - // if no of items is less than items per page we disable carousel - _checkDisabled: function () { - - if (this.getNoOfItems() <= this._getItemsPerPage()) { - this.elements.runner.css(this.helperStr.pos, ''); - this.disable(); - } - else { - this.enable(); - } + add: function (items) { + + this.elements.runner.append(items); + this.refresh(); return; }, - // refresh carousel - refresh: function (recache) { + remove: function (selector) { - // assume true (undefined should pass condition) - if (recache !== false) { - this.recacheItems(); - } + if (this.getNoOfItems() > 0) { - this._addClasses(); - this._setPages(); - this._addPagination(); - this._checkDisabled(); - this._setRunnerWidth(); - this.page = this._makeValid(this.page); - this.goToPage(this.page, false); + this.elements.items + .filter(selector) + .remove(); + + this.refresh(); + } return; }, - // re-cache items in case new items have been added, - // moved to own method so continuous can easily override - // to avoid clones - recacheItems: function () { + // returns current index + getIndex: function () { - this.elements.items = this.elements.runner - .children('.' + this.widgetBaseClass + '-item'); + return this.index; + + }, + + // returns prev index + getPrevIndex: function () { + + return this.prevIndex; - return; }, // returns carousel to original state destroy: function () { var elems = this.elements, + fullName = this.widgetFullName, cssProps = {}; this.element - .removeClass() - .addClass(this.oldClass); - + .removeClass(fullName) + .removeClass(fullName + '-horizontal') + .removeClass(fullName + '-vertical'); + elems.mask + .removeClass(fullName + '-mask'); + elems.runner + .removeClass(fullName + '-runner'); + elems.items + .removeClass(fullName + '-item'); + if (this.maskAdded) { elems.runner - .unwrap('.' + this.widgetBaseClass + '-mask'); + .unwrap(); } - cssProps[this.helperStr.pos] = ''; - cssProps[this.helperStr.dim] = ''; - elems.runner.css(cssProps); - + cssProps[this.isHorizontal ? 'left' : 'top'] = ''; + cssProps[this.isHorizontal ? 'width' : 'height'] = ''; + cssProps.transform = ''; + elems.runner + .css(cssProps); + this._removePagination(); this._removeNextPrevActions(); - + + elems.runner + .unbind(this.eventNamespace); + _super.destroy.apply(this, arguments); return; } }); - - $.rs.carousel.version = '0.8.1'; })(jQuery); + diff --git a/js/jquery.ui.touch-punch.min.js b/js/jquery.ui.touch-punch.min.js new file mode 100644 index 0000000..33d6f97 --- /dev/null +++ b/js/jquery.ui.touch-punch.min.js @@ -0,0 +1,11 @@ +/* + * jQuery UI Touch Punch 0.2.2 + * + * Copyright 2011, Dave Furfero + * Dual licensed under the MIT or GPL Version 2 licenses. + * + * Depends: + * jquery.ui.widget.js + * jquery.ui.mouse.js + */ +(function(b){b.support.touch="ontouchend" in document;if(!b.support.touch){return;}var c=b.ui.mouse.prototype,e=c._mouseInit,a;function d(g,h){if(g.originalEvent.touches.length>1){return;}g.preventDefault();var i=g.originalEvent.changedTouches[0],f=document.createEvent("MouseEvents");f.initMouseEvent(h,true,true,window,1,i.screenX,i.screenY,i.clientX,i.clientY,false,false,false,false,0,null);g.target.dispatchEvent(f);}c._touchStart=function(g){var f=this;if(a||!f._mouseCapture(g.originalEvent.changedTouches[0])){return;}a=true;f._touchMoved=false;d(g,"mouseover");d(g,"mousemove");d(g,"mousedown");};c._touchMove=function(f){if(!a){return;}this._touchMoved=true;d(f,"mousemove");};c._touchEnd=function(f){if(!a){return;}d(f,"mouseup");d(f,"mouseout");if(!this._touchMoved){d(f,"click");}a=false;};c._mouseInit=function(){var f=this;f.element.bind("touchstart",b.proxy(f,"_touchStart")).bind("touchmove",b.proxy(f,"_touchMove")).bind("touchend",b.proxy(f,"_touchEnd"));e.call(f);};})(jQuery); \ No newline at end of file diff --git a/js/ting_search_carousel.js b/js/ting_search_carousel.js index ca3f09e..f361be7 100644 --- a/js/ting_search_carousel.js +++ b/js/ting_search_carousel.js @@ -1,39 +1,235 @@ +/** + * @file + * Handles the carousels loading of content and changes between tabs. There are + * two selectors to change tabs based on breaks points (which is handle by the + * theme). + * + * For large screens the normal tab list (ul -> li) is used while on small + * screens (mobile/tables) a select dropdown is used. + * + */ (function ($) { - var carousel = false; - - Drupal.behaviors.tingSearchCarousel = { - attach: function(context) { - carousel_init(0); - - $('.search-controller li').click(function() { - $(this).parent().find('li').removeClass('active'); - $(this).addClass('active'); - - carousel_init($(this).index()); - - return false; - }); + "use strict"; + + var TingSearchCarousel = (function() { + + var cache = []; + var carousel; + var current_tab = 0; + var navigation; + + /** + * Private: Ensures that the tabs have the same size. This is purly a design + * thing. + */ + function _equale_tab_width() { + // Get the list of tabs and the number of tabs in the list. + var tabsList = $('.rs-carousel-list-tabs'); + var childCount = tabsList.children('li').length; + + // Only do somehting if there actually is tabs + if (childCount > 0) { + + // Get the width of the
        list element. + var parentWidth = tabsList.width(); + + // Calculate the width of the
      • 's. + var childWidth = Math.floor(parentWidth / childCount); + + // Calculate the last
      • width to combined childrens width it self not + // included. + var childWidthLast = parentWidth - ( childWidth * (childCount -1) ); + + // Set the tabs css widths. + tabsList.children().css({'width' : childWidth + 'px'}); + tabsList.children(':last-child').css({'width' : childWidthLast + 'px'}); + } } - } - - carousel_init = function(index) { - $.ajax({ - type: 'get', - url : Drupal.settings.basePath + 'ting_search_carousel/results/ajax/' + index, - dataType : 'json', - success : function(msg) { - $('.ting-search-carousel .subtitle').html(msg.subtitle); - - if (!carousel) { - $('.ting-search-carousel .ting-rs-carousel .rs-carousel-runner').html(msg.content); - carousel = $('.ting-search-carousel .ting-rs-carousel').carousel(); - } - else { - carousel.carousel('destroy'); - $('.ting-search-carousel .ting-rs-carousel .rs-carousel-runner').html(msg.content); - carousel.carousel(); + + /** + * Private: Handler activated when the user changes tab. + */ + function _change_tab(index) { + // Remove navigation selection. + navigation.find('.active').removeClass('active'); + navigation.find(':selected').removeAttr('selected'); + + // Add new navigation seletions. + $(navigation.find('li')[index]).addClass('active'); + $(navigation.find('option')[index]).attr('selected', true); + + // Remove current content and show spinner. + $('.rs-carousel-title').html(''); + $('.rs-carousel .rs-carousel-runner').children().remove(); + $('.rs-carousel-inner .ajax-loader').removeClass('element-hidden'); + + // Hide navigation arrows. + $('.rs-carousel-action-prev').hide(); + $('.rs-carousel-action-next').hide(); + + current_tab = index; + _update(current_tab); + } + + /** + * Private: Check is the device have support for touch events. + */ + function _is_touch_device() { + // First part work in most browser the last in IE 10. + return !!('ontouchstart' in window) || !!('onmsgesturechange' in window); + } + + /** + * Private: Enable draggable touch support to the carousel, but only if the + * device is touch enabled. + */ + function _add_touch_support() { + if (_is_touch_device()) { + // Add support for touch displays (requires jQuery Touch Punch). + $('.rs-carousel-runner').draggable({ + axis: "x", + stop: function() { + var left = $('.rs-carousel-runner').position().left; + + // Left side reached. + if (left > 0) { + carousel.carousel('goToPage', 0); + } + + // Right side reached. + if ($('.rs-carousel-mask').width() - $('.rs-carousel-runner').width() > left) { + var lastIndex = carousel.carousel('getNoOfPages') - 1; + carousel.carousel('goToPage', lastIndex); + } + } + }); + + // Hide navigation arrows. + $('.rs-carousel-action-prev').hide(); + $('.rs-carousel-action-next').hide(); + } + } + + /** + * Private: Start the tables and attach event handler for click and change + * events. + */ + function _init_tabs() { + // Select navigation wrapper. + navigation = $('.rs-carousel-tabs'); + + // Sett equal with on the tab navigation menu. + _equale_tab_width(); + + // Attach click events to tabs. + $('.rs-carousel-list-tabs').on("click", "li", ( + function(e) { + e.preventDefault(); + _change_tab($(this).index()); + return false; } + )); + + // Add change event to tabs. + $('.rs-carousel-select-tabs').live('change', function() { + _change_tab($(this).find(':selected').index()); + }); + } + + /** + * Private: Updates the content when the user changes tabs. It will fetch + * the content from the server if it's not fetched allready. + */ + function _update(index) { + // Get content from cache, if it have been fetched. + if (!(index in cache)) { + _fetch(index); + + // Return as the fetch will call update once more when the Ajax call + // have completed. + return; } - }); - } + + var data = cache[index]; + + // Remove spinner. + $('.rs-carousel-inner .ajax-loader').addClass('element-hidden'); + + // Update content. + $('.rs-carousel-title').html(data.subtitle); + $('.rs-carousel .rs-carousel-runner').append(data.content); + + // Show navigation arrows. + $('.rs-carousel-action-prev').show(); + $('.rs-carousel-action-next').show(); + + // Get the carousel running. + carousel.carousel('refresh'); + carousel.carousel('goToPage', 0); + } + + /** + * Private: Makes an ajax call to the server to get new content for the + * active navigation tab. + */ + function _fetch(index) { + $.ajax({ + type: 'get', + url : Drupal.settings.basePath + 'ting_search_carousel/results/ajax/' + index, + dataType : 'json', + success : function(data) { + cache[index] = { + 'subtitle' : data.subtitle, + 'content' : data.content + }; + + // If we still are on the same tab update it elese the content have + // been saved to the cache. + if (current_tab == data.index) { + _update(index); + } + } + }); + } + + /** + * Public: Init the carousel and fetch content for the first tab. + */ + function init() { + // Select the carousel element. + carousel = $('.rs-carousel-items'); + + // Fix the tables and fetch the first tabs content. + _init_tabs(); + + // Start the carousel. + carousel.carousel({ + noOfRows: 1, + orientation: 'horizontal', + itemsPerTransition: 'auto' + }); + + // Maybe add support for touch devices (will only be applied on touch + // enabled devices). + _add_touch_support(); + + // Will get content for the first tab. + _change_tab(0); + } + + /** + * Expoes public functions. + */ + return { + name: 'ting_search_carousel', + init: init + }; + })(); + + /** + * Start the carousel when the document is ready. + */ + $(document).ready(function() { + TingSearchCarousel.init(); + }); })(jQuery); diff --git a/plugins/content_types/carousel.inc b/plugins/content_types/carousel.inc index 31dbdab..4dc8021 100644 --- a/plugins/content_types/carousel.inc +++ b/plugins/content_types/carousel.inc @@ -1,5 +1,4 @@ content = theme('ting_search_carousel', array('searches' => $searches)); + $block->content = theme('ting_search_carousel', array('searches' => $searches, 'tab_position' => $tab_position)); return $block; } diff --git a/templates/ting_search_carousel.tpl.php b/templates/ting_search_carousel.tpl.php new file mode 100644 index 0000000..1c61ba5 --- /dev/null +++ b/templates/ting_search_carousel.tpl.php @@ -0,0 +1,48 @@ + + + + diff --git a/templates/ting_search_carousel_collection.tpl.php b/templates/ting_search_carousel_collection.tpl.php new file mode 100644 index 0000000..950f145 --- /dev/null +++ b/templates/ting_search_carousel_collection.tpl.php @@ -0,0 +1,10 @@ + +
      • diff --git a/ting_search_carousel.admin.inc b/ting_search_carousel.admin.inc index ef97494..00459f3 100644 --- a/ting_search_carousel.admin.inc +++ b/ting_search_carousel.admin.inc @@ -18,6 +18,39 @@ function ting_search_carousel_admin_page() { return drupal_get_form('ting_search_carousel_admin_form'); } +/** + * Admin settings form for frontend display of carousel. + */ +function ting_search_carousel_settings_admin_form($form, &$form_state) { + $form = array(); + + $form['ting_search_carousel_settings'] = array( + '#type' => 'fieldset', + '#title' => t('Frontend settings'), + ); + + $form['ting_search_carousel_settings']['ting_search_carousel_tabs_position'] = array( + '#title' => t('Position of navigational tabs'), + '#type' => 'select', + '#options' => array( + 'bottom' => t("Bottom"), + ), + '#default_value' => variable_get('ting_search_carousel_tabs_position', 'bottom'), + '#required' => TRUE, + '#description' => t('Select where you want the tabs for displaying the various searches to be placed in frontend.'), + ); + + $form['ting_search_carousel_settings']['ting_search_carousel_description_toggle'] = array( + '#title' => t('Show descriptions for navigational tabs'), + '#type' => 'checkbox', + '#default_value' => variable_get('ting_search_carousel_description_toggle', 0), + '#required' => FALSE, + '#description' => t('Toggle whether descriptions of the various searches are displayed or not.'), + ); + + return system_settings_form($form); +} + /** * Search queries admin form. * @@ -289,9 +322,15 @@ function ting_search_carousel_search_submit($form, &$form_state) { } } + /** + * @TODO: Add clear cache button and detect changes in the query input fields + * so only partial cache can be rebuild. This will slow down the submit but + * may speed up the presentation for the users by kick starting the cache. + */ + // Save the queries as a persistent variable. variable_set('ting_carousel_search_queries', $searches); - // Make ting search requests. - ting_search_carousel_do_request(); + // Clear carousel search cache + cache_clear_all('ting_search_carousel_result', 'cache'); } diff --git a/ting_search_carousel.install b/ting_search_carousel.install index 1f22cc1..8e30b55 100644 --- a/ting_search_carousel.install +++ b/ting_search_carousel.install @@ -1,7 +1,7 @@ 'Ting search carousel', - 'description' => 'Manage frontpage carousel', - 'page callback' => 'ting_search_carousel_admin_page', - 'page arguments' => array(), - 'access arguments' => array('access administration pages'), - 'file' => 'ting_search_carousel.admin.inc', - 'weight' => 30, - 'position' => 'left', - ); - $items['ting_search_carousel/results/ajax'] = array( 'title' => 'Show search carousel results', 'page callback' => 'ting_search_carousel_result', @@ -29,58 +18,91 @@ function ting_search_carousel_menu() { 'type' => MENU_CALLBACK, ); - return $items; -} + $items['admin/config/ting/ting_search_carousel'] = array( + 'title' => 'Ting search carousel', + 'description' => 'Manage content for frontpage carousel.', + 'page callback' => 'ting_search_carousel_admin_page', + 'page arguments' => array(), + 'access arguments' => array('configure carousel'), + 'file' => 'ting_search_carousel.admin.inc', + ); -/** - * Implements hook_block_info(). - */ -function ting_search_carousel_block_info() { - $blocks = array(); + $items['admin/config/ting/ting_search_carousel/settings'] = array( + 'title' => 'Content', + 'description' => 'Manage content for frontpage carousel.', + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'weight' => -10, + ); - $blocks['ting_search_carousel'] = array( - 'info' => 'Ting search carousel' + $items['admin/config/ting/ting_search_carousel/frontend_settings'] = array( + 'title' => 'Frontend settings', + 'description' => 'Manage settings for the display of frontpage carousel', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('ting_search_carousel_settings_admin_form'), + 'access arguments' => array('configure carousel'), + 'file' => 'ting_search_carousel.admin.inc', + 'type' => MENU_LOCAL_TASK, + 'weight' => 0, ); - return $blocks; + return $items; } /** - * Implements hook_block_view(). + * Implements hook_permission(). */ -function ting_search_carousel_block_view($delta) { - $block = array(); - - switch ($delta) { - case 'ting_search_carousel': - $searches = variable_get('ting_carousel_search_queries', array()); - $block['content'] = theme('ting_search_carousel', array('searches' => $searches)); - break; - } - - return $block; +function ting_search_carousel_permission() { + return array( + 'configure carousel' => array( + 'title' => t('Configure carousel'), + 'description' => t('Allow role to configure carousel.'), + ), + ); } /** * Implements hook_theme(). */ function ting_search_carousel_theme($existing, $type, $theme, $path) { - $hooks = array(); - $hooks['ting_search_carousel'] = array( - 'variables' => array('searches' => NULL), - 'template' => 'ting_search_carousel' + return array( + 'ting_search_carousel' => array( + 'variables' => array('searches' => NULL, 'tab_position' => NULL), + 'template' => 'templates/ting_search_carousel', + 'file' => 'ting_search_carousel.theme.inc', + ), + 'ting_search_carousel_collection' => array( + 'variables' => array('collection' => NULL), + 'template' => 'templates/ting_search_carousel_collection', + ), + 'ting_search_carousel_admin_form' => array( + 'render element' => 'form', + ), ); +} - $hooks['ting_search_carousel_collection'] = array( - 'variables' => array('collection' => NULL), - 'template' => 'ting_search_carousel_collection' +/** + * Implements hook_block_info(). + */ +function ting_search_carousel_block_info() { + return array( + 'ting_search_carousel' => array( + 'info' => 'Ting search carousel, block', + ), ); +} - $hooks['ting_search_carousel_admin_form'] = array( - 'render element' => 'form', +/** + * Implements hook_block_view(). + * + * We ignore delta in this block view, as we only have one block. + */ +function ting_search_carousel_block_view($delta) { + return array( + 'content' => theme('ting_search_carousel', array( + 'searches' => variable_get('ting_carousel_search_queries', array()), + 'tab_position' => variable_get('ting_search_carousel_tabs_position'), + )), ); - - return $hooks; } /** @@ -92,138 +114,212 @@ function ting_search_carousel_ctools_plugin_directory($module, $plugin) { } } -/** - * Implements hook_cron(). - */ -function ting_search_carousel_cron() { - ting_search_carousel_do_request(TRUE); -} - /** * Perform a ting search, retrieve covers and store some of data in cache. + * + * @param int $index + * Search carsousel tab index. */ -function ting_search_carousel_do_request($full = FALSE) { - $search_queries = variable_get('ting_carousel_search_queries', array()); - $search_items = array(); - - // There are any queries set. - if (count($search_queries) > 0) { +function ting_search_carousel_do_request($index) { + // Get query based in the index parameter. + $queries = variable_get('ting_carousel_search_queries', array()); + $query = isset($queries[$index]) ? $queries[$index] : FALSE; + + // If a query is defined, do a look-up else do nothing. + if ($query) { + // Load the ting client. module_load_include('client.inc', 'ting'); - module_load_include('pages.inc', 'ting_covers'); - - $service = new AdditionalInformationService(variable_get('addi_wsdl_url'), variable_get('addi_username'), variable_get('addi_group'), variable_get('addi_password')); - $j = 0; - - // Search for each query up to 100 items. - foreach ($search_queries as $item) { - $result = ''; - - // Whether we need to fetch all items, perform indepth search. - // Seek first 100 collections otherwise. - if ($full) { - $page = 1; - $collections = array(); - // Make recursive ting calls - while ($result = ting_do_search($item['query'], $page++, 50, array('facets' => array(), 'enrich' => TRUE, 'allObjects' => FALSE))) { - if ($result->numTotalCollections == 0) { - break; - } - - $collections[] = $result; - } - - // Rearrange fetched results, as if they were retrieved via single call - $result = new stdClass(); - $result->collections = array(); - foreach ($collections as $collection) { - foreach ($collection->collections as $item) { - $result->collections[] = $item; - } - } + + // Try to fetch results 50 at a time. + $result = NULL; + $page = 1; + $collections = array(); + while ($result = ting_do_search($query['query'], $page++, 50, array('facets' => array(), 'enrich' => TRUE, 'allObjects' => FALSE))) { + $collections[] = $result; + + if (!$result->more) { + // If no more result exists break the loop. + break; } - else { - $result = ting_do_search($item['query'], 1, 100, array('facets' => array(), 'enrich' => TRUE, 'allObjects' => FALSE)); + } + } + + // Loop over the search results collecting basic object information. + $local_ids = array(); + $objects = array(); + foreach ($collections as $collection) { + foreach ($collection->collections as $ting_collection) { + foreach ($ting_collection->reply->objects as $object) { + $local_ids[] = $object->localId; + $objects[$object->localId] = array( + 'id' => $object->id, + 'title' => isset($object->record['dc:title'][''][0]) ? $object->record['dc:title'][''][0] : '', + 'creator' => isset($object->record['dc:creator']['oss:aut'][0]) ? $object->record['dc:creator']['oss:aut'][0] : '', + ); } + } + } + + // Load helper function form the covers module. + module_load_include('pages.inc', 'ting_covers'); + + // Get cover service client. + $service = new AdditionalInformationService( + variable_get('addi_wsdl_url'), + variable_get('addi_username'), + variable_get('addi_group'), + variable_get('addi_password')); + + // Initialize items array. + $items = array(); + + // Get covers for the ids. + $covers = $service->getByFaustNumber($local_ids); - // For each found collection, find a related item with cover. - foreach ($result->collections as $collection) { - foreach ($collection->reply->objects as $object) { - $path = 'public://ting_search_carousel/' . md5($object->localId) . '.jpg'; - - $ting_item_cover = ''; - $file = ''; - - // If image is not present, try fetch it - if (!file_exists($path)) { - $ting_item_cover = $service->getByFaustNumber($object->localId); - - // No cover found, go to next item in collection - if (count($ting_item_cover) == 0) { - break; - } - - $k = array_keys($ting_item_cover); - $source_url = ''; - - if ($ting_item_cover[$k[0]]->thumbnailUrl) { - $source_url = $ting_item_cover[$k[0]]->thumbnailUrl; - } - elseif ($ting_item_cover[$k[0]]->detailUrl) { - $source_url = $ting_item_cover[$k[0]]->detailUrl; - } - - // Saves and returns new image file object - $file = _ting_covers_pages_fetch_image('public://ting_search_carousel/' . md5($object->localId) . '.jpg', $source_url); - } - - $item = new stdClass(); - $item->id = $object->id; - $item->creator = isset($object->record['dc:creator']['oss:aut'][0]) ? $object->record['dc:creator']['oss:aut'][0] : ''; - $item->title = isset($object->record['dc:title'][''][0]) ? $object->record['dc:title'][''][0] : ''; - $item->image = image_style_url('ting_search_carousel', isset($file->uri) ? $file->uri : $path); - - $search_items[$j][] = $item; - break; - } + // Loop over the fetched covers and build items. + foreach ($covers as $local_id => $cover) { + // Build cover filename. + $filename = 'public://ting_search_carousel/' . md5($local_id) . '.jpg'; + + // Check if file have been downloaded. + if (!file_exists($filename)) { + // Extract the image source url. + $source_url = FALSE; + if ($cover->detailUrl) { + $source_url = $cover->detailUrl; + } + elseif ($cover->thumbnailUrl) { + $source_url = $cover->thumbnailUrl; } - $j++; + // Download the cover image and get local uri. + $file = _ting_covers_pages_fetch_image($filename, $source_url); + $filename = isset($file->uri) ? $file->uri : $filename; + } + + // Build object with the information collected. + $item = new stdClass(); + $item->id = $objects[$local_id]['id']; + $item->title = $objects[$local_id]['title']; + $item->creator = $objects[$local_id]['creator']; + $item->image = image_style_url('ting_search_carousel', $filename); + + $items[] = $item; + } + + // Update the cache with the fetched items. + _ting_search_carousel_set_cache($index, $items); +} + +/** + * Returns the content for a search carousel tab. It utilizes both static and + * database cache to optimize the cover page search. It will slowly fill the + * cache based on the tab index provide. This index matches the search carousel + * query index set in the administration backend, when defining the tabs. + * + * If no cache is found it tries to search the data well and calls it self once + * more to fetch the newly fetch data. If data still do not exists in the cache + * an empty string is returned. + * + * @param int $index + * Search carsousel tab index. + * + * @param bool $search + * If FALSE an attempt to fetch data _not_ be tried. Defaults to TRUE. + * + * @return mixed + * Returns an array with ting covers or the empty string if non is found. + */ +function _ting_search_carousel_get_content($index, $search = TRUE) { + // Utilize the static cache. + $static = &drupal_static(__FUNCTION__); + if (isset($static[$index]) && !empty($static[$index])) { + return $static[$index]; + } + + // Check if there is a database cache of the search results. + if ($cache = cache_get('ting_search_carousel_result')) { + // Get cached data and store it in the static cache. + $data = $cache->data; + $static = $data; + + // Check if data exists for the index. + if (isset($data[$index]) && !empty($data[$index])) { + return $data[$index]; } } - // Clear & set the cache. - cache_clear_all('ting_search_result', 'cache'); - cache_set('ting_search_result', serialize($search_items), 'cache'); + // No cache found for that index, so try getting information from the + // data well and cover service. + if ($search) { + ting_search_carousel_do_request($index); + return _ting_search_carousel_get_content($index, FALSE); + } + + // No form for cached data was found. + return ''; +} + +/** + * Set database cache base on tab index in the front-end. + * + * @param int $index + * Search carousel tab index. + * + * @param mixed $items + * Search carousel items, containing id, title, creator and image url. + */ +function _ting_search_carousel_set_cache($index, $items) { + $data = array(); + if ($cache = cache_get('ting_search_carousel_result')) { + $data = $cache->data; + } + $data[$index] = $items; + cache_set('ting_search_carousel_result', $data, 'cache'); } /** * Implements hook_image_default_styles(). */ function ting_search_carousel_image_default_styles() { - $styles = array(); - - $styles['ting_search_carousel'] = array( - 'name' => 'ting_search_carousel', - 'effects' => array( - 0 => array( - 'label' => 'Scale', - 'help' => 'Scaling will maintain the aspect-ratio of the original image. If only a single dimension is specified, the other dimension will be calculated.', - 'effect callback' => 'image_scale_effect', - 'dimensions callback' => 'image_scale_dimensions', - 'form callback' => 'image_scale_form', - 'summary theme' => 'image_scale_summary', - 'module' => 'image', - 'name' => 'image_scale', - 'data' => array( - 'width' => '', - 'height' => '140', - 'upscale' => 0, + return array( + 'ting_search_carousel' => array( + 'name' => 'ting_search_carousel', + 'effects' => array( + array( + 'label' => 'Scale and crop', + 'effect callback' => 'image_scale_and_crop_effect', + 'dimensions callback' => 'image_scale_dimensions', + 'module' => 'image', + 'name' => 'image_scale_and_crop', + 'data' => array( + 'width' => '200', + 'height' => '290', + ), + 'weight' => '1', ), - 'weight' => '1', ), ), ); +} - return $styles; +/** + * Wrapper function that adds the modules JavaScript. + */ +function _ting_search_carousel_add_javascript() { + $path = drupal_get_path('module', 'ting_search_carousel'); + + drupal_add_library('system', 'ui.widget'); + drupal_add_library('system', 'ui.draggable'); + drupal_add_js($path . '/js/jquery.ui.touch-punch.min.js'); + drupal_add_js($path . '/js/jquery.rs.carousel.js'); + drupal_add_js($path . '/js/ting_search_carousel.js'); } +/** + * Wrapper function that adds the modules css. + */ +function _ting_search_carousel_add_css() { + $path = drupal_get_path('module', 'ting_search_carousel'); + drupal_add_css($path . '/css/ting_search_carousel.css'); +} diff --git a/ting_search_carousel.pages.inc b/ting_search_carousel.pages.inc index 22a0202..4178cd6 100644 --- a/ting_search_carousel.pages.inc +++ b/ting_search_carousel.pages.inc @@ -1,36 +1,39 @@ data); - $ting_seach_queries = variable_get('ting_carousel_search_queries', array()); + // Set default return values. + $collections = _ting_search_carousel_get_content($index); $subtitle = ''; + // Get configuration. + $ting_seach_queries = variable_get('ting_carousel_search_queries', array()); if (isset($ting_seach_queries[$index])) { $subtitle = $ting_seach_queries[$index]['subtitle']; - foreach ($cache_ting_search_result[$index] as $k => $v) { - $result .= theme('ting_search_carousel_collection', array('collection' => $v)); + + // If cache have been found theme search carousel pages. + if (!empty($collections)) { + foreach ($collections as $collection) { + $content .= theme('ting_search_carousel_collection', array('collection' => $collection)); + } } } - echo drupal_json_encode(array('subtitle' => $subtitle, 'content' => $result)); - exit(); + // Return JSON output. + drupal_json_output(array( + 'subtitle' => $subtitle, + 'content' => $content, + 'index' => $index, + )); } diff --git a/ting_search_carousel.theme.inc b/ting_search_carousel.theme.inc new file mode 100644 index 0000000..abcb732 --- /dev/null +++ b/ting_search_carousel.theme.inc @@ -0,0 +1,19 @@ + - diff --git a/ting_search_carousel_collection.tpl.php b/ting_search_carousel_collection.tpl.php deleted file mode 100644 index 500c5dc..0000000 --- a/ting_search_carousel_collection.tpl.php +++ /dev/null @@ -1,15 +0,0 @@ - -