/* ocms-extensions.js */
/*
	2012-Jan-13 dwl fix operator precedance oversight in reporting page context;
	                added pathToScriptFileResource()
	2011-Dec-04 dwl reformatted source; now automatically calls addBrowserClasses and addAuthorClasses
	2011-Dec-03 man fixed a bug in hintText and added support function for submission
	2011-Dec-01 dwl added $.fn.supplantTable()
	2011-Nov-30 dwl added OCMS.urlParameters
	2011-Nov-23 dwl added fn.partial() as a super curry() and fn.defaults()
	2011-Nov-21 dwl isIE8() joins isIE7()
	2011-Oct-01 dwl always establish properties inPageEditor and inPagePreview, without having to
					call addAuthorClasses()
	2011-Jul-20 dwl slight upgrade to consoleMsg() for webkit; deserves more attention
	2011-Jul-19 dwl month name support added to Date object
	2011-Jun-21 dwl support for cms api's page_mode
	2011-May-06 dwl cleaning up dmStudio's version of this module that had added OCMS.lastFocus,
					 changed addBrowserClasses(), and added email address support
*/

var OCMS;

try {

    if (!OCMS) {
        /* OCMS becomes a global var */
        OCMS = {};
    }

    (function ($) {

        // ==== EXTENSIONS TO STANDARD JAVASCRIPT OBJECTS ====

        //helper function for adding methods
        Function.prototype.method = function (name, func) {
            if (!this.prototype[name]) {
                this.prototype[name]= func;
                return this;
            }
        };   // method


        // based on http://ejohn.org/blog/partial-functions-in-javascript/
        // customFunc = myFunc.partial(undefined, 10, 'yes', undefined);
        // produces a function whose 2nd and 3rd parameters have *HARD-WIRED* default values
        // customFunc(a, b) ==> myFunc(1, 10, 'yes', b)
        Function.method('partial', function () {
            var fn = this,
                args = Array.prototype.slice.call(arguments);

            return function () {
                var i,
                    arg = 0;

                for (i = 0; i < args.length && arg < arguments.length; i++ ) {
                    // if argument at creation time was undefined, use the argument of the invocation
                    if (args[i] === undefined) {
                        args[i] = arguments[arg++];
                    }
                }

                return fn.apply(this, args);
            };
          });	// partial


        // provide default argument values, but let them be overridden by the caller
        // customFunc = myFunc.defaults(undefined, 27, 'blue');
        // produces a function whose 2nd and 3rd parameters have default values of 27 and 'blue'
        // customFunc(x, y) ==> myFunc(x, y, 'blue')
        Function.method('defaults', function () {
            var fn = this,
                defArgs = Array.prototype.slice.call(arguments);

            return function () {
                var def = 0,
                    theseArgs = Array.prototype.slice.call(arguments),
                    maxArg = Math.max(defArgs.length, theseArgs.length);

                for (def = 0; def < maxArg; def++) {
                    // if invocation argument is empty, take from the defaults
                    // always let invocation argument override defaults
                    if (theseArgs[def] === undefined) {
                        theseArgs[def] = defArgs[def];
                    }
                }

                return fn.apply(this, theseArgs);
            };
          });	// defaults


        // properly rounds both +ve and -ve numbers
        Number.method('integer', function () {
            return Math[this < 0 ? 'ceil' : 'floor'](this);
        }); // integer


        String.method('trim', function () {
            return this.replace(/^\s+|\s+$/g, '');
        }); // trim


        // template string fill-er in-ner
        // based on source in Crockford's JavaScript: The Good Parts
        // 'Hello {you}'.supplant({you: 'world!'});
        // 'Hello {1}'.supplant(['or use an array!', 'world!', 'unused']);
        String.method('supplant', function (o) {
            return this.replace(/{([^{}]*)}/g,
                function (m, k) {
                    var r = o[k];
                    return typeof r === 'string' ? r : m;
                }
            );
        }); // supplant


        Date.method('getMonthName', function () {
            return ['January', 'February', 'March', 'April', 'May', 'June',
                    'July', 'August', 'September', 'October', 'November', 'December'][this.getMonth()];
        }); // getMonthName -- only English, full name


        Date.method('getMonthNameAbbr', function () {
            return this.getMonthName().slice(0, 3);
        }); // getMonthNameAbbr


        // ==== EXTENSIONS TO JQUERY ====

        // - call these when fading element has alpha transparency
        // - reverts to show/hide for IE < 9

        $.fn.fadIn = function (dur) {
            return this.each(function () {
                if ($.browser.msie && $.browser.version < 9) {
                    $(this).show();
                } else {
                    $(this).fadeIn(dur || null);
                }
            });
        };

        $.fn.fadOut = function (dur) {
            return this.each(function () {
                if ($.browser.msie && $.browser.version < 9) {
                    $(this).hide();
                } else {
                    $(this).fadeOut(dur || null);
                }
            });
        };

       /**************************************************************************
           - simple in-place hint text for input[type='text'], textarea controls
           - see hintTextSubmit for submit support
       */
       $.fn.hintText = function (txt, className) {
           className = className || 'hintText';

           return this.each(function () {
                function toFocus () {
                    if (this.value === $(this).data('ocmshinttext')) {
                        $(this)
                            .val('')
                            .removeClass(className);
                    }
                }    // toFocus


                function toBlur () {
                    if (this.value === '') {
                        $(this)
                            .val($(this).data('ocmshinttext'))
                            .addClass(className);
                    }
                }    // toBlur


                // txt need not be provide as long as data-ocmshinttext is, but the parameter
                // has priority if both have been specified
                // note: this does not support empty hint text
                txt = txt || $(this).data('ocmshinttext');
                if (txt) {
                    $(this)
                        .data('ocmshinttext', txt)
                        .focus(toFocus)
                        .blur(toBlur);

                    // establish initial conditions
                    toBlur.apply(this);
                } else {
                    OCMS.consoleMsg('⦻ hint text requested but not provided for field');
                }
           });
       };   // hintText


       /**************************************************************************
           - applied to a form
           - before submission, for all fields that are still marked as showing their
             hint text, it will clear their text and remove the hint text class
       */
       $.fn.hintTextSubmit = function(className) {
           className = className || 'hintText';
           return $(this).each(function() {
               $(this).submit(function() {
                   $(this).find('.' + className).each(function() {
                       $(this).val('').removeClass(className);
                   });

                   return true;
               });
           });
       };   // hintTextSubmit


       /**************************************************************************
           - applied to a form
           - call after the form has been submitted to restore hint text to the appropriate fields
        */
       $.fn.hintTextDoneSubmit = function(className) {
           className = className || 'hintText';
           return $(this).each(function() {
               $(this).find('input[type="text"], textarea').each(function() {
                   if ($(this).val() === '' && $(this).data('ocmshinttext')) {
                       $(this).val($(this).data('ocmshinttext')).addClass(className);
                   }
               });
           });
       };   // hintTextDoneSubmit


        // FUTURE: if nMaxChars not given, look for it in a data- attribute
        // element with class .charCount (or provided selector) will be updated with
        // the number of characters remaining before nMaxChars or 500 if none given
        $.fn.installCharCounter = function installCharCounter (ccSelector, nMaxChars) {
            return this.each(function () {
                ccSelector = ccSelector || '.charCount';
                var $charCount = $(ccSelector);

                if ($charCount.length) {
                    nMaxChars = nMaxChars || 500;

                    $(this).keyup(function () {
                        var sStory = $(this).val(),
                            nChars = nMaxChars - sStory.length;	// nChars: REMAINING

                        $charCount.text(nChars);
                        if (nChars < 0) {
                            $(this).val(sStory.slice(0, nMaxChars)).scrollTop($(this)[0].scrollHeight);
                            $charCount.text(0);
                        }
                    }).keyup();
                } else {
                    OCMS.consoleMsg('couldn\'t find ' + ccSelector);
                }
            }
        )}; // installCharCounter



        // The contents of given container is entirely replaced by the provided HTML template, with the content of
        // each of the TH's and TD's within the container available for replacement in to the template
        // through placeholder variables named th1, th2, td1, td2, etc. enclosed by {}.
        // The table cell variables are indexed using a 1-based system (not 0-based).
        // Cell attributes are ignored; we're only interested in what's in the table cell.
        // Note that this could include TH's and TD's from multiple tables, although typically the given
        // container (the 'this') will be a table.
        //
        // the second parameter aliases is optional
        // in aliases = {userLabel: '{td3}', userInput: '{td4}'}
        // whereever '{userLabel}' appears in the template, it is replaced with '{td3}' before doing
        // the replacement
        // therefore, with the right aliases mapping, the template parameter may use more friendly placeholder names

        $.fn.supplantTable = function (template, aliases) {
            var $this = $(this);

            if ($this.length > 1) {
                OCMS.consoleMsg('supplantTable: multiple containers!');
            }

            return $this.each(function () {
                var $ths = $('th', this),
                    $tds = $('td', this),
                    html = '',
                    cellMap = {};

                $ths.each(function (nIt, th) {
                    cellMap['th' + (nIt + 1)] = $(th).html();
                });
                $tds.each(function (nIt, td) {
                    cellMap['td' + (nIt + 1)] = $(td).html();
                });

                if (aliases) {
                    template = template.supplant(aliases);
                }

                $(this).html(template.supplant(cellMap));
            });
        };	// supplantTable


        // jQuery function for converting Change Password ('old user') table to another format
        // warning for caller: remember to preserve div.cperrors
        $.fn.detableChangePasswordOldUser = $.fn.supplantTable.partial(undefined, {
            oldPasswordLabel: 		'{td1}',
            oldPassword: 			'{td2}',
            newPasswordLabel:	 	'{td3}',
            newPassword: 			'{td4}',
            verifyPasswordLabel: 	'{td5}',
            verifyPassword: 		'{td6}',
            submit: 				'{td7}'
        });


        // jQuery function for converting Change Password ('old user') table to another format
        // warning for caller: remember to preserve div.cperrors
        $.fn.detableChangePasswordNewUser = $.fn.supplantTable.partial(undefined, {
            newPasswordLabel:	 	'{td1}',
            newPassword: 			'{td2}',
            verifyPasswordLabel: 	'{td3}',
            verifyPassword: 		'{td4}',
            submit: 				'{td5}'
        });


        // jQuery function for converting Forgot Password table to another format
        // warning for caller: remember to preserve div.fperrors
        $.fn.detableForgotPassword = $.fn.supplantTable.partial(undefined, {
            userNameLabel:	'{td1}',
            userName: 		'{td2}',
            submit:			'{td3}'
        });


        OCMS.addAuthorClasses = function () {
            if (OCMS.inPageEditor) {
                $('body').addClass('OCMS-Edit');
            } else {
                if (OCMS.inPagePreview) {
                    $('body').addClass('OCMS-Preview');
                }
            }
        };	// addAuthorClasses


        // remove hints intended only for the OCMS page editor from the DOM
        // note: requires that OCMS.addAuthorClasses has previously been called
        // will be obsolete if hints are always added to page template with the following:
        //  <apex:outputText rendered="{!api.page_mode == 'edit'}"><div class="ocmsHint">...
        OCMS.removeEditorHints = function () {
            if (!OCMS.inPageEditor) {
                $('div.ocmsHint').remove();
            }
        };

        // ************************************************************************
        // OCMS.addBrowserClasses
        //
        // - add a browser-identifying class name
        // - when doing IE, always adds IEn class as well
        // - optionally, add only if the current browser engine corresponds to the if string in
        //		parameter object
        // - most frequently we only care about the travesty that is IE7
        // - use IE conditional comments instead of this JavaScript-based solution when possible
        // - note that Safari and Chrome both identify as WEBKIT
        // - for more detailed browser/version sniffing if necessary, do it yourself, use $.support
        //		or feature detection techniques
        // - examples
        //		- OCMS.addBrowserClasses()
        //				body will be have a browser-specific class added for all recognized browsers
        //		- OCMS.addBrowserClasses({selector: 'div.pg'})
        //				browser-specific class added to div.pg instead of body
        //		- OCMS.addBrowserClasses({onlyIf: 'IE'})
        //				IE and IEn classes added to body within IE, nothing for other browsers
        OCMS.addBrowserClasses = function (args) {
            var	settings = {
                    selector: 'body',
                    onlyIf: null			// default: we do it for all browsers
                },
                classList = '';

            $.extend(settings, args);
            if (settings.onlyIf) {
                settings.onlyIf = settings.onlyIf.toUpperCase();
            }

            if ($.browser.msie && (!settings.onlyIf || settings.onlyIf.slice(0, 1) === 'IE')) {
                classList = 'IE IE' + parseInt($.browser.version, 10);
            } else if ($.browser.webkit && (!settings.onlyIf || settings.onlyIf === 'WEBKIT')) {
                classList = 'WEBKIT';
            } else if ($.browser.mozilla && (!settings.onlyIf || settings.onlyIf === 'MOZILLA')) {
                classList = 'MOZILLA';
            } else if ($.browser.opera && (!settings.onlyIf || settings.onlyIf === 'OPERA')) {
                classList = 'OPERA';
            }

            if (classList.length !== 0) {
                $(settings.selector).addClass(classList);
            }
        }; // OCMS.addBrowserClasses


        // candidate regexp's
        // ^[a-zA-Z0-9._-]+@[a-zA-Z0-9-]+\.[a-zA-Z.]{2,5}$		-- original: assumes too much about valid chars
        // ^([^.@]+)(\.[^.@]+)*@([^.@]+\.)+([^.@]+)$			-- doesn't sanity check a,b,c in a@b.c
        // ^[^<>\s\@]+(\@[^<>\s\@]+(\.[^<>\s\@]+)+)$			-- forbids spaces, <, > in a,b,c
        // ^[^<>\s\@]+(\@[^<>\s\@\.]+\.([^<>\s\@\.]+)+)$		-- forbids spaces, <, > in a,b,c

        OCMS.isValidEmailAddress = function (el) {
            var reEMail = OCMS.isValidEmailAddress.reEmail_ALTERNATE || OCMS.isValidEmailAddress.reEmail_DEFAULT;

            // if el is null, this: the email text field in question (invoked as a method)
            el = el || this;

            return reEMail.test($(el).val().trim());
        };  // isValidEmailAddress


        OCMS.isValidEmailAddress.reEmail_DEFAULT = /^[^<>\s\@]+\@[^<>\s\@\.]+\.[^<>\s\@\.]+$/;
        OCMS.isValidEmailAddress.reEmail_ALTERNATE = null;


        // pass a regular expression to use a different pattern for email validity checking
        // pass null to reset to default
        OCMS.setEmailAddressRE = function (re) {
            OCMS.isValidEmailAddress.reEmail_ALTERNATE = re;
            OCMS.consoleMsg('new email RE: ' + re);
        };  // setEmailAddressRE


        /**************************************************************************
            - simple start of a safe console wrapper
            - TO DO!
            - NOT yet proven appropriate for all browsers!
        */
        OCMS.consoleMsg = function () {

            // IE8 says Object doesn't support 'apply'
            //console.info.apply(console, Array.prototype.slice.call(arguments));
            //return;

            // okay for Chrome. What about Safari?
            if ($.browser.mozilla || $.browser.webkit) {
                console.info.apply(console, Array.prototype.slice.call(arguments));
            } else {
                console.info(Array.prototype.slice.call(arguments));
            }
        };	// OCMS.consoleMsg


        /**************************************************************************
            - track what control previously had the input focus
            - intentionally ignore command buttons
        */
        OCMS.trackLastFocus = function () {
            $('input[type="text"], input[type="radio"], input[type="checkbox"], select, textarea')
                .live('focusin', function (evt) {
                if (evt.type === 'focusin') {
                    OCMS.lastFocus = this;
                    //OCMS.consoleMsg('focusin ' + this.tagName + this.value);
                }
            });
        };  // trackLastFocus


        /**************************************************************************
            - written for DenMat.com when standard mouse enter/leave events triggering
                failed or proved unreliable
            - note that parameter is a jQuery collection of arbitrary length, and the elements
                do not have to be visible now (but are expected to not move, shrink, or grow!)
            - returns two methods: isInside and isOutside
        */
        OCMS.boundsChecker = function ($args) {
            var bounds = [];

            function _init () {
                function tlbr (el) {
                    var $this = $(el);

                    // if object isn't currently visible, set nulls but include object
                    // in order to build tlbr co-ordinates later when asked about a point
                    if ($this.is(':visible')) {
                        return {
                            t: $this.offset().top,
                            l: $this.offset().left,
                            b: $this.offset().top + $this.height(),
                            r: $this.offset().left + $this.width()
                        }
                    } else {
                        return {
                            t: null,
                            l: null,
                            b: null,
                            r: null,
                            $that: $this
                        }
                    }
                }   // tlbr


                for (var i = 0; i < $args.length; ++i) {
                    var reck = tlbr($args.eq(i));
                    bounds.push(reck);
                }
            }   // _init


            function isInside (pt) {
                // pt must have pageX and pageY properties, as found in an event object
                var r,
                    bInside = false,
                    rect;

                for (r = 0; !bInside && r < bounds.length; ++r) {
                    rect = bounds[r];

                    if (rect.t !== null) {
                        bInside = 	pt.pageX >= rect.l &&
                                    pt.pageX <= rect.r &&
                                    pt.pageY >= rect.t &&
                                    pt.pageY <= rect.b;
                    } else {
                        if (rect.$that.is(':visible')) {
                            rect.t = rect.$that.offset().top;
                            rect.l = rect.$that.offset().left;
                            rect.b = rect.$that.offset().top + rect.$that.height();
                            rect.r = rect.$that.offset().left + rect.$that.width();

                            bInside = 	pt.pageX >= rect.l &&
                                        pt.pageX <= rect.r &&
                                        pt.pageY >= rect.t &&
                                        pt.pageY <= rect.b;
                        }
                    }
                }

                return bInside;
            }   // isInside


            function isOutside (args) {
                return !isInside(args);
            }   // isOutside


            function dump () {
                var r;
                for (r = 0; r < bounds.length; ++r) {
                    window.status += 	'{' + bounds[r].l + '->' + bounds[r].r +
                                        '|' + bounds[r].t + 'vv' + bounds[r].b + '} ';
                }
            }   // dump

            _init();

            return {
                    isInside: isInside,
                    isOutside: isOutside
            }
        }   // OCMS.boundsChecker


        // this is unproven!
        OCMS.constructPageURL = function (
            page,		// string: required, NOT expected to include a query string, therefore NOT
                        //			appropriate for processing Orchestra created content links
            args,		// string: optional, omits leading ?, includes embedded & but not a leading &, assumed non-empty string
            site)		// string: optional, seldom required, but its ommission assumes availability of standard api vars
        {
            var cmsData = $(document).data('cms'),
                basePage = 'cms__Main',		// default: production
                pageString,			// = '?name=' + page,
                siteString,			//= (typeof site === 'string') ? '&sname=' + site : '',
                argString = (typeof args === 'string') ? '&' + args : '',
                queryStart;

            // remove leading / if present (presumably a URL was passed, not a page name)
            if (page[0] === '/') {
                page = page.slice(1);
            }
            pageString = '?name=' + page;

            if (typeof site !== 'string') {
                // if site left empty, grab it from in-page cms api vars (assumed to be present!)
                site = cmsData.site_name;
            }
            if (site.length) {
                siteString = '&sname=' + site;
            }

            if (cmsData.page_mode === 'prev') {
                basePage = 'preview';
            }

            return basePage + pageString + siteString + argString;
        };	// OCMS.constructPageURL


        // set by user JS content like: OCMS.pageOptions({opt1: value1, opt2: value2});
        // retrieved individually by page script OCMS.pageOptions('opt2'); as required
        OCMS.pageOptions = function (arg) {
            // more to come!

            if (typeof arg === 'string') {
                // getter
                // returns null if not found
                OCMS.consoleMsg('OCMS.pageOptions(' + arg + ') => ' + OCMS.pageOptions.options[arg]);
                return OCMS.pageOptions.options[arg];
            } else {
                // setter call; passed an object map of proerty/value pairs
                OCMS.consoleMsg('OCMS.pageOptions [set] ' + arg);
                $.extend(true, OCMS.pageOptions.options, arg);
            }
        };
        OCMS.pageOptions.options = {};


        // returns an array of query string values, indexed by query string names
        OCMS.getURLParameters = function () {
            var q = window.location.search.slice(1).split('&'),
                nIt,
                keyVal,
                aQueryVars = {};	// important! Start as an property list (but use array notation)

            for (nIt = 0; nIt < q.length; nIt++) {
                keyVal = q[nIt].split('=');
                aQueryVars[unescape(keyVal[0])] = unescape(keyVal[1] || '');
            }

            return aQueryVars;
        };	// OCMS.getURLParameters



        OCMS.pathToScriptFileResource = function (fileName) {
            // note: by default, it returns the path to the resource that holds ocms-extensions.js
            // returns: undefined if can't find script; null or script full path if unexpected format
    		var fileName = fileName || 'ocms-extensions.js',
    		    // note: using jQuery Ends With Selector $=
    		    $thisScript = $('script[src$="' + fileName + '"]'),
	    		rPath;

            if ($thisScript.length) {
                rPath = $thisScript.attr('src');

                // if rPath does not start with /resource/, we return what the full path to fileName
                // which is unpredictable: was it in a sub-folder?, what was that called?, how deep?
                if (rPath.slice(0, 10) === '/resource/') {
                    // we're assuming path is of the form /resource/1234907485/{root}/etc.../file.js
                    // and we'll take up to and including the numeric folder, including the trailing /
                    rPath = rPath.match('/resource/[0-9]+/');
                    // if this fails, returns null
                }
            }

            return rPath;
        }	// pathToScriptFileResource


        // ---- begin ocms-extensions ----
        //
        // Note: this code is run automatically upon DOM inclusion, NOT through a $(document).ready()
        // Therefore:
        //      - $(document).data('cms') must already be defined
        //      - subsequent JS must not clear body of added classes

        var cmsInfo = $(document).data('cms'),
            location;

        // provide NO-OP functions for when these development functions aren't available
        if (typeof console === 'undefined') {
            console = {
                log:		function () {},
                info:		function () {},
                warning:	function () {},
                assert:		function () {}
            };
        }

        if (cmsInfo && cmsInfo.page_mode) {
            OCMS.inPageEditor = cmsInfo.page_mode === 'edit';
            OCMS.inPagePreview = cmsInfo.page_mode === 'prev';
            // or === 'production'
            // or === ... ?
            OCMS.consoleMsg('page_mode: ' + cmsInfo.page_mode);
        } else {
            location = window.location.href.toLowerCase();
            OCMS.inPageEditor = location.indexOf('/apex/edit?') > 0;
            OCMS.inPagePreview = location.indexOf('/apex/preview?') > 0;
            OCMS.consoleMsg('page context: ' + (OCMS.inPageEditor ?
                'inPageEditor' : (OCMS.inPagePreview ? 'inPagePreview' : 'other')));
        }

		$(document).ready(function () {
			// we now automatically call these: their side-effects are now assumed to be standard
	        OCMS.addAuthorClasses();        // e.g. body.OCMS-Edit
	        OCMS.addBrowserClasses();       // e.g. body.WEBKIT, or body.IE.IE8
		});

        OCMS.isIE7 = ($.browser.msie && parseInt($.browser.version, 10) === 7) || false;
        OCMS.isIE8 = ($.browser.msie && parseInt($.browser.version, 10) === 8) || false;


        // other initialization code

        // public global
        OCMS.urlParameters = OCMS.getURLParameters();

        // public global for OCMS.trackLastFocus
        OCMS.lastFocus = null;
    })(jQuery);
} catch (ex) {
    if (console.log) {
        console.log(ex);
        if (ex.type) {
            console.log('\t' + ex.type);
        }
        if (ex.arguments && ex.arguments.length) {
            for (var nIt = 0; nIt < ex.arguments.length; ++nIt) {
                console.log('\t' + ex.arguments[nIt]);
            }
        }
    }
}
