<commit_before>/*
Input Mask plugin for jquery
http://github.com/RobinHerbots/jquery.inputmask
Copyright (c) 2010 Robin Herbots
Licensed under the MIT license (http://www.opensource.org/licenses/mit-license.php)
Version: 0.0.4

This plugin is based on the masked input plugin written by Josh Bush (digitalbush.com)
*/

(function($) {

    $.fn.inputmask = function(fn, options) {
        //options default
        var defaults = {
            placeholder: "_",
            mask: null,
            onComplete: null,
            repeat: 0, //repetitions of the mask
            definitions: {
                '9': {
                    "validator": "[0-9]",
                    "cardinality": 1,
                    'prevalidator': null
                },
                'a': {
                    "validator": "[A-Za-z]",
                    "cardinality": 1,
                    "prevalidator": null
                },
                '*': {
                    "validator": "[A-Za-z0-9]",
                    "cardinality": 1,
                    "prevalidator": null
                },
                'd': { //day
                    "validator": "0[1-9]|[12][0-9]|3[01]",
                    "cardinality": 2,
                    "prevalidator": [{ "validator": "[0-3]", "cardinality": 1}]
                },
                'm': { //month
                    "validator": "0[1-9]|1[012]",
                    "cardinality": 2,
                    "prevalidator": [{ "validator": "[01]", "cardinality": 1}]
                },
                'y': { //year
                    "validator": "(19|20)\\d\\d",
                    "cardinality": 4,
                    "prevalidator": [
                        { "validator": "[12]", "cardinality": 1 },
                        { "validator": "(19|20)", "cardinality": 2 },
                        { "validator": "(19|20)\\d", "cardinality": 3 }
                        ]
                }
            },
            keyCode: { ALT: 18, BACKSPACE: 8, CAPS_LOCK: 20, COMMA: 188, COMMAND: 91, COMMAND_LEFT: 91, COMMAND_RIGHT: 93, CONTROL: 17, DELETE: 46, DOWN: 40, END: 35, ENTER: 13, ESCAPE: 27, HOME: 36, INSERT: 45, LEFT: 37, MENU: 93, NUMPAD_ADD: 107, NUMPAD_DECIMAL: 110, NUMPAD_DIVIDE: 111, NUMPAD_ENTER: 108,
                NUMPAD_MULTIPLY: 106, NUMPAD_SUBTRACT: 109, PAGE_DOWN: 34, PAGE_UP: 33, PERIOD: 190, RIGHT: 39, SHIFT: 16, SPACE: 32, TAB: 9, UP: 38, WINDOWS: 91
            }
        };

        var opts = $.extend({}, defaults, options);

        if (typeof fn == "string") {
            if (fn == 'mask') {
                //init buffer
                var _buffer = getMaskTemplate();
                var pasteEventName = $.browser.msie ? 'paste' : 'input';
                var iPhone = (window.orientation != undefined);
                var tests = getTestingChain();

                return this.each(function() {
                    mask($(this));
                });
            } else if (fn == 'unmaskedvalue') {
                return unmaskedvalue($(this));
            } else { //maybe fn is a mask so we try
            	//set mask
            	opts.mask = fn;

            	//init buffer
                var _buffer = getMaskTemplate();
                var pasteEventName = $.browser.msie ? 'paste' : 'input';
                var iPhone = (window.orientation != undefined);
                var tests = getTestingChain();

                return this.each(function() {
                    mask($(this));
                });
            }
        } if (typeof fn == "object") {
        		opts = $.extend({}, defaults, fn);

        		//init buffer
                var _buffer = getMaskTemplate();
                var pasteEventName = $.browser.msie ? 'paste' : 'input';
                var iPhone = (window.orientation != undefined);
                var tests = getTestingChain();

                return this.each(function() {
                    mask($(this));
                });
        }

        //functions
        function getMaskTemplate() {
            var singleMask = $.map(opts.mask.split(""), function(element, index) {
                var outElem = [];
                var maskdef = opts.definitions[element];
                if (maskdef) {
                    for (i = 0; i < maskdef.cardinality; i++) {
                        outElem.push(opts.placeholder);
                    }
                } else outElem.push(element);

                return outElem;
            });

            var repeatedMask = singleMask.slice();
				for(var i = 0; i < opts.repeat; i++){
					repeatedMask = repeatedMask.concat(singleMask.slice());
				}
			return repeatedMask;
        };

        function getTestingChain() {
            return $.map(opts.mask.split(""), function(element, index) {
                var outElem = [];
                var maskdef = opts.definitions[element];
                if (maskdef) {
                    for (i = 1; i < maskdef.cardinality; i++) {
                        var prevalidator = maskdef.prevalidator[i - 1];
                        outElem.push([new RegExp(prevalidator.validator), prevalidator.cardinality]);
                    }
                    outElem.push([new RegExp(maskdef.validator), maskdef.cardinality]);
                } else outElem.push(null);

                return outElem;
            });
        };

        function unmaskedvalue(el) {
            var tests = el.data('tests');
            if (tests) {
                return $.map(el.val().split(""), function(element, index) {
                    return tests[index] ? element : null;
                }).join('');
            }
            else {
                return el.val();
            }
        }

        function mask(el) {
            var input = $(el);
            //store tests in the input element - used to get the unmasked value
            input.data('tests', tests);

            //init buffer
            var buffer = _buffer.slice();
            var undoBuffer = input.val();
            var ignore = false;              //Variable for ignoring control keys
            var len = _buffer.length;

            //bind events
            if (!input.attr("readonly")) {
                input.bind("focus", function() {
                    input.addClass('focus');
                    undoBuffer = input.val();
                    var pos = checkVal();
                    writeBuffer();
                    setTimeout(function() {
                        if (pos == _buffer.length)
                            caret(input, 0, pos);
                        else
                            caret(input, pos);
                    }, 0);
                }).bind("mouseenter", function() {
                    if (!input.hasClass('focus') && input.val().length == 0)
                        writeBuffer();
                }).bind("blur", function() {
                    input.removeClass('focus');
                    if (input.val() == _buffer.join('')) {
                        input.val('');
                    } else {
                        checkVal();
                        if (input.val() != undoBuffer)
                            input.change();
                    }
                }).bind("mouseleave", function() {
                    if (!input.hasClass('focus') && input.val() == _buffer.join(''))
                        input.val('');
                }).bind("keydown", keydownEvent
                ).bind("keypress", keypressEvent
                ).bind(pasteEventName, function() {
                    setTimeout(function() { caret(input, checkVal(true)); }, 0);
                });

            }

            checkVal();

            //private functions
            function writeBuffer() { return input.val(buffer.join('')).val(); };
            function clearBuffer(start, end) {
                for (var i = start; i < end && i < len; i++) {
                    buffer[i] = _buffer[i];
                }
            };

            function seekNext(pos) {
                while (++pos <= len && !isMask(pos));
                return pos;
            };

            function shiftL(pos) {
                while (!isMask(pos) && --pos >= 0);
                for (var i = pos; i < len; i++) {
                    if (isMask(i)) {
                        buffer[i] = opts.placeholder;
                        var j = seekNext(i);
                        if (j < len && isValid(i, buffer[j])) {
                            buffer[i] = buffer[j];
                        } else
                            break;
                    }
                }
                writeBuffer();
                caret(input, pos);
            };

            function shiftR(pos) {
                for (var i = pos, c = opts.placeholder; i < len; i++) {
                    if (isMask(i)) {
                        var j = seekNext(i);
                        var t = buffer[i];
                        buffer[i] = c;
                        if (j < len && isValid(j, t))
                            c = t;
                        else
                            break;
                    }
                }
            };

            function caret(input, begin, end) {
                if (input.length == 0) return;
                if (typeof begin == 'number') {
                    end = (typeof end == 'number') ? end : begin;
                    return input.each(function() {
                        if (this.setSelectionRange) {
                            this.focus();
                            this.setSelectionRange(begin, end);
                        } else if (this.createTextRange) {
                            var range = this.createTextRange();
                            range.collapse(true);
                            range.moveEnd('character', end);
                            range.moveStart('character', begin);
                            range.select();
                        }
                    });
                } else {
                    if (input[0].setSelectionRange) {
                        begin = input[0].selectionStart;
                        end = input[0].selectionEnd;
                    } else if (document.selection && document.selection.createRange) {
                        var range = document.selection.createRange();
                        begin = 0 - range.duplicate().moveStart('character', -100000);
                        end = begin + range.text.length;
                    }
                    return { begin: begin, end: end };
                }
            };

            function keydownEvent(e) {
                var pos = caret($(this));
                var k = e.keyCode;
                ignore = (k < 16 || (k > 16 && k < 32) || (k > 32 && k < 41));

                //delete selection before proceeding
                if ((pos.begin - pos.end) != 0 && (!ignore || k == opts.keyCode.BACKSPACE || k == opts.keyCode.DELETE))
                    clearBuffer(pos.begin, pos.end);

                //backspace, delete, and escape get special treatment
                if (k == opts.keyCode.BACKSPACE || k == opts.keyCode.DELETE || (iPhone && k == 127)) {//backspace/delete
                    shiftL(pos.begin + (k == opts.keyCode.DELETE ? 0 : -1));
                    return false;
                } else if (k == opts.keyCode.ESCAPE) {//escape
                    input.val(undoBuffer);
                    caret(input, 0, checkVal());
                    return false;
                }
            }

            function keypressEvent(e) {
                if (ignore) {
                    ignore = false;
                    //Fixes Mac FF bug on backspace
                    return (e.keyCode == 8) ? false : null;
                }
                e = e || window.event;
                var k = e.charCode || e.keyCode || e.which;
                var pos = caret($(this));

                if (e.ctrlKey || e.altKey || e.metaKey) {//Ignore
                    return true;
                } else if ((k >= 32 && k <= 125) || k > 186) {//typeable characters
                    var p = seekNext(pos.begin - 1);
                    if (p < len) {
                        var c = String.fromCharCode(k);
                        if (isValid(p, c)) {
                            shiftR(p);
                            buffer[p] = c;
                            writeBuffer();
                            var next = seekNext(p);
                            caret($(this), next);
                            if (opts.onComplete && next == len)
                                opts.onComplete.call(input);
                        }
                    }
                }
                return false;
            }

            function isValid(pos, c) {
                var testPos = determineTestPosition(pos);

                var loopend = 0;
                if (c) { loopend = 1; ; }

                var chrs = '';
                for (var i = tests[testPos][1]; i > loopend; i--) {
                    chrs += buffer[testPos - (i - 1)];
                }

                if (c) { chrs += c; }

                return tests[testPos][0].test(chrs);
            }

            function isMask(pos) {
                return tests[determineTestPosition(pos)];
            }

            function determineTestPosition(pos) {
            	return pos % tests.length;
            }

            function checkVal(clearInvalid) {
                var inputValue = input.val();
                var lastMatch = -1;
                for (var i = 0; i < inputValue.length; i++) {
                    for (var pos = lastMatch+1; pos < len; pos++) {
                    	if(isMask(pos)){
                        	buffer[pos] = opts.placeholder;
                        	if (isValid(pos, inputValue.charAt(i))) {
                           		buffer[pos] = inputValue.charAt(i);
                           		lastMatch = pos;
                        	}
                        	break;
                        }else {   //nonmask
                           lastMatch++;
                        }
                    }
                }
                if (clearInvalid) {
                    writeBuffer();
                }
                return seekNext(lastMatch);
            };
        }
    };

})(jQuery);<commit_msg>fix selection on focus when mask is complete<commit_after>/*
Input Mask plugin for jquery
http://github.com/RobinHerbots/jquery.inputmask
Copyright (c) 2010 Robin Herbots
Licensed under the MIT license (http://www.opensource.org/licenses/mit-license.php)
Version: 0.0.4

This plugin is based on the masked input plugin written by Josh Bush (digitalbush.com)
*/

(function($) {

    $.fn.inputmask = function(fn, options) {
        //options default
        var defaults = {
            placeholder: "_",
            mask: null,
            onComplete: null,
            repeat: 0, //repetitions of the mask
            definitions: {
                '9': {
                    "validator": "[0-9]",
                    "cardinality": 1,
                    'prevalidator': null
                },
                'a': {
                    "validator": "[A-Za-z]",
                    "cardinality": 1,
                    "prevalidator": null
                },
                '*': {
                    "validator": "[A-Za-z0-9]",
                    "cardinality": 1,
                    "prevalidator": null
                },
                'd': { //day
                    "validator": "0[1-9]|[12][0-9]|3[01]",
                    "cardinality": 2,
                    "prevalidator": [{ "validator": "[0-3]", "cardinality": 1}]
                },
                'm': { //month
                    "validator": "0[1-9]|1[012]",
                    "cardinality": 2,
                    "prevalidator": [{ "validator": "[01]", "cardinality": 1}]
                },
                'y': { //year
                    "validator": "(19|20)\\d\\d",
                    "cardinality": 4,
                    "prevalidator": [
                        { "validator": "[12]", "cardinality": 1 },
                        { "validator": "(19|20)", "cardinality": 2 },
                        { "validator": "(19|20)\\d", "cardinality": 3 }
                        ]
                }
            },
            keyCode: { ALT: 18, BACKSPACE: 8, CAPS_LOCK: 20, COMMA: 188, COMMAND: 91, COMMAND_LEFT: 91, COMMAND_RIGHT: 93, CONTROL: 17, DELETE: 46, DOWN: 40, END: 35, ENTER: 13, ESCAPE: 27, HOME: 36, INSERT: 45, LEFT: 37, MENU: 93, NUMPAD_ADD: 107, NUMPAD_DECIMAL: 110, NUMPAD_DIVIDE: 111, NUMPAD_ENTER: 108,
                NUMPAD_MULTIPLY: 106, NUMPAD_SUBTRACT: 109, PAGE_DOWN: 34, PAGE_UP: 33, PERIOD: 190, RIGHT: 39, SHIFT: 16, SPACE: 32, TAB: 9, UP: 38, WINDOWS: 91
            }
        };

        var opts = $.extend({}, defaults, options);

        if (typeof fn == "string") {
            if (fn == 'mask') {
                //init buffer
                var _buffer = getMaskTemplate();
                var pasteEventName = $.browser.msie ? 'paste' : 'input';
                var iPhone = (window.orientation != undefined);
                var tests = getTestingChain();

                return this.each(function() {
                    mask($(this));
                });
            } else if (fn == 'unmaskedvalue') {
                return unmaskedvalue($(this));
            } else { //maybe fn is a mask so we try
            	//set mask
            	opts.mask = fn;

            	//init buffer
                var _buffer = getMaskTemplate();
                var pasteEventName = $.browser.msie ? 'paste' : 'input';
                var iPhone = (window.orientation != undefined);
                var tests = getTestingChain();

                return this.each(function() {
                    mask($(this));
                });
            }
        } if (typeof fn == "object") {
        		opts = $.extend({}, defaults, fn);

        		//init buffer
                var _buffer = getMaskTemplate();
                var pasteEventName = $.browser.msie ? 'paste' : 'input';
                var iPhone = (window.orientation != undefined);
                var tests = getTestingChain();

                return this.each(function() {
                    mask($(this));
                });
        }

        //functions
        function getMaskTemplate() {
            var singleMask = $.map(opts.mask.split(""), function(element, index) {
                var outElem = [];
                var maskdef = opts.definitions[element];
                if (maskdef) {
                    for (i = 0; i < maskdef.cardinality; i++) {
                        outElem.push(opts.placeholder);
                    }
                } else outElem.push(element);

                return outElem;
            });

            var repeatedMask = singleMask.slice();
				for(var i = 0; i < opts.repeat; i++){
					repeatedMask = repeatedMask.concat(singleMask.slice());
				}
			return repeatedMask;
        };

        function getTestingChain() {
            return $.map(opts.mask.split(""), function(element, index) {
                var outElem = [];
                var maskdef = opts.definitions[element];
                if (maskdef) {
                    for (i = 1; i < maskdef.cardinality; i++) {
                        var prevalidator = maskdef.prevalidator[i - 1];
                        outElem.push([new RegExp(prevalidator.validator), prevalidator.cardinality]);
                    }
                    outElem.push([new RegExp(maskdef.validator), maskdef.cardinality]);
                } else outElem.push(null);

                return outElem;
            });
        };

        function unmaskedvalue(el) {
            var tests = el.data('tests');
            if (tests) {
                return $.map(el.val().split(""), function(element, index) {
                    return tests[index] ? element : null;
                }).join('');
            }
            else {
                return el.val();
            }
        }

        function mask(el) {
            var input = $(el);
            //store tests in the input element - used to get the unmasked value
            input.data('tests', tests);

            //init buffer
            var buffer = _buffer.slice();
            var undoBuffer = input.val();
            var ignore = false;              //Variable for ignoring control keys
            var len = _buffer.length;

            //bind events
            if (!input.attr("readonly")) {
                input.bind("focus", function() {
                    input.addClass('focus');
                    undoBuffer = input.val();
                    var pos = checkVal();
                    writeBuffer();
                    setTimeout(function() {
                        if (pos >= _buffer.length)
                            caret(input, 0, pos);
                        else
                            caret(input, pos);
                    }, 0);
                }).bind("mouseenter", function() {
                    if (!input.hasClass('focus') && input.val().length == 0)
                        writeBuffer();
                }).bind("blur", function() {
                    input.removeClass('focus');
                    if (input.val() == _buffer.join('')) {
                        input.val('');
                    } else {
                        checkVal();
                        if (input.val() != undoBuffer)
                            input.change();
                    }
                }).bind("mouseleave", function() {
                    if (!input.hasClass('focus') && input.val() == _buffer.join(''))
                        input.val('');
                }).bind("keydown", keydownEvent
                ).bind("keypress", keypressEvent
                ).bind(pasteEventName, function() {
                    setTimeout(function() { caret(input, checkVal(true)); }, 0);
                });

            }

            checkVal();

            //private functions
            function writeBuffer() { return input.val(buffer.join('')).val(); };
            function clearBuffer(start, end) {
                for (var i = start; i < end && i < len; i++) {
                    buffer[i] = _buffer[i];
                }
            };

            function seekNext(pos) {
                while (++pos <= len && !isMask(pos));
                return pos;
            };

            function shiftL(pos) {
                while (!isMask(pos) && --pos >= 0);
                for (var i = pos; i < len; i++) {
                    if (isMask(i)) {
                        buffer[i] = opts.placeholder;
                        var j = seekNext(i);
                        if (j < len && isValid(i, buffer[j])) {
                            buffer[i] = buffer[j];
                        } else
                            break;
                    }
                }
                writeBuffer();
                caret(input, pos);
            };

            function shiftR(pos) {
                for (var i = pos, c = opts.placeholder; i < len; i++) {
                    if (isMask(i)) {
                        var j = seekNext(i);
                        var t = buffer[i];
                        buffer[i] = c;
                        if (j < len && isValid(j, t))
                            c = t;
                        else
                            break;
                    }
                }
            };

            function caret(input, begin, end) {
                if (input.length == 0) return;
                if (typeof begin == 'number') {
                    end = (typeof end == 'number') ? end : begin;
                    return input.each(function() {
                        if (this.setSelectionRange) {
                            this.focus();
                            this.setSelectionRange(begin, end);
                        } else if (this.createTextRange) {
                            var range = this.createTextRange();
                            range.collapse(true);
                            range.moveEnd('character', end);
                            range.moveStart('character', begin);
                            range.select();
                        }
                    });
                } else {
                    if (input[0].setSelectionRange) {
                        begin = input[0].selectionStart;
                        end = input[0].selectionEnd;
                    } else if (document.selection && document.selection.createRange) {
                        var range = document.selection.createRange();
                        begin = 0 - range.duplicate().moveStart('character', -100000);
                        end = begin + range.text.length;
                    }
                    return { begin: begin, end: end };
                }
            };

            function keydownEvent(e) {
                var pos = caret($(this));
                var k = e.keyCode;
                ignore = (k < 16 || (k > 16 && k < 32) || (k > 32 && k < 41));

                //delete selection before proceeding
                if ((pos.begin - pos.end) != 0 && (!ignore || k == opts.keyCode.BACKSPACE || k == opts.keyCode.DELETE))
                    clearBuffer(pos.begin, pos.end);

                //backspace, delete, and escape get special treatment
                if (k == opts.keyCode.BACKSPACE || k == opts.keyCode.DELETE || (iPhone && k == 127)) {//backspace/delete
                    shiftL(pos.begin + (k == opts.keyCode.DELETE ? 0 : -1));
                    return false;
                } else if (k == opts.keyCode.ESCAPE) {//escape
                    input.val(undoBuffer);
                    caret(input, 0, checkVal());
                    return false;
                }
            }

            function keypressEvent(e) {
                if (ignore) {
                    ignore = false;
                    //Fixes Mac FF bug on backspace
                    return (e.keyCode == 8) ? false : null;
                }
                e = e || window.event;
                var k = e.charCode || e.keyCode || e.which;
                var pos = caret($(this));

                if (e.ctrlKey || e.altKey || e.metaKey) {//Ignore
                    return true;
                } else if ((k >= 32 && k <= 125) || k > 186) {//typeable characters
                    var p = seekNext(pos.begin - 1);
                    if (p < len) {
                        var c = String.fromCharCode(k);
                        if (isValid(p, c)) {
                            shiftR(p);
                            buffer[p] = c;
                            writeBuffer();
                            var next = seekNext(p);
                            caret($(this), next);
                            if (opts.onComplete && next == len)
                                opts.onComplete.call(input);
                        }
                    }
                }
                return false;
            }

            function isValid(pos, c) {
                var testPos = determineTestPosition(pos);

                var loopend = 0;
                if (c) { loopend = 1; ; }

                var chrs = '';
                for (var i = tests[testPos][1]; i > loopend; i--) {
                    chrs += buffer[testPos - (i - 1)];
                }

                if (c) { chrs += c; }

                return tests[testPos][0].test(chrs);
            }

            function isMask(pos) {
                return tests[determineTestPosition(pos)];
            }

            function determineTestPosition(pos) {
            	return pos % tests.length;
            }

            function checkVal(clearInvalid) {
                var inputValue = input.val();
                var lastMatch = -1;
                for (var i = 0; i < inputValue.length; i++) {
                    for (var pos = lastMatch+1; pos < len; pos++) {
                    	if(isMask(pos)){
                        	buffer[pos] = opts.placeholder;
                        	if (isValid(pos, inputValue.charAt(i))) {
                           		buffer[pos] = inputValue.charAt(i);
                           		lastMatch = pos;
                        	}
                        	break;
                        }else {   //nonmask
                           lastMatch++;
                        }
                    }
                }
                if (clearInvalid) {
                    writeBuffer();
                }
                return seekNext(lastMatch);
            };
        }
    };

})(jQuery);<|endoftext|>


<commit_before>/* eslint-disable no-console */
/*
 * module to provide a contentful like SDK to our local cache
 */
"use strict";

const path = require("path");
const fs = require("fs");
const glob = require("glob");

let clientResolveLinks, defaultLocale, localCachePath, locales, currentSpace;

const resolveLinksLimitMax = 10;

/**
 * sdk relies heavily on sys metadata
 * so we cannot omit the sys property on sdk level
 */
function normalizeSelect (query) {
    if (query.select && !/sys/i.test(query.select)) {
        query.select += ",sys";
    }
}

/**
 * fieldWithLocaleFallback
 */
function fieldWithLocaleFallback (field, value, preferredLocaleCode) {

    let newValue, locale;

    // get the locale record from the current metadata
    locale = locales.filter(l => l.code === preferredLocaleCode).shift();

    if (!locale) {
        throw new Error("Unknown locale: " + preferredLocaleCode);
    }

    if (locale.code in value) {
        newValue = value[locale.code];
    } else if (locale.fallbackCode) {
        newValue = fieldWithLocaleFallback(field, value, locale.fallbackCode);
    }

    return newValue;

}

/**
 * filter record by locale
 */
// TODO fallback locale list
function filterLocale(record, preferredLocaleCode) {

    let clone, field, locale;

    // get the locale record from the current metadata
    locale = locales.filter(l => l.code === preferredLocaleCode).shift();

    if (!locale) {
        throw new Error("Unknown locale: " + preferredLocaleCode);
    }

    // clone record
    clone = JSON.parse(JSON.stringify(record));

    if (clone.fields) {

        for (field in clone.fields) {
            if (clone.fields.hasOwnProperty(field)) {
                clone.fields[field] = fieldWithLocaleFallback(field, clone.fields[field], locale.code);
            }
        }

    }

    return clone;

}

/**
 * resolveLinks
 */
function resolveLinks (record, query) {
    return new Promise((resolve, reject) => {

        let fieldsWithLinks, promisesForRecords;

        // to avoid recursive loops, resolving links has a limit
        if (query.include > 0) {

            // find links in record fields
            if (record.fields) {

                // get a list of fields that are links or arrays of links
                fieldsWithLinks = Object.keys(record.fields).filter(key => {

                    if (Array.isArray(record.fields[key])) {
                        if (record.fields[key][0] && record.fields[key][0].sys) {
                            return record.fields[key][0].sys.type === "Link";
                        }
                    } else {
                        if (record.fields[key] && record.fields[key].sys) {
                            return record.fields[key].sys.type === "Link";
                        }
                    }

                });

                // get list of promises for the linked records
                promisesForRecords = fieldsWithLinks.map(key => {

                    let p, sys;

                    // switch on link type
                    sys = record.fields[key].sys;
                    switch(sys.linkType) {
                    case "Entry":
                        p = getEntry(sys.id, { "include": query.include - 1 });
                        break;
                    case "Asset":
                        p = getAsset(sys.id, { "include": query.include - 1 });
                        break;
                    default:
                        p = Promise.reject(
                            new Error("Unknown link type: %s", sys.linkType)
                        );
                        break;
                    }

                    return p;

                });

                // resolve promises and update record with linked records
                Promise.all(promisesForRecords).then(linkedRecords => {

                    fieldsWithLinks.forEach(key => {

                        if (Array.isArray(record.fields[key])) {

                            // field value has multiple links
                            record.fields[key] = linkedRecords.splice(0, record.fields[key].length);

                        } else {

                            // set the field to the current linked record
                            record.fields[key] = linkedRecords.shift();
                        }

                    });

                    resolve(record);

                }).catch(reject);

            } else {

                resolve(record);

            }

        } else {

            resolve(record);

        }

    });
}

/**
 * read file and optionally filter fields by locale
 */
// TODO filter by query
function readFileFilterLocale (file, query) {
    return new Promise((resolve, reject) => {

        fs.readFile(file, "utf8", function (err, data) {

            if (err) {
                reject(err);
            } else {

                let record = JSON.parse(data);

                getSpace(record)
                    .then(space => {
                        return new Promise((resolve) => {
                            // set default locale
                            locales = space.locales;
                            defaultLocale = locales.filter(locale => locale.default)[0];
                            resolve(defaultLocale);
                        });
                    })
                    .then(defaultLocale => {
                        return new Promise((resolve) => {
                            // filter to locale
                            record = filterLocale(record, query.locale || defaultLocale.code);

                            resolve(record);
                        });
                    })
                    .then(record => resolveLinks(record, query))
                    .then(resolve)
                    .catch(reject);

            }

        });

    });
}

/**
 * helper fuction to get the value of a nest property of an object by "path"
 * path is a string with property dot notation
 * i.e. "sys.createdAt" or "fields.image.title"
 */
function objectValueByPath(obj, path) {
    return path.split(".").reduce(function(acc, v) {
        return acc ? acc[v] : undefined;
    }, obj);
}

/**
 * sort fuction to sort arrays of objects by the value of the property
 * (possibly nested) refernced by the path string
 */
function compareObjectsByValueAtPath (path, reverse) {

    let direction = reverse ? -1 : 1;

    return function (a, b) {

        // get values
        let aValue = objectValueByPath(a, path);
        let bValue = objectValueByPath(b, path);

        // compare values and apply direction
        if (aValue > bValue) {
            return 1 * direction;
        } else if (aValue < bValue) {
            return -1 * direction;
        } else {
            return 0;
        }

    };

}

/**
 * sort item by order property in query
 * @arg {array} items to sort
 * @arg {query} query object
 * @returns {Promise<array>} a promise of an array of items
 */
function sortItemsByQuery (items, query) {
    return new Promise(resolve => {

        let order = query.order;

        if (order) {

            // sort items by order property in query
            // order property document here:
            // https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/order

            // split up multiple attributes by comma
            // and field name and decending or accending
            let attributes = order.split(",").map(attribute => {

                if (attribute.charAt(0) === "-") {
                    return {
                        "field": attribute.slice(1),
                        "reverse": true
                    };
                } else {
                    return {
                        "field": attribute,
                        "reverse": false
                    };
                }

            });

            // sort the items by the least specified attribute and repeat
            attributes.reverse().forEach(attribute => {

                items.sort(compareObjectsByValueAtPath(
                    attribute.field, attribute.reverse
                ));

            });

            resolve(items);

        } else {

            // no order specified, so just return the items
            resolve(items);

        }

    });
}

function readFilesAndFilter (dir, query) {
    return new Promise((resolve, reject) => {

        glob(path.join(dir, "**/*.json"), function (err, files) {

            if (err) {

                reject(err);

            } else {

                if (files && files.length > 0) {

                    Promise.all(
                        files.map(file => readFileFilterLocale(file, query))
                    )
                    .then(results => sortItemsByQuery(results, query))
                    .then(results => {
                        resolve({
                            "total": results.length,
                            "skip": 0,
                            "limit": Infinity,
                            "items": results
                        });
                    }).catch(reject);

                } else {

                    resolve({
                        "total": 0,
                        "skip": 0,
                        "limit": Infinity,
                        "items": []
                    });

                }

            }

        });

    });
}

/**
 * process query properties
 */
function processQueryProperties (query) {

    if (query.resolveLinks === undefined) {

        // use client setting if unset
        query.resolveLinks = clientResolveLinks;

    }

    if (query.resolveLinks === undefined) {

        // default to true for resolve links
        query.resolveLinks = true;

    }

    if (query.include === undefined) {

        // default to include one level of links
        query.include = 1;

    }

    if (query.include > resolveLinksLimitMax) {

        // limit the maximum number of inclusions
        query.include = resolveLinksLimitMax;

    }

    return query;

}

/**
 * Gets the Space which the client is currently configured to use
 * @memberof ContentfulClientAPI
 * @return {Promise<Entities.Space>} Promise for a Space
 * @example
 * client.getSpace()
 * .then(space => console.log(space))
 */
function getSpace () {
    return new Promise((resolve, reject) => {

        let file = path.resolve(localCachePath, currentSpace, "space.json");

        fs.readFile(file, "utf8", function (err, data) {
            if (err) {
                reject(err);
            } else {
                resolve(JSON.parse(data));
            }
        });

    });
}

/**
 * Gets a Content Type
 * @memberof ContentfulClientAPI
 * @param  {string} id
 * @return {Promise<Entities.ContentType>} Promise for a Content Type
 * @example
 * client.getContentType('contentTypeId')
 * .then(contentType => console.log(contentType))
 */
function getContentType () {
    throw new Error("Not implemented.");
}

/**
 * Gets a collection of Content Types
 * @memberof ContentfulClientAPI
 * @param  {Object=} query - Object with search parameters. Check the JS SDK tutorial and the REST API reference for more details.
 * @return {Promise<Entities.ContentTypeCollection>} Promise for a collection of Content Types
 * @example
 * client.getContentTypes()
 * .then(contentTypes => console.log(contentTypes.items))
 */
function getContentTypes () {
    throw new Error("Not implemented.");
}

/**
 * Gets an Entry
 * @memberof ContentfulClientAPI
 * @param  {string} id
 * @param  {Object=} query - Object with search parameters. In this method it's only useful for `locale`.
 * @return {Promise<Entities.Entry>} Promise for an Entry
 * @example
 * client.getEntry('entryId')
 * .then(entry => console.log(entry))
 */
function getEntry (id, query = {}) {

    let dir, file;

    query = processQueryProperties(query);

    normalizeSelect(query);

    dir = path.resolve(localCachePath, currentSpace, "entries");
    if (query.content_type) {

        // limit to content type in query
        dir = path.resolve(dir, query.content_type);
        file = path.join(dir, `${query.content_type}_${id}.json`);

    } else {

        // search for id in all content types
        let pattern = `${dir}/**/*_${id}.json`;
        let files = glob.sync(pattern);
        if (files && files.length > 0) {

            file = files[0];

        }

    }

    return readFileFilterLocale(file, query);

}

/**
 * Gets a collection of Entries
 * @memberof ContentfulClientAPI
 * @param  {Object=} query - Object with search parameters. Check the JS SDK tutorial and the REST API reference for more details.
 * @param  {boolean=} query.resolveLinks - When true, links to other Entries or Assets are resolved. Default: true.
 * @return {Promise<Entities.EntryCollection>} Promise for a collection of Entries
 * @example
 * client.getEntries({content_type: 'contentTypeId'})
 * .then(entries => console.log(entries.items))
 */
function getEntries (query = {}) {

    let dir;

    query = processQueryProperties(query);

    dir = path.resolve(localCachePath, currentSpace, "entries");
    if (query.content_type) {

        // limit to content type in query
        dir = path.resolve(dir, query.content_type);

    }

    return readFilesAndFilter(dir, query);

}

function getAsset (id, query = {}) {

    query = processQueryProperties(query);

    normalizeSelect(query);
    let dir = path.resolve(localCachePath, currentSpace, "assets");
    return readFileFilterLocale(path.join(dir, `${id}.json`), query);

}

function getAssets (query = {}) {

    query = processQueryProperties(query);

    normalizeSelect(query);
    let dir = path.resolve(localCachePath, currentSpace, "assets");

    return readFilesAndFilter(dir, query);

}

function parseEntries () {
    throw new Error("Not implemented.");
}

function sync () {
    throw new Error("Not implemented.");
}

/**
 * unWrap - Contentful JSON to Plain JSON
 * @arg {Object} entry
 * @returns {Object} plain entry
 */
function unWrap (entry) {

    try {
        return Promise.resolve(unWrapSync(entry));
    } catch (err) {
        return Promise.reject(err);
    }

}

/**
 * unWrap without a promise
 */
function unWrapSync (entry) {

    let po;

    if (entry.items) {

        // return collections with thier items unWrapped
        entry.items = unWrapCollectionSync(entry);
        return entry;

    } else if (entry.fields) {

        po = Object.keys(entry.fields).reduce((acc, v) => {

            if (entry.fields[v] && entry.fields[v].sys) {

                acc[v] = unWrapSync(entry.fields[v]);

            } else {

                acc[v] = entry.fields[v];

            }

            return acc;

        }, {});

    }

    return po;

}

/**
 * unWrapCollectionSync - Contentful JSON Collection to Plain JSON Array
 * @arg {Object} records
 * @returns {Object} plain objects
 */
function unWrapCollectionSync (collection) {

    return collection.items.map(unWrapSync);

}

/**
 * unWrapCollection - Contentful JSON Collection to Plain JSON Array
 * @arg {Object} records
 * @returns {Promise<Object>} Promise of plain objects
 */
function unWrapCollection (collection) {

    try {
        return Promise.resolve(unWrapCollectionSync(collection));
    } catch (err) {
        return Promise.reject(err);
    }

}

function createClient (params = {}) {

    currentSpace = params.space;
    clientResolveLinks = params.resolveLinks;
    localCachePath = params.localPath;

    return {
        getSpace: getSpace,
        getContentType: getContentType,
        getContentTypes: getContentTypes,
        getEntry: getEntry,
        getEntries: getEntries,
        getAsset: getAsset,
        getAssets: getAssets,
        parseEntries: parseEntries,
        sync: sync,
        unWrap: unWrap,
        unWrapCollection: unWrapCollection
    };

}

module.exports = {
    createClient: createClient
};
<commit_msg>trimmed whitespace from order attribute before sorting<commit_after>/* eslint-disable no-console */
/*
 * module to provide a contentful like SDK to our local cache
 */
"use strict";

const path = require("path");
const fs = require("fs");
const glob = require("glob");

let clientResolveLinks, defaultLocale, localCachePath, locales, currentSpace;

const resolveLinksLimitMax = 10;

/**
 * sdk relies heavily on sys metadata
 * so we cannot omit the sys property on sdk level
 */
function normalizeSelect (query) {
    if (query.select && !/sys/i.test(query.select)) {
        query.select += ",sys";
    }
}

/**
 * fieldWithLocaleFallback
 */
function fieldWithLocaleFallback (field, value, preferredLocaleCode) {

    let newValue, locale;

    // get the locale record from the current metadata
    locale = locales.filter(l => l.code === preferredLocaleCode).shift();

    if (!locale) {
        throw new Error("Unknown locale: " + preferredLocaleCode);
    }

    if (locale.code in value) {
        newValue = value[locale.code];
    } else if (locale.fallbackCode) {
        newValue = fieldWithLocaleFallback(field, value, locale.fallbackCode);
    }

    return newValue;

}

/**
 * filter record by locale
 */
// TODO fallback locale list
function filterLocale(record, preferredLocaleCode) {

    let clone, field, locale;

    // get the locale record from the current metadata
    locale = locales.filter(l => l.code === preferredLocaleCode).shift();

    if (!locale) {
        throw new Error("Unknown locale: " + preferredLocaleCode);
    }

    // clone record
    clone = JSON.parse(JSON.stringify(record));

    if (clone.fields) {

        for (field in clone.fields) {
            if (clone.fields.hasOwnProperty(field)) {
                clone.fields[field] = fieldWithLocaleFallback(field, clone.fields[field], locale.code);
            }
        }

    }

    return clone;

}

/**
 * resolveLinks
 */
function resolveLinks (record, query) {
    return new Promise((resolve, reject) => {

        let fieldsWithLinks, promisesForRecords;

        // to avoid recursive loops, resolving links has a limit
        if (query.include > 0) {

            // find links in record fields
            if (record.fields) {

                // get a list of fields that are links or arrays of links
                fieldsWithLinks = Object.keys(record.fields).filter(key => {

                    if (Array.isArray(record.fields[key])) {
                        if (record.fields[key][0] && record.fields[key][0].sys) {
                            return record.fields[key][0].sys.type === "Link";
                        }
                    } else {
                        if (record.fields[key] && record.fields[key].sys) {
                            return record.fields[key].sys.type === "Link";
                        }
                    }

                });

                // get list of promises for the linked records
                promisesForRecords = fieldsWithLinks.map(key => {

                    let p, sys;

                    // switch on link type
                    sys = record.fields[key].sys;
                    switch(sys.linkType) {
                    case "Entry":
                        p = getEntry(sys.id, { "include": query.include - 1 });
                        break;
                    case "Asset":
                        p = getAsset(sys.id, { "include": query.include - 1 });
                        break;
                    default:
                        p = Promise.reject(
                            new Error("Unknown link type: %s", sys.linkType)
                        );
                        break;
                    }

                    return p;

                });

                // resolve promises and update record with linked records
                Promise.all(promisesForRecords).then(linkedRecords => {

                    fieldsWithLinks.forEach(key => {

                        if (Array.isArray(record.fields[key])) {

                            // field value has multiple links
                            record.fields[key] = linkedRecords.splice(0, record.fields[key].length);

                        } else {

                            // set the field to the current linked record
                            record.fields[key] = linkedRecords.shift();
                        }

                    });

                    resolve(record);

                }).catch(reject);

            } else {

                resolve(record);

            }

        } else {

            resolve(record);

        }

    });
}

/**
 * read file and optionally filter fields by locale
 */
// TODO filter by query
function readFileFilterLocale (file, query) {
    return new Promise((resolve, reject) => {

        fs.readFile(file, "utf8", function (err, data) {

            if (err) {
                reject(err);
            } else {

                let record = JSON.parse(data);

                getSpace(record)
                    .then(space => {
                        return new Promise((resolve) => {
                            // set default locale
                            locales = space.locales;
                            defaultLocale = locales.filter(locale => locale.default)[0];
                            resolve(defaultLocale);
                        });
                    })
                    .then(defaultLocale => {
                        return new Promise((resolve) => {
                            // filter to locale
                            record = filterLocale(record, query.locale || defaultLocale.code);

                            resolve(record);
                        });
                    })
                    .then(record => resolveLinks(record, query))
                    .then(resolve)
                    .catch(reject);

            }

        });

    });
}

/**
 * helper fuction to get the value of a nest property of an object by "path"
 * path is a string with property dot notation
 * i.e. "sys.createdAt" or "fields.image.title"
 */
function objectValueByPath(obj, path) {
    return path.split(".").reduce(function(acc, v) {
        return acc ? acc[v] : undefined;
    }, obj);
}

/**
 * sort fuction to sort arrays of objects by the value of the property
 * (possibly nested) refernced by the path string
 */
function compareObjectsByValueAtPath (path, reverse) {

    let direction = reverse ? -1 : 1;

    return function (a, b) {

        // get values
        let aValue = objectValueByPath(a, path);
        let bValue = objectValueByPath(b, path);

        // compare values and apply direction
        if (aValue > bValue) {
            return 1 * direction;
        } else if (aValue < bValue) {
            return -1 * direction;
        } else {
            return 0;
        }

    };

}

/**
 * sort item by order property in query
 * @arg {array} items to sort
 * @arg {query} query object
 * @returns {Promise<array>} a promise of an array of items
 */
function sortItemsByQuery (items, query) {
    return new Promise(resolve => {

        let order = query.order;

        if (order) {

            // sort items by order property in query
            // order property document here:
            // https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/order

            // split up multiple attributes by comma
            // and field name and decending or accending
            let attributes = order.split(",").map(attribute => {

                attribute = attribute.trim();

                if (attribute.charAt(0) === "-") {
                    return {
                        "field": attribute.slice(1),
                        "reverse": true
                    };
                } else {
                    return {
                        "field": attribute,
                        "reverse": false
                    };
                }

            });

            // sort the items by the least specified attribute and repeat
            attributes.reverse().forEach(attribute => {

                items.sort(compareObjectsByValueAtPath(
                    attribute.field, attribute.reverse
                ));

            });

            resolve(items);

        } else {

            // no order specified, so just return the items
            resolve(items);

        }

    });
}

function readFilesAndFilter (dir, query) {
    return new Promise((resolve, reject) => {

        glob(path.join(dir, "**/*.json"), function (err, files) {

            if (err) {

                reject(err);

            } else {

                if (files && files.length > 0) {

                    Promise.all(
                        files.map(file => readFileFilterLocale(file, query))
                    )
                    .then(results => sortItemsByQuery(results, query))
                    .then(results => {
                        resolve({
                            "total": results.length,
                            "skip": 0,
                            "limit": Infinity,
                            "items": results
                        });
                    }).catch(reject);

                } else {

                    resolve({
                        "total": 0,
                        "skip": 0,
                        "limit": Infinity,
                        "items": []
                    });

                }

            }

        });

    });
}

/**
 * process query properties
 */
function processQueryProperties (query) {

    if (query.resolveLinks === undefined) {

        // use client setting if unset
        query.resolveLinks = clientResolveLinks;

    }

    if (query.resolveLinks === undefined) {

        // default to true for resolve links
        query.resolveLinks = true;

    }

    if (query.include === undefined) {

        // default to include one level of links
        query.include = 1;

    }

    if (query.include > resolveLinksLimitMax) {

        // limit the maximum number of inclusions
        query.include = resolveLinksLimitMax;

    }

    return query;

}

/**
 * Gets the Space which the client is currently configured to use
 * @memberof ContentfulClientAPI
 * @return {Promise<Entities.Space>} Promise for a Space
 * @example
 * client.getSpace()
 * .then(space => console.log(space))
 */
function getSpace () {
    return new Promise((resolve, reject) => {

        let file = path.resolve(localCachePath, currentSpace, "space.json");

        fs.readFile(file, "utf8", function (err, data) {
            if (err) {
                reject(err);
            } else {
                resolve(JSON.parse(data));
            }
        });

    });
}

/**
 * Gets a Content Type
 * @memberof ContentfulClientAPI
 * @param  {string} id
 * @return {Promise<Entities.ContentType>} Promise for a Content Type
 * @example
 * client.getContentType('contentTypeId')
 * .then(contentType => console.log(contentType))
 */
function getContentType () {
    throw new Error("Not implemented.");
}

/**
 * Gets a collection of Content Types
 * @memberof ContentfulClientAPI
 * @param  {Object=} query - Object with search parameters. Check the JS SDK tutorial and the REST API reference for more details.
 * @return {Promise<Entities.ContentTypeCollection>} Promise for a collection of Content Types
 * @example
 * client.getContentTypes()
 * .then(contentTypes => console.log(contentTypes.items))
 */
function getContentTypes () {
    throw new Error("Not implemented.");
}

/**
 * Gets an Entry
 * @memberof ContentfulClientAPI
 * @param  {string} id
 * @param  {Object=} query - Object with search parameters. In this method it's only useful for `locale`.
 * @return {Promise<Entities.Entry>} Promise for an Entry
 * @example
 * client.getEntry('entryId')
 * .then(entry => console.log(entry))
 */
function getEntry (id, query = {}) {

    let dir, file;

    query = processQueryProperties(query);

    normalizeSelect(query);

    dir = path.resolve(localCachePath, currentSpace, "entries");
    if (query.content_type) {

        // limit to content type in query
        dir = path.resolve(dir, query.content_type);
        file = path.join(dir, `${query.content_type}_${id}.json`);

    } else {

        // search for id in all content types
        let pattern = `${dir}/**/*_${id}.json`;
        let files = glob.sync(pattern);
        if (files && files.length > 0) {

            file = files[0];

        }

    }

    return readFileFilterLocale(file, query);

}

/**
 * Gets a collection of Entries
 * @memberof ContentfulClientAPI
 * @param  {Object=} query - Object with search parameters. Check the JS SDK tutorial and the REST API reference for more details.
 * @param  {boolean=} query.resolveLinks - When true, links to other Entries or Assets are resolved. Default: true.
 * @return {Promise<Entities.EntryCollection>} Promise for a collection of Entries
 * @example
 * client.getEntries({content_type: 'contentTypeId'})
 * .then(entries => console.log(entries.items))
 */
function getEntries (query = {}) {

    let dir;

    query = processQueryProperties(query);

    dir = path.resolve(localCachePath, currentSpace, "entries");
    if (query.content_type) {

        // limit to content type in query
        dir = path.resolve(dir, query.content_type);

    }

    return readFilesAndFilter(dir, query);

}

function getAsset (id, query = {}) {

    query = processQueryProperties(query);

    normalizeSelect(query);
    let dir = path.resolve(localCachePath, currentSpace, "assets");
    return readFileFilterLocale(path.join(dir, `${id}.json`), query);

}

function getAssets (query = {}) {

    query = processQueryProperties(query);

    normalizeSelect(query);
    let dir = path.resolve(localCachePath, currentSpace, "assets");

    return readFilesAndFilter(dir, query);

}

function parseEntries () {
    throw new Error("Not implemented.");
}

function sync () {
    throw new Error("Not implemented.");
}

/**
 * unWrap - Contentful JSON to Plain JSON
 * @arg {Object} entry
 * @returns {Object} plain entry
 */
function unWrap (entry) {

    try {
        return Promise.resolve(unWrapSync(entry));
    } catch (err) {
        return Promise.reject(err);
    }

}

/**
 * unWrap without a promise
 */
function unWrapSync (entry) {

    let po;

    if (entry.items) {

        // return collections with thier items unWrapped
        entry.items = unWrapCollectionSync(entry);
        return entry;

    } else if (entry.fields) {

        po = Object.keys(entry.fields).reduce((acc, v) => {

            if (entry.fields[v] && entry.fields[v].sys) {

                acc[v] = unWrapSync(entry.fields[v]);

            } else {

                acc[v] = entry.fields[v];

            }

            return acc;

        }, {});

    }

    return po;

}

/**
 * unWrapCollectionSync - Contentful JSON Collection to Plain JSON Array
 * @arg {Object} records
 * @returns {Object} plain objects
 */
function unWrapCollectionSync (collection) {

    return collection.items.map(unWrapSync);

}

/**
 * unWrapCollection - Contentful JSON Collection to Plain JSON Array
 * @arg {Object} records
 * @returns {Promise<Object>} Promise of plain objects
 */
function unWrapCollection (collection) {

    try {
        return Promise.resolve(unWrapCollectionSync(collection));
    } catch (err) {
        return Promise.reject(err);
    }

}

function createClient (params = {}) {

    currentSpace = params.space;
    clientResolveLinks = params.resolveLinks;
    localCachePath = params.localPath;

    return {
        getSpace: getSpace,
        getContentType: getContentType,
        getContentTypes: getContentTypes,
        getEntry: getEntry,
        getEntries: getEntries,
        getAsset: getAsset,
        getAssets: getAssets,
        parseEntries: parseEntries,
        sync: sync,
        unWrap: unWrap,
        unWrapCollection: unWrapCollection
    };

}

module.exports = {
    createClient: createClient
};
<|endoftext|>


<commit_before>(function($) {
	$.fn.jsonList = function( options ) {
		return this.each(function() {
			new JSONList().init(this, options);
		});
	};
	var JSONList = function() {};
	JSONList.prototype = {
		init: function( el, options ) {
			this.el = $(el);
			// fill options with default values
			this.options = options = $.extend({
				type: 'groupedItems',
				url: null,
				data: null,
				groupLabel: 'name',
				itemLabel: 'name',
				onSuccess: function( jsonList ) {},
				onListItem: function( event, listItem, data, isGroup ) {},
				onResponse: function( data, textStatus ) {}
			}, options);
			var self = this;
			$(this)
				.bind('success', this.options.onSuccess)
				.bind('listItem', this.options.onListItem)
				.bind('response', this.options.onResponse);
			$.getJSON(options.url, options.data, function(data, textStatus) {
				self.handleResponse(data, textStatus);
			});
		},
		handleResponse: function( data, textStatus ) {
			$(this).trigger('response', [data, textStatus]);
			if( this.options.type == 'groupedItems' ) {
				this.createListForGroupedItems(data, textStatus);
			}
			$(this).trigger('success', [this.el]);
		},
		createListForGroupedItems: function( data, textStatus ) {
			var opts = $.extend({
					groups: 'groups',
					items: 'items'
				}, this.options),
				groups = {},
				items = {};
			this.groups = groups;
			this.items = items;
			$.each(data[opts.items], function(i, item) {
				items[item.id] = item;
			});
			$.each(data[opts.groups], function(i, group) {
				var children = [];
				if( group.children ) {
					$.each(group.children, function(i, childId) {
						children.push(items[childId]);
					});
				}
				group.children = children;
				groups[group.id] = group;
			});
			$.each(groups, function(id, group) {
				var subGroups = [];
				if( group.subGroups ) {
					$.each(group.subGroups, function(i, childId) {
						subGroups.push(groups[childId]);
						delete groups[childId];
					});
				}
				group.subGroups = subGroups;
			});
			this.createList(groups);
		},
		createList: function( groups ) {
			if( this.el.is('ol, ul') )
				this.appendGroupItems(groups, this.el);
			else
				this.el.append(this.appendGroupItems(groups, $('
')));
		},
		appendGroupItems: function( groups, list ) {
			var self = this;
			$.each(groups, function(id, group) {
				var listItem = $('
' + self.getGroupLabel(group) + '
');
				$(self).trigger('listItem', [listItem, group, true]);
				if( group.subGroups || group.children ) {
					var subList = $('
');
					if( group.subGroups ) {
						self.appendGroupItems(group.subGroups, subList);
					}
					if( group.children ) {
						self.appendItems(group.children, subList);
					}
					listItem.append(subList);
				}
				list.append(listItem);
			});
			return list;
		},
		appendItems: function( items, list ) {
			var self = this;
			$.each(items, function(id, item) {
				var listItem = $('
' + self.getItemLabel(item) + '
');
				$(self).trigger('listItem', [listItem, item, false]);
				list.append(listItem);
			});
		},
		getGroupLabel: function( group ) {
			return group[this.options.groupLabel];
		},
		getItemLabel: function( item ) {
			return item[this.options.itemLabel];
		}
	};
}(jQuery));<commit_msg>Code formatting changes<commit_after>(function($) {
  $.fn.jsonList = function(options) {
    return this.each(function() {
      new JSONList().init(this, options);
    });
  };
  var JSONList = function() {
  };
  JSONList.prototype = {
    init: function(el, options) {
      this.el = $(el);
      // fill options with default values
      this.options = options = $.extend({
        type: 'groupedItems',
        url: null,
        data: null,
        groupLabel: 'name',
        itemLabel: 'name',
        onSuccess: function(jsonList) {},
        onListItem: function(event, listItem, data, isGroup) {},
        onResponse: function(data, textStatus) {}
      }, options);
      var self = this;
      $(this)
        .bind('success', this.options.onSuccess)
        .bind('listItem', this.options.onListItem)
        .bind('response', this.options.onResponse);
      $.getJSON(options.url, options.data, function(data, textStatus) {
        self.handleResponse(data, textStatus);
      });
    },
    handleResponse: function(data, textStatus) {
      $(this).trigger('response', [data, textStatus]);
      if (this.options.type == 'groupedItems') {
        this.createListForGroupedItems(data, textStatus);
      }
      $(this).trigger('success', [this]);
    },
    createListForGroupedItems: function(data, textStatus) {
      var opts = $.extend({
            groups: 'groups',
            items: 'items'
          }, this.options),
          groups = {},
          items = {};
      this.groups = groups;
      this.items = items;
      $.each(data[opts.items], function(i, item) {
        items[item.id] = item;
      });
      $.each(data[opts.groups], function(i, group) {
        var children = [];
        if (group.children) {
          $.each(group.children, function(i, childId) {
            children.push(items[childId]);
          });
        }
        group.children = children;
        groups[group.id] = group;
      });
      $.each(groups, function(id, group) {
        var subGroups = [];
        if (group.subGroups) {
          $.each(group.subGroups, function(i, childId) {
            subGroups.push(groups[childId]);
            delete groups[childId];
          });
        }
        group.subGroups = subGroups;
      });
      this.createList(groups);
    },
    createList: function(groups) {
      if (this.el.is('ol, ul')) {
        this.appendGroupItems(groups, this.el);
      } else {
        this.el.append(this.appendGroupItems(groups, $('
')));
      }
    },
    appendGroupItems: function(groups, list) {
      var self = this;
      $.each(groups, function(id, group) {
        var listItem = $('
' + self.getGroupLabel(group) + '
');
        $(self).trigger('listItem', [listItem, group, true]);
        if (group.subGroups || group.children) {
          var subList = $('
');
          if (group.subGroups) {
            self.appendGroupItems(group.subGroups, subList);
          }
          if (group.children) {
            self.appendItems(group.children, subList);
          }
          listItem.append(subList);
        }
        list.append(listItem);
      });
      return list;
    },
    appendItems: function(items, list) {
      var self = this;
      $.each(items, function(id, item) {
        var listItem = $('
' + self.getItemLabel(item) + '
');
        $(self).trigger('listItem', [listItem, item, false]);
        list.append(listItem);
      });
    },
    getGroupLabel: function(group) {
      return group[this.options.groupLabel];
    },
    getItemLabel: function(item) {
      return item[this.options.itemLabel];
    }
  };
}(jQuery));
<|endoftext|>


<commit_before>/*
 * Scrollbar - a jQuery plugin for custom scrollbars
 *
 * @author     Thomas Duerr, me@thomd.net
 * @date       03.2010
 * @requires   jquery v1.4.2
 *
 *
 * Usage:
 * ======
 *
 *
 *
 *
 *
 *
 * CSS:
 * ====
 *
 *
 */
;(function($){

    $.fn.scrollbar = function(options){

        // Extend default options
        var options = $.extend({}, $.fn.scrollbar.defaults, options);

        //
        // append scrollbar to every found container and return jquery object for chainability
        //
        return this.each(function(i){

            var container = $(this),
                props = {};

            // determine container height
            props.containerHeight = container.height();

            // determine inner content height
            props.contentHeight = 0;
            container.children().each(function(){
                props.contentHeight += $(this).outerHeight();
            });

            // do nothing and return if a scrollbar is not neccessary
            if(props.contentHeight <= props.containerHeight) return true;

            // create scrollbar
            var scrollbar = new $.fn.scrollbar.Scrollbar(container, props, options);
        });
    }

    //
    // default options
    //
    $.fn.scrollbar.defaults = {
        handleMinHeight: 30,         // min-height of handle (height is actually dependent on content height)
        scrollSpeed: 100,       // TODO
        scrollStep: 10      // TODO
    };

    //
    // Scrollbar class properties
    //
    $.fn.scrollbar.Scrollbar = function(container, props, options){
        this.container = container;
        this.props =     props;
        this.opts =      options;
        this.mouse =     {};

        // initialize
        this.buildHtml();
        this.initHandle();
        this.appendEvents();
    };

    //
    // Scrollbar class methods
    //
    $.fn.scrollbar.Scrollbar.prototype = {

        //
        // build DOM nodes for pane and scroll-handle
        //
        //

        //

        //              [...]
        //

        //

        //

        //

        //

        //

        //

        //
        buildHtml: function(){
            this.container.children().wrapAll('
');
            this.container.append('

')
                .append('
')
                .append('
');

            this.pane = this.container.find('.scrollbar-pane');
            this.handle = this.container.find('.scrollbar-handle');
            this.handleContainer = this.container.find('.scrollbar-handle-container');
            this.handleArrows = this.container.find('.scrollbar-handle-up, .scrollbar-handle-down');
        },

        //
        // append events on handle and handle-container
        //
        appendEvents: function(){

            // append hover event on scrollbar-handle
            this.handle.bind('mouseenter mouseleave', this.hoverHandle);

            // append hover event on scrollbar-arrows
            this.handleArrows.bind('mouseenter mouseleave', this.hoverHandle);

            // append drag-drop event on scrollbar-handle
            this.handle.bind('mousedown.handle', $.proxy(this, 'startOfHandleMove'));

            // append click event on scrollbar-handle-container
            this.handleContainer.bind('mousedown.handle', $.proxy(this, 'clickHandleContainer'));

            // append click event on scrollbar-up- and scrollbar-down-handles
            this.handleArrows.bind('mousedown.arrows', $.proxy(this, 'clickHandleArrows'));

            // appen mousewheel event on content container
            this.container.bind('mousewheel.container', $.proxy(this, 'onMouseWheel'));
        },

        //
        // calculate height of handle.
        // height of handle should indicate height of content.
        //
        initHandle: function(){
            this.props.handleContainerHeight = this.handleContainer.height();

            // we need to calculate content-height again: due to the added scrollbar, the width decreased - hence the height increased!
            var contentHeight = 0;
            this.pane.children().each(function(){
                contentHeight += $(this).outerHeight();
            });
            this.props.contentHeight = contentHeight;

            this.props.handleHeight = Math.max(this.props.containerHeight * this.props.handleContainerHeight / this.props.contentHeight, this.opts.handleMinHeight);
            this.props.handleTop = {
                min: 0,
                max: this.props.handleContainerHeight - this.props.handleHeight
            };
            this.handle.height(this.props.handleHeight);
            this.handle.top = 0;
            this.handleContentRatio = (this.props.contentHeight - this.props.containerHeight) / (this.props.handleContainerHeight - this.props.handleHeight);
        },

        //
        // get mouse position helper
        //
        mousePosition: function(ev) {
			return ev.pageY || (ev.clientY + (document.documentElement.scrollTop || document.body.scrollTop)) || 0;
		},

        // ---------- event handler ---------------------------------------------------------------

        //
        // start moving of handle
        //
        startOfHandleMove: function(ev){
            ev.preventDefault();

            // set start top-position of mouse
            this.mouse.top = this.mousePosition(ev);

            // bind mousemove- and mouseout-event to document (binding it to document allows having a mousepointer outside handle while moving)
    		$(document).bind('mousemove.handle', $.proxy(this, 'onHandleMove')).bind('mouseup.handle', $.proxy(this, 'endOfHandleMove'));

            // remove hover event on scrollbar-arrows (until end of handle move)
    		this.handleArrows.unbind('mouseenter mouseleave', this.hoverHandle);

    		// add class for visual change while moving handle
            this.handle.addClass('move');
        },

        //
        // on moving of handle
        //
        onHandleMove: function(ev){

            var mousePos = this.mousePosition(ev);

            // calculate distance since last fireing of this handler
            var delta = mousePos - this.mouse.top;
            this.mouse.top = mousePos;

            // calculate new handle position
            this.handle.top += delta;

            // update positions
            this.setHandlePosition(this.handle.top);
            this.setContentPosition();

        },

        //
        // end moving of handle
        //
        endOfHandleMove: function(ev){

            // remove handle events
    		$(document).unbind('.handle');

            // re-attach hover event on scrollbar-arrows
            this.handleArrows.bind('mouseenter mouseleave', this.hoverHandle);

            // remove class for visual change
            this.handle.removeClass('move');
        },

        //
        // set position of handle
        //
        setHandlePosition: function(delta){

            // stay within range [handleTop.min, handleTop.max]
            this.handle.top = (this.handle.top > this.props.handleTop.max) ? this.props.handleTop.max : this.handle.top;
            this.handle.top = (this.handle.top < this.props.handleTop.min) ? this.props.handleTop.min : this.handle.top;

            this.handle[0].style.top = this.handle.top + 'px';
        },

        //
        // set position of content
        //
        setContentPosition: function(){

            // derive position of content from position of handle
            this.pane.top = -1 * this.handleContentRatio * this.handle.top;

            this.pane[0].style.top = this.pane.top + 'px';
        },

        //
        // mouse wheel movement
        //
        onMouseWheel: function(ev, delta){

            // calculate new handle position
            this.handle.top -= delta;

            this.setHandlePosition(this.handle.top);
            this.setContentPosition();
        },

        //
        // TODO: document!
        // append click handler on handle-container (to click up and down the handle)
        //
        clickHandleContainer: function(ev){
            ev.preventDefault();
            if(!$(ev.target).hasClass('scrollbar-handle-container')) return false;

            var direction = (this.handle.offset().top < this.mousePosition(ev)) ? 1 : -1;
            this.handle.start = this.handle.top = direction == 1 ? this.handle.top + (this.props.handleTop.max - this.handle.top) * 0.5 : this.handle.top - (this.handle.top - this.props.handleTop.min) * 0.5;
            this.handle[0].style.top = this.handle.top + 'px';
            this.setContentPosition();
        },

        //
        // TODO: document!
        // append click handler on handle-arrows
        //
        clickHandleArrows: function(ev){
            ev.preventDefault();
            var direction = $(ev.target).hasClass('scrollbar-handle-up') ? -1 : 1;

            var timer = setInterval($.proxy(function(){
                this.handle.start = this.handle.top = direction == 1 ? Math.min(this.handle.top + this.opts.scrollStep, this.props.handleTop.max) : Math.max(this.handle.top - this.opts.scrollStep, this.props.handleTop.min);
                this.handle[0].style.top = this.handle.top + 'px';
                this.setContentPosition();
            }, this), this.opts.scrollSpeed);

    		var clearTimer = function(){
    		    clearInterval(timer);
        		$(document).unbind('mouseup.arrows', clearTimer);
    		}
    		$(document).bind('mouseup.arrows', clearTimer);
        },

        //
        // TODO: document!
        // event handler for hovering the scrollbar-handle
        //
        hoverHandle: function(ev){
            if(ev.type == 'mouseenter'){
                $(this).addClass('hover');
            } else {
                $(this).removeClass('hover');
            }
        }
    };

    // ----- mousewheel event ---------------------------------------------------------------------

    $.event.special.mousewheel = {

        setup: function(){
            if (this.addEventListener){
                this.addEventListener('mousewheel', handler, false);
                this.addEventListener('DOMMouseScroll', handler, false);
            } else {
                this.onmousewheel = handler;
            }
        },

        teardown: function(){
            if (this.removeEventListener){
                this.removeEventListener('mousewheel', handler, false);
                this.removeEventListener('DOMMouseScroll', handler, false);
            } else {
                this.onmousewheel = null;
            }
        }
    };

    $.fn.extend({
        mousewheel: function(fn){
            return fn ? this.bind("mousewheel", fn) : this.trigger("mousewheel");
        },

        unmousewheel: function(fn){
            return this.unbind("mousewheel", fn);
        }
    });

    function handler(event) {
        var orgEvent = event || window.event,
            args = [].slice.call(arguments, 1),
            delta = 0,
            returnValue = true,
            deltaX = 0,
            deltaY = 0;

        event = $.event.fix(orgEvent);
        event.type = "mousewheel";

        // Old school scrollwheel delta
        if(event.wheelDelta){
            delta = event.wheelDelta / 120;
        }
        if(event.detail){
            delta = -event.detail / 3;
        }

        // Gecko
        if(orgEvent.axis !== undefined && orgEvent.axis === orgEvent.HORIZONTAL_AXIS){
            deltaY = 0;
            deltaX = -1 * delta;
        }

        // Webkit
        if(orgEvent.wheelDeltaY !== undefined){
            deltaY = orgEvent.wheelDeltaY / 120;
        }
        if(orgEvent.wheelDeltaX !== undefined){
            deltaX = -1 * orgEvent.wheelDeltaX / 120;
        }

        // Add event and delta to the front of the arguments
        args.unshift(event, delta, deltaX, deltaY);

        return $.event.handle.apply(this, args);
    }

})(jQuery);  // inject global jQuery object

<commit_msg>documentation of builder method<commit_after>/*
 * Scrollbar - a jQuery plugin for custom scrollbars
 *
 * @author     Thomas Duerr, me@thomd.net
 * @date       03.2010
 * @requires   jquery v1.4.2
 *
 *
 * Usage:
 * ======
 *
 *
 *
 *
 *
 *
 * CSS:
 * ====
 *
 *
 */
;(function($){

    $.fn.scrollbar = function(options){

        // Extend default options
        var options = $.extend({}, $.fn.scrollbar.defaults, options);

        //
        // append scrollbar to every found container and return jquery object for chainability
        //
        return this.each(function(i){

            var container = $(this),
                props = {};

            // determine container height
            props.containerHeight = container.height();

            // determine inner content height
            props.contentHeight = 0;
            container.children().each(function(){
                props.contentHeight += $(this).outerHeight();
            });

            // do nothing and return if a scrollbar is not neccessary
            if(props.contentHeight <= props.containerHeight) return true;

            // create scrollbar
            var scrollbar = new $.fn.scrollbar.Scrollbar(container, props, options);
        });
    }

    //
    // default options
    //
    $.fn.scrollbar.defaults = {
        handleMinHeight: 30,         // min-height of handle (height is actually dependent on content height)
        scrollSpeed: 100,       // TODO
        scrollStep: 10      // TODO
    };

    //
    // Scrollbar class properties
    //
    $.fn.scrollbar.Scrollbar = function(container, props, options){
        this.container = container;
        this.props =     props;
        this.opts =      options;
        this.mouse =     {};

        // initialize
        this.buildHtml();
        this.initHandle();
        this.appendEvents();
    };

    //
    // Scrollbar class methods
    //
    $.fn.scrollbar.Scrollbar.prototype = {

        //
        // build DOM nodes for pane and scroll-handle
        //
        //   from:
        //
        //
                             --> arbitrary element with class="scrollbar"
        //          [...]
        //

        //
        //   to:
        //
        //
                             --> this.container
        //
                    --> this.pane
        //              [...]
        //

        //
        --> this.handleContainer
        //

        --> this.handle
        //

        //

         --> this.handleArrows
        //

       --> this.handleArrows
        //

        //
        buildHtml: function(){

            // set some necessary CSS attributes
console.log(this.container.css('position'));
            var position = this.container.css('position') == 'absolute' ? 'absolute' : 'relative';
            this.container.css({'overflow': 'hidden', 'position': position});

            // build some DOM nodes
            this.container.children().wrapAll('
');
            this.container.append('

')
                          .append('
')
                          .append('
');

            // set scrollbar properties
            this.pane =            this.container.find('.scrollbar-pane');
            this.handle =          this.container.find('.scrollbar-handle');
            this.handleContainer = this.container.find('.scrollbar-handle-container');
            this.handleArrows =    this.container.find('.scrollbar-handle-up, .scrollbar-handle-down');
        },

        //
        // append events on handle and handle-container
        //
        appendEvents: function(){

            // append hover event on scrollbar-handle
            this.handle.bind('mouseenter mouseleave', this.hoverHandle);

            // append hover event on scrollbar-arrows
            this.handleArrows.bind('mouseenter mouseleave', this.hoverHandle);

            // append drag-drop event on scrollbar-handle
            this.handle.bind('mousedown.handle', $.proxy(this, 'startOfHandleMove'));

            // append click event on scrollbar-handle-container
            this.handleContainer.bind('mousedown.handle', $.proxy(this, 'clickHandleContainer'));

            // append click event on scrollbar-up- and scrollbar-down-handles
            this.handleArrows.bind('mousedown.arrows', $.proxy(this, 'clickHandleArrows'));

            // appen mousewheel event on content container
            this.container.bind('mousewheel.container', $.proxy(this, 'onMouseWheel'));
        },

        //
        // calculate height of handle.
        // height of handle should indicate height of content.
        //
        initHandle: function(){
            this.props.handleContainerHeight = this.handleContainer.height();

            // we need to calculate content-height again: due to the added scrollbar, the width decreased - hence the height increased!
            var contentHeight = 0;
            this.pane.children().each(function(){
                contentHeight += $(this).outerHeight();
            });
            this.props.contentHeight = contentHeight;

            this.props.handleHeight = Math.max(this.props.containerHeight * this.props.handleContainerHeight / this.props.contentHeight, this.opts.handleMinHeight);
            this.props.handleTop = {
                min: 0,
                max: this.props.handleContainerHeight - this.props.handleHeight
            };
            this.handle.height(this.props.handleHeight);
            this.handle.top = 0;
            this.handleContentRatio = (this.props.contentHeight - this.props.containerHeight) / (this.props.handleContainerHeight - this.props.handleHeight);
        },

        //
        // get mouse position helper
        //
        mousePosition: function(ev) {
			return ev.pageY || (ev.clientY + (document.documentElement.scrollTop || document.body.scrollTop)) || 0;
		},

        // ---------- event handler ---------------------------------------------------------------

        //
        // start moving of handle
        //
        startOfHandleMove: function(ev){
            ev.preventDefault();

            // set start top-position of mouse
            this.mouse.top = this.mousePosition(ev);

            // bind mousemove- and mouseout-event to document (binding it to document allows having a mousepointer outside handle while moving)
    		$(document).bind('mousemove.handle', $.proxy(this, 'onHandleMove')).bind('mouseup.handle', $.proxy(this, 'endOfHandleMove'));

            // remove hover event on scrollbar-arrows (until end of handle move)
    		this.handleArrows.unbind('mouseenter mouseleave', this.hoverHandle);

    		// add class for visual change while moving handle
            this.handle.addClass('move');
        },

        //
        // on moving of handle
        //
        onHandleMove: function(ev){

            var mousePos = this.mousePosition(ev);

            // calculate distance since last fireing of this handler
            var delta = mousePos - this.mouse.top;
//console.log(delta);
            this.mouse.top = mousePos;

            // calculate new handle position
            this.handle.top += delta;

            // update positions
            this.setHandlePosition(this.handle.top);
            this.setContentPosition();

        },

        //
        // end moving of handle
        //
        endOfHandleMove: function(ev){

            // remove handle events
    		$(document).unbind('.handle');

            // re-attach hover event on scrollbar-arrows
            this.handleArrows.bind('mouseenter mouseleave', this.hoverHandle);

            // remove class for visual change
            this.handle.removeClass('move');
        },

        //
        // set position of handle
        //
        setHandlePosition: function(delta){

            // stay within range [handleTop.min, handleTop.max]
            this.handle.top = (this.handle.top > this.props.handleTop.max) ? this.props.handleTop.max : this.handle.top;
            this.handle.top = (this.handle.top < this.props.handleTop.min) ? this.props.handleTop.min : this.handle.top;

            this.handle[0].style.top = this.handle.top + 'px';
        },

        //
        // set position of content
        //
        setContentPosition: function(){

            // derive position of content from position of handle
            this.pane.top = -1 * this.handleContentRatio * this.handle.top;

            this.pane[0].style.top = this.pane.top + 'px';
        },

        //
        // mouse wheel movement
        //
        onMouseWheel: function(ev, delta){

            // calculate new handle position
            this.handle.top -= delta;

            this.setHandlePosition(this.handle.top);
            this.setContentPosition();
        },

        //
        // TODO: document!
        // append click handler on handle-container (to click up and down the handle)
        //
        clickHandleContainer: function(ev){
            ev.preventDefault();
            if(!$(ev.target).hasClass('scrollbar-handle-container')) return false;

            var direction = (this.handle.offset().top < this.mousePosition(ev)) ? 1 : -1;
            this.handle.start = this.handle.top = direction == 1 ? this.handle.top + (this.props.handleTop.max - this.handle.top) * 0.5 : this.handle.top - (this.handle.top - this.props.handleTop.min) * 0.5;
            this.handle[0].style.top = this.handle.top + 'px';
            this.setContentPosition();
        },

        //
        // TODO: document!
        // append click handler on handle-arrows
        //
        clickHandleArrows: function(ev){
            ev.preventDefault();
            var direction = $(ev.target).hasClass('scrollbar-handle-up') ? -1 : 1;

            var timer = setInterval($.proxy(function(){
                this.handle.start = this.handle.top = direction == 1 ? Math.min(this.handle.top + this.opts.scrollStep, this.props.handleTop.max) : Math.max(this.handle.top - this.opts.scrollStep, this.props.handleTop.min);
                this.handle[0].style.top = this.handle.top + 'px';
                this.setContentPosition();
            }, this), this.opts.scrollSpeed);

    		var clearTimer = function(){
    		    clearInterval(timer);
        		$(document).unbind('mouseup.arrows', clearTimer);
    		}
    		$(document).bind('mouseup.arrows', clearTimer);
        },

        //
        // TODO: document!
        // event handler for hovering the scrollbar-handle
        //
        hoverHandle: function(ev){
            if(ev.type == 'mouseenter'){
                $(this).addClass('hover');
            } else {
                $(this).removeClass('hover');
            }
        }
    };

    // ----- mousewheel event ---------------------------------------------------------------------

    $.event.special.mousewheel = {

        setup: function(){
            if (this.addEventListener){
                this.addEventListener('mousewheel', handler, false);
                this.addEventListener('DOMMouseScroll', handler, false);
            } else {
                this.onmousewheel = handler;
            }
        },

        teardown: function(){
            if (this.removeEventListener){
                this.removeEventListener('mousewheel', handler, false);
                this.removeEventListener('DOMMouseScroll', handler, false);
            } else {
                this.onmousewheel = null;
            }
        }
    };

    $.fn.extend({
        mousewheel: function(fn){
            return fn ? this.bind("mousewheel", fn) : this.trigger("mousewheel");
        },

        unmousewheel: function(fn){
            return this.unbind("mousewheel", fn);
        }
    });

    function handler(event) {
        var orgEvent = event || window.event,
            args = [].slice.call(arguments, 1),
            delta = 0,
            returnValue = true,
            deltaX = 0,
            deltaY = 0;

        event = $.event.fix(orgEvent);
        event.type = "mousewheel";

        // Old school scrollwheel delta
        if(event.wheelDelta){
            delta = event.wheelDelta / 120;
        }
        if(event.detail){
            delta = -event.detail / 3;
        }

        // Gecko
        if(orgEvent.axis !== undefined && orgEvent.axis === orgEvent.HORIZONTAL_AXIS){
            deltaY = 0;
            deltaX = -1 * delta;
        }

        // Webkit
        if(orgEvent.wheelDeltaY !== undefined){
            deltaY = orgEvent.wheelDeltaY / 120;
        }
        if(orgEvent.wheelDeltaX !== undefined){
            deltaX = -1 * orgEvent.wheelDeltaX / 120;
        }

        // Add event and delta to the front of the arguments
        args.unshift(event, delta, deltaX, deltaY);

        return $.event.handle.apply(this, args);
    }

})(jQuery);  // inject global jQuery object

<|endoftext|>