Bug 11516: make OPAC search term highlighting work in results browser
[koha.git] / koha-tmpl / opac-tmpl / bootstrap / lib / enquire.js
1 // enquire.js v2.0.1 - Awesome Media Queries in JavaScript
2 // Copyright (c) 2013 Nick Williams - http://wicky.nillia.ms/enquire.js
3 // License: MIT (http://www.opensource.org/licenses/mit-license.php)
4
5 ;(function(global) {
6
7     'use strict';
8
9     var matchMedia = global.matchMedia;
10     /*jshint -W098 */
11     /**
12      * Helper function for iterating over a collection
13      *
14      * @param collection
15      * @param fn
16      */
17     function each(collection, fn) {
18         var i      = 0,
19             length = collection.length,
20             cont;
21
22         for(i; i < length; i++) {
23             cont = fn(collection[i], i);
24             if(cont === false) {
25                 break; //allow early exit
26             }
27         }
28     }
29
30     /**
31      * Helper function for determining whether target object is an array
32      *
33      * @param target the object under test
34      * @return {Boolean} true if array, false otherwise
35      */
36     function isArray(target) {
37         return Object.prototype.toString.apply(target) === '[object Array]';
38     }
39
40     /**
41      * Helper function for determining whether target object is a function
42      *
43      * @param target the object under test
44      * @return {Boolean} true if function, false otherwise
45      */
46     function isFunction(target) {
47         return typeof target === 'function';
48     }
49
50     /**
51      * Delegate to handle a media query being matched and unmatched.
52      *
53      * @param {object} options
54      * @param {function} options.match callback for when the media query is matched
55      * @param {function} [options.unmatch] callback for when the media query is unmatched
56      * @param {function} [options.setup] one-time callback triggered the first time a query is matched
57      * @param {boolean} [options.deferSetup=false] should the setup callback be run immediately, rather than first time query is matched?
58      * @constructor
59      */
60     function QueryHandler(options) {
61         this.options = options;
62         !options.deferSetup && this.setup();
63     }
64     QueryHandler.prototype = {
65
66         /**
67          * coordinates setup of the handler
68          *
69          * @function
70          */
71         setup : function() {
72             if(this.options.setup) {
73                 this.options.setup();
74             }
75             this.initialised = true;
76         },
77
78         /**
79          * coordinates setup and triggering of the handler
80          *
81          * @function
82          */
83         on : function() {
84             !this.initialised && this.setup();
85             this.options.match && this.options.match();
86         },
87
88         /**
89          * coordinates the unmatch event for the handler
90          *
91          * @function
92          */
93         off : function() {
94             this.options.unmatch && this.options.unmatch();
95         },
96
97         /**
98          * called when a handler is to be destroyed.
99          * delegates to the destroy or unmatch callbacks, depending on availability.
100          *
101          * @function
102          */
103         destroy : function() {
104             this.options.destroy ? this.options.destroy() : this.off();
105         },
106
107         /**
108          * determines equality by reference.
109          * if object is supplied compare options, if function, compare match callback
110          *
111          * @function
112          * @param {object || function} [target] the target for comparison
113          */
114         equals : function(target) {
115             return this.options === target || this.options.match === target;
116         }
117
118     };
119 /**
120  * Represents a single media query, manages it's state and registered handlers for this query
121  *
122  * @constructor
123  * @param {string} query the media query string
124  * @param {boolean} [isUnconditional=false] whether the media query should run regardless of whether the conditions are met. Primarily for helping older browsers deal with mobile-first design
125  */
126 function MediaQuery(query, isUnconditional) {
127     this.query = query;
128     this.isUnconditional = isUnconditional;
129     this.handlers = [];
130     this.mql = matchMedia(query);
131
132     var self = this;
133     this.listener = function(mql) {
134         self.mql = mql;
135         self.assess();
136     };
137     this.mql.addListener(this.listener);
138 }
139 MediaQuery.prototype = {
140
141     /**
142      * add a handler for this query, triggering if already active
143      *
144      * @param {object} handler
145      * @param {function} handler.match callback for when query is activated
146      * @param {function} [handler.unmatch] callback for when query is deactivated
147      * @param {function} [handler.setup] callback for immediate execution when a query handler is registered
148      * @param {boolean} [handler.deferSetup=false] should the setup callback be deferred until the first time the handler is matched?
149      */
150     addHandler : function(handler) {
151         var qh = new QueryHandler(handler);
152         this.handlers.push(qh);
153
154         this.matches() && qh.on();
155     },
156
157     /**
158      * removes the given handler from the collection, and calls it's destroy methods
159      *
160      * @param {object || function} handler the handler to remove
161      */
162     removeHandler : function(handler) {
163         var handlers = this.handlers;
164         each(handlers, function(h, i) {
165             if(h.equals(handler)) {
166                 h.destroy();
167                 return !handlers.splice(i,1); //remove from array and exit each early
168             }
169         });
170     },
171
172     /**
173      * Determine whether the media query should be considered a match
174      *
175      * @return {Boolean} true if media query can be considered a match, false otherwise
176      */
177     matches : function() {
178         return this.mql.matches || this.isUnconditional;
179     },
180
181     /**
182      * Clears all handlers and unbinds events
183      */
184     clear : function() {
185         each(this.handlers, function(handler) {
186             handler.destroy();
187         });
188         this.mql.removeListener(this.listener);
189         this.handlers.length = 0; //clear array
190     },
191
192     /*
193      * Assesses the query, turning on all handlers if it matches, turning them off if it doesn't match
194      */
195     assess : function() {
196         var action = this.matches() ? 'on' : 'off';
197
198         each(this.handlers, function(handler) {
199             handler[action]();
200         });
201     }
202 };
203     /**
204      * Allows for registration of query handlers.
205      * Manages the query handler's state and is responsible for wiring up browser events
206      *
207      * @constructor
208      */
209     function MediaQueryDispatch () {
210         if(!matchMedia) {
211             throw new Error('matchMedia not present, legacy browsers require a polyfill');
212         }
213
214         this.queries = {};
215         this.browserIsIncapable = !matchMedia('only all').matches;
216     }
217
218     MediaQueryDispatch.prototype = {
219
220         /**
221          * Registers a handler for the given media query
222          *
223          * @param {string} q the media query
224          * @param {object || Array || Function} options either a single query handler object, a function, or an array of query handlers
225          * @param {function} options.match fired when query matched
226          * @param {function} [options.unmatch] fired when a query is no longer matched
227          * @param {function} [options.setup] fired when handler first triggered
228          * @param {boolean} [options.deferSetup=false] whether setup should be run immediately or deferred until query is first matched
229          * @param {boolean} [shouldDegrade=false] whether this particular media query should always run on incapable browsers
230          */
231         register : function(q, options, shouldDegrade) {
232             var queries         = this.queries,
233                 isUnconditional = shouldDegrade && this.browserIsIncapable;
234
235             if(!queries[q]) {
236                 queries[q] = new MediaQuery(q, isUnconditional);
237             }
238
239             //normalise to object in an array
240             if(isFunction(options)) {
241                 options = { match : options };
242             }
243             if(!isArray(options)) {
244                 options = [options];
245             }
246             each(options, function(handler) {
247                 queries[q].addHandler(handler);
248             });
249
250             return this;
251         },
252
253         /**
254          * unregisters a query and all it's handlers, or a specific handler for a query
255          *
256          * @param {string} q the media query to target
257          * @param {object || function} [handler] specific handler to unregister
258          */
259         unregister : function(q, handler) {
260             var query = this.queries[q];
261
262             if(query) {
263                 if(handler) {
264                     query.removeHandler(handler);
265                 }
266                 else {
267                     query.clear();
268                     delete this.queries[q];
269                 }
270             }
271
272             return this;
273         }
274     };
275
276
277     global.enquire = global.enquire || new MediaQueryDispatch();
278
279 }(this));