diff --git a/app/app/app.coffee b/app/app/app.coffee index b480d22c..3d1202c4 100644 --- a/app/app/app.coffee +++ b/app/app/app.coffee @@ -13,12 +13,10 @@ define [ ($, _, Backbone, Handlebars) -> - - app = root: '/' pendingTemplateReqs: {} - JST = window.JST = window.JST || {} + JST = app.JST = app.JST || {} Backbone.NMLayout = Backbone.Layout.extend # this function checks if the layout has initially been rendered. This is useful for setting views in a layout later, @@ -28,6 +26,10 @@ define [ @.setView selector, view if @.__manager__.hasRendered then view.render() + insertViewAndRenderMaybe: (selector, view) -> + @insertView selector, view + if @.__manager__.hasRendered then view.render() + Backbone.Layout.configure manage: true diff --git a/app/app/app.js b/app/app/app.js index b96b7f01..41a8a6ad 100644 --- a/app/app/app.js +++ b/app/app/app.js @@ -6,13 +6,19 @@ define(['jquery', 'underscore', 'backbone', 'handlebars', 'plugins/backbone.layo root: '/', pendingTemplateReqs: {} }; - JST = window.JST = window.JST || {}; + JST = app.JST = app.JST || {}; Backbone.NMLayout = Backbone.Layout.extend({ setViewAndRenderMaybe: function(selector, view) { this.setView(selector, view); if (this.__manager__.hasRendered) { return view.render(); } + }, + insertViewAndRenderMaybe: function(selector, view) { + this.insertView(selector, view); + if (this.__manager__.hasRendered) { + return view.render(); + } } }); Backbone.Layout.configure({ diff --git a/app/app/config.coffee b/app/app/config.coffee index a7700fbd..75e9b2d4 100644 --- a/app/app/config.coffee +++ b/app/app/config.coffee @@ -57,3 +57,8 @@ require.config 'plugins/backbone.JJRestApi' : ['backbone'] 'modules/NMMarkdownParser' : ['plugins/editor/jquery.jjmarkdown'] + + 'plugins/visualsearch/jquery.ui.autocomplete' : ['plugins/visualsearch/jquery.ui.widget'] + 'plugins/visualsearch/jquery.ui.menu' : ['plugins/visualsearch/jquery.ui.widget'] + 'plugins/visualsearch/visualsearch' : ['plugins/backbone.layoutmanager', 'plugins/visualsearch/jquery.ui.core', 'plugins/visualsearch/jquery.ui.autocomplete', 'plugins/visualsearch/jquery.ui.menu'] + diff --git a/app/app/config.js b/app/app/config.js index 1ea0ac1e..b6e7d559 100644 --- a/app/app/config.js +++ b/app/app/config.js @@ -42,6 +42,9 @@ require.config({ 'plugins/backbone.layoutmanager': ['backbone'], 'plugins/backbone.JJRelational': ['backbone'], 'plugins/backbone.JJRestApi': ['backbone'], - 'modules/NMMarkdownParser': ['plugins/editor/jquery.jjmarkdown'] + 'modules/NMMarkdownParser': ['plugins/editor/jquery.jjmarkdown'], + 'plugins/visualsearch/jquery.ui.autocomplete': ['plugins/visualsearch/jquery.ui.widget'], + 'plugins/visualsearch/jquery.ui.menu': ['plugins/visualsearch/jquery.ui.widget'], + 'plugins/visualsearch/visualsearch': ['plugins/backbone.layoutmanager', 'plugins/visualsearch/jquery.ui.core', 'plugins/visualsearch/jquery.ui.autocomplete', 'plugins/visualsearch/jquery.ui.menu'] } }); diff --git a/app/app/main.coffee b/app/app/main.coffee index 02a2b42d..96f3902e 100644 --- a/app/app/main.coffee +++ b/app/app/main.coffee @@ -301,6 +301,15 @@ require [ app.resolveClassTypeByHash = (uglyHash) -> @.Config.ClassEnc[uglyHash.substr(0,1)] + app.wholePortfolioJSON = -> + wholePortfolio = @Cache.WholePortfolio + unless @Cache.WholePortfolioJSON + tmp = [] + for model in wholePortfolio + tmp.push model.toJSON() + @Cache.WholePortfolioJSON = tmp + @Cache.WholePortfolioJSON + app.bindListeners() diff --git a/app/app/main.js b/app/app/main.js index 867a1699..40fca8dd 100644 --- a/app/app/main.js +++ b/app/app/main.js @@ -378,6 +378,20 @@ require(['app', 'router', 'modules/Auth', 'modules/Project', 'modules/Person', ' app.resolveClassTypeByHash = function(uglyHash) { return this.Config.ClassEnc[uglyHash.substr(0, 1)]; }; + app.wholePortfolioJSON = function() { + var model, tmp, wholePortfolio, _i, _len; + + wholePortfolio = this.Cache.WholePortfolio; + if (!this.Cache.WholePortfolioJSON) { + tmp = []; + for (_i = 0, _len = wholePortfolio.length; _i < _len; _i++) { + model = wholePortfolio[_i]; + tmp.push(model.toJSON()); + } + this.Cache.WholePortfolioJSON = tmp; + } + return this.Cache.WholePortfolioJSON; + }; app.bindListeners(); Backbone.View.prototype.showMessageAt = function(msg, $appendTo, className) { var $el; diff --git a/app/app/modules/DataRetrieval.coffee b/app/app/modules/DataRetrieval.coffee index c4cac8cf..4f0a53b6 100644 --- a/app/app/modules/DataRetrieval.coffee +++ b/app/app/modules/DataRetrieval.coffee @@ -37,22 +37,17 @@ define [ returnDfd.resolve() returnDfd.promise() + # function that takes a search term used to filter the whole portfolio filterProjectTypesBySearchTerm: (searchTerm) -> wholePortfolio = app.Cache.WholePortfolio - # because of simplicity reasons we iterate over an array of json objects. let's cache the whole json portfolio - unless app.Cache.WholePortfolioJSON - tmp = [] - for model in wholePortfolio - tmp.push model.toJSON() - app.Cache.WholePortfolioJSON = tmp - # transform the searchTerm searchObj = ProjectSearch.transformSearchTerm searchTerm - console.log searchObj + console.log 'Search obj found by data retrieval: %o', searchObj - result = _.filter app.Cache.WholePortfolioJSON, (model) -> + # because of simplicity reasons we iterate over an array of json objects. + result = _.filter app.wholePortfolioJSON(), (model) -> result = true _.each searchObj, (vals, key) -> if not ProjectSearch.test(model, key, vals) then result = false diff --git a/app/app/modules/DataRetrieval.js b/app/app/modules/DataRetrieval.js index 340eb648..bf5d2dea 100644 --- a/app/app/modules/DataRetrieval.js +++ b/app/app/modules/DataRetrieval.js @@ -36,20 +36,12 @@ define(['app', 'modules/ProjectSearch'], function(app, ProjectSearch) { return returnDfd.promise(); }, filterProjectTypesBySearchTerm: function(searchTerm) { - var model, out, result, searchObj, tmp, wholePortfolio, _i, _len; + var out, result, searchObj, wholePortfolio; wholePortfolio = app.Cache.WholePortfolio; - if (!app.Cache.WholePortfolioJSON) { - tmp = []; - for (_i = 0, _len = wholePortfolio.length; _i < _len; _i++) { - model = wholePortfolio[_i]; - tmp.push(model.toJSON()); - } - app.Cache.WholePortfolioJSON = tmp; - } searchObj = ProjectSearch.transformSearchTerm(searchTerm); - console.log(searchObj); - result = _.filter(app.Cache.WholePortfolioJSON, function(model) { + console.log('Search obj found by data retrieval: %o', searchObj); + result = _.filter(app.wholePortfolioJSON(), function(model) { result = true; _.each(searchObj, function(vals, key) { if (!ProjectSearch.test(model, key, vals)) { diff --git a/app/app/modules/Portfolio.coffee b/app/app/modules/Portfolio.coffee index ca8f118d..2b54e39d 100644 --- a/app/app/modules/Portfolio.coffee +++ b/app/app/modules/Portfolio.coffee @@ -20,8 +20,25 @@ define [ Backbone.Events.on 'search', @handleSearch, @ handleSearch: (searchResults) -> - console.log 'searched for: %o', searchResults - console.log @ + if @__manager__.hasRendered + @triggerSearchOnChildren searchResults + else + @searchResults = searchResults + @doSearchAfterRender = true + + triggerSearchOnChildren: (searchResults) -> + console.log searchResults + # iterate over child views and check if their model is present in the search results + _.each @views['.packery'], (childView) -> + model = childView.model + # always show when there is no search + unless searchResults + childView.doShow() + else + found = _.find searchResults, (result) -> + return result is childView.model + method = if found then 'doShow' else 'doHide' + childView[method]() beforeRender: -> @@ -32,7 +49,10 @@ define [ @.insertView '.packery', new Portfolio.Views.ListItem({ model: model, linkTo: @.options.linkTo }) _afterRender: -> - console.log @ + if @doSearchAfterRender + @triggerSearchOnChildren @searchResults + @doSearchAfterRender = false + @searchResults = null # debugger @@ -40,7 +60,16 @@ define [ tagName: 'article' className: 'packery-item resizable' template: 'packery-list-item' - serialize: () -> + + doShow: -> + console.log 'showing %o', @model + @.$el.removeClass 'hidden' + + doHide: -> + console.log 'hiding %o', @model + @.$el.addClass 'hidden' + + serialize: -> data = if @.model then @.model.toJSON() else {} data.Persons = _.sortBy data.Persons, (person) -> return person.Surname @@ -117,6 +146,14 @@ define [ else return niceDate + Handlebars.registerHelper 'SpaceAndLocation', -> + out = [] + if @Space then out.push @Space + if @Location then out.push @Location + out.join ', ' + if out + "

#{out}

" + Handlebars.registerHelper 'portfoliolist', (items, title, options) -> if not options options = title diff --git a/app/app/modules/Portfolio.js b/app/app/modules/Portfolio.js index 90876b00..8e7bf8ed 100644 --- a/app/app/modules/Portfolio.js +++ b/app/app/modules/Portfolio.js @@ -15,8 +15,29 @@ define(['app', 'modules/JJPackery'], function(app, JJPackery) { return Backbone.Events.on('search', this.handleSearch, this); }, handleSearch: function(searchResults) { - console.log('searched for: %o', searchResults); - return console.log(this); + if (this.__manager__.hasRendered) { + return this.triggerSearchOnChildren(searchResults); + } else { + this.searchResults = searchResults; + return this.doSearchAfterRender = true; + } + }, + triggerSearchOnChildren: function(searchResults) { + console.log(searchResults); + return _.each(this.views['.packery'], function(childView) { + var found, method, model; + + model = childView.model; + if (!searchResults) { + return childView.doShow(); + } else { + found = _.find(searchResults, function(result) { + return result === childView.model; + }); + method = found ? 'doShow' : 'doHide'; + return childView[method](); + } + }); }, beforeRender: function() { var model, modelArray, _i, _len, _results; @@ -36,13 +57,25 @@ define(['app', 'modules/JJPackery'], function(app, JJPackery) { } }, _afterRender: function() { - return console.log(this); + if (this.doSearchAfterRender) { + this.triggerSearchOnChildren(this.searchResults); + this.doSearchAfterRender = false; + return this.searchResults = null; + } } }); Portfolio.Views.ListItem = Backbone.View.extend({ tagName: 'article', className: 'packery-item resizable', template: 'packery-list-item', + doShow: function() { + console.log('showing %o', this.model); + return this.$el.removeClass('hidden'); + }, + doHide: function() { + console.log('hiding %o', this.model); + return this.$el.addClass('hidden'); + }, serialize: function() { var data; @@ -142,6 +175,21 @@ define(['app', 'modules/JJPackery'], function(app, JJPackery) { return niceDate; } }); + Handlebars.registerHelper('SpaceAndLocation', function() { + var out; + + out = []; + if (this.Space) { + out.push(this.Space); + } + if (this.Location) { + out.push(this.Location); + } + out.join(', '); + if (out) { + return "

" + out + "

"; + } + }); Handlebars.registerHelper('portfoliolist', function(items, title, options) { var length, out; diff --git a/app/app/modules/ProjectSearch.coffee b/app/app/modules/ProjectSearch.coffee index 1513af12..8358d6ab 100644 --- a/app/app/modules/ProjectSearch.coffee +++ b/app/app/modules/ProjectSearch.coffee @@ -1,20 +1,46 @@ define [ 'app' + 'plugins/visualsearch/visualsearch' ], (app) -> ProjectSearch = fields : 'Title' : 'partial' - 'Teaser' : (obj, valArray) -> - return @.partialMatchFilter obj, 'TeaserText', valArray - 'Name' : (obj, valArray) -> - return true + 'Space' : 'partial' + 'Location': 'partial' + 'Text': (obj, valArray) -> + return ProjectSearch.test obj, 'TeaserText', valArray, 'partial' + 'Type': (obj, valArray) -> + return ProjectSearch.test obj, 'ClassName', valArray, 'partial' 'Category': (obj, valArray) => - out = false - _.each obj.Categories, (cat) => - out = ProjectSearch.exactMatchFilter cat, 'Title', valArray - out + if obj.Categories and obj.Categories.length + result = true + # iterate over values + _.each valArray, (val) -> + out = false + # then check the categories, if anyone of them matches our value. all values must be matched by at least one obj! + _.each obj.Categories, (cat) -> + if ProjectSearch.exactMatchFilter(cat, 'Title', val) then out = true + if not out then result = false + + return result + false + 'Person': (obj, valArray) => + if obj.Persons and obj.Persons.length + result = true + # iterate over values + _.each ProjectSearch.partializeArray(valArray), (val) -> + out = false + # then check the persons, if anyone of them matches our value. all values must be matched by at least on obj! + _.each obj.Persons, (person) -> + fullName = (if person.FirstName then person.FirstName + ' ' else '') + (if person.Surname then person.Surname else '') + if fullName.indexOf(val) >= 0 then out = true + if not out then result = false + + return result + false + ###* * transforms a string into an object with the searchable field as key and the possible OR values as array @@ -23,6 +49,7 @@ define [ ### transformSearchTerm : (term) -> out = {} + term = decodeURI term for segment in term.split(';') els = segment.split ':' vals = null @@ -31,41 +58,210 @@ define [ out[els[0]] = vals out - test: (obj, key, valArray) -> + # opposite of transform + makeSearchTerm: (obj) -> + a = [] + for key, val of obj + a2 = [] + if not _.isArray(val) then val = [val] + for v in val + if v then a2.push v + if a2.length then a.push "#{key}:#{a2.join('|')}" + encodeURI a.join(';') + + + # useful function when matching partially: the searched terms are split by whitespaces and + # added as single possibilities + partializeArray: (valArray) -> + out = [] + for val in valArray + out = out.concat val.split(' ') + out + + test: (obj, key, valArray, forceMethod) -> + result = true if not _.isArray(valArray) then valArray = [valArray] - if type = @.fields[key] + type = forceMethod || @.fields[key] + if type # type has its own testing functionality if _.isFunction type - return type.call(@, obj, valArray) + result = type.call(@, obj, valArray) # check if 'partial' or 'exact'. defaults to 'partial' else if type is 'exact' - return @.exactMatchFilter obj, key, valArray - else if type - return @.partialMatchFilter obj, key, valArray + method = 'exactMatchFilter' + else + method = 'partialMatchFilter' + valArray = @partializeArray valArray + + for val in valArray + if not @[method](obj, key, val) then return false + # no method/filter specified for `key`, defaults to true - false + result - partialMatchFilter: (obj, key, valArray) -> - query = valArray.join '|' - pattern = new RegExp $.trim(query), 'i' - console.log obj[key] - if obj.hasOwnProperty(key) and pattern.test(obj[key]) then return true + + # tests obj[key] against a value. of one of them matches obj[key] partially, the test passes + partialMatchFilter: (obj, key, val) -> + if not obj.hasOwnProperty(key) then return false + against = obj[key].toLowerCase() + if against.indexOf(val.toLowerCase()) >= 0 then return true false - exactMatchFilter: (obj, key, valArray) -> + + # tests obj[key] against a value. if one of them matches obj[key] exactly, the test passes + exactMatchFilter: (obj, key, val) -> if not obj.hasOwnProperty(key) then return false - for val in valArray - query = "^#{val}$" - pattern = new RegExp query, 'i' - if pattern.test(val) then return true + if val.toLowerCase() is obj[key].toLowerCase() then return true false + # collect all possible auto complete matches from the current portfolio + ProjectSearch.getVisualSearchMatches = -> + wholePortfolio = app.wholePortfolioJSON() + matches = + Title: [] + Space: [] + Location: [] + Person: [] + Year: [] + Type: ['Project', 'Exhibition', 'Excursion', 'Workshop'] + + years = [] + persons = [] + used = [] + + _.each wholePortfolio, (m) -> + matches.Title.push(m.Title) if m.Title + matches.Space.push(m.Space) if m.Space + matches.Location.push(m.Location) if m.Location + + d = parseInt(m.YearSearch) if m.YearSearch + years.push d if d + + _.each m.Persons, (person) -> + if person.FirstName and person.Surname + fullname = "#{person.Surname}, #{person.FirstName}" + if _.indexOf(used, fullname) < 0 + persons.push { label: fullname, value: "#{person.FirstName} #{person.Surname}" } + used.push fullname + + # unique persons and sort + matches.Person = _.sortBy persons, (p) -> + p.label + + # sort years + years = _.sortBy _.uniq(years), (y) -> + y * -1 + for year in years + matches.Year.push year.toString() + + matches + + ProjectSearch.View = Backbone.View.extend + template: 'searchbar' + id: 'searchbar' + + search: + 'Category': [] + + events: + 'click .category-filter a': 'updateCategorySearch' + + initialize: (opts) -> + if opts.searchTerm then @search = ProjectSearch.transformSearchTerm opts.searchTerm + @search.Category = [] if not @search.Category + + # takes @search-obj, trasnforms it into a valid search url and fires it. rest is handled by router and DataRetrieval + doSearch: -> + console.group 'searching for' + console.log @search + searchTerm = ProjectSearch.makeSearchTerm @search + console.log searchTerm + console.groupEnd() + directTo = if searchTerm then "/portfolio/search/#{searchTerm}/" else '/portfolio/' + Backbone.history.navigate directTo, true + + updateCategorySearch: (e) -> + e.preventDefault() + + $a = $(e.target) + $a.blur() + title = $a.data('title') + i = _.indexOf @search.Category, title + if i < 0 + @search.Category.push title + meth = 'addClass' + else + @search.Category.splice i, 1 + meth = 'removeClass' + $a[meth]('active') + @doSearch() + false + + initVisualSearch: -> + $visSearch = @.$el.find '.visualsearch' + + autoMatches = ProjectSearch.VisualSearchMatches = ProjectSearch.VisualSearchMatches || ProjectSearch.getVisualSearchMatches() + + @visualSearch = VS.init + container: $visSearch + remainder: 'Text' + callbacks: + search: (query, searchCollection) => + # don't lose categories + @search = + Category: @search.Category + searchCollection.each (facet) => + cat = facet.get 'category' + @search[cat] = [] if not @search[cat] + @search[cat].push facet.get('value') + console.log @search + @doSearch() + facetMatches: (callback) -> + callback ['Type', 'Person', 'Title', 'Year', 'Space', 'Location'] + valueMatches: (facet, searchTerm, callback) -> + switch facet + when 'Person' then callback autoMatches.Person + when 'Title' then callback autoMatches.Title + when 'Year' then callback autoMatches.Year + when 'Space' then callback autoMatches.Space + when 'Location' then callback autoMatches.Location + when 'Type' then callback autoMatches.Type + + @prePopulateSearchBox() + console.log 'Project search view: %o', @search + console.log @visualSearch + + prePopulateSearchBox: -> + query = '' + for key, val of @search + # leave out categories + if key isnt 'Category' + for v in val + query += "\"#{key}\": \"#{v}\" " + @visualSearch.searchBox.value query + + updateCategoryClasses: -> + _this = @ + @.$el.find('.category-filter a').each -> + $this = $ @ + title = $this.data('title').toLowerCase() + + for cat in _this.search.Category + if cat.toLowerCase() is title then $this.addClass 'active' + afterRender: -> + @updateCategoryClasses() + @initVisualSearch() + serialize: -> + json = {} + json.Categories = _.map app.Collections.Category.models, (cat) -> + { ID: cat.id, Title: cat.get('Title') } + json ProjectSearch \ No newline at end of file diff --git a/app/app/modules/ProjectSearch.js b/app/app/modules/ProjectSearch.js index 07e05b00..aca11e7a 100644 --- a/app/app/modules/ProjectSearch.js +++ b/app/app/modules/ProjectSearch.js @@ -1,25 +1,65 @@ // Generated by CoffeeScript 1.6.2 -define(['app'], function(app) { +define(['app', 'plugins/visualsearch/visualsearch'], function(app) { var ProjectSearch, _this = this; ProjectSearch = { fields: { 'Title': 'partial', - 'Teaser': function(obj, valArray) { - return this.partialMatchFilter(obj, 'TeaserText', valArray); + 'Space': 'partial', + 'Location': 'partial', + 'Text': function(obj, valArray) { + return ProjectSearch.test(obj, 'TeaserText', valArray, 'partial'); }, - 'Name': function(obj, valArray) { - return true; + 'Type': function(obj, valArray) { + return ProjectSearch.test(obj, 'ClassName', valArray, 'partial'); }, 'Category': function(obj, valArray) { - var out; + var result; + + if (obj.Categories && obj.Categories.length) { + result = true; + _.each(valArray, function(val) { + var out; + + out = false; + _.each(obj.Categories, function(cat) { + if (ProjectSearch.exactMatchFilter(cat, 'Title', val)) { + return out = true; + } + }); + if (!out) { + return result = false; + } + }); + return result; + } + return false; + }, + 'Person': function(obj, valArray) { + var result; + + if (obj.Persons && obj.Persons.length) { + result = true; + _.each(ProjectSearch.partializeArray(valArray), function(val) { + var out; + + out = false; + _.each(obj.Persons, function(person) { + var fullName; - out = false; - _.each(obj.Categories, function(cat) { - return out = ProjectSearch.exactMatchFilter(cat, 'Title', valArray); - }); - return out; + fullName = (person.FirstName ? person.FirstName + ' ' : '') + (person.Surname ? person.Surname : ''); + if (fullName.indexOf(val) >= 0) { + return out = true; + } + }); + if (!out) { + return result = false; + } + }); + return result; + } + return false; } }, /** @@ -32,6 +72,7 @@ define(['app'], function(app) { var els, out, segment, vals, _i, _len, _ref; out = {}; + term = decodeURI(term); _ref = term.split(';'); for (_i = 0, _len = _ref.length; _i < _len; _i++) { segment = _ref[_i]; @@ -44,52 +85,298 @@ define(['app'], function(app) { } return out; }, - test: function(obj, key, valArray) { - var type; + makeSearchTerm: function(obj) { + var a, a2, key, v, val, _i, _len; + a = []; + for (key in obj) { + val = obj[key]; + a2 = []; + if (!_.isArray(val)) { + val = [val]; + } + for (_i = 0, _len = val.length; _i < _len; _i++) { + v = val[_i]; + if (v) { + a2.push(v); + } + } + if (a2.length) { + a.push("" + key + ":" + (a2.join('|'))); + } + } + return encodeURI(a.join(';')); + }, + partializeArray: function(valArray) { + var out, val, _i, _len; + + out = []; + for (_i = 0, _len = valArray.length; _i < _len; _i++) { + val = valArray[_i]; + out = out.concat(val.split(' ')); + } + return out; + }, + test: function(obj, key, valArray, forceMethod) { + var method, result, type, val, _i, _len; + + result = true; if (!_.isArray(valArray)) { valArray = [valArray]; } - if (type = this.fields[key]) { + type = forceMethod || this.fields[key]; + if (type) { if (_.isFunction(type)) { - return type.call(this, obj, valArray); + result = type.call(this, obj, valArray); } else { if (type === 'exact') { - return this.exactMatchFilter(obj, key, valArray); - } else if (type) { - return this.partialMatchFilter(obj, key, valArray); + method = 'exactMatchFilter'; + } else { + method = 'partialMatchFilter'; + valArray = this.partializeArray(valArray); + } + for (_i = 0, _len = valArray.length; _i < _len; _i++) { + val = valArray[_i]; + if (!this[method](obj, key, val)) { + return false; + } } } } - return false; + return result; }, - partialMatchFilter: function(obj, key, valArray) { - var pattern, query; + partialMatchFilter: function(obj, key, val) { + var against; - query = valArray.join('|'); - pattern = new RegExp($.trim(query), 'i'); - console.log(obj[key]); - if (obj.hasOwnProperty(key) && pattern.test(obj[key])) { + if (!obj.hasOwnProperty(key)) { + return false; + } + against = obj[key].toLowerCase(); + if (against.indexOf(val.toLowerCase()) >= 0) { return true; } return false; }, - exactMatchFilter: function(obj, key, valArray) { - var pattern, query, val, _i, _len; - + exactMatchFilter: function(obj, key, val) { if (!obj.hasOwnProperty(key)) { return false; } - for (_i = 0, _len = valArray.length; _i < _len; _i++) { - val = valArray[_i]; - query = "^" + val + "$"; - pattern = new RegExp(query, 'i'); - if (pattern.test(val)) { - return true; - } + if (val.toLowerCase() === obj[key].toLowerCase()) { + return true; } return false; } }; + ProjectSearch.getVisualSearchMatches = function() { + var matches, persons, used, wholePortfolio, year, years, _i, _len; + + wholePortfolio = app.wholePortfolioJSON(); + matches = { + Title: [], + Space: [], + Location: [], + Person: [], + Year: [], + Type: ['Project', 'Exhibition', 'Excursion', 'Workshop'] + }; + years = []; + persons = []; + used = []; + _.each(wholePortfolio, function(m) { + var d; + + if (m.Title) { + matches.Title.push(m.Title); + } + if (m.Space) { + matches.Space.push(m.Space); + } + if (m.Location) { + matches.Location.push(m.Location); + } + if (m.YearSearch) { + d = parseInt(m.YearSearch); + } + if (d) { + years.push(d); + } + return _.each(m.Persons, function(person) { + var fullname; + + if (person.FirstName && person.Surname) { + fullname = "" + person.Surname + ", " + person.FirstName; + if (_.indexOf(used, fullname) < 0) { + persons.push({ + label: fullname, + value: "" + person.FirstName + " " + person.Surname + }); + return used.push(fullname); + } + } + }); + }); + matches.Person = _.sortBy(persons, function(p) { + return p.label; + }); + years = _.sortBy(_.uniq(years), function(y) { + return y * -1; + }); + for (_i = 0, _len = years.length; _i < _len; _i++) { + year = years[_i]; + matches.Year.push(year.toString()); + } + return matches; + }; + ProjectSearch.View = Backbone.View.extend({ + template: 'searchbar', + id: 'searchbar', + search: { + 'Category': [] + }, + events: { + 'click .category-filter a': 'updateCategorySearch' + }, + initialize: function(opts) { + if (opts.searchTerm) { + this.search = ProjectSearch.transformSearchTerm(opts.searchTerm); + } + if (!this.search.Category) { + return this.search.Category = []; + } + }, + doSearch: function() { + var directTo, searchTerm; + + console.group('searching for'); + console.log(this.search); + searchTerm = ProjectSearch.makeSearchTerm(this.search); + console.log(searchTerm); + console.groupEnd(); + directTo = searchTerm ? "/portfolio/search/" + searchTerm + "/" : '/portfolio/'; + return Backbone.history.navigate(directTo, true); + }, + updateCategorySearch: function(e) { + var $a, i, meth, title; + + e.preventDefault(); + $a = $(e.target); + $a.blur(); + title = $a.data('title'); + i = _.indexOf(this.search.Category, title); + if (i < 0) { + this.search.Category.push(title); + meth = 'addClass'; + } else { + this.search.Category.splice(i, 1); + meth = 'removeClass'; + } + $a[meth]('active'); + this.doSearch(); + return false; + }, + initVisualSearch: function() { + var $visSearch, autoMatches, + _this = this; + + $visSearch = this.$el.find('.visualsearch'); + autoMatches = ProjectSearch.VisualSearchMatches = ProjectSearch.VisualSearchMatches || ProjectSearch.getVisualSearchMatches(); + this.visualSearch = VS.init({ + container: $visSearch, + remainder: 'Text', + callbacks: { + search: function(query, searchCollection) { + _this.search = { + Category: _this.search.Category + }; + searchCollection.each(function(facet) { + var cat; + + cat = facet.get('category'); + if (!_this.search[cat]) { + _this.search[cat] = []; + } + return _this.search[cat].push(facet.get('value')); + }); + console.log(_this.search); + return _this.doSearch(); + }, + facetMatches: function(callback) { + return callback(['Type', 'Person', 'Title', 'Year', 'Space', 'Location']); + }, + valueMatches: function(facet, searchTerm, callback) { + switch (facet) { + case 'Person': + return callback(autoMatches.Person); + case 'Title': + return callback(autoMatches.Title); + case 'Year': + return callback(autoMatches.Year); + case 'Space': + return callback(autoMatches.Space); + case 'Location': + return callback(autoMatches.Location); + case 'Type': + return callback(autoMatches.Type); + } + } + } + }); + this.prePopulateSearchBox(); + console.log('Project search view: %o', this.search); + return console.log(this.visualSearch); + }, + prePopulateSearchBox: function() { + var key, query, v, val, _i, _len, _ref; + + query = ''; + _ref = this.search; + for (key in _ref) { + val = _ref[key]; + if (key !== 'Category') { + for (_i = 0, _len = val.length; _i < _len; _i++) { + v = val[_i]; + query += "\"" + key + "\": \"" + v + "\" "; + } + } + } + return this.visualSearch.searchBox.value(query); + }, + updateCategoryClasses: function() { + _this = this; + return this.$el.find('.category-filter a').each(function() { + var $this, cat, title, _i, _len, _ref, _results; + + $this = $(this); + title = $this.data('title').toLowerCase(); + _ref = _this.search.Category; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + cat = _ref[_i]; + if (cat.toLowerCase() === title) { + _results.push($this.addClass('active')); + } else { + _results.push(void 0); + } + } + return _results; + }); + }, + afterRender: function() { + this.updateCategoryClasses(); + return this.initVisualSearch(); + }, + serialize: function() { + var json; + + json = {}; + json.Categories = _.map(app.Collections.Category.models, function(cat) { + return { + ID: cat.id, + Title: cat.get('Title') + }; + }); + return json; + } + }); return ProjectSearch; }); diff --git a/app/app/router.coffee b/app/app/router.coffee index 76c8331a..bb6b343d 100644 --- a/app/app/router.coffee +++ b/app/app/router.coffee @@ -202,10 +202,13 @@ define [ unless justUpdate @.showPackeryViewForModels modelsArray, 'portfolio', layout + layout.insertViewAndRenderMaybe '', new ProjectSearch.View({searchTerm: searchTerm}) - if searchTerm - searchedForArray = DataRetrieval.filterProjectTypesBySearchTerm searchTerm - Backbone.Events.trigger 'search', searchedForArray + # handle search + searchedFor = if searchTerm then DataRetrieval.filterProjectTypesBySearchTerm(searchTerm) else null + console.log 'foobar' + console.log searchedFor + Backbone.Events.trigger 'search', searchedFor @@ -233,7 +236,8 @@ define [ detailView = if not template then new Portfolio.Views.Detail({ model: model }) else new Person.Views.Custom({ model: model, template: template }) layout.setViewAndRenderMaybe '', detailView else - mainDfd.done @.fourOhFour + mainDfd.done => + @.fourOhFour() mainDfd.resolve() showCalendar: () -> diff --git a/app/app/router.js b/app/app/router.js index e1ce8a7f..a5da07af 100644 --- a/app/app/router.js +++ b/app/app/router.js @@ -200,7 +200,7 @@ define(['app', 'modules/Auth', 'modules/Project', 'modules/Person', 'modules/Exc } justUpdate = app.currentLayoutName === 'portfolio' ? true : false; return mainDfd.done(function() { - var layout, modelsArray, searchedForArray; + var layout, modelsArray, searchedFor; if (!justUpdate) { layout = app.useLayout('portfolio'); @@ -214,11 +214,14 @@ define(['app', 'modules/Auth', 'modules/Project', 'modules/Person', 'modules/Exc modelsArray = app.Cache.WholePortfolio; if (!justUpdate) { _this.showPackeryViewForModels(modelsArray, 'portfolio', layout); + layout.insertViewAndRenderMaybe('', new ProjectSearch.View({ + searchTerm: searchTerm + })); } - if (searchTerm) { - searchedForArray = DataRetrieval.filterProjectTypesBySearchTerm(searchTerm); - return Backbone.Events.trigger('search', searchedForArray); - } + searchedFor = searchTerm ? DataRetrieval.filterProjectTypesBySearchTerm(searchTerm) : null; + console.log('foobar'); + console.log(searchedFor); + return Backbone.Events.trigger('search', searchedFor); }); }, showPortfolioDetailed: function(uglyHash, nameSlug) { @@ -262,7 +265,9 @@ define(['app', 'modules/Auth', 'modules/Project', 'modules/Person', 'modules/Exc return layout.setViewAndRenderMaybe('', detailView); }); } else { - mainDfd.done(this.fourOhFour); + mainDfd.done(function() { + return _this.fourOhFour(); + }); return mainDfd.resolve(); } }, diff --git a/app/app/templates/packery-container.html b/app/app/templates/packery-container.html index 78060758..300cdce7 100644 --- a/app/app/templates/packery-container.html +++ b/app/app/templates/packery-container.html @@ -27,8 +27,6 @@
-
- \ No newline at end of file diff --git a/app/app/templates/portfolio-detail.html b/app/app/templates/portfolio-detail.html index 48ffadfb..815a8c89 100644 --- a/app/app/templates/portfolio-detail.html +++ b/app/app/templates/portfolio-detail.html @@ -5,6 +5,9 @@

{{Title}}

{{{nameSummary Persons}}}

{{niceDate this true}}

+ {{#stringDiff "Project" ClassName}} + {{{SpaceAndLocation}}} + {{/stringDiff}} {{#if Websites}}

{{{commaSeparatedWebsites Websites}}}

{{/if}} diff --git a/app/app/templates/searchbar.html b/app/app/templates/searchbar.html new file mode 100644 index 00000000..3d4bfd03 --- /dev/null +++ b/app/app/templates/searchbar.html @@ -0,0 +1,359 @@ + + + +
+ {{#if Categories}} + + {{/if}} +
+
+ + + + + + + + \ No newline at end of file diff --git a/app/app/templates/security/editor-project-preview.html b/app/app/templates/security/editor-project-preview.html index 93c13680..18b7dcb1 100644 --- a/app/app/templates/security/editor-project-preview.html +++ b/app/app/templates/security/editor-project-preview.html @@ -17,6 +17,10 @@

{{niceDate this}}

{{/stringCompare}} + {{#stringDiff "Project" ClassName}} +

{{Space}}

+

{{Location}}

+ {{/stringDiff}}

{{TeaserText}}

\ No newline at end of file diff --git a/app/assets/js/plugins/editor/jquery.editor-popover.coffee b/app/assets/js/plugins/editor/jquery.editor-popover.coffee index 63a55600..a016bdb7 100644 --- a/app/assets/js/plugins/editor/jquery.editor-popover.coffee +++ b/app/assets/js/plugins/editor/jquery.editor-popover.coffee @@ -50,7 +50,7 @@ do ($ = jQuery) -> # @param int start # @param int end ### - $.fn.selectRange = (start, end) -> + $.fn._selectRange = (start, end) -> end = start unless end @each -> if @['setSelectionRange'] @@ -836,7 +836,7 @@ do ($ = jQuery) -> $input = $('input, textarea', @api.tooltip).eq 0 # set cursor to the end of the first input or textarea element try - $input.selectRange $input.val().length + $input._selectRange $input.val().length catch e false diff --git a/app/assets/js/plugins/editor/jquery.editor-popover.js b/app/assets/js/plugins/editor/jquery.editor-popover.js index 39f43eac..d02d7ad6 100644 --- a/app/assets/js/plugins/editor/jquery.editor-popover.js +++ b/app/assets/js/plugins/editor/jquery.editor-popover.js @@ -75,7 +75,7 @@ var __hasProp = {}.hasOwnProperty, var DateEditable, InlineEditable, JJEditable, JJEditor, JJPopoverEditable, MarkdownEditable, ModalEditable, SelectEditable, SelectListConfirmEditable, SelectListEditable, SelectPersonEditable, SplitMarkdownEditable, _ref, _ref1, _ref2, _ref3, _ref4, _ref5, _ref6, _ref7, _ref8; - $.fn.selectRange = function(start, end) { + $.fn._selectRange = function(start, end) { if (!end) { end = start; } @@ -1085,7 +1085,7 @@ var __hasProp = {}.hasOwnProperty, $input = $('input, textarea', _this.api.tooltip).eq(0); try { - $input.selectRange($input.val().length); + $input._selectRange($input.val().length); } catch (_error) { e = _error; false; diff --git a/app/assets/js/plugins/visualsearch/jquery.ui.autocomplete.js b/app/assets/js/plugins/visualsearch/jquery.ui.autocomplete.js new file mode 100644 index 00000000..a7f5065c --- /dev/null +++ b/app/assets/js/plugins/visualsearch/jquery.ui.autocomplete.js @@ -0,0 +1,614 @@ +/*! + * jQuery UI Autocomplete 1.10.0 + * http://jqueryui.com + * + * Copyright 2013 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/autocomplete/ + * + * Depends: + * jquery.ui.core.js + * jquery.ui.widget.js + * jquery.ui.position.js + * jquery.ui.menu.js + */ +(function( $, undefined ) { + +// used to prevent race conditions with remote data sources +var requestIndex = 0; + +$.widget( "ui.autocomplete", { + version: "1.10.0", + defaultElement: "", + options: { + appendTo: null, + autoFocus: false, + delay: 300, + minLength: 1, + position: { + my: "left top", + at: "left bottom", + collision: "none" + }, + source: null, + + // callbacks + change: null, + close: null, + focus: null, + open: null, + response: null, + search: null, + select: null + }, + + pending: 0, + + _create: function() { + // Some browsers only repeat keydown events, not keypress events, + // so we use the suppressKeyPress flag to determine if we've already + // handled the keydown event. #7269 + // Unfortunately the code for & in keypress is the same as the up arrow, + // so we use the suppressKeyPressRepeat flag to avoid handling keypress + // events when we know the keydown event was used to modify the + // search term. #7799 + var suppressKeyPress, suppressKeyPressRepeat, suppressInput; + + this.isMultiLine = this._isMultiLine(); + this.valueMethod = this.element[ this.element.is( "input,textarea" ) ? "val" : "text" ]; + this.isNewMenu = true; + + this.element + .addClass( "ui-autocomplete-input" ) + .attr( "autocomplete", "off" ); + + this._on( this.element, { + keydown: function( event ) { + /*jshint maxcomplexity:15*/ + if ( this.element.prop( "readOnly" ) ) { + suppressKeyPress = true; + suppressInput = true; + suppressKeyPressRepeat = true; + return; + } + + suppressKeyPress = false; + suppressInput = false; + suppressKeyPressRepeat = false; + var keyCode = $.ui.keyCode; + switch( event.keyCode ) { + case keyCode.PAGE_UP: + suppressKeyPress = true; + this._move( "previousPage", event ); + break; + case keyCode.PAGE_DOWN: + suppressKeyPress = true; + this._move( "nextPage", event ); + break; + case keyCode.UP: + suppressKeyPress = true; + this._keyEvent( "previous", event ); + break; + case keyCode.DOWN: + suppressKeyPress = true; + this._keyEvent( "next", event ); + break; + case keyCode.ENTER: + case keyCode.NUMPAD_ENTER: + // when menu is open and has focus + if ( this.menu.active ) { + // #6055 - Opera still allows the keypress to occur + // which causes forms to submit + suppressKeyPress = true; + event.preventDefault(); + this.menu.select( event ); + } + break; + case keyCode.TAB: + if ( this.menu.active ) { + this.menu.select( event ); + } + break; + case keyCode.ESCAPE: + if ( this.menu.element.is( ":visible" ) ) { + this._value( this.term ); + this.close( event ); + // Different browsers have different default behavior for escape + // Single press can mean undo or clear + // Double press in IE means clear the whole form + event.preventDefault(); + } + break; + default: + suppressKeyPressRepeat = true; + // search timeout should be triggered before the input value is changed + this._searchTimeout( event ); + break; + } + }, + keypress: function( event ) { + if ( suppressKeyPress ) { + suppressKeyPress = false; + event.preventDefault(); + return; + } + if ( suppressKeyPressRepeat ) { + return; + } + + // replicate some key handlers to allow them to repeat in Firefox and Opera + var keyCode = $.ui.keyCode; + switch( event.keyCode ) { + case keyCode.PAGE_UP: + this._move( "previousPage", event ); + break; + case keyCode.PAGE_DOWN: + this._move( "nextPage", event ); + break; + case keyCode.UP: + this._keyEvent( "previous", event ); + break; + case keyCode.DOWN: + this._keyEvent( "next", event ); + break; + } + }, + input: function( event ) { + if ( suppressInput ) { + suppressInput = false; + event.preventDefault(); + return; + } + this._searchTimeout( event ); + }, + focus: function() { + this.selectedItem = null; + this.previous = this._value(); + }, + blur: function( event ) { + if ( this.cancelBlur ) { + delete this.cancelBlur; + return; + } + + clearTimeout( this.searching ); + this.close( event ); + this._change( event ); + } + }); + + this._initSource(); + this.menu = $( "