<commit_before>/*
 * Copyright (c) 2015 Dickson Tam
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 *
 */

(function() {

  function Tokenizer() {

  }

  function TOKENIZER_OVERRIDE_NOT_IMPLEMENTED(fxname) {
    Tokenizer.prototype[fxname] = function() {
      throw new Error('retoken: Function ' + fxname + ' not yet implemented in tokenizer');
    }
  }

  TOKENIZER_OVERRIDE_NOT_IMPLEMENTED('splice');
  TOKENIZER_OVERRIDE_NOT_IMPLEMENTED('sort');
  TOKENIZER_OVERRIDE_NOT_IMPLEMENTED('reverse');

  Tokenizer.prototype.__proto__ = Array.prototype

  Tokenizer.prototype.extract = function(times) {
    times = times || 1;

    if (this.length < 1)  return this;

    var matches = this[this.length - 1].match(this.regex);
    var newToken, rest;

    if (matches) {
      if (this.opts.preserveDelimiter) {
        if (matches[1].length > 0) {
          newToken = matches[1];
          rest = matches[2] + matches[matches.length - 1];
        } else {
          newToken = matches[1] + matches[2];
          rest = matches[matches.length - 1];
        }
        this.delimiters.splice(this.delimiters.length - 2, 0, '');
      } else {
        newToken = matches[1];
        this.delimiters.splice(this.delimiters.length - 2, 0, matches[2]);
        rest = matches[3];
      }

      this[this.length - 1] = newToken;
      this[this.length] = rest;
    }

    return (times > 1)? this.extract(times - 1) : this;
  }

  Tokenizer.prototype.push = function(str, delimiter) {
    Array.prototype.push.call(this, str);
    this.delimiters.push(delimiter || '');
    return this;
  }

  Tokenizer.prototype.unshift = function(str, delimiter) {
    Array.prototype.unshift.call(this, str);
    this.delimiters.unshift(delimiter || '');
    return this;
  }

  Tokenizer.prototype.shift = function() {
    var elem = Array.prototype.shift.call(this);
    this.delimiters.shift();
    return elem;
  }

  Tokenizer.prototype.pop = function() {
    var elem = Array.prototype.pop.call(this);
    this.delimiters.pop();
    return elem;
  }

  Tokenizer.prototype.retract = function(times) {
    times = times || 1;
    if (this.length < 2) return;

    var delimiter = this.delimiters.splice(this.delimiters.length - 2, 1)[0] || '';

    if (typeof(this[this.length - 2]) != 'string' || typeof(this[this.length - 1]) != 'string')
      throw new Error('retoken: Tried to retract a token that is not a string');

    if (typeof(delimiter) != 'string')
      throw new Error('retoken: Tried to retract a delmiter that is not a string');

    this[this.length - 2] += delimiter + this[this.length - 1];
    this.pop();

    return (times > 1)? this.retract(times - 1) : this;
  }

  Tokenizer.prototype.replaceToken = function(token, replacement) {
     var index = this.indexOf(token);
     if (!~index) return this;

     this[index] = replacement;

     return this;
  }

  function tokenizer(delimiter, opts) {
    var instance = [ ];

    instance.__proto__ = Tokenizer.prototype;

    instance.opts = opts || {};
    instance.delimiters = [];

    var reCore;

    if (delimiter instanceof RegExp) {
      reCore = delimiter.source.replace('/^\^/', '').replace('/\$$/', '');
    } else {
      reCore = delimiter.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
    }

    instance.regex = new RegExp('^(.*?)(' + reCore + ')(.*)$');

    return instance;
  }

  if (typeof module !== 'undefined' && module && module.exports) { // Node.js & CommonJS
    module.exports = tokenizer;
  } else if (typeof define === 'function' && define.amd) {
    define('retoken', [], function() {
      return tokenizer;
    });
  } else { // Browser
    window.retoken = tokenizer;
  }

})();
<commit_msg>Changed regex to read-only property.<commit_after>/*
 * Copyright (c) 2015 Dickson Tam
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 *
 */

(function() {

  function Tokenizer() {

  }

  function TOKENIZER_OVERRIDE_NOT_IMPLEMENTED(fxname) {
    Tokenizer.prototype[fxname] = function() {
      throw new Error('retoken: Function ' + fxname + ' not yet implemented in tokenizer');
    }
  }

  TOKENIZER_OVERRIDE_NOT_IMPLEMENTED('splice');
  TOKENIZER_OVERRIDE_NOT_IMPLEMENTED('sort');
  TOKENIZER_OVERRIDE_NOT_IMPLEMENTED('reverse');

  Tokenizer.prototype.__proto__ = Array.prototype

  Tokenizer.prototype.extract = function(times) {
    times = times || 1;

    if (this.length < 1)  return this;

    var matches = this[this.length - 1].match(this.regex);
    var newToken, rest;

    if (matches) {
      if (this.opts.preserveDelimiter) {
        if (matches[1].length > 0) {
          newToken = matches[1];
          rest = matches[2] + matches[matches.length - 1];
        } else {
          newToken = matches[1] + matches[2];
          rest = matches[matches.length - 1];
        }
        this.delimiters.splice(this.delimiters.length - 2, 0, '');
      } else {
        newToken = matches[1];
        this.delimiters.splice(this.delimiters.length - 2, 0, matches[2]);
        rest = matches[3];
      }

      this[this.length - 1] = newToken;
      this[this.length] = rest;
    }

    return (times > 1)? this.extract(times - 1) : this;
  }

  Tokenizer.prototype.push = function(str, delimiter) {
    Array.prototype.push.call(this, str);
    this.delimiters.push(delimiter || '');
    return this;
  }

  Tokenizer.prototype.unshift = function(str, delimiter) {
    Array.prototype.unshift.call(this, str);
    this.delimiters.unshift(delimiter || '');
    return this;
  }

  Tokenizer.prototype.shift = function() {
    var elem = Array.prototype.shift.call(this);
    this.delimiters.shift();
    return elem;
  }

  Tokenizer.prototype.pop = function() {
    var elem = Array.prototype.pop.call(this);
    this.delimiters.pop();
    return elem;
  }

  Tokenizer.prototype.retract = function(times) {
    times = times || 1;
    if (this.length < 2) return;

    var delimiter = this.delimiters.splice(this.delimiters.length - 2, 1)[0] || '';

    if (typeof(this[this.length - 2]) != 'string' || typeof(this[this.length - 1]) != 'string')
      throw new Error('retoken: Tried to retract a token that is not a string');

    if (typeof(delimiter) != 'string')
      throw new Error('retoken: Tried to retract a delmiter that is not a string');

    this[this.length - 2] += delimiter + this[this.length - 1];
    this.pop();

    return (times > 1)? this.retract(times - 1) : this;
  }

  Tokenizer.prototype.replaceToken = function(token, replacement) {
     var index = this.indexOf(token);
     if (!~index) return this;

     this[index] = replacement;

     return this;
  }

  function tokenizer(delimiter, opts) {
    var instance = [ ];

    instance.__proto__ = Tokenizer.prototype;

    instance.opts = opts || {};
    instance.delimiters = [];

    var reCore;

    if (delimiter instanceof RegExp) {
      reCore = delimiter.source.replace('/^\^/', '').replace('/\$$/', '');
    } else {
      reCore = delimiter.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
    }

    Object.defineProperty(instance, 'regex', {
      value: new RegExp('^(.*?)(' + reCore + ')(.*)$'),
      enumerable: true
    });

    return instance;
  }

  if (typeof module !== 'undefined' && module && module.exports) { // Node.js & CommonJS
    module.exports = tokenizer;
  } else if (typeof define === 'function' && define.amd) {
    define('retoken', [], function() {
      return tokenizer;
    });
  } else { // Browser
    window.retoken = tokenizer;
  }

})();
<|endoftext|>


<commit_before>var taskList = [];

function TKTask(title, hours, minutes) {
    this.title = title;
    this.hours = hours;
    this.minutes = minutes;
    this.uid = generateUID();
}

function generateUID(){
    var d = new Date().getTime();
    var uid = 'txxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
        var r = (d + Math.random()*16)%16 | 0;
        d = Math.floor(d/16);
        return (c=='x' ? r : (r&0x7|0x8)).toString(16);
    });
    return uid;
};

function zeroPad(n, p, c) {
    var pad_char = typeof c !== 'undefined' ? c : '0';
    var pad = new Array(1 + p).join(pad_char);
    return (pad + n).slice(-pad.length);
}

function redrawTaskList() {
    $('#taskList ul').empty();
    for (var i = taskList.length; i-->0; ) {
        var currentTask = taskList[i];
        var task = "

" + currentTask.title + "

" + zeroPad(currentTask.hours, 2) + ":" + zeroPad(currentTask.minutes, 2) + "

";
        $('#taskList ul').append(task).listview('refresh');
    }
}

// Array Remove - By John Resig (MIT Licensed)
Array.prototype.remove = function(from, to) {
  var rest = this.slice((to || from) + 1 || this.length);
  this.length = from < 0 ? this.length + from : from;
  return this.push.apply(this, rest);
};

function removeTaskFromList(uid) {
    for (var i = taskList.length; i-->0; ) {
        var currentTask = taskList[i];
        if(currentTask.uid == uid) {
            taskList.remove(i);
            break;
        }
    }
    window.localStorage.setItem('taskList', JSON.stringify(taskList));
    redrawTaskList();
}

$(document).ready( function () {
    $('#addTaskOk').click(function () {
        var title = $('#addTaskTitle').val();
        var hours = parseInt($('#addTaskDurationH').val(), 10) || 0;
        var minutes = parseInt($('#addTaskDurationM').val(), 10) || 0;
        var newTask = new TKTask(title, hours, minutes);
        taskList.push(newTask);
        window.localStorage.setItem('taskList', JSON.stringify(taskList));
        $('#addTaskTitle').val("");
        $('#addTaskDurationH').val("");
        $('#addTaskDurationM').val("");
        redrawTaskList();
    });

    $('#listClearButton').click(function () {
        taskList = [];
        window.localStorage.removeItem('taskList');
        $('#taskList ul').empty();
    });

    taskList = JSON.parse(window.localStorage.getItem('taskList')) || [];
    if (taskList.length != 0) {
        redrawTaskList();
    }
});

<commit_msg>added separator by date between tasks<commit_after>var taskList = [];

function TKTask(title, hours, minutes) {
    this.title = title;
    this.hours = hours;
    this.minutes = minutes;
    this.uid = generateUID();
    this.dateAdded = Date(); // when de-serialized it will return a string anyway
}

function isTaskInDateRange(task, dateFrom, dateTo) {
    // set all hours to zero to compare only year, month and day
    currentTaskDate = new Date(task.dateAdded);
    currentTaskDate.setHour(0, 0, 0, 0);
    normalizedDateFrom = new Date(dateFrom);
    normalizedDateTo = new Date(dateTo);
    normalizedDateFrom.setHour(0, 0, 0, 0);
    normalizedDateTo.setHour(0, 0, 0, 0);
    return currentTaskDate >= normalizedDateFrom && currentTaskDate <= normalizedDateTo;
}

function generateUID(){
    var d = new Date().getTime();
    var uid = 'txxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
        var r = (d + Math.random()*16)%16 | 0;
        d = Math.floor(d/16);
        return (c=='x' ? r : (r&0x7|0x8)).toString(16);
    });
    return uid;
};

function zeroPad(n, p, c) {
    var pad_char = typeof c !== 'undefined' ? c : '0';
    var pad = new Array(1 + p).join(pad_char);
    return (pad + n).slice(-pad.length);
}

function redrawTaskList() {
    $('#taskList ul').empty();
    var firstElement = true;
    for (var i = taskList.length; i-->0; ) {
        var currentTask = taskList[i];
        var currentTaskNormDate = new Date(taskList[i].dateAdded);
        currentTaskNormDate.setHours(0, 0, 0, 0);
        if(!firstElement) {
            var previousTaskNormDate = new Date(taskList[i+1].dateAdded);
            previousTaskNormDate.setHours(0, 0, 0, 0);
            if (currentTaskNormDate.getTime() != previousTaskNormDate.getTime()) {
                addDateSeparator($('#taskList ul'), currentTaskNormDate);
            }
        } else {
            firstElement = false;
            addDateSeparator($('#taskList ul'), currentTaskNormDate);
        }
        var task = "

" + currentTask.title + "

" + zeroPad(currentTask.hours, 2) + ":" + zeroPad(currentTask.minutes, 2) + "

";
        $('#taskList ul').append(task).listview('refresh');
    }
}

var monthNames = [ "January", "February", "March", "April", "May", "June",
    "July", "August", "September", "October", "November", "December" ];

function addDateSeparator(listView, date) {
    var divider = "
" +
        monthNames[date.getMonth()] + " " +
        date.getDate() + " " +
        date.getFullYear() + "
";
    listView.append(divider);
}

// Array Remove - By John Resig (MIT Licensed)
Array.prototype.remove = function(from, to) {
  var rest = this.slice((to || from) + 1 || this.length);
  this.length = from < 0 ? this.length + from : from;
  return this.push.apply(this, rest);
};

function removeTaskFromList(uid) {
    for (var i = taskList.length; i-->0; ) {
        var currentTask = taskList[i];
        if(currentTask.uid == uid) {
            taskList.remove(i);
            break;
        }
    }
    window.localStorage.setItem('taskList', JSON.stringify(taskList));
    redrawTaskList();
}

$(document).ready( function () {
    $('#addTaskOk').click(function () {
        var title = $('#addTaskTitle').val();
        var hours = parseInt($('#addTaskDurationH').val(), 10) || 0;
        var minutes = parseInt($('#addTaskDurationM').val(), 10) || 0;
        var newTask = new TKTask(title, hours, minutes);
        taskList.push(newTask);
        window.localStorage.setItem('taskList', JSON.stringify(taskList));
        $('#addTaskTitle').val("");
        $('#addTaskDurationH').val("");
        $('#addTaskDurationM').val("");
        redrawTaskList();
    });

    $('#listClearButton').click(function () {
        taskList = [];
        window.localStorage.removeItem('taskList');
        $('#taskList ul').empty();
    });

    taskList = JSON.parse(window.localStorage.getItem('taskList')) || [];
    if (taskList.length != 0) {
        redrawTaskList();
    }
});

<|endoftext|>


<commit_before>(function($) {

    window.elrFlexibleGrid = function(params) {
        var self = {};
        var spec = params || {};
        var gridClass = spec.gridClass || 'elr-flexible-grid';
        var imagesPerRow = spec.imagesPerRow || 4;
        var flex = spec.flex || true;
        var $grid = $('.' + gridClass);
        var tags;

        var addFilterButtons = function(tags) {

            $.each(tags, function(k, v) {
                var $tagButton = $('<button></button>', {
                    'class': 'elr-grid-filter',
                    text: elr.captitalize(v),
                    'data-filter': v
                });

                $tagButton.appendTo($gridNav);
            });

            $gridNav.find('.elr-grid-filter').first().addClass('active');
        };

        var resizeOverlay = function() {
            var overlay = $grid.find('.overlay');

            $.each(overlay, function(k, v) {
                var $that = $(v),
                    imageHeight = $that.parent('.elr-grid-item').find('img').outerHeight(true);

                $that.height(imageHeight).hide();
            });
        };

        var addListItems = function($items) {
            $grid.empty();
            $items.appendTo($grid).hide(0, function() {
                positionListItems($items);
            });
        };

        var positionListItems = function($items) {
            var resizeHolder = function($items) {
                var tallestColumn = 0;
                var columnHeights = [];

                for (var i = 0; i < imagesPerRow; i++) {
                    columnHeights.push(0);
                }

                $.each($items, function(k, v) {
                    var $that = $(v);
                    var columnNum = $that.data('column');
                    var height = $that.outerHeight(true);

                    return columnHeights[columnNum] += height;
                });

                $.each(columnHeights, function(k, v) {
                    if (v > tallestColumn) {
                        tallestColumn = v;

                        return tallestColumn;
                    }
                });

                $grid.css({'height': tallestColumn + 40});
            };

            $.each($items, function(k, v) {
                var $that = $(v);
                var index = k + 1;
                var columnNum = ((index % imagesPerRow) === 0) ? imagesPerRow - 1 : ((index % imagesPerRow) - 1);
                var $prevImage = (index > imagesPerRow) ? $grid.find('.elr-grid-item').eq(index - (imagesPerRow + 1)) : null;

                $that.attr('data-column', columnNum);
                $that.attr('data_num', index);

                if ((typeof $prevImage !== 'undefined') && ($prevImage !== null)) {
                    var margin = $prevImage.outerWidth(true) - $prevImage.outerWidth(false),
                        top = (index < ((self.imagesPerRow * 2) + 1)) ? $prevImage.outerHeight(false) + margin : $prevImage.outerHeight(false) + margin + $prevImage.position().top;
                        left = ($prevImage.outerWidth(false) * columnNum) + (margin * columnNum);

                    $that.css({'top' : top, 'left' : left, 'position': 'absolute'});
                } else {
                    $that.css({'top' : 0, 'left' : 0, 'position': 'relative'});
                }

                $that.show();
            });

            resizeHolder($items);
        };

        var filterListItems = function(filter, $items) {
            // filter list items by tag
            var $filteredItems;

            filter = (window.location.hash) ? filter : 'all';

            window.location.hash = filter;

            if ( ( $.inArray( filter, tags ) !== -1 ) || filter === 'all' ) {
                if ( filter === 'all' ) {
                    $filteredItems = $items;
                } else {
                    $filteredItems = $items.has('ul.caption-tags li:containsNC(' + filter + ')');
                }

                addListItems($filteredItems);
            } else {
                $('

', {
                    text: 'no items match'
                }).appendTo($grid);
            }
        };

        if ( $grid.length ) {
            var hash = window.location.hash;

            $gridNav = $('.elr-grid-nav');
            $items = $grid.find('.elr-grid-item').hide();

            $(window).load(function() {
                var filter = hash ? hash.replace(/^#/, '') : null;

                tags = elr.toArray($grid.find('ul.caption-tags li'), true);

                addFilterButtons(tags);
                filterListItems(filter, $items);

                if ( typeof filter !== 'undefined' ) {
                    var $activeButton = $('button.elr-grid-filter[data-filter=' + filter + ']');

                    $activeButton.siblings('button').removeClass('active');
                    $activeButton.addClass('active');
                }
            });

            if ( flex === true ) {
                $(window).resize(function() {
                    positionListItems($items);
                    resizeOverlay();
                });
            }

            $(window).load(resizeOverlay);

            $grid.on('mouseenter', '.elr-grid-item', function() {
                $(this).find('.overlay').stop().fadeIn('fast');
            });

            $grid.on('mouseleave', '.elr-grid-item', function() {
                $(this).find('.overlay').stop().fadeOut('fast');
            });

            $gridNav.on('click', 'button.elr-grid-filter', function(e) {
                var $that = $(this);
                var filter = $that.data('filter').toLowerCase();

                filterListItems(filter, $items);
                $that.siblings('button').removeClass('active');
                $that.addClass('active');
                e.preventDefault();
            });

            $grid.on('click', '.caption-tags li', function(e) {
                var filter = $(this).data('filter').toLowerCase();

                filterListItems(filter, $items);
                $button = $gridNav.find('button[data-filter=' + filter + ']');
                $button.siblings('button').removeClass('active');
                $button.addClass('active');
                e.preventDefault();
            });
        }

        return self;
    };
})(jQuery);<commit_msg>bug fix<commit_after>(function($) {

    window.elrFlexibleGrid = function(params) {
        var self = {};
        var spec = params || {};
        var gridClass = spec.gridClass || 'elr-flexible-grid';
        var imagesPerRow = spec.imagesPerRow || 4;
        var flex = spec.flex || true;
        var $grid = $('.' + gridClass);
        var tags;

        var addFilterButtons = function(tags) {

            $.each(tags, function(k, v) {
                var $tagButton = $('<button></button>', {
                    'class': 'elr-grid-filter',
                    text: elr.capitalize(v),
                    'data-filter': v
                });

                $tagButton.appendTo($gridNav);
            });

            $gridNav.find('.elr-grid-filter').first().addClass('active');
        };

        var resizeOverlay = function() {
            var overlay = $grid.find('.overlay');

            $.each(overlay, function(k, v) {
                var $that = $(v),
                    imageHeight = $that.parent('.elr-grid-item').find('img').outerHeight(true);

                $that.height(imageHeight).hide();
            });
        };

        var addListItems = function($items) {
            $grid.empty();
            $items.appendTo($grid).hide(0, function() {
                positionListItems($items);
            });
        };

        var positionListItems = function($items) {
            var resizeHolder = function($items) {
                var tallestColumn = 0;
                var columnHeights = [];

                for (var i = 0; i < imagesPerRow; i++) {
                    columnHeights.push(0);
                }

                $.each($items, function(k, v) {
                    var $that = $(v);
                    var columnNum = $that.data('column');
                    var height = $that.outerHeight(true);

                    return columnHeights[columnNum] += height;
                });

                $.each(columnHeights, function(k, v) {
                    if (v > tallestColumn) {
                        tallestColumn = v;

                        return tallestColumn;
                    }
                });

                $grid.css({'height': tallestColumn + 40});
            };

            $.each($items, function(k, v) {
                var $that = $(v);
                var index = k + 1;
                var columnNum = ((index % imagesPerRow) === 0) ? imagesPerRow - 1 : ((index % imagesPerRow) - 1);
                var $prevImage = (index > imagesPerRow) ? $grid.find('.elr-grid-item').eq(index - (imagesPerRow + 1)) : null;

                $that.attr('data-column', columnNum);
                $that.attr('data_num', index);

                if ((typeof $prevImage !== 'undefined') && ($prevImage !== null)) {
                    var margin = $prevImage.outerWidth(true) - $prevImage.outerWidth(false),
                        top = (index < ((self.imagesPerRow * 2) + 1)) ? $prevImage.outerHeight(false) + margin : $prevImage.outerHeight(false) + margin + $prevImage.position().top;
                        left = ($prevImage.outerWidth(false) * columnNum) + (margin * columnNum);

                    $that.css({'top' : top, 'left' : left, 'position': 'absolute'});
                } else {
                    $that.css({'top' : 0, 'left' : 0, 'position': 'relative'});
                }

                $that.show();
            });

            resizeHolder($items);
        };

        var filterListItems = function(filter, $items) {
            // filter list items by tag
            var $filteredItems;

            filter = (window.location.hash) ? filter : 'all';

            window.location.hash = filter;

            if ( ( $.inArray( filter, tags ) !== -1 ) || filter === 'all' ) {
                if ( filter === 'all' ) {
                    $filteredItems = $items;
                } else {
                    $filteredItems = $items.has('ul.caption-tags li:containsNC(' + filter + ')');
                }

                addListItems($filteredItems);
            } else {
                $('

', {
                    text: 'no items match'
                }).appendTo($grid);
            }
        };

        if ( $grid.length ) {
            var hash = window.location.hash;

            $gridNav = $('.elr-grid-nav');
            $items = $grid.find('.elr-grid-item').hide();

            $(window).load(function() {
                var filter = hash ? hash.replace(/^#/, '') : null;

                tags = elr.toArray($grid.find('ul.caption-tags li'), true);

                addFilterButtons(tags);
                filterListItems(filter, $items);

                if ( typeof filter !== 'undefined' ) {
                    var $activeButton = $('button.elr-grid-filter[data-filter=' + filter + ']');

                    $activeButton.siblings('button').removeClass('active');
                    $activeButton.addClass('active');
                }
            });

            if ( flex === true ) {
                $(window).resize(function() {
                    positionListItems($items);
                    resizeOverlay();
                });
            }

            $(window).load(resizeOverlay);

            $grid.on('mouseenter', '.elr-grid-item', function() {
                $(this).find('.overlay').stop().fadeIn('fast');
            });

            $grid.on('mouseleave', '.elr-grid-item', function() {
                $(this).find('.overlay').stop().fadeOut('fast');
            });

            $gridNav.on('click', 'button.elr-grid-filter', function(e) {
                var $that = $(this);
                var filter = $that.data('filter').toLowerCase();

                filterListItems(filter, $items);
                $that.siblings('button').removeClass('active');
                $that.addClass('active');
                e.preventDefault();
            });

            $grid.on('click', '.caption-tags li', function(e) {
                var filter = $(this).data('filter').toLowerCase();

                filterListItems(filter, $items);
                $button = $gridNav.find('button[data-filter=' + filter + ']');
                $button.siblings('button').removeClass('active');
                $button.addClass('active');
                e.preventDefault();
            });
        }

        return self;
    };
})(jQuery);<|endoftext|>


<commit_before>////////////////////////////////////////////////
/*   Provided Code - Please Don't Edit   */
////////////////////////////////////////////////
'use strict';

function getInput() {
    console.log("Please choose either 'rock', 'paper', or 'scissors'.")
    return prompt();
}
function randomPlay() {
    var randomNumber = Math.random();
    if (randomNumber < 0.33) {
        return "rock";
    } else if (randomNumber < 0.66) {
        return "paper";
    } else {
        return "scissors";
    }
}
////////////////////////////////////////////////
/*           Write Your Code Below            */
////////////////////////////////////////////////

function getPlayerMove(move) {
    // Write an expression that operates on a variable called `move`
    // If a `move` has a value, your expression should evaluate to that value.
    // However, if `move` is not specified / is null, your expression should equal `getInput()`.
    return move || getInput();
}

function getComputerMove(move) {
    // Write an expression that operates on a variable called `move`
    // If a `move` has a value, your expression should evaluate to that value.
    // However, if `move` is not specified / is null, your expression should equal `randomPlay()`.
    return move || randomPlay();
}

function getWinner(playerMove,computerMove) {
    var winner;
    // Write code that will set winner to either 'player', 'computer', or 'tie' based on the values of playerMove and computerMove.
    // Assume that the only values playerMove and computerMove can have are 'rock', 'paper', and 'scissors'.
    // The rules of the game are that 'rock' beats 'scissors', 'scissors' beats 'paper', and 'paper' beats 'rock'.
    /* YOUR CODE HERE */
    return winner;
}

function playToFive() {
    console.log("Let's play Rock, Paper, Scissors");
    var playerWins = 0;
    var computerWins = 0;
    // Write code that plays 'Rock, Paper, Scissors' until either the player or the computer has won five times.
    /* YOUR CODE HERE */
    return [playerWins, computerWins];
}

<commit_msg>Added if/else statement to getWinner<commit_after>////////////////////////////////////////////////
/*   Provided Code - Please Don't Edit   */
////////////////////////////////////////////////
'use strict';

function getInput() {
    console.log("Please choose either 'rock', 'paper', or 'scissors'.")
    return prompt();
}
function randomPlay() {
    var randomNumber = Math.random();
    if (randomNumber < 0.33) {
        return "rock";
    } else if (randomNumber < 0.66) {
        return "paper";
    } else {
        return "scissors";
    }
}
////////////////////////////////////////////////
/*           Write Your Code Below            */
////////////////////////////////////////////////

function getPlayerMove(move) {
    // Write an expression that operates on a variable called `move`
    // If a `move` has a value, your expression should evaluate to that value.
    // However, if `move` is not specified / is null, your expression should equal `getInput()`.
    var move;
    return move || getInput();
}

function getComputerMove(move) {
    // Write an expression that operates on a variable called `move`
    // If a `move` has a value, your expression should evaluate to that value.
    // However, if `move` is not specified / is null, your expression should equal `randomPlay()`.
    var move;
    return move || randomPlay();
}

function getWinner(playerMove,computerMove) {
    var winner;
    // Write code that will set winner to either 'player', 'computer', or 'tie' based on the values of playerMove and computerMove.
    // Assume that the only values playerMove and computerMove can have are 'rock', 'paper', and 'scissors'.
    // The rules of the game are that 'rock' beats 'scissors', 'scissors' beats 'paper', and 'paper' beats 'rock'.
    /* YOUR CODE HERE */
    if(playerMove==="scissors"&&computerMove==="scissors"){
        winner = "tie";
    }
    else if(playerMove==="scissors"&&computerMove==="rock"){
        winner = "computer";
    }
    else if(playerMove==="scissors"&&computerMove==="paper"){
        winner = "player";
    }
    else if(playerMove==="rock"&&computerMove==="rock"){
        winner = "tie";
    }
    else if(playerMove==="rock"&&computerMove==="scissors"){
        winner = "player";
    }
    else if(playerMove==="rock"&&computerMove==="paper"){
        winner = "computer";
    }
    else if(playerMove==="paper"&&computerMove==="paper"){
        winner = "tie";
    }
    else if(playerMove==="paper"&&computerMove==="rock"){
        winner = "player";
    }
    else{
        winner = "computer";
    }

    return winner;
}

function playToFive() {
    console.log("Let's play Rock, Paper, Scissors");
    var playerWins = 0;
    var computerWins = 0;
    // Write code that plays 'Rock, Paper, Scissors' until either the player or the computer has won five times.
    /* YOUR CODE HERE */
    return [playerWins, computerWins];
}

<|endoftext|>


<commit_before>// Utility for creating objects in older browsers
if ( typeof Object.create !== 'function' )
{
  Object.create = function( obj )
  {
    function F() {}
    F.prototype = obj;
    return new F();
  };
}

/*!
 * jQuery panelSnap
 * Version 0.4.0
 *
 * Requires:
 * - jQuery 1.7.1 or higher (Works with the API changes from 1.9.1 too)
 * - jQuery scrollEvents.js (included in the package)
 *
 * https://github.com/guidobouman/jquery-panelsnap
 *
 * Copyright 2013, Guido Bouman
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * Date: Wed Feb 13 16:05:00 2013 +0100
 */
(function($, window, document, undefined)
{
  var pluginName = 'panelSnap';
  var storageName = 'plugin_' + pluginName;

  var pluginObject = {
    isMouseDown: false,
    isSnapping: false,
    scrollOffset: 0,

    init: function(options, container)
    {
      var self = this;

      self.$window = $(window);
      self.$document = $(document);

      self.container = container;
      self.$container = $(container);

      self.$eventContainer = self.$container;
      if(self.$container.is('body'))
      {
        self.$eventContainer = self.$document;
      }

      self.options = $.extend(true, {}, $.fn.panelSnap.options, options);

      self.bind();

      return self;
    },

    bind: function()
    {
      var self = this;

      self.bindProxied(self.$eventContainer, 'scrollstop', self.processScroll);
      self.bindProxied(self.$eventContainer, 'scrollstart', self.initScroll);
      self.bindProxied(self.$eventContainer, 'mousewheel', self.holdScroll);
      self.bindProxied(self.$eventContainer, 'mousedown', self.mouseDown);
      self.bindProxied(self.$eventContainer, 'mouseup', self.mouseUp);

      self.bindProxied(self.$window, 'resize', self.processScroll);

      if(self.options.$menu !== false)
      {
        self.bindProxied($('a', self.options.$menu), 'click', self.captureMenuClick);
      }
    },

    bindProxied: function($element, event, method)
    {
      var self = this;

      $element.on(event + self.options.namespace, $.proxy(function(e)
      {
        return method.call(self, e);
      }, self));
    },

    destroy: function()
    {
      var self = this;

      // Gotta love namespaced events!
      self.$eventContainer.off(self.options.namespace);

      self.$window.off(self.options.namespace);

      if(self.options.$menu !== false)
      {
        $('a', self.options.$menu).off(self.options.namespace);
      }

      self.$container.removeData(storageName);
    },

    captureMenuClick: function(e)
    {
      var self = this;

      var $target = $(self.options.panelSelector + '.' + $(e.currentTarget).data('panel'), self.$container);

      self.snapToPanel($target);

      return false;
    },

    holdScroll: function(e)
    {
      var self = this;

      self.$container.stop(true);
      self.isSnapping = false;
    },

    initScroll: function(e)
    {
      var self = this;

      // Deprecated, moved storage of scrollOffset to callback of panelSnap
      // self.scrollOffset = self.$container.scrollTop();
    },

    processScroll: function(e)
    {
      var self = this;

      if(self.isMouseDown)
      {
        self.$container.one('mouseup', self.processScroll);
        return;
      }

      if(self.isSnapping)
      {
        return;
      }

      var interval = self.$container.height();
      var offset = self.$container.scrollTop();
      var scrollDifference = offset - self.scrollOffset;
      var maxOffset = self.$container[0].scrollHeight - interval;

      if(
        scrollDifference === 0 ||
        offset < 0 ||
        offset > maxOffset
      )
      {
        return;
      }

      var child_number;
      if(scrollDifference < -self.options.directionThreshold && scrollDifference > -interval)
      {
        child_number = Math.floor(offset / interval);
      }
      else if(scrollDifference > self.options.directionThreshold && scrollDifference < interval)
      {
        child_number = Math.ceil(offset / interval);
      }
      else
      {
        child_number = Math.round(offset / interval);
      }

      child_number += 1;

      var $target = $(self.options.panelSelector + ':nth-of-type(' + child_number + ')', self.$container);

      self.snapToPanel($target);
    },

    mouseDown: function(e)
    {
      var self = this;

      self.isMouseDown = true;
    },

    mouseUp: function(e)
    {
      var self = this;

      self.isMouseDown = false;
    },

    snapToPanel: function($target)
    {
      var self = this;

      self.isSnapping = true;

      self.options.onSnapStart.call(self, $target);

      var scrollTarget = 0;
      if(self.$container.is('body'))
      {
        scrollTarget = $target.offset().top;
      }
      else
      {
        scrollTarget = self.$container.scrollTop() + $target.position().top;
      }

      self.$container.animate(
      {
        scrollTop: scrollTarget
      }, self.options.slideSpeed, function()
      {
        self.scrollOffset = scrollTarget;
        self.isSnapping = false;

        // Call callback
        self.options.onSnapFinish.call(self, $target);
      });

      if(self.options.$menu !== false)
      {
        $('a.active', self.options.$menu).removeClass('active');
        var $activeItem = $('a[data-panel=' + $target.data('panel') + ']', self.options.$menu);
        $activeItem.addClass('active');
      }
    }
  };

  $.fn[pluginName] = function(options)
  {
    return this.each(function()
    {
      var pluginInstance = $.data(this, storageName);
      if(typeof options === 'object' || options === 'init' || ! options)
      {
        if(!pluginInstance)
        {
          if(options === 'init')
          {
            options = Array.prototype.slice.call(arguments, 1);
          }

          pluginInstance = $.data(this, storageName, Object.create(pluginObject).init(options, this));
        }
        else
        {
          $.error('Plugin is already initialized for this object.');
          return;
        }
      }
      else if(!pluginInstance)
      {
        $.error('Plugin is not initialized for this object yet.');
        return;
      }
      else if(pluginInstance[options])
      {
        method = options;
        options = Array.prototype.slice.call(arguments, 1);
        pluginInstance[method](options);
      }
      else
      {
        $.error('Method ' +  options + ' does not exist on jQuery.panelSnap.');
        return;
      }
    });
  };

  $.fn.panelSnap.options = {
    $menu: false,
    panelSelector: 'section',
    namespace: '.panelSnap',
    onSnapStart: function(){},
    onSnapFinish: function(){},
    directionThreshold: 50,
    slideSpeed: 200
  };

})(jQuery, window, document);<commit_msg>Adds an option to set the menu selector & fixes linked menu not working.<commit_after>// Utility for creating objects in older browsers
if ( typeof Object.create !== 'function' )
{
  Object.create = function( obj )
  {
    function F() {}
    F.prototype = obj;
    return new F();
  };
}

/*!
 * jQuery panelSnap
 * Version 0.4.0
 *
 * Requires:
 * - jQuery 1.7.1 or higher (Works with the API changes from 1.9.1 too)
 * - jQuery scrollEvents.js (included in the package)
 *
 * https://github.com/guidobouman/jquery-panelsnap
 *
 * Copyright 2013, Guido Bouman
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * Date: Wed Feb 13 16:05:00 2013 +0100
 */
(function($, window, document, undefined)
{
  var pluginName = 'panelSnap';
  var storageName = 'plugin_' + pluginName;

  var pluginObject = {
    isMouseDown: false,
    isSnapping: false,
    scrollOffset: 0,

    init: function(options, container)
    {
      var self = this;

      self.$window = $(window);
      self.$document = $(document);

      self.container = container;
      self.$container = $(container);

      self.$eventContainer = self.$container;
      if(self.$container.is('body'))
      {
        self.$eventContainer = self.$document;
      }

      self.options = $.extend(true, {}, $.fn.panelSnap.options, options);

      self.bind();

      return self;
    },

    bind: function()
    {
      var self = this;

      self.bindProxied(self.$eventContainer, 'scrollstop', self.processScroll);
      self.bindProxied(self.$eventContainer, 'scrollstart', self.initScroll);
      self.bindProxied(self.$eventContainer, 'mousewheel', self.holdScroll);
      self.bindProxied(self.$eventContainer, 'mousedown', self.mouseDown);
      self.bindProxied(self.$eventContainer, 'mouseup', self.mouseUp);

      self.bindProxied(self.$window, 'resize', self.processScroll);

      if(self.options.$menu !== false)
      {
        self.bindProxied($(self.options.menuSelector, self.options.$menu), 'click', self.captureMenuClick);
      }
    },

    bindProxied: function($element, event, method)
    {
      var self = this;

      $element.on(event + self.options.namespace, $.proxy(function(e)
      {
        return method.call(self, e);
      }, self));
    },

    destroy: function()
    {
      var self = this;

      // Gotta love namespaced events!
      self.$eventContainer.off(self.options.namespace);

      self.$window.off(self.options.namespace);

      if(self.options.$menu !== false)
      {
        $(self.options.menuSelector, self.options.$menu).off(self.options.namespace);
      }

      self.$container.removeData(storageName);
    },

    captureMenuClick: function(e)
    {
      var self = this;
      var itemSelector = self.options.panelSelector + '[data-panel=' + $(e.currentTarget).data('panel') + ']';
      var $target = $(itemSelector, self.$container);

      self.snapToPanel($target);

      return false;
    },

    holdScroll: function(e)
    {
      var self = this;

      self.$container.stop(true);
      self.isSnapping = false;
    },

    initScroll: function(e)
    {
      var self = this;

      // Deprecated, moved storage of scrollOffset to callback of panelSnap
      // self.scrollOffset = self.$container.scrollTop();
    },

    processScroll: function(e)
    {
      var self = this;

      if(self.isMouseDown)
      {
        self.$container.one('mouseup', self.processScroll);
        return;
      }

      if(self.isSnapping)
      {
        return;
      }

      var interval = self.$container.height();
      var offset = self.$container.scrollTop();
      var scrollDifference = offset - self.scrollOffset;
      var maxOffset = self.$container[0].scrollHeight - interval;

      if(
        scrollDifference === 0 ||
        offset < 0 ||
        offset > maxOffset
      )
      {
        return;
      }

      var child_number;
      if(scrollDifference < -self.options.directionThreshold && scrollDifference > -interval)
      {
        child_number = Math.floor(offset / interval);
      }
      else if(scrollDifference > self.options.directionThreshold && scrollDifference < interval)
      {
        child_number = Math.ceil(offset / interval);
      }
      else
      {
        child_number = Math.round(offset / interval);
      }

      child_number += 1;

      var $target = $(self.options.panelSelector + ':nth-of-type(' + child_number + ')', self.$container);

      self.snapToPanel($target);
    },

    mouseDown: function(e)
    {
      var self = this;

      self.isMouseDown = true;
    },

    mouseUp: function(e)
    {
      var self = this;

      self.isMouseDown = false;
    },

    snapToPanel: function($target)
    {
      var self = this;

      self.isSnapping = true;

      self.options.onSnapStart.call(self, $target);

      var scrollTarget = 0;
      if(self.$container.is('body'))
      {
        scrollTarget = $target.offset().top;
      }
      else
      {
        scrollTarget = self.$container.scrollTop() + $target.position().top;
      }

      self.$container.animate(
      {
        scrollTop: scrollTarget
      }, self.options.slideSpeed, function()
      {
        self.scrollOffset = scrollTarget;
        self.isSnapping = false;

        // Call callback
        self.options.onSnapFinish.call(self, $target);
      });

      if(self.options.$menu !== false)
      {
        $(self.options.menuSelector + '.active', self.options.$menu).removeClass('active');
        var itemSelector = self.options.menuSelector + '[data-panel=' + $target.data('panel') + ']';
        var $activeItem = $(itemSelector, self.options.$menu);
        $activeItem.addClass('active');
      }
    }
  };

  $.fn[pluginName] = function(options)
  {
    return this.each(function()
    {
      var pluginInstance = $.data(this, storageName);
      if(typeof options === 'object' || options === 'init' || ! options)
      {
        if(!pluginInstance)
        {
          if(options === 'init')
          {
            options = Array.prototype.slice.call(arguments, 1);
          }

          pluginInstance = $.data(this, storageName, Object.create(pluginObject).init(options, this));
        }
        else
        {
          $.error('Plugin is already initialized for this object.');
          return;
        }
      }
      else if(!pluginInstance)
      {
        $.error('Plugin is not initialized for this object yet.');
        return;
      }
      else if(pluginInstance[options])
      {
        method = options;
        options = Array.prototype.slice.call(arguments, 1);
        pluginInstance[method](options);
      }
      else
      {
        $.error('Method ' +  options + ' does not exist on jQuery.panelSnap.');
        return;
      }
    });
  };

  $.fn.panelSnap.options = {
    $menu: false,
    menuSelector: 'a',
    panelSelector: 'section',
    namespace: '.panelSnap',
    onSnapStart: function(){},
    onSnapFinish: function(){},
    directionThreshold: 50,
    slideSpeed: 200
  };

})(jQuery, window, document);
<|endoftext|>


<commit_before>/*jslint indent: 2, nomen: true, maxlen: 100, white: true  plusplus: true, browser: true*/
/*global describe, beforeEach, afterEach, it, */
/*global spyOn, runs, expect, waitsFor, jasmine*/
/*global _, $, uiMatchers */

(function() {
  "use strict";

  describe("The Modal view Singleton", function() {

    var testee, div;

    beforeEach(function() {
      div = document.createElement("div");
      div.id = "modalPlaceholder";
      document.body.appendChild(div);
      testee = new window.ModalView();
      uiMatchers.define(this);
    });

    afterEach(function() {
      document.body.removeChild(div);
    });

    describe("buttons", function() {

      var testShow;

      beforeEach(function() {
        testShow = testee.show.bind(testee, "modalTable.ejs", "My Modal");
      });

      it("should create all button types", function() {
        expect(testee.buttons.SUCCESS).toEqual("success");
        expect(testee.buttons.NOTIFICATION).toEqual("notification");
        expect(testee.buttons.NEUTRAL).toEqual("neutral");
      });

      it("should automatically create the close button", function() {
        testShow();
        var btn = $(".button-close", $(div));
        expect(btn.length).toEqual(1);
        spyOn(testee, "hide").andCallThrough();
        btn.click();
        expect(testee.hide).toHaveBeenCalled();
      });

      it("should be able to bind a callback to a button", function() {
        var btnObj = {},
          title = "Save",
          buttons = [btnObj],
          cbs = {
            callback: function() {
              testee.hide();
            }
          },
          btn;

        spyOn(cbs, "callback").andCallThrough();
        btnObj.title = title;
        btnObj.type = testee.buttons.SUCCESS;
        btnObj.callback = cbs.callback;
        testShow(buttons);
        btn = $(".button-" + btnObj.type, $(div));
        expect(btn.length).toEqual(1);
        expect(btn.text()).toEqual(title);
        btn.click();
        expect(cbs.callback).toHaveBeenCalled();
      });

      it("should be able to create several buttons", function() {
        var btnObj1 = {},
          btnObj2 = {},
          btnObj3 = {},
          title = "Button_",
          buttons = [btnObj1, btnObj2, btnObj3],
          cbs = {
            cb1: function() {
              throw "Should never be invoked";
            },
            cb2: function() {
              throw "Should never be invoked";
            },
            cb3: function() {
              throw "Should never be invoked";
            }
          },
          btn;

        spyOn(cbs, "cb1");
        btnObj1.title = title + 1;
        btnObj1.type = testee.buttons.SUCCESS;
        btnObj1.callback = cbs.cb1;
        spyOn(cbs, "cb2");
        btnObj2.title = title + 2;
        btnObj2.type = testee.buttons.NEUTRAL;
        btnObj2.callback = cbs.cb2;
        spyOn(cbs, "cb3");
        btnObj3.title = title + 3;
        btnObj3.type = testee.buttons.NOTIFICATION;
        btnObj3.callback = cbs.cb3;
        testShow(buttons);
        btn = $("button[class*=button-]", $(div));
        //Three defined above + cancel
        expect(btn.length).toEqual(4);
        //Check click events for all buttons
        btn = $(".button-" + btnObj1.type, $(div));
        btn.click();
        expect(cbs.cb1).toHaveBeenCalled();
        btn = $(".button-" + btnObj2.type, $(div));
        btn.click();
        expect(cbs.cb2).toHaveBeenCalled();
        btn = $(".button-" + btnObj3.type, $(div));
        btn.click();
        expect(cbs.cb3).toHaveBeenCalled();
        testee.hide();
      });
    });

    describe("table content", function() {

      var testShow,
        tdSelector,
        readOnlyDef,
        textDef,
        pwDef,
        cbxDef,
        selectDef;

      beforeEach(function() {
        testShow = testee.show.bind(testee, "modalTable.ejs", "My Modal", null);
        tdSelector = ".modal-body table tr th";
        readOnlyDef = {
          type: testee.tables.READONLY,
          value: "Readonly text",
          label: "ReadOnly",
          info: "ReadOnly Description"
        };
        textDef = {
          type: testee.tables.TEXT,
          value: "Text value",
          label: "Text",
          info: "Text Description",
          id: "textId",
          placeholder: "Text Placeholder"
        };
        pwDef = {
          type: testee.tables.PASSWORD,
          value: "secret",
          label: "Password",
          info: "Password Description",
          id: "pwdId",
          placeholder: "PWD Placeholder"
        };
        cbxDef = {
          type: testee.tables.CHECKBOX,
          value: "myBox",
          label: "Checkbox",
          info: "Checkbox Description",
          checked: true,
          id: "cbxId"
        };
        selectDef = {
          type: testee.tables.SELECT,
          label: "Select",
          id: "selectId",
          selected: "v2",
          options: [
            {
              value: "v1",
              label: "l1"
            },
            {
              value: "v2",
              label: "l2"
            }
          ]
        };
      });

      it("should render readonly", function() {
        var content = [readOnlyDef],
          fields;

        testShow(content);
        fields = $(tdSelector);
        expect(fields.length).toEqual(3);
        expect($(fields[0]).text().trim()).toEqual(readOnlyDef.label + ":");
        expect($(fields[1]).text().trim()).toEqual(readOnlyDef.value);
        // expect($(fields[2]).text()).toEqual(lbl);
      });

      it("should render text-input", function() {
        var content = [textDef],
          fields,
          input;

        testShow(content);
        fields = $(tdSelector);
        expect($(fields[0]).text()).toEqual(textDef.label + ":");
        input = $("#" + textDef.id);
        expect(input[0]).toEqual($(fields[1]).find(" > input")[0]);
        expect(input[0]).toBeTag("input");
        expect(input.attr("type")).toEqual("text");
        expect(input.attr("placeholder")).toEqual(textDef.placeholder);
        expect(input.val()).toEqual(textDef.value);
        // expect($(fields[2]).text()).toEqual(lbl);
      });

      it("should render password-input", function() {
        var content = [pwDef],
          fields,
          input;

        testShow(content);
        fields = $(tdSelector);
        expect($(fields[0]).text()).toEqual(pwDef.label + ":");
        input = $("#" + pwDef.id);
        expect(input[0]).toEqual($(fields[1]).find(" > input")[0]);
        expect(input[0]).toBeTag("input");
        expect(input.attr("type")).toEqual("password");
        expect(input.attr("placeholder")).toEqual(pwDef.placeholder);
        expect(input.val()).toEqual(pwDef.value);
        // expect($(fields[2]).text()).toEqual(lbl);
      });

      it("should render checkbox", function() {
        var content = [cbxDef],
          fields,
          cbx;

        testShow(content);
        fields = $(tdSelector);
        expect($(fields[0]).text()).toEqual(cbxDef.label + ":");
        cbx = $("#" + cbxDef.id);
        expect(cbx[0]).toEqual($(fields[1]).find(" > input")[0]);
        expect(cbx.attr("type")).toEqual("checkbox");

        // expect($(fields[2]).text()).toEqual(lbl);
      });

      it("should render select", function() {
        var content = [selectDef],
          fields,
          select;

        testShow(content);
        fields = $(tdSelector);
        expect($(fields[0]).text()).toEqual(selectDef.label + ":");
        select = $("#" + selectDef.id);
        expect(select[0]).toEqual($(fields[1]).find(" > select")[0]);
        _.each(select.children(), function(o, i) {
          var opt = selectDef.options[i];
          expect($(o).attr("value")).toEqual(opt.value);
          expect($(o).text()).toEqual(opt.label);
        });
        expect(select.val()).toEqual(selectDef.selected);

        // expect($(fields[2]).text()).toEqual(lbl);
      });

      it("should render several types in a mix", function() {
        var content = [
            readOnlyDef,
            textDef,
            pwDef,
            cbxDef,
            selectDef
          ],
          rows;

        rows = $(".modal-body table tr");
        _.each(rows, function(v, k) {
          expect($("td:first-child", $(v)).text()).toEqual(content[k].label + ":");
        });

      });

    });

  });
}());
<commit_msg>Added test for delete button<commit_after>/*jslint indent: 2, nomen: true, maxlen: 100, white: true  plusplus: true, browser: true*/
/*global describe, beforeEach, afterEach, it, */
/*global spyOn, runs, expect, waitsFor, jasmine*/
/*global _, $, uiMatchers */

(function() {
  "use strict";

  describe("The Modal view Singleton", function() {

    var testee, div;

    beforeEach(function() {
      div = document.createElement("div");
      div.id = "modalPlaceholder";
      document.body.appendChild(div);
      testee = new window.ModalView();
      uiMatchers.define(this);
    });

    afterEach(function() {
      document.body.removeChild(div);
    });

    describe("buttons", function() {

      var testShow;

      beforeEach(function() {
        testShow = testee.show.bind(testee, "modalTable.ejs", "My Modal");
      });

      it("should create all button types", function() {
        expect(testee.buttons.SUCCESS).toEqual("success");
        expect(testee.buttons.NOTIFICATION).toEqual("notification");
        expect(testee.buttons.NEUTRAL).toEqual("neutral");
        expect(testee.buttons.DELETE).toEqual("danger");
      });

      it("should automatically create the close button", function() {
        testShow();
        var btn = $(".button-close", $(div));
        expect(btn.length).toEqual(1);
        spyOn(testee, "hide").andCallThrough();
        btn.click();
        expect(testee.hide).toHaveBeenCalled();
      });

      it("should be able to bind a callback to a button", function() {
        var btnObj = {},
          title = "Save",
          buttons = [btnObj],
          cbs = {
            callback: function() {
              testee.hide();
            }
          },
          btn;

        spyOn(cbs, "callback").andCallThrough();
        btnObj.title = title;
        btnObj.type = testee.buttons.SUCCESS;
        btnObj.callback = cbs.callback;
        testShow(buttons);
        btn = $(".button-" + btnObj.type, $(div));
        expect(btn.length).toEqual(1);
        expect(btn.text()).toEqual(title);
        btn.click();
        expect(cbs.callback).toHaveBeenCalled();
      });

      it("should be able to create several buttons", function() {
        var btnObj1 = {},
          btnObj2 = {},
          btnObj3 = {},
          title = "Button_",
          buttons = [btnObj1, btnObj2, btnObj3],
          cbs = {
            cb1: function() {
              throw "Should never be invoked";
            },
            cb2: function() {
              throw "Should never be invoked";
            },
            cb3: function() {
              throw "Should never be invoked";
            }
          },
          btn;

        spyOn(cbs, "cb1");
        btnObj1.title = title + 1;
        btnObj1.type = testee.buttons.SUCCESS;
        btnObj1.callback = cbs.cb1;
        spyOn(cbs, "cb2");
        btnObj2.title = title + 2;
        btnObj2.type = testee.buttons.NEUTRAL;
        btnObj2.callback = cbs.cb2;
        spyOn(cbs, "cb3");
        btnObj3.title = title + 3;
        btnObj3.type = testee.buttons.NOTIFICATION;
        btnObj3.callback = cbs.cb3;
        testShow(buttons);
        btn = $("button[class*=button-]", $(div));
        //Three defined above + cancel
        expect(btn.length).toEqual(4);
        //Check click events for all buttons
        btn = $(".button-" + btnObj1.type, $(div));
        btn.click();
        expect(cbs.cb1).toHaveBeenCalled();
        btn = $(".button-" + btnObj2.type, $(div));
        btn.click();
        expect(cbs.cb2).toHaveBeenCalled();
        btn = $(".button-" + btnObj3.type, $(div));
        btn.click();
        expect(cbs.cb3).toHaveBeenCalled();
        testee.hide();
      });

      describe("should create a delete button", function() {
        var cbs;

        beforeEach(function() {
          var btnObj = {},
            title = "Delete",
            buttons = [btnObj],
            btn;

          cbs = {
            callback: function() {
              testee.hide();
            }
          };
          spyOn(cbs, "callback").andCallThrough();
          btnObj.title = title;
          btnObj.type = testee.buttons.DELETE;
          btnObj.callback = cbs.callback;
          testShow(buttons);
          btn = $(".button-" + btnObj.type, $(div));
          expect(btn.length).toEqual(1);
          expect(btn.text()).toEqual(title);
          btn.click();
          expect(cbs.callback).not.toHaveBeenCalled();
        });

        it("with confirm dialog", function() {
          expect($("#modal-delete-confirmation").css("display")).toEqual("block");
          $("#modal-confirm-delete", $(div)).click();
          expect(cbs.callback).toHaveBeenCalled();
        });

        it("should hide confirm dialog", function() {
          expect($("#modal-delete-confirmation").css("display")).toEqual("block");
          $("#modal-abort-delete", $(div)).click();
          expect(cbs.callback).not.toHaveBeenCalled();
          expect($("#modal-delete-confirmation").css("display")).toEqual("none");
        });

      });

    });

    describe("table content", function() {

      var testShow,
        tdSelector,
        readOnlyDef,
        textDef,
        pwDef,
        cbxDef,
        selectDef;

      beforeEach(function() {
        testShow = testee.show.bind(testee, "modalTable.ejs", "My Modal", null);
        tdSelector = ".modal-body table tr th";
        readOnlyDef = {
          type: testee.tables.READONLY,
          value: "Readonly text",
          label: "ReadOnly",
          info: "ReadOnly Description"
        };
        textDef = {
          type: testee.tables.TEXT,
          value: "Text value",
          label: "Text",
          info: "Text Description",
          id: "textId",
          placeholder: "Text Placeholder"
        };
        pwDef = {
          type: testee.tables.PASSWORD,
          value: "secret",
          label: "Password",
          info: "Password Description",
          id: "pwdId",
          placeholder: "PWD Placeholder"
        };
        cbxDef = {
          type: testee.tables.CHECKBOX,
          value: "myBox",
          label: "Checkbox",
          info: "Checkbox Description",
          checked: true,
          id: "cbxId"
        };
        selectDef = {
          type: testee.tables.SELECT,
          label: "Select",
          id: "selectId",
          selected: "v2",
          options: [
            {
              value: "v1",
              label: "l1"
            },
            {
              value: "v2",
              label: "l2"
            }
          ]
        };
      });

      it("should render readonly", function() {
        var content = [readOnlyDef],
          fields;

        testShow(content);
        fields = $(tdSelector);
        expect(fields.length).toEqual(3);
        expect($(fields[0]).text().trim()).toEqual(readOnlyDef.label + ":");
        expect($(fields[1]).text().trim()).toEqual(readOnlyDef.value);
        // expect($(fields[2]).text()).toEqual(lbl);
      });

      it("should render text-input", function() {
        var content = [textDef],
          fields,
          input;

        testShow(content);
        fields = $(tdSelector);
        expect($(fields[0]).text()).toEqual(textDef.label + ":");
        input = $("#" + textDef.id);
        expect(input[0]).toEqual($(fields[1]).find(" > input")[0]);
        expect(input[0]).toBeTag("input");
        expect(input.attr("type")).toEqual("text");
        expect(input.attr("placeholder")).toEqual(textDef.placeholder);
        expect(input.val()).toEqual(textDef.value);
        // expect($(fields[2]).text()).toEqual(lbl);
      });

      it("should render password-input", function() {
        var content = [pwDef],
          fields,
          input;

        testShow(content);
        fields = $(tdSelector);
        expect($(fields[0]).text()).toEqual(pwDef.label + ":");
        input = $("#" + pwDef.id);
        expect(input[0]).toEqual($(fields[1]).find(" > input")[0]);
        expect(input[0]).toBeTag("input");
        expect(input.attr("type")).toEqual("password");
        expect(input.attr("placeholder")).toEqual(pwDef.placeholder);
        expect(input.val()).toEqual(pwDef.value);
        // expect($(fields[2]).text()).toEqual(lbl);
      });

      it("should render checkbox", function() {
        var content = [cbxDef],
          fields,
          cbx;

        testShow(content);
        fields = $(tdSelector);
        expect($(fields[0]).text()).toEqual(cbxDef.label + ":");
        cbx = $("#" + cbxDef.id);
        expect(cbx[0]).toEqual($(fields[1]).find(" > input")[0]);
        expect(cbx.attr("type")).toEqual("checkbox");

        // expect($(fields[2]).text()).toEqual(lbl);
      });

      it("should render select", function() {
        var content = [selectDef],
          fields,
          select;

        testShow(content);
        fields = $(tdSelector);
        expect($(fields[0]).text()).toEqual(selectDef.label + ":");
        select = $("#" + selectDef.id);
        expect(select[0]).toEqual($(fields[1]).find(" > select")[0]);
        _.each(select.children(), function(o, i) {
          var opt = selectDef.options[i];
          expect($(o).attr("value")).toEqual(opt.value);
          expect($(o).text()).toEqual(opt.label);
        });
        expect(select.val()).toEqual(selectDef.selected);

        // expect($(fields[2]).text()).toEqual(lbl);
      });

      it("should render several types in a mix", function() {
        var content = [
            readOnlyDef,
            textDef,
            pwDef,
            cbxDef,
            selectDef
          ],
          rows;

        rows = $(".modal-body table tr");
        _.each(rows, function(v, k) {
          expect($("td:first-child", $(v)).text()).toEqual(content[k].label + ":");
        });

      });

    });

  });
}());
<|endoftext|>


<commit_before>define([
           'dojo/_base/declare',
           'dojo/_base/array',
           'dojo/has',
           'JBrowse/Store/LRUCache',
           'jszlib/arrayCopy'
       ],
       function( declare, array, has, LRUCache, arrayCopy ) {

// contains chunks of files, stitches them together if necessary, wraps, and returns them
// to satisfy requests
return declare( null,

/**
 * @lends JBrowse.Store.RemoteBinaryFile
 */
{
    constructor: function( args ) {
        this.name = args.name;

        this._fetchCount = 0;
        this._arrayCopyCount = 0;

        this.minChunkSize = 'minChunkSize' in args ? args.minChunkSize : 32768;
        this.chunkCache = new LRUCache({
            name: args.name + ' chunk cache',
            fillCallback: dojo.hitch( this, '_fetch' ),
            maxSize: args.maxSize || 20000000 // 20MB max cache size
        });

        this.totalSizes = {};
    },

    _escapeRegExp: function(str) {
        return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
    },

    _relevantExistingChunks: function( url, start, end ) {
        // we can't actually use any existing chunks if we don't have an
        // end defined.  not possible in the HTTP spec to ask for all except
        // the first X bytes of a file
        if( !end )
            return [];

        start = start || 0;

        var fileChunks = this.chunkCache
                .query( new RegExp( '^'+this._escapeRegExp( url + ' (bytes' ) ) );

        // set 'start' and 'end' on any records that don't have them, but should
        array.forEach( fileChunks, function(c) {
                           if( c.size ) {
                               if( ! c.key.start )
                                   c.key.start = 0;
                               if( ! c.key.end )
                                   c.key.end = c.key.start + ( c.size || c.value.byteLength );
                           }
                       });

        // sort the records by start coordinate, then by length descending (so that we preferentially use big chunks)
        fileChunks = fileChunks.sort( function( a, b ) {
            return ( a.key.start - b.key.start ) || ((b.key.end - b.key.start) - ( a.key.end - a.key.start ));
        });

        // filter for the chunks that can actually be used for this request
        return array.filter( fileChunks,
                             function( chunk ) {
                                 return !( chunk.key.start > end || chunk.key.end < start );
                             }, this);
    },

    _fetchChunks: function( url, start, end, callback ) {
        start = start || 0;

        // if we already know how big the file is, use that information for the end
        if( typeof end != 'number' && this.totalSizes[url] ) {
            end = this.totalSizes[ url ]-1;
        }
        // if we know the size of the file, and end is beyond it, then clamp it
        else if( end >= this.totalSizes[url] ) {
            end = this.totalSizes[url] - 1;
        }
        // NOTE: if end is undefined, we take that to mean fetch all the way to the end of the file

        // what chunks do we already have in the chunk cache?
        var existingChunks = this._relevantExistingChunks( url, start, end );
        this._log( 'existing', existingChunks );

        var chunkToString = function() {
            return this.url+" (bytes "+this.start+".."+this.end+")";
        };

        // assemble a 'golden path' of chunks to use to fulfill this
        // request, using existing chunks where we have them cached,
        // and where we don't, making records for chunks to fetch
        var goldenPath = [];
        if( typeof end != 'number' ) { // if we don't have an end coordinate, we just have to fetch the whole file
            goldenPath.push({ key: { url: url, start: 0, end: undefined, toString: chunkToString } } );
        }
        else {
            for( var currOffset = start; currOffset <= end; currOffset = goldenPath[goldenPath.length-1].key.end+1 ) {
                if( existingChunks[0] && existingChunks[0].key.start <= currOffset ) {
                    goldenPath.push( existingChunks.shift() );
                } else {
                    goldenPath.push({ key: {
                                          url: url,
                                          start: currOffset,
                                          end: existingChunks[0] ? existingChunks[0].key.start-1 : end,
                                          toString: chunkToString
                                      }
                                    });
                }
            }
        }

        // filter the blocks in the golden path that
        // have not already been fetched to try to align them to chunk boundaries: multiples of minChunkSize
        array.forEach( goldenPath, function(c) {
                           if( c.value )
                               return;
                           var k = c.key;
                           k.start = Math.floor( k.start / this.minChunkSize ) * this.minChunkSize;
                           if( k.end )
                               k.end = Math.ceil( (k.end+1) / this.minChunkSize ) * this.minChunkSize - 1;
                       }, this );

        // merge and filter request blocks in the golden path
        goldenPath = this._optimizeGoldenPath( goldenPath );

        var needed = array.filter( goldenPath, function(n) { return ! n.value; });

        this._log( 'needed', needed );

        // now fetch all the needed chunks
        // remember that chunk records in the 'needed' array are also
        // present in the 'goldenPath' array, so setting their value
        // will affect both places
        if( needed.length ) {
            var fetchedCount = 0;
            array.forEach( needed, function( c ) {
                this.chunkCache.get( c.key, function( data ) {
                    c.value = data;
                    if( ++fetchedCount == needed.length )
                        callback( goldenPath );
                });
            }, this );
        }
        // or we might already have all the chunks we need
        else {
            callback( goldenPath );
        }
    },

    _optimizeGoldenPath: function( goldenPath ) {
        var goldenPath2 = [ goldenPath[0] ];
        for( var i = 1; i<goldenPath.length; i++ ) {
            var chunk = goldenPath[i];
            var prev = goldenPath[i-1];
            var lastGolden = goldenPath2[ goldenPath2.length-1];

            if( chunk.value ) { // use an existing chunk if it is not rendered superfluous by the previous chunk
                if( chunk.key.end > lastGolden.key.end )
                    goldenPath2.push( chunk );
                // else don't use this chunk
            }
            else {
                // if the last thing on the golden path is also
                // something we need to fetch, merge with it
                if( ! lastGolden.value ) {
                    lastGolden.key.end = chunk.key.end;
                }
                // otherwise, use this fetch
                else {
                    goldenPath2.push( chunk );
                }
            }
        }
        return goldenPath2;
    },

    _fetch: function( request, callback, attempt, truncatedLength ) {

        this._log( 'fetch', request.url, request.start, request.end );
        this._fetchCount++;

        attempt = attempt || 1;
        if( attempt > 3 ) {
            callback(null);
            return;
        }

        var req = new XMLHttpRequest();
        var length;
        var url = request.url;
        if( has('safari') ) {
            url = url + ( url.indexOf('?') > -1 ? '&' : '?' ) + 'safari_cache_bug=' + Date.now();
        }
        req.open('GET', url, true );
        if( req.overrideMimeType )
            req.overrideMimeType('text/plain; charset=x-user-defined');
        if (request.end) {
            req.setRequestHeader('Range', 'bytes=' + request.start + '-' + request.end);
            length = request.end - request.start + 1;
        }
        req.responseType = 'arraybuffer';

        var respond = function( response ) {
            if( response ) {
                if( ! request.start )
                    request.start = 0;
                if( ! request.end )
                    request.end = request.start + response.byteLength;
            }
            callback( response );
        };

        req.onreadystatechange = dojo.hitch( this, function() {
            if (req.readyState == 4) {
                if (req.status == 200 || req.status == 206) {

                    // if this response tells us the file's total size, remember that
                    this.totalSizes[request.url] = (function() {
                        var contentRange = req.getResponseHeader('Content-Range');
                        if( ! contentRange )
                            return undefined;
                        var match = contentRange.match(/\/(\d+)$/);
                        return match ? parseInt(match[1]) : undefined;
                    })();

                    var response = req.response || req.mozResponseArrayBuffer || (function() {
                        try{
                            var r = req.responseText;
                            if (length && length != r.length && (!truncatedLength || r.length != truncatedLength)) {
                                this._fetch( request, callback, attempt + 1, r.length );
                                return;
                            } else {
                                respond( this._stringToBuffer(req.responseText) );
                                return;
                            }
                        } catch (x) {
                            console.error(''+x, x.stack, x);
                            // the response must have successful but
                            // empty, so respond with a zero-length
                            // arraybuffer
                            respond( new ArrayBuffer() );
                            return;
                        }
                    }).call(this);
                    if( response ) {
                        callback( response );
                    }
                } else {
                    return this._fetch( request, callback, attempt + 1);
                }
            }
            return null;
        });
        // if (this.opts.credentials) {
        //     req.withCredentials = true;
        //  }
        req.send('');
    },

    /**
     * @param args.url     {String} url to fetch
     * @param args.start   {Number|undefined} start byte offset
     * @param args.end     {Number|undefined} end byte offset
     * @param args.success {Function} success callback
     * @param args.failure {Function} failure callback
     */
    get: function( args ) {
        this._log( 'get', args.url, args.start, args.end );

        var start = args.start || 0;
        var end = args.end;
        if( start && !end )
            throw "cannot specify a fetch start without a fetch end";

        this._fetchChunks(
            args.url,
            start,
            end,
            dojo.hitch( this,  function( chunks ) {

                 var totalSize = this.totalSizes[ args.url ];

                 this._assembleChunks(
                         start,
                         end,
                         function( resultBuffer ) {
                             if( typeof totalSize == 'number' )
                                 resultBuffer.fileSize = totalSize;
                             try {
                                 args.success.call( this, resultBuffer );
                             } catch( e ) {
                                 console.error(''+e, e.stack, e);
                                 if( args.failure )
                                     args.failure( e );
                             }
                         },
                         args.failure,
                         chunks
                 );
            })
        );
    },

    _assembleChunks: function( start, end, successCallback, failureCallback, chunks ) {
        this._log( 'golden path', chunks);

        var returnBuffer;

        if( ! Uint8Array ) {
            failureCallback( 'Browser does not support typed arrays');
            return;
        }

        // if we just have one chunk, return either it, or a subarray of it.  don't have to do any array copying
        if( chunks.length == 1 && chunks[0].key.start == start && (!end || chunks[0].key.end == end) ) {
            returnBuffer = chunks[0].value;
        } else {

            // calculate the actual range end from the chunks we're
            // using, can't always trust the `end` we're passed,
            // because it might actually be beyond the end of the
            // file.
            var fetchEnd = Math.max.apply(
                Math,
                array.map(
                    chunks,
                    function(c) {
                        return c.key.start + ((c.value||{}).byteLength || 0 ) - 1;
                    })
            );

            // if we have an end, we shouldn't go larger than it, though
            if( end )
                fetchEnd = Math.min( fetchEnd, end );

            var fetchLength = fetchEnd - start + 1;

            // stitch them together into one ArrayBuffer to return
            returnBuffer = new Uint8Array( fetchLength );
            var cursor = 0;
            array.forEach( chunks, function( chunk ) {
                if( !( chunk.value && chunk.value.byteLength ) ) // skip if the chunk has no data
                    return;

                var b = new Uint8Array( chunk.value );
                var bOffset = (start+cursor) - chunk.key.start; if( bOffset < 0 ) this._error('chunking error');
                var length = Math.min( b.byteLength - bOffset, fetchLength - cursor );
                this._log( 'arrayCopy', b, bOffset, returnBuffer, cursor, length );
                arrayCopy( b, bOffset, returnBuffer, cursor, length );
                this._arrayCopyCount++;
                cursor += length;
            },this);
            returnBuffer = returnBuffer.buffer;
        }

        // return the data buffer
        successCallback( returnBuffer );
    },

    _stringToBuffer: function(result) {
        if( ! result || typeof Uint8Array != 'function' )
            return null;

        var ba = new Uint8Array( result.length );
        for ( var i = 0; i < ba.length; i++ ) {
            ba[i] = result.charCodeAt(i);
        }
        return ba.buffer;
    },

    _log: function() {
        //console.log.apply( console, this._logf.apply(this,arguments) );
    },
    _warn: function() {
        console.warn.apply( console, this._logf.apply(this,arguments) );
    },
    _error: function() {
        console.error.apply( console,  this._logf.apply(this,arguments) );
        throw 'file error';
    },
    _logf: function() {
        arguments[0] = this.name+' '+arguments[0];
        if( typeof arguments[0] == 'string' )
            while( arguments[0].length < 15 )
                arguments[0] += ' ';
        return arguments;
    }

});
});<commit_msg>make the safari cache bug workaround use the byte range itself rather than the date<commit_after>define([
           'dojo/_base/declare',
           'dojo/_base/array',
           'dojo/has',
           'JBrowse/Store/LRUCache',
           'jszlib/arrayCopy'
       ],
       function( declare, array, has, LRUCache, arrayCopy ) {

// contains chunks of files, stitches them together if necessary, wraps, and returns them
// to satisfy requests
return declare( null,

/**
 * @lends JBrowse.Store.RemoteBinaryFile
 */
{
    constructor: function( args ) {
        this.name = args.name;

        this._fetchCount = 0;
        this._arrayCopyCount = 0;

        this.minChunkSize = 'minChunkSize' in args ? args.minChunkSize : 32768;
        this.chunkCache = new LRUCache({
            name: args.name + ' chunk cache',
            fillCallback: dojo.hitch( this, '_fetch' ),
            maxSize: args.maxSize || 20000000 // 20MB max cache size
        });

        this.totalSizes = {};
    },

    _escapeRegExp: function(str) {
        return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
    },

    _relevantExistingChunks: function( url, start, end ) {
        // we can't actually use any existing chunks if we don't have an
        // end defined.  not possible in the HTTP spec to ask for all except
        // the first X bytes of a file
        if( !end )
            return [];

        start = start || 0;

        var fileChunks = this.chunkCache
                .query( new RegExp( '^'+this._escapeRegExp( url + ' (bytes' ) ) );

        // set 'start' and 'end' on any records that don't have them, but should
        array.forEach( fileChunks, function(c) {
                           if( c.size ) {
                               if( ! c.key.start )
                                   c.key.start = 0;
                               if( ! c.key.end )
                                   c.key.end = c.key.start + ( c.size || c.value.byteLength );
                           }
                       });

        // sort the records by start coordinate, then by length descending (so that we preferentially use big chunks)
        fileChunks = fileChunks.sort( function( a, b ) {
            return ( a.key.start - b.key.start ) || ((b.key.end - b.key.start) - ( a.key.end - a.key.start ));
        });

        // filter for the chunks that can actually be used for this request
        return array.filter( fileChunks,
                             function( chunk ) {
                                 return !( chunk.key.start > end || chunk.key.end < start );
                             }, this);
    },

    _fetchChunks: function( url, start, end, callback ) {
        start = start || 0;

        // if we already know how big the file is, use that information for the end
        if( typeof end != 'number' && this.totalSizes[url] ) {
            end = this.totalSizes[ url ]-1;
        }
        // if we know the size of the file, and end is beyond it, then clamp it
        else if( end >= this.totalSizes[url] ) {
            end = this.totalSizes[url] - 1;
        }
        // NOTE: if end is undefined, we take that to mean fetch all the way to the end of the file

        // what chunks do we already have in the chunk cache?
        var existingChunks = this._relevantExistingChunks( url, start, end );
        this._log( 'existing', existingChunks );

        var chunkToString = function() {
            return this.url+" (bytes "+this.start+".."+this.end+")";
        };

        // assemble a 'golden path' of chunks to use to fulfill this
        // request, using existing chunks where we have them cached,
        // and where we don't, making records for chunks to fetch
        var goldenPath = [];
        if( typeof end != 'number' ) { // if we don't have an end coordinate, we just have to fetch the whole file
            goldenPath.push({ key: { url: url, start: 0, end: undefined, toString: chunkToString } } );
        }
        else {
            for( var currOffset = start; currOffset <= end; currOffset = goldenPath[goldenPath.length-1].key.end+1 ) {
                if( existingChunks[0] && existingChunks[0].key.start <= currOffset ) {
                    goldenPath.push( existingChunks.shift() );
                } else {
                    goldenPath.push({ key: {
                                          url: url,
                                          start: currOffset,
                                          end: existingChunks[0] ? existingChunks[0].key.start-1 : end,
                                          toString: chunkToString
                                      }
                                    });
                }
            }
        }

        // filter the blocks in the golden path that
        // have not already been fetched to try to align them to chunk boundaries: multiples of minChunkSize
        array.forEach( goldenPath, function(c) {
                           if( c.value )
                               return;
                           var k = c.key;
                           k.start = Math.floor( k.start / this.minChunkSize ) * this.minChunkSize;
                           if( k.end )
                               k.end = Math.ceil( (k.end+1) / this.minChunkSize ) * this.minChunkSize - 1;
                       }, this );

        // merge and filter request blocks in the golden path
        goldenPath = this._optimizeGoldenPath( goldenPath );

        var needed = array.filter( goldenPath, function(n) { return ! n.value; });

        this._log( 'needed', needed );

        // now fetch all the needed chunks
        // remember that chunk records in the 'needed' array are also
        // present in the 'goldenPath' array, so setting their value
        // will affect both places
        if( needed.length ) {
            var fetchedCount = 0;
            array.forEach( needed, function( c ) {
                this.chunkCache.get( c.key, function( data ) {
                    c.value = data;
                    if( ++fetchedCount == needed.length )
                        callback( goldenPath );
                });
            }, this );
        }
        // or we might already have all the chunks we need
        else {
            callback( goldenPath );
        }
    },

    _optimizeGoldenPath: function( goldenPath ) {
        var goldenPath2 = [ goldenPath[0] ];
        for( var i = 1; i<goldenPath.length; i++ ) {
            var chunk = goldenPath[i];
            var prev = goldenPath[i-1];
            var lastGolden = goldenPath2[ goldenPath2.length-1];

            if( chunk.value ) { // use an existing chunk if it is not rendered superfluous by the previous chunk
                if( chunk.key.end > lastGolden.key.end )
                    goldenPath2.push( chunk );
                // else don't use this chunk
            }
            else {
                // if the last thing on the golden path is also
                // something we need to fetch, merge with it
                if( ! lastGolden.value ) {
                    lastGolden.key.end = chunk.key.end;
                }
                // otherwise, use this fetch
                else {
                    goldenPath2.push( chunk );
                }
            }
        }
        return goldenPath2;
    },

    _fetch: function( request, callback, attempt, truncatedLength ) {

        this._log( 'fetch', request.url, request.start, request.end );
        this._fetchCount++;

        attempt = attempt || 1;
        if( attempt > 3 ) {
            callback(null);
            return;
        }

        var req = new XMLHttpRequest();
        var length;
        var url = request.url;

        // Safari browsers cache XHRs to a single resource, regardless
        // of the byte range.  So, requesting the first 32K, then
        // requesting second 32K, can result in getting the first 32K
        // twice.  Seen first-hand on Safari 6, and @dasmoth reports
        // the same thing on mobile Safari on IOS.  So, if running
        // Safari, put the byte range in a query param at the end of
        // the URL to force Safari to pay attention to it.
        if( has('safari') && request.end ) {
            url = url + ( url.indexOf('?') > -1 ? '&' : '?' ) + 'safari_range=' + request.start +'-'+request.end;
        }

        req.open('GET', url, true );
        if( req.overrideMimeType )
            req.overrideMimeType('text/plain; charset=x-user-defined');
        if (request.end) {
            req.setRequestHeader('Range', 'bytes=' + request.start + '-' + request.end);
            length = request.end - request.start + 1;
        }
        req.responseType = 'arraybuffer';

        var respond = function( response ) {
            if( response ) {
                if( ! request.start )
                    request.start = 0;
                if( ! request.end )
                    request.end = request.start + response.byteLength;
            }
            callback( response );
        };

        req.onreadystatechange = dojo.hitch( this, function() {
            if (req.readyState == 4) {
                if (req.status == 200 || req.status == 206) {

                    // if this response tells us the file's total size, remember that
                    this.totalSizes[request.url] = (function() {
                        var contentRange = req.getResponseHeader('Content-Range');
                        if( ! contentRange )
                            return undefined;
                        var match = contentRange.match(/\/(\d+)$/);
                        return match ? parseInt(match[1]) : undefined;
                    })();

                    var response = req.response || req.mozResponseArrayBuffer || (function() {
                        try{
                            var r = req.responseText;
                            if (length && length != r.length && (!truncatedLength || r.length != truncatedLength)) {
                                this._fetch( request, callback, attempt + 1, r.length );
                                return;
                            } else {
                                respond( this._stringToBuffer(req.responseText) );
                                return;
                            }
                        } catch (x) {
                            console.error(''+x, x.stack, x);
                            // the response must have successful but
                            // empty, so respond with a zero-length
                            // arraybuffer
                            respond( new ArrayBuffer() );
                            return;
                        }
                    }).call(this);
                    if( response ) {
                        callback( response );
                    }
                } else {
                    return this._fetch( request, callback, attempt + 1);
                }
            }
            return null;
        });
        // if (this.opts.credentials) {
        //     req.withCredentials = true;
        //  }
        req.send('');
    },

    /**
     * @param args.url     {String} url to fetch
     * @param args.start   {Number|undefined} start byte offset
     * @param args.end     {Number|undefined} end byte offset
     * @param args.success {Function} success callback
     * @param args.failure {Function} failure callback
     */
    get: function( args ) {
        this._log( 'get', args.url, args.start, args.end );

        var start = args.start || 0;
        var end = args.end;
        if( start && !end )
            throw "cannot specify a fetch start without a fetch end";

        this._fetchChunks(
            args.url,
            start,
            end,
            dojo.hitch( this,  function( chunks ) {

                 var totalSize = this.totalSizes[ args.url ];

                 this._assembleChunks(
                         start,
                         end,
                         function( resultBuffer ) {
                             if( typeof totalSize == 'number' )
                                 resultBuffer.fileSize = totalSize;
                             try {
                                 args.success.call( this, resultBuffer );
                             } catch( e ) {
                                 console.error(''+e, e.stack, e);
                                 if( args.failure )
                                     args.failure( e );
                             }
                         },
                         args.failure,
                         chunks
                 );
            })
        );
    },

    _assembleChunks: function( start, end, successCallback, failureCallback, chunks ) {
        this._log( 'golden path', chunks);

        var returnBuffer;

        if( ! Uint8Array ) {
            failureCallback( 'Browser does not support typed arrays');
            return;
        }

        // if we just have one chunk, return either it, or a subarray of it.  don't have to do any array copying
        if( chunks.length == 1 && chunks[0].key.start == start && (!end || chunks[0].key.end == end) ) {
            returnBuffer = chunks[0].value;
        } else {

            // calculate the actual range end from the chunks we're
            // using, can't always trust the `end` we're passed,
            // because it might actually be beyond the end of the
            // file.
            var fetchEnd = Math.max.apply(
                Math,
                array.map(
                    chunks,
                    function(c) {
                        return c.key.start + ((c.value||{}).byteLength || 0 ) - 1;
                    })
            );

            // if we have an end, we shouldn't go larger than it, though
            if( end )
                fetchEnd = Math.min( fetchEnd, end );

            var fetchLength = fetchEnd - start + 1;

            // stitch them together into one ArrayBuffer to return
            returnBuffer = new Uint8Array( fetchLength );
            var cursor = 0;
            array.forEach( chunks, function( chunk ) {
                if( !( chunk.value && chunk.value.byteLength ) ) // skip if the chunk has no data
                    return;

                var b = new Uint8Array( chunk.value );
                var bOffset = (start+cursor) - chunk.key.start; if( bOffset < 0 ) this._error('chunking error');
                var length = Math.min( b.byteLength - bOffset, fetchLength - cursor );
                this._log( 'arrayCopy', b, bOffset, returnBuffer, cursor, length );
                arrayCopy( b, bOffset, returnBuffer, cursor, length );
                this._arrayCopyCount++;
                cursor += length;
            },this);
            returnBuffer = returnBuffer.buffer;
        }

        // return the data buffer
        successCallback( returnBuffer );
    },

    _stringToBuffer: function(result) {
        if( ! result || typeof Uint8Array != 'function' )
            return null;

        var ba = new Uint8Array( result.length );
        for ( var i = 0; i < ba.length; i++ ) {
            ba[i] = result.charCodeAt(i);
        }
        return ba.buffer;
    },

    _log: function() {
        //console.log.apply( console, this._logf.apply(this,arguments) );
    },
    _warn: function() {
        console.warn.apply( console, this._logf.apply(this,arguments) );
    },
    _error: function() {
        console.error.apply( console,  this._logf.apply(this,arguments) );
        throw 'file error';
    },
    _logf: function() {
        arguments[0] = this.name+' '+arguments[0];
        if( typeof arguments[0] == 'string' )
            while( arguments[0].length < 15 )
                arguments[0] += ' ';
        return arguments;
    }

});
});<|endoftext|>