Fetch on resize: added resize-event that hides/shows banners accordingly
[openx] / jquery.openx.js
1 /*
2  * (C) Copyright 2012 juplo (http://juplo.de/).
3  *
4  * All rights reserved. This program and the accompanying materials
5  * are made available under the terms of the GNU Lesser General Public License
6  * (LGPL) version 3.0 which accompanies this distribution, and is available at
7  * http://www.gnu.org/licenses/lgpl-3.0.html
8  *
9  * This library is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12  * Lesser General Public License for more details.
13  *
14  * Contributors:
15  * - Kai Moritz
16  */
17
18 /*
19  * See http://coding.smashingmagazine.com/2011/10/11/essential-jquery-plugin-patterns/
20  * for detailed explanations for the applied best practices.
21  *
22  * The semicolon guides our code for poorly written concatenated scripts.
23  */
24 ;(function( $, window, document, undefined ) {
25
26   var
27
28   settings, _options, domain, id, node,
29
30   count = 0,
31   slots = {},
32   min_width = {},
33   max_width = {},
34   width,
35   rendered = {},
36   visible = {},
37   rendering = false,
38   resize_timer,
39   queue = [],
40   output = [];
41
42
43   /*
44    * Configuration-Options for jQuery.openx
45    *
46    * Since the domain-name of the ad-server is the only required parameter,
47    * jQuery.openx for convenience can be configured with only that one
48    * parameter. For example: "jQuery.openx('openx.example.org');". If more
49    * configuration-options are needed, they must be specified as an object.
50    * For example: "jQuery.openx({'server': 'openx.example.org', ... });".
51    *
52    *
53    * Server-Settings:
54    *
55    * server:        string  Name of the server, without protocol or port. For
56    *                        example "openx.example.org". This option is
57    *                        REQUIRED.
58    * protocol:              Optional parameter.
59    *                http:   All connections to the ad-server are made via HTTP.
60    *                https:  All connections to the ad-server are made via HTTPS.
61    *                        If empty, document.location.protocol will be used.
62    * http_port:     number  Port-Number for HTTP-connections to the ad-server
63    *                        (only needed, when it is not the default-value 80).
64    * https_port:            Port-Number for HTTPS-connections to the ad-server
65    *                        (only needed, when it is not the default-value 443).
66    *
67    *
68    * Seldom needed special Server-Settings (these parameters are only needed,
69    * if the default delivery-configration of the OpenX-Server was changed):
70    *
71    * path:          string  Path to delivery-scripts. DEFAULT: "/www/delivery".
72    * fl:            string  Flash-Include-Script. DEFAULT: "fl.js".
73    *
74    *
75    * Delivery-Options (for details and explanations see the see:
76    * http://www.openx.com/docs/2.8/userguide/single%20page%20call):
77    *
78    * block:         1       Don't show the banner again on the same page.
79    *                0       A Banner might be shown multiple times on the same
80    *                        page (DEFAULT).
81    * blockcampaign: 1       Don't show a banner from the same campaign again on
82    *                        the same page.
83    *                0       A Banner from the same campaign might be shown
84    *                        muliple times on the same page (DEFAULT).
85    * target:        string  The value is addes as the HTML TARGET attribute in
86    *                        the ad code. Examples for sensible values: "_blank",
87    *                        "_top".
88    * withtext:      1       Show text below banner. Enter this text in the
89    *                        Banner properties page.
90    *                0       Ignore the text-field from the banner-properties
91                             (DEFAULT).
92    * charset:       string  Charset used, when delivering the banner-codes.
93    *                        If empty, the charset is guessed by OpenX. Examples
94    *                        for sensible values: "UTF-8", "ISO-8859-1".
95    *
96    *
97    * Other settings:
98    *
99    * selector:      string  A selector for selecting the DOM-elements, that
100    *                        should display ad-banners. DEFAULT: ".oa".
101    *                        See: http://api.jquery.com/category/selectors/
102    * min_prefix:    string  Prefix for the encoding of the minmal width as
103    *                        CSS-classname. DEFAULT: "min_".
104    * max_prefix:    string  Prefix for the encoding of the maximal width as
105    *                        CSS-classname. DEFAULT: "max_".
106    * resize_delay:  number  Number of milliseconds to wait, before a
107    *                        recalculation of the visible ads is scheduled.
108    *                        If the value is choosen to small, a recalculation
109    *                        might be scheduled, while resizing is still in
110    *                        progress. DEFAULT: 200.
111    */
112   $.openx = function( options ) {
113
114     if (domain) {
115       if (console.error) {
116         console.error('jQuery.openx was already initialized!');
117         console.log('Configured options: ', _options);
118       }
119       return;
120     }
121
122     /** Enable convenient-configuration */
123     if (typeof(options) == 'string')
124       options = { 'server': options };
125
126     _options = options;
127
128     if (!options.server) {
129       if (console.error) {
130         console.error('Required option "server" is missing!');
131         console.log('options: ', options);
132       }
133       return;
134     }
135
136     settings = $.extend(
137       {
138         'protocol': document.location.protocol,
139         'delivery': '/www/delivery',
140         'fl': 'fl.js',
141         'selector': '.oa',
142         'min_prefix': 'min_',
143         'max_prefix': 'max_',
144         'resize_delay': 200
145       },
146       options
147       );
148
149     domain = settings.protocol + '//';
150     domain += settings.server;
151     if (settings.protocol === 'http:' && settings.http_port)
152       domain += ':' + settings.http_port;
153     if (settings.protocol === 'https:' && settings.https_port)
154       domain += ':' + settings.https_port;
155
156     /**
157      * Without this option, jQuery appends an timestamp to every URL, that
158      * is fetched via $.getScript(). This can mess up badly written
159      * third-party-ad-scripts, that assume that the called URL's are not
160      * altered.
161      */
162     $.ajaxSetup({ 'cache': true });
163
164     /**
165      * jQuery.openx only works with "named zones", because it does not know,
166      * which zones belong to which website. For mor informations about
167      * "named zones" see:
168      * http://www.openx.com/docs/2.8/userguide/single%20page%20call
169      *
170      * For convenience, jQuery.openx only fetches banners, that are really
171      * included in the actual page. This way, you can configure jQuery.openx
172      * with all zones available for your website - for example in a central
173      * template - and does not have to worry about performance penalties due
174      * to unnecessarily fetched banners.
175      */
176     for(name in OA_zones) {
177       $(settings.selector).each(function() {
178         var
179         id,
180         classes,
181         i,
182         min = new RegExp('^' + settings.min_prefix + '([0-9]+)$'),
183         max = new RegExp('^' + settings.max_prefix + '([0-9]+)$'),
184         match;
185         if (this.id === name) {
186           id = 'oa_' + ++count;
187           slots[id] = this;
188           min_width[id] = 0;
189           max_width[id] = Number.MAX_VALUE;
190           classes = this.className.split(/\s+/);
191           for (i=0; i<classes.length; i++) {
192             match = min.exec(classes[i]);
193             if (match)
194               min_width[id] = match[1];
195             match = max.exec(classes[i]);
196             if (match)
197               max_width[id] = match[1];
198           }
199           rendered[id] = false;
200           visible[id] = false;
201         }
202       });
203     }
204
205     /** Add resize-event */
206     $(window).resize(function() {
207       clearTimeout(resize_timer);
208       resize_timer = setTimeout(recalculate_visible , settings.resize_timeout);
209     });
210
211     /** Fetch the JavaScript for Flash and schedule the initial fetch */
212     $.getScript(domain + settings.delivery + '/' + settings.fl, recalculate_visible);
213
214   }
215
216   function recalculate_visible() {
217
218     width = $(document).width();
219     if (!rendering)
220       fetch_ads();
221     
222   }
223
224   function fetch_ads() {
225
226     /** Guide rendering-process for early restarts */
227     rendering = true;
228
229     var name, src = domain + settings.delivery + '/spc.php';
230
231     /** Order banners for all zones that were found on the page */
232     src += '?zones=';
233     for(id in slots) {
234       visible[id] = width >= min_width[id] && width <= max_width[id];
235       if (visible[id]) {
236         if (!rendered[id]) {
237           queue.push(id);
238           src += escape(id + '=' + OA_zones[slots[id].id] + "|");
239           rendered[id] = true;
240         }
241         else {
242           /** Unhide already fetched visible banners */
243           $(slots[id]).slideDown();
244         }
245       }
246       else {
247         /** Hide unvisible banners */
248         $(slots[id]).hide();
249       }
250     }
251     src += '&nz=1'; // << We want to fetch named zones!
252
253     /**
254      * These are some additions to the URL of spc.php, that are originally
255      * made in spcjs.php
256      */
257     src += '&r=' + Math.floor(Math.random()*99999999);
258     if (window.location)   src += "&loc=" + escape(window.location);
259     if (document.referrer) src += "&referer=" + escape(document.referrer);
260
261     /** Add the configured options */
262     if (settings.block === 1)
263       src += '&block=1';
264     if (settings.blockcampaign === 1)
265       src += '&blockcampaign=1';
266     if (settings.target)
267       src += '&target=' + settings.target;
268     if (settings.withtext === 1)
269       src += '&withtext=1';
270     if (settings.charset)
271       src += '&charset=' + settings.charset;
272
273     /** Add the source-code - if present */
274     if (typeof OA_source !== 'undefined')
275       src += "&source=" + escape(OA_source);
276
277     /** Signal, that this task is done / in progress */
278     width = undefined;
279
280     /** Fetch data from OpenX and schedule the render-preparation */
281     $.getScript(src, init_ads);
282
283   }
284
285   function init_ads() {
286
287     var i, id, ads = [];
288     for (i=0; i<queue.length; i++) {
289       id = queue[i];
290       if (typeof(OA_output[id]) != 'undefined' && OA_output[id] != '')
291         ads.push(id);
292     }
293     queue = ads;
294
295     document.write = document_write;
296     document.writeln = document_write;
297
298     render_ads();
299
300   }
301
302   function render_ads() {
303
304     while (queue.length > 0) {
305
306       var result, src, inline;
307
308       id = queue.shift();
309       node = $(slots[id]);
310
311       node.slideDown();
312
313       // node.append(id + ": " + node.attr('class'));
314
315       /**
316        * If output was added via document.write(), this output must be
317        * rendered before other banner-code from the OpenX-server is rendered!
318        */
319       insert_output();
320
321       while ((result = /<script/i.exec(OA_output[id])) != null) {
322         node.append(OA_output[id].slice(0,result.index));
323         /** Strip all text before "<script" from OA_output[id] */
324         OA_output[id] = OA_output[id].slice(result.index,OA_output[id].length);
325         result = /^([^>]*)>([\s\S]*?)<\\?\/script>/i.exec(OA_output[id]);
326         if (result == null) {
327           /** Invalid syntax in delivered banner-code: ignoring the rest of this banner-code! */
328           // alert(OA_output[id]);
329           OA_output[id] = "";
330         }
331         else {
332           /** Remember iinline-code, if present */
333           src = result[1] + ' ' // << simplifies the following regular expression: the string ends with a space in any case, so that the src-URL cannot be followed by the end of the string emediately!
334           inline = result[2];
335           /** Strip all text up to and including "</script>" from OA_output[id] */
336           OA_output[id] = OA_output[id].slice(result[0].length,OA_output[id].length);
337           result = /src\s*=\s*['"]?([^'"]*)['"]?\s/i.exec(src);
338           if (result == null) {
339             /** script-tag with inline-code: execute inline-code! */
340             result = /^\s*<.*$/m.exec(inline);
341             if (result != null) {
342               /** Remove leading HTML-comments, because IE will stumble otherwise */
343               inline = inline.slice(result[0].length,inline.length);
344             }
345             $.globalEval(inline);
346             insert_output(); // << The executed inline-code might have called document.write()!
347           }
348           else {
349             /** script-tag with src-URL! */
350             if (OA_output[id].length > 0)
351               /** The banner-code was not rendered completely yet! */
352               queue.unshift(id);
353             /** Load the script and halt all work until the script is loaded and executed... */
354             $.getScript(result[1], render_ads); // << jQuery.getScript() generates onload-Handler for _all_ browsers ;)
355             return;
356           }
357         }
358       }
359
360       node.append(OA_output[id]);
361       OA_output[id] = "";
362     }
363
364     /** All entries from OA_output were rendered */
365
366     id = undefined;
367     node = undefined;
368     rendering = false;
369
370     /** Restart rendering, if new task was queued */
371     if (width)
372       fetch_ads();
373
374   }
375
376   /** This function is used to overwrite document.write and document.writeln */
377   function document_write() {
378
379     if (id == undefined)
380       return;
381
382     for (var i=0; i<arguments.length; i++)
383       output.push(arguments[i]);
384
385     if (id != queue[0])
386       /**
387        * Re-Add the last banner-code to the working-queue, because included
388        * scripts had added markup via document.write(), which is not
389        * proccessed yet.
390        * Otherwise the added markup would be falsely rendered together with
391        * the markup from the following banner-code.
392        */
393       queue.unshift(id);
394
395   }
396
397   /**
398    * This function prepends the collected output from calls to
399    * document_write() to the current banner-code.
400    */
401   function insert_output() {
402
403     if (output.length > 0) {
404       output.push(OA_output[id]);
405       OA_output[id] = "";
406       for (i=0; i<output.length; i++)
407         OA_output[id] += output[i];
408       output = [];
409     }
410
411   }
412
413 })( jQuery, window, document );
414
415 var OA_output = {}; // << Needed, because IE will complain loudly otherwise!