<commit_before>(function($) {

    window.DJNesting = (typeof DJNesting == 'object') ? DJNesting : {};

    var spliceInitialForm = function(oldFormsetPrefix, newFormsetPrefix, $splicingForm) {
        var newFormsetPrefixLen = newFormsetPrefix.length,
            $group = $('#' + newFormsetPrefix + '-group'),
            oldFormId = ($splicingForm) ? $splicingForm.attr('id') : undefined,
            forms = {},
            initialForms = [],
            newForms = [],
            index, totalFormCount;

        $group.find('.nested-inline-form').each(function(i, form) {
            var formId = form.getAttribute('id'),
                formIndex = formId.substr(newFormsetPrefixLen);
            if (formId == oldFormId) {
                return;
            }
            if (formId.indexOf(newFormsetPrefix) === 0 && !formIndex.match(/\D/)) {
                var $form = $(form),
                    isInitial = $form.data('isInitial');
                formIndex = parseInt(formIndex, 10);
                forms[formId] = {
                    form: $form,
                    isInitial: isInitial,
                    index: formIndex
                };
                if (isInitial) {
                    initialForms.push($form);
                } else {
                    newForms.push($form);
                }
            }
        });
        initialForms.push($splicingForm);

        index = totalFormCount = initialForms.length + newForms.length;

        while (index >= 0) {
            index--;
            var newIndex = index, oldIndex = newIndex - 1;
            var formData = forms[newFormsetPrefix + oldIndex];
            if (!formData) {
                continue;
            }
            if (!formData.isInitial) {
                var $form = formData.form;
                var oldFormPrefix = $form.attr('id').replace(/_set(\d+)$/, '_set-$1');
                var oldFormsetPrefixRegex = new RegExp("^(id_)?" + DJNesting.regexQuote(oldFormPrefix));
                $form.attr('id', newFormsetPrefix + newIndex);
                DJNesting.updateFormAttributes($form, oldFormsetPrefixRegex, "$1" + newFormsetPrefix + "-" + newIndex);
            } else {
                break;
            }
        }

        var $form = $splicingForm;
        // Replace the ids for the splice form, then stop iterating
        var oldFormPrefix = oldFormId.replace(/_set(\d+)$/, '_set-$1');
        var oldFormsetPrefixRegex = new RegExp("^(id_)?" + DJNesting.regexQuote(oldFormPrefix));
        var newIndex = initialForms.length - 1;
        $form.attr('id', newFormsetPrefix + newIndex);
        DJNesting.updateFormAttributes($form, oldFormsetPrefixRegex, "$1" + newFormsetPrefix + "-" + newIndex);
    };

    DJNesting.createSortable = function($group) {
        return $group.children('div.items').nestedSortable({
            handle: '> div > h3.djnesting-drag-handler',
            /**
             * items: The selector for ONLY the items underneath a given
             *        container at that level. Not to be confused with
             *        listItemSelector, which is the selector for all list
             *        items in the nestedSortable
             */
            items: '> .nested-sortable-item',
            forcePlaceholderSize: true,
            placeholder: 'ui-sortable-placeholder',
            helper: 'clone',
            opacity: 0.6,
            maxLevels: 3,
            connectWith: '.djnesting-stacked-root > div.items',
            tolerance: 'intersection',
            cursorAt: {left: 5},
            // Don't allow dragging beneath an inline that is marked for deletion
            isAllowed: function(currentItem, parentItem, placeholder) {
                if (parentItem && parentItem.children('.nested-inline-form').hasClass('predelete')) {
                    return false;
                }
                return true;
            },
            // fixedNestingDepth: not a standard ui.sortable parameter.
            // Prevents dragging items up or down levels
            fixedNestingDepth: true,
            /**
             * The original jquery.ui.nestedSortable assumed that one
             * would only ever deal with
 and
 elements in the
             * sortable, so when a list item was dragged under another item
             * and to the right, such that it needed to create a new list
             * nested one level deeper, it would simply perform
             *    document.createElement('ol')
             *
             * Since we're dealing with the django admin, we use
             *
 and
             *

             * instead of
 and
, respectively.
             *
             * This method stands in place of the original plugin's hard-coded
             * document.createElement('ol');
             *
             * The parent element and the insert type (String, 'prepend' or
             * 'append') are passed to the method in case they are useful, but
             * for most purposes they can be ignored.
             */
            createContainerElement:  DJNesting.createContainerElement,
            // The selector for ALL list containers in the nested sortable.
            containerElementSelector: '.nested-sortable-container',
            // The selector for ALL list items in the nested sortable.
            listItemSelector: '.nested-sortable-item',
            /**
             * Triggered when a sortable is removed from a container (to be
             * dropped in another; before 'receive' is triggered)
             *
             * This method decrements the TOTAL_FORMS input in the formset
             * from which the form is being removed.
             *
             * @param event - A jQuery Event
             * @param ui - An instance of the ui.nestedSortable widget
             */
            remove: function(event, ui) {
                var $inline = $(this).closest('.djnesting-stacked');
                var $form = ui.item.find('> .nested-inline-form'),
                    prefix = $form.djangoFormsetPrefix();

                var removedFormCounts = {
                    initial: ($form.data('isInitial')) ? 1 : 0,
                    total: 1
                };

                var $TOTAL_FORMS = $inline.find('> input[name$="TOTAL_FORMS"]').first();
                if ($TOTAL_FORMS.length) {
                    var previousTotalForms = parseInt($TOTAL_FORMS.val(), 10);
                    if (!isNaN(previousTotalForms)) {
                        $TOTAL_FORMS.val(Math.max(0, previousTotalForms - removedFormCounts['total']));
                    }
                }
                var $INITIAL_FORMS = $inline.find('> input[name$="INITIAL_FORMS"]').first();
                if ($INITIAL_FORMS.length) {
                    var previousInitialForms = parseInt($INITIAL_FORMS.val(), 10);
                    if (!isNaN(previousInitialForms)) {
                        $INITIAL_FORMS.val(Math.max(0, previousInitialForms - removedFormCounts['initial']));
                    }
                }
            },
            start: function(event, ui) {
                ui.item.addClass('nested-sortable-item-dragging');
                ui.item.show();
            },
            stop: function(event, ui) {
                ui.item.removeClass('nested-sortable-item-dragging');
            },
            /**
             * Triggered when a sortable is dropped into a container
             *
             * This method:
             *   - increments the TOTAL_FORMS input in the formset to
             *     which the form is being added
             *   - updates the position field in both the formset the
             *     sortable was removed from and the formset it has
             *     been added to
             *   - Updates "id", "name", and "for" attributes to match
             *     the new parent formset's prefix and the old parent
             *     formset's prefix.
             *
             * @param event - A jQuery Event
             * @param ui - An instance of the ui.nestedSortable widget
             */
            receive: function(event, ui) {
                var $form = ui.item.find('> .module'),
                    $inline = $(this).closest('.djnesting-stacked'),
                    $TOTAL_FORMS = $inline.find('> input[name$="TOTAL_FORMS"]').first(),
                    $INITIAL_FORMS = $inline.find('> input[name$="INITIAL_FORMS"]').first(),
                    previousTotalFormCount = 0,
                    previousInitialFormCount = 0,
                    prefix = $form.djangoFormsetPrefix();

                $form = ($form.length == 1) ? $form : $form.first();

                var oldFormsetPrefix = $form.djangoFormsetPrefix(),
                    newFormsetPrefix = (!$TOTAL_FORMS.length)
                                     ? oldFormsetPrefix
                                     : ($TOTAL_FORMS.attr('id').match(/^id_(.+)-TOTAL_FORMS$/) || [null, null])[1];

                var addedFormCounts = {
                    total: 1,
                    initial: ($form.data('isInitial')) ? 1 : 0
                };

                if ($TOTAL_FORMS.length) {
                    previousTotalFormCount = parseInt($TOTAL_FORMS.val(), 10);
                    if (!isNaN(previousTotalFormCount)) {
                        $TOTAL_FORMS.val(previousTotalFormCount + addedFormCounts['total']);
                    }
                }

                if ($TOTAL_FORMS.length && $form.length) {
                    if (oldFormsetPrefix && newFormsetPrefix) {
                        if ($INITIAL_FORMS.length) {
                            previousInitialFormCount = parseInt($INITIAL_FORMS.val(), 10);
                            if (!isNaN(previousInitialFormCount)) {
                                $INITIAL_FORMS.val(previousInitialFormCount + addedFormCounts['initial']);
                            }
                        }

                        $form.each(function(i, newForm) {
                            var $newForm = $(newForm);
                            if ($newForm.data('isInitial')) {
                                spliceInitialForm(oldFormsetPrefix, newFormsetPrefix, $newForm);
                            } else {
                                var oldFormPrefixRegex = new RegExp("^(id_)?" + DJNesting.regexQuote($form.djangoFormPrefix()));
                                var newFormIndex = previousTotalFormCount + i;
                                var newFormPrefix = newFormsetPrefix + '-' + newFormIndex + "-";
                                $newForm.attr('id', newFormsetPrefix + newFormIndex);
                                DJNesting.updateFormAttributes($newForm.parent(), oldFormPrefixRegex, "$1" + newFormPrefix);
                            }
                        });
                    }
                    if (oldFormsetPrefix) {
                        DJNesting.updatePositions(oldFormsetPrefix);
                    }
                    if (newFormsetPrefix) {
                        DJNesting.updatePositions(newFormsetPrefix);
                    }
                }
            },
            update: function(event, ui) {
                // Ensure that

                // is the first child of the nested-sortable-container. If there
                // is another
 before the
                // .do-not-drag element then the drag-and-drop placeholder
                // margins don't work correctly.
                var $nextItem = ui.item.nextAll('.nested-sortable-item').first();
                if ($nextItem.hasClass('nested-do-not-drag')) {
                    var nextItem = $nextItem[0];
                    var parent = nextItem.parentNode;
                    parent.insertBefore(nextItem, parent.firstChild);
                }
                var groupId = $(event.target).parent().attr('id'),
                    $form = ui.item.children('.module'),
                    $parentGroup = $form.closest('#' + groupId);
                if ($form.data('updateOperation') == 'removed') {
                    $form.removeAttr('data-update-operation');
                    return;
                }
                if (!$parentGroup.length) {
                    $form.attr('data-update-operation', 'removed');
                    return;
                }
                DJNesting.updatePositions($form.djangoFormsetPrefix());
            }
        });
    };

})((typeof grp == 'object' && grp.jQuery) ? grp.jQuery : django.jQuery);
<commit_msg>Cleanup DJNesting.createSortable<commit_after>(function($) {

    window.DJNesting = (typeof DJNesting == 'object') ? DJNesting : {};

    var spliceInitialForm = function(oldFormsetPrefix, newFormsetPrefix, $splicingForm) {
        var newFormsetPrefixLen = newFormsetPrefix.length,
            $group = $('#' + newFormsetPrefix + '-group'),
            oldFormId = ($splicingForm) ? $splicingForm.attr('id') : undefined,
            forms = {},
            initialForms = [],
            newForms = [],
            index, totalFormCount;

        $group.find('.nested-inline-form').each(function(i, form) {
            var formId = form.getAttribute('id'),
                formIndex = formId.substr(newFormsetPrefixLen);
            if (formId == oldFormId) {
                return;
            }
            if (formId.indexOf(newFormsetPrefix) === 0 && !formIndex.match(/\D/)) {
                var $form = $(form),
                    isInitial = $form.data('isInitial');
                formIndex = parseInt(formIndex, 10);
                forms[formId] = {
                    form: $form,
                    isInitial: isInitial,
                    index: formIndex
                };
                if (isInitial) {
                    initialForms.push($form);
                } else {
                    newForms.push($form);
                }
            }
        });
        initialForms.push($splicingForm);

        index = totalFormCount = initialForms.length + newForms.length;

        var $form, oldFormPrefixRegex, newIndex;

        while (index >= 0) {
            index--;
            newIndex = index;
            var oldIndex = newIndex - 1;
            var formData = forms[newFormsetPrefix + oldIndex];
            if (!formData) {
                continue;
            }
            if (!formData.isInitial) {
                $form = formData.form;
                oldFormPrefixRegex = new RegExp("^(id_)?"
                    + DJNesting.regexQuote($form.attr('id').replace(/_set(\d+)$/, '_set-$1')));
                $form.attr('id', newFormsetPrefix + newIndex);
                DJNesting.updateFormAttributes($form, oldFormPrefixRegex, "$1" + newFormsetPrefix + "-" + newIndex);
            } else {
                break;
            }
        }

        $form = $splicingForm;
        // Replace the ids for the splice form, then stop iterating
        oldFormPrefixRegex = new RegExp("^(id_)?"
            + DJNesting.regexQuote($form.attr('id').replace(/_set(\d+)$/, '_set-$1')));
        newIndex = initialForms.length - 1;
        $form.attr('id', newFormsetPrefix + newIndex);
        DJNesting.updateFormAttributes($form, oldFormPrefixRegex, "$1" + newFormsetPrefix + "-" + newIndex);
    };

    DJNesting.createSortable = function($group) {
        return $group.children('div.items').nestedSortable({
            handle: '> div > h3.djnesting-drag-handler',
            /**
             * items: The selector for ONLY the items underneath a given
             *        container at that level. Not to be confused with
             *        listItemSelector, which is the selector for all list
             *        items in the nestedSortable
             */
            items: '> .nested-sortable-item',
            forcePlaceholderSize: true,
            placeholder: 'ui-sortable-placeholder',
            helper: 'clone',
            opacity: 0.6,
            maxLevels: 3,
            connectWith: '.djnesting-stacked-root > div.items',
            tolerance: 'intersection',
            cursorAt: {left: 5},
            // Don't allow dragging beneath an inline that is marked for deletion
            isAllowed: function(currentItem, parentItem, placeholder) {
                if (parentItem && parentItem.children('.nested-inline-form').hasClass('predelete')) {
                    return false;
                }
                return true;
            },
            // fixedNestingDepth: not a standard ui.sortable parameter.
            // Prevents dragging items up or down levels
            fixedNestingDepth: true,
            /**
             * The original jquery.ui.nestedSortable assumed that one
             * would only ever deal with
 and
 elements in the
             * sortable, so when a list item was dragged under another item
             * and to the right, such that it needed to create a new list
             * nested one level deeper, it would simply perform
             *    document.createElement('ol')
             *
             * Since we're dealing with the django admin, we use
             *
 and
             *

             * instead of
 and
, respectively.
             *
             * This method stands in place of the original plugin's hard-coded
             * document.createElement('ol');
             *
             * The parent element and the insert type (String, 'prepend' or
             * 'append') are passed to the method in case they are useful, but
             * for most purposes they can be ignored.
             */
            createContainerElement:  DJNesting.createContainerElement,
            // The selector for ALL list containers in the nested sortable.
            containerElementSelector: '.nested-sortable-container',
            // The selector for ALL list items in the nested sortable.
            listItemSelector: '.nested-sortable-item',
            /**
             * Triggered when a sortable is removed from a container (to be
             * dropped in another; before 'receive' is triggered)
             *
             * This method decrements the TOTAL_FORMS input in the formset
             * from which the form is being removed.
             *
             * @param event - A jQuery Event
             * @param ui - An instance of the ui.nestedSortable widget
             */
            remove: function(event, ui) {
                var $inline = $(this).closest('.djnesting-stacked'),
                    $form = ui.item.find('> .nested-inline-form'),
                    nestedFormset = $inline.nestedFormset(),
                    previousTotalFormCount = nestedFormset.mgmtVal('TOTAL_FORMS'),
                    previousInitialFormCount = nestedFormset.mgmtVal('INITIAL_FORMS');

                nestedFormset.mgmtVal('TOTAL_FORMS', previousTotalFormCount - 1);
                if ($form.data('isInitial')) {
                    nestedFormset.mgmtVal('INITIAL_FORMS', previousInitialFormCount - 1);
                }
            },
            start: function(event, ui) {
                ui.item.addClass('nested-sortable-item-dragging');
                ui.item.show();
            },
            stop: function(event, ui) {
                ui.item.removeClass('nested-sortable-item-dragging');
            },
            /**
             * Triggered when a sortable is dropped into a container
             *
             * This method:
             *   - increments the TOTAL_FORMS input in the formset to
             *     which the form is being added
             *   - updates the position field in both the formset the
             *     sortable was removed from and the formset it has
             *     been added to
             *   - Updates "id", "name", and "for" attributes to match
             *     the new parent formset's prefix and the old parent
             *     formset's prefix.
             *
             * @param event - A jQuery Event
             * @param ui - An instance of the ui.nestedSortable widget
             */
            receive: function(event, ui) {
                var $form = ui.item.find('> .module').first(),
                    $inline = $(this).closest('.djnesting-stacked'),
                    nestedFormset = $inline.nestedFormset(),
                    previousTotalFormCount = nestedFormset.mgmtVal('TOTAL_FORMS'),
                    previousInitialFormCount = nestedFormset.mgmtVal('INITIAL_FORMS'),
                    oldFormsetPrefix = $form.djangoFormsetPrefix(),
                    newFormsetPrefix = nestedFormset.prefix;

                nestedFormset.mgmtVal('TOTAL_FORMS', previousTotalFormCount + 1);

                if ($form.data('isInitial')) {
                    nestedFormset.mgmtVal('INITIAL_FORMS', previousInitialFormCount + 1);
                    spliceInitialForm(oldFormsetPrefix, newFormsetPrefix, $form);
                } else {
                    var oldFormPrefixRegex = new RegExp("^(id_)?"
                        + DJNesting.regexQuote($form.djangoFormPrefix()));
                    var newFormPrefix = newFormsetPrefix + '-' + previousTotalFormCount + "-";
                    $form.attr('id', newFormsetPrefix + previousTotalFormCount);
                    DJNesting.updateFormAttributes($form.parent(), oldFormPrefixRegex, "$1" + newFormPrefix);
                }
                DJNesting.updatePositions(oldFormsetPrefix);
                DJNesting.updatePositions(newFormsetPrefix);
            },
            update: function(event, ui) {
                // Ensure that

                // is the first child of the nested-sortable-container. If there
                // is another
 before the
                // .do-not-drag element then the drag-and-drop placeholder
                // margins don't work correctly.
                var $nextItem = ui.item.nextAll('.nested-sortable-item').first();
                if ($nextItem.hasClass('nested-do-not-drag')) {
                    var nextItem = $nextItem[0];
                    var parent = nextItem.parentNode;
                    parent.insertBefore(nextItem, parent.firstChild);
                }
                var groupId = $(event.target).parent().attr('id'),
                    $form = ui.item.children('.module'),
                    $parentGroup = $form.closest('#' + groupId);
                if ($form.data('updateOperation') == 'removed') {
                    $form.removeAttr('data-update-operation');
                    return;
                }
                if (!$parentGroup.length) {
                    $form.attr('data-update-operation', 'removed');
                    return;
                }
                DJNesting.updatePositions($form.djangoFormsetPrefix());
            }
        });
    };

})((typeof grp == 'object' && grp.jQuery) ? grp.jQuery : django.jQuery);
<|endoftext|>


<commit_before>/*
 *
 *
 *
 * Copyright (c) 2015 PAIO
 * Licensed under the MIT license.
 */

(function() {
  'use strict';

  var $ = window.jQuery;
  var ChildrenWrapper = require('./ChildrenWrapper');
  var Child = require('./Child');

  /**
   * constructor
   * @param panel
   * @constructor
   */
  var Carousel3d = function (carousel) {
    this.el = carousel;
    this._makeOption();

    //create chlidrenWrapper, children
    var $children = $(carousel).children();
    var childrenWrapperObj = new ChildrenWrapper(this);
    var currentIndex = 0;
    this.appendChildrenWrapper(childrenWrapperObj);
    $children.each(function (index, childContent) {
      if ($(childContent).attr('selected')) {
        currentIndex = index;
      }
      this.appendChild(childContent);
    }.bind(this));

    //create prev/next buttons
    this._prevButton = $('

')[0];
    $(this.el).append(this._prevButton);
    $(this._prevButton).click(this.prev.bind(this));
    this._nextButton = $('

')[0];
    $(this.el).append(this._nextButton);
    $(this._nextButton).click(this.next.bind(this));

    this.rotate(currentIndex);
  };

  /**
   * Carousel-3D panel element
   * @type {element}
   */
  Carousel3d.prototype.el = null;

  /**
   *
   * @type {{animationDuration: number}}
   */
  Carousel3d.prototype.option = {
    animationDuration: 1000
  };

  /**
   * populate option
   * @private
   */
  Carousel3d.prototype._makeOption = function () {
    (function () {
      var $wrapper = $('

').hide().appendTo(this.el);
      var duration = $('

').hide().appendTo($wrapper).css('transition-duration');
      $wrapper.remove();
      if (duration) {
        if (duration.indexOf('ms') > 0) {
          this.option.animationDuration = parseInt(duration);
        }
        else if (duration.indexOf('s') > 0) {
          this.option.animationDuration = parseInt(duration) * 1000;
        }
      }
    }.bind(this))();

  };

  /**
   * append chlid
   * @param content
   */
  Carousel3d.prototype.appendChild = function (content) {
    this._childrenWrapperObj.appendChild(new Child(this._childrenWrapperObj, content));
  };

  /**
   * append ChildrenWrapper object
   * @param childrenWrapperObj
   */
  Carousel3d.prototype.appendChildrenWrapper = function (childrenWrapperObj) {
    this._childrenWrapperObj = childrenWrapperObj;
    $(this.el).append(childrenWrapperObj.el);
  };

  /**
   * rotate carousel
   * @param index
   */
  Carousel3d.prototype.rotate = function (index) {
    var numChildren = this._childrenWrapperObj.numChildren();
    var iFloor = Math.floor(this._childrenWrapperObj.currentIndex() - numChildren / 2);
    var iCeil = Math.ceil(this._childrenWrapperObj.currentIndex() + numChildren / 2);
    while (index < iFloor) {
      index += numChildren;
    }
    while (iCeil < index) {
      index -= numChildren;
    }

    this._childrenWrapperObj.rotate(index);
    window.setTimeout(function () {
      var i = index;
      while(i < 0) {
        i += this._childrenWrapperObj.numChildren();
      }
      $(this.el).trigger('select', i % this._childrenWrapperObj.numChildren());
    }.bind(this), this.option.animationDuration);
  };

  /**
   * make carousel spin prev
   */
  Carousel3d.prototype.prev = function () {
    this.rotate(this._childrenWrapperObj.currentIndex() - 1);
  };

  /**
   * make carousel spin next
   */
  Carousel3d.prototype.next = function () {
    this.rotate(this._childrenWrapperObj.currentIndex() + 1);
  };

  /**
   * Exposed to jquery.
   * @returns {*}
   */
  $.fn.Carousel3d = function() {
    var self = this, opt = arguments[0], args = Array.prototype.slice.call(arguments,1), l = self.length, i = 0, ret;
    for(i; i < l; i += 1) {
      if (typeof opt === 'object' || typeof opt === 'undefined') {
        self[i].Carousel3d =  new Carousel3d(self[i], opt);
      }
      else {
        ret = self[i].Carousel3d[opt].apply(self[i].Carousel3d, args);
      }
      if (ret !== undefined) {
        return ret;
      }
    }
    return self;
  };

  /**
   * initialize on load
   */
  $(function () {
    $('[data-carousel-3d]').Carousel3d();
  });

})();
<commit_msg>Do not append prev & next button when just one image for Carousel3d<commit_after>/*
 *
 *
 *
 * Copyright (c) 2015 PAIO
 * Licensed under the MIT license.
 */

(function() {
  'use strict';

  var $ = window.jQuery;
  var ChildrenWrapper = require('./ChildrenWrapper');
  var Child = require('./Child');

  /**
   * constructor
   * @param panel
   * @constructor
   */
  var Carousel3d = function (carousel) {
    this.el = carousel;
    this._makeOption();

    //create chlidrenWrapper, children
    var $children = $(carousel).children();
    var childrenWrapperObj = new ChildrenWrapper(this);
    var currentIndex = 0;
    this.appendChildrenWrapper(childrenWrapperObj);
    $children.each(function (index, childContent) {
      if ($(childContent).attr('selected')) {
        currentIndex = index;
      }
      this.appendChild(childContent);
    }.bind(this));

    if (childrenWrapperObj.numChildren() > 1) {
        //create prev/next buttons
        this._prevButton = $('

')[0];
        $(this.el).append(this._prevButton);
        $(this._prevButton).click(this.prev.bind(this));
        this._nextButton = $('

')[0];
        $(this.el).append(this._nextButton);
        $(this._nextButton).click(this.next.bind(this));
    }
    this.rotate(currentIndex);
  };

  /**
   * Carousel-3D panel element
   * @type {element}
   */
  Carousel3d.prototype.el = null;

  /**
   *
   * @type {{animationDuration: number}}
   */
  Carousel3d.prototype.option = {
    animationDuration: 1000
  };

  /**
   * populate option
   * @private
   */
  Carousel3d.prototype._makeOption = function () {
    (function () {
      var $wrapper = $('

').hide().appendTo(this.el);
      var duration = $('

').hide().appendTo($wrapper).css('transition-duration');
      $wrapper.remove();
      if (duration) {
        if (duration.indexOf('ms') > 0) {
          this.option.animationDuration = parseInt(duration);
        }
        else if (duration.indexOf('s') > 0) {
          this.option.animationDuration = parseInt(duration) * 1000;
        }
      }
    }.bind(this))();

  };

  /**
   * append chlid
   * @param content
   */
  Carousel3d.prototype.appendChild = function (content) {
    this._childrenWrapperObj.appendChild(new Child(this._childrenWrapperObj, content));
  };

  /**
   * append ChildrenWrapper object
   * @param childrenWrapperObj
   */
  Carousel3d.prototype.appendChildrenWrapper = function (childrenWrapperObj) {
    this._childrenWrapperObj = childrenWrapperObj;
    $(this.el).append(childrenWrapperObj.el);
  };

  /**
   * rotate carousel
   * @param index
   */
  Carousel3d.prototype.rotate = function (index) {
    var numChildren = this._childrenWrapperObj.numChildren();
    var iFloor = Math.floor(this._childrenWrapperObj.currentIndex() - numChildren / 2);
    var iCeil = Math.ceil(this._childrenWrapperObj.currentIndex() + numChildren / 2);
    while (index < iFloor) {
      index += numChildren;
    }
    while (iCeil < index) {
      index -= numChildren;
    }

    this._childrenWrapperObj.rotate(index);
    window.setTimeout(function () {
      var i = index;
      while(i < 0) {
        i += this._childrenWrapperObj.numChildren();
      }
      $(this.el).trigger('select', i % this._childrenWrapperObj.numChildren());
    }.bind(this), this.option.animationDuration);
  };

  /**
   * make carousel spin prev
   */
  Carousel3d.prototype.prev = function () {
    this.rotate(this._childrenWrapperObj.currentIndex() - 1);
  };

  /**
   * make carousel spin next
   */
  Carousel3d.prototype.next = function () {
    this.rotate(this._childrenWrapperObj.currentIndex() + 1);
  };

  /**
   * Exposed to jquery.
   * @returns {*}
   */
  $.fn.Carousel3d = function() {
    var self = this, opt = arguments[0], args = Array.prototype.slice.call(arguments,1), l = self.length, i = 0, ret;
    for(i; i < l; i += 1) {
      if (typeof opt === 'object' || typeof opt === 'undefined') {
        self[i].Carousel3d =  new Carousel3d(self[i], opt);
      }
      else {
        ret = self[i].Carousel3d[opt].apply(self[i].Carousel3d, args);
      }
      if (ret !== undefined) {
        return ret;
      }
    }
    return self;
  };

  /**
   * initialize on load
   */
  $(function () {
    $('[data-carousel-3d]').Carousel3d();
  });

})();
<|endoftext|>


<commit_before>import _ from 'lodash';
import Promise from 'bluebird'; // eslint-disable-line
import type from 'type-of';
import Collection from 'naomi/lib/Collection';
import Schema from './Schema';
import compileFindQuery from './querycompilers/find';
import compileCountQuery from './querycompilers/count';
import compileInsertQuery from './querycompilers/insert';
import compileUpsertQuery from './querycompilers/upsert';
import compileUpdateQuery from './querycompilers/update';
import compileRemoveQuery from './querycompilers/remove';

class MySqlCollection extends Collection {

  /**
   * Validates the given selection AST against the collection schema.
   * @param {Array} ast
   * @return {Array}
   * @throws {SelectionParseError} if selection is invalid
   */
  validateSelection(ast) {
    return this.validateKeysInAST(ast);
  }

  /**
   * Validates the given projection AST against the collection schema.
   * @param {Array} ast
   * @return {Array}
   * @throws {ProjectionParseError} if selection is invalid
   */
  validateProjection(ast) {
    // handle nil arguments
    if (_.isNil(ast[1])) {
      const projection = _.chain(this.schema.getKeys()).map((k) => [k, 1]).fromPairs().value();
      return this.parseProjection(projection);
    }

    // make sure projection keys exist in schema
    ast = this.validateKeysInAST(ast);

    // invert negative projection
    if (ast[0] === 'NPROJECTION') {
      const keys = this.extractKeysFromAST(ast);
      const projection = _.chain(keys).xor(this.schema.getKeys()).map((k) => [k, 1]).fromPairs().value();
      return this.parseProjection(projection);
    }

    return ast;
  }

  /**
   * Validates the given orderby AST against the collection schema.
   * @param {Array} ast
   * @return {Array}
   * @throws {OrderByParseError} if selection is invalid
   */
  validateOrderBy(ast) {
    return this.validateKeysInAST(ast);
  }

  // replaceIdInAST(ast: Array): Array {
  //   this.schema.hasAtomicPrimaryKey();

  //   return ast.map((e) => {
  //     if (_.isArray(e)) {
  //       return this.replaceIdInAST(e);
  //     }

  //     if (e === 'ID') {
  //       return
  //     }

  //   })

  //   if (ast[0] === 'ID') {
  //     return
  //   }
  // }

  find(selector, options, callback) {
    // validate selector argument
    if (_.isFunction(selector)) {
      callback = selector;
      selector = undefined;
      options = {};
    } else if (
      !_.isUndefined(selector) &&
      !_.isBoolean(selector) &&
      !_.isNumber(selector) &&
      !_.isString(selector) &&
      !_.isDate(selector) &&
      !_.isPlainObject(selector) &&
      !_.isArray(selector) &&
      !Buffer.isBuffer(selector)
    ) {
      throw new TypeError('Invalid "selector" argument; ' +
        'expected boolean, number, string, date, object, array or buffer ' +
        `received ${type(selector)}`);
    }

    // validate options argument
    if (_.isFunction(options)) {
      callback = options;
      options = {};
    } else if (_.isUndefined(options)) {
      options = {};
    } else if (!_.isObject(options)) {
      throw new TypeError(`Invalid "options" argument; expected object, received ${type(options)}`);
    }

    // parse input
    const collection = ['COLLECTION', ['KEY', this.name]];
    const selection = this.validateSelection(this.parseSelection(selector));
    const projection = this.validateProjection(this.parseProjection(options.projection));
    const orderby = this.validateOrderBy(this.parseOrderBy(options.orderby));
    const limit = this.parseLimit(options.limit);
    const offset = this.parseOffset(options.offset);

    // compile parameterized SQL query
    const query = compileFindQuery({ collection, selection, projection, orderby, limit, offset });

    // execute query
    return this.db.execute(query).nodeify(callback);
  }

  findOne(selector, options, callback) {
    // validate options argument
    if (_.isFunction(options)) {
      callback = options;
      options = {};
    } else if (_.isUndefined(options)) {
      options = {};
    } else if (!_.isObject(options)) {
      throw new TypeError(`Invalid "options" argument; expected object, received ${type(options)}`);
    }

    // overload options argument
    options.limit = 1;

    // execute query
    return this.find(selector, options)

      .then((records) => {
        return records[0]; // return single record
      })

      .nodeify(callback);
  }

  count(selector, options, callback) {
    // validate selector
    if (_.isFunction(selector)) {
      callback = selector;
      selector = undefined;
      options = {};
    } else if (!(
      _.isUndefined(selector) ||
      _.isBoolean(selector) ||
      _.isNumber(selector) ||
      _.isString(selector) ||
      _.isDate(selector) ||
      _.isPlainObject(selector) ||
      _.isArray(selector) ||
      Buffer.isBuffer(selector)
    )) {
      throw new TypeError('Invalid "selector" argument; ' +
        'expected boolean, number, string, date, object, array or buffer ' +
        `received ${type(selector)}`);
    }

    // validate options
    if (_.isFunction(options)) {
      callback = options;
      options = {};
    } else if (_.isUndefined(options)) {
      options = {};
    } else if (!_.isObject(options)) {
      throw new TypeError(`Invalid "options" argument; expected object, received ${type(options)}`);
    }

    // parse input
    const collection = ['COLLECTION', ['KEY', this.name]];
    const selection = this.validateSelection(this.parseSelection(selector));
    const orderby = this.validateOrderBy(this.parseOrderBy(options.orderby));
    const limit = this.parseLimit(options.limit);
    const offset = this.parseOffset(options.offset);

    // compile parameterized SQL query
    const query = compileCountQuery({ collection, selection, orderby, limit, offset });

    // execute query
    return this.db.execute(query)

      .then((records) => {
        return records[0].count || 0; // return count
      })

      .nodeify(callback);
  }

  remove(selector, options, callback) {
    // validate selector
    if (_.isFunction(selector)) {
      callback = selector;
      selector = undefined;
      options = {};
    } else if (!(
      _.isUndefined(selector) ||
      _.isBoolean(selector) ||
      _.isNumber(selector) ||
      _.isString(selector) ||
      _.isDate(selector) ||
      _.isPlainObject(selector) ||
      _.isArray(selector) ||
      Buffer.isBuffer(selector)
    )) {
      throw new TypeError('Invalid "selector" argument; ' +
        'expected boolean, number, string, date, object, array or buffer ' +
        `received ${type(selector)}`);
    }

    // validate options
    if (_.isFunction(options)) {
      callback = options;
      options = {};
    } else if (_.isUndefined(options)) {
      options = {};
    } else if (!_.isObject(options)) {
      throw new TypeError(`Invalid "options" argument; expected object, received ${type(options)}`);
    }

    // parse input
    const collection = ['COLLECTION', ['KEY', this.name]];
    const selection = this.validateSelection(this.parseSelection(selector));
    const orderby = this.validateOrderBy(this.parseOrderBy(options.orderby));
    const limit = this.parseLimit(options.limit);

    // compile parameterized SQL query
    const query = compileRemoveQuery({ collection, selection, orderby, limit });

    // execute query
    return this.db.execute(query).nodeify(callback);
  }

  insert(records, options, callback) {
    let isRecordsObject = false;

    // validate records
    if (_.isPlainObject(records)) {
      isRecordsObject = true;
      records = [records]; // make sure records is array
    } else if (!_.isArray(records)) {
      throw new TypeError(`Invalid records argument; exprected object or array, received ${type(records)}`);
    }

    // validate options
    if (_.isFunction(options)) {
      callback = options;
      options = {};
    } else if (_.isUndefined(options)) {
      options = {};
    }

    // validate records content
    return Promise.map(records, (record) => this.schema.validate(record))

      // compile + run query
      .then((values) => {
        const collection = ['COLLECTION', ['KEY', this.name]];
        const keys = this.schema.getKeys();
        const ignore = options.ignore === true;

        const query = compileInsertQuery({ collection, keys, values, ignore });

        // execute query
        return this.db.execute(query)

          // return record indices in db
          .then((result) => {
            const autoinc = this.schema.hasAutoIncPrimaryKey();

            return values.map((obj, i) => {
              // check if table has simple auto-inc primary key
              if (autoinc) {
                return { [this.schema.getPrimaryKey()[0]]: result.insertId + i };
              }

              // extract primary key from obj
              return _.pick(obj, this.schema.getPrimaryKey());
            });
          })

          // return object, if object was received
          .then((arr) => {
            if (isRecordsObject) return arr[0];
            return arr;
          });
      })

      .nodeify(callback);
  }

  upsert(records, options, callback) {
    let isRecordsObject = false;

    // validate records
    if (_.isPlainObject(records)) {
      isRecordsObject = true;
      records = [records]; // make sure records is array
    } else if (!_.isArray(records)) {
      throw new TypeError(`Invalid records argument; exprected object or array, received ${type(records)}`);
    }

    // validate options
    if (_.isFunction(options)) {
      callback = options;
      options = {};
    } else if (_.isUndefined(options)) {
      options = {};
    }

    // validate records
    return Promise.map(records, (record) => this.schema.validate(record))

      // compile + run query
      .then((values) => {
        const collection = ['COLLECTION', ['KEY', this.name]];
        const keys = this.schema.getKeys();
        const updateKeys = _.difference(keys, this.schema.getPrimaryKeys());

        const query = compileUpsertQuery({ collection, keys, updateKeys, values });

        // execute query
        return this.db.execute(query)

          // return record indices in db
          .then((result) => {
            const autoinc = this.schema.hasAutoIncPrimaryKey();
            let insertedRows = 0;

            return values.map((obj) => {
              // check if obj contains primary key
              const containsPrimaryKey = this.schema.getPrimaryKey().every((k) => {
                return obj.hasOwnProperty(k);
              });

              if (containsPrimaryKey) {
                return _.pick(obj, this.schema.getPrimaryKey());
              }

              // check if table has simple auto-inc primary key
              if (autoinc) {
                return { [this.schema.getPrimaryKey()[0]]: result.insertId + (insertedRows++) };
              }

              // return empty object by default
              return {};
            });
          })

          // return object, if object was received
          .then((arr) => {
            if (isRecordsObject) return arr[0];
            return arr;
          });
      })

      .nodeify(callback);
  }

  update(selector, payload, options, callback) {
    // validate payload
    if (!_.isPlainObject(payload)) {
      throw new TypeError(`Invalid "payload" argument; exprected plain object, received ${type(payload)}`);
    }

    // validate options
    if (_.isFunction(options)) {
      callback = options;
      options = {};
    } else if (_.isUndefined(options)) {
      options = {};
    }

    // validate values
    return Promise.resolve()

      .then(() => this.schema.validate(payload, _.keys(payload)))

      // parse input + compile query
      .then((attrs) => {
        const collection = ['COLLECTION', ['KEY', this.name]];
        const selection = this.validateSelection(this.parseSelection(selector));
        const orderby = this.validateOrderBy(this.parseOrderBy(options.orderby));
        const limit = this.parseLimit(options.limit);

        return compileUpdateQuery({ attrs, collection, selection, orderby, limit });
      })

      // execute query
      .then((query) => this.db.execute(query))

      // // compile find query
      // .then(() => {
      //   const collection = ['COLLECTION', ['KEY', this.name]];
      //   const projection = this.validateProjection(['PROJECTION', null]);
      //   const selection = this.validateSelection(this.parseSelection(selector));
      //   const orderby = this.validateOrderBy(this.parseOrderBy(options.orderby));
      //   const limit = this.parseLimit(options.limit);
      //   const offset = this.parseOffset(null);

      //   return compileFindQuery({collection, selection, projection, orderby, limit, offset});
      // })

      // // execute find query
      // .then((query) => {
      //   console.log(query);
      //   return this.db.query(query.sql, query.params);
      // })

      // // return record indices in db
      // .map((record) => _.pick(record, this.schema.primaryKey()))

      // .then((results) => {
      //   console.log(results);
      //   return results;
      // })

      .nodeify(callback);
  }

  /**
   * Reverse engineers the collection's schema from metadata retrieved from the database.
   * This function will update the collection's schema.
   * @param {Function<Error>} [callback] an optional callback function.
   * @return {Promise}
   */
  reverseEngineer(callback) {
    const sql = `
      SELECT
        \`ORDINAL_POSITION\`,
        \`COLUMN_NAME\`,
        \`DATA_TYPE\`,
        \`COLUMN_TYPE\`,
        \`CHARACTER_MAXIMUM_LENGTH\`,
        \`NUMERIC_PRECISION\`,
        \`NUMERIC_SCALE\`,
        \`DATETIME_PRECISION\`
        \`IS_NULLABLE\`,
        \`EXTRA\`,
        \`COLUMN_DEFAULT\`,
        \`CHARACTER_SET_NAME\`,
        \`COLLATION_NAME\`,
        \`COLUMN_COMMENT\`
      FROM information_schema.COLUMNS
      WHERE \`TABLE_SCHEMA\` = ? AND \`TABLE_NAME\` = ?
      ORDER BY \`ORDINAL_POSITION\` ASC;
    `;
    const params = [this.db.name, this.name];

    return this.db.execute({ sql, params })
      .then((metadata) => Schema.fromMetadata(metadata))
      .nodeify(callback);
  }

}

export default MySqlCollection;
<commit_msg>Implement Collection#findStream()<commit_after>import _ from 'lodash';
import Promise from 'bluebird'; // eslint-disable-line
import type from 'type-of';
import Collection from 'naomi/lib/Collection';
import Schema from './Schema';
import compileFindQuery from './querycompilers/find';
import compileCountQuery from './querycompilers/count';
import compileInsertQuery from './querycompilers/insert';
import compileUpsertQuery from './querycompilers/upsert';
import compileUpdateQuery from './querycompilers/update';
import compileRemoveQuery from './querycompilers/remove';

class MySqlCollection extends Collection {

  /**
   * Validates the given selection AST against the collection schema.
   * @param {Array} ast
   * @return {Array}
   * @throws {SelectionParseError} if selection is invalid
   */
  validateSelection(ast) {
    return this.validateKeysInAST(ast);
  }

  /**
   * Validates the given projection AST against the collection schema.
   * @param {Array} ast
   * @return {Array}
   * @throws {ProjectionParseError} if selection is invalid
   */
  validateProjection(ast) {
    // handle nil arguments
    if (_.isNil(ast[1])) {
      const projection = _.chain(this.schema.getKeys()).map((k) => [k, 1]).fromPairs().value();
      return this.parseProjection(projection);
    }

    // make sure projection keys exist in schema
    ast = this.validateKeysInAST(ast);

    // invert negative projection
    if (ast[0] === 'NPROJECTION') {
      const keys = this.extractKeysFromAST(ast);
      const projection = _.chain(keys).xor(this.schema.getKeys()).map((k) => [k, 1]).fromPairs().value();
      return this.parseProjection(projection);
    }

    return ast;
  }

  /**
   * Validates the given orderby AST against the collection schema.
   * @param {Array} ast
   * @return {Array}
   * @throws {OrderByParseError} if selection is invalid
   */
  validateOrderBy(ast) {
    return this.validateKeysInAST(ast);
  }

  // replaceIdInAST(ast: Array): Array {
  //   this.schema.hasAtomicPrimaryKey();

  //   return ast.map((e) => {
  //     if (_.isArray(e)) {
  //       return this.replaceIdInAST(e);
  //     }

  //     if (e === 'ID') {
  //       return
  //     }

  //   })

  //   if (ast[0] === 'ID') {
  //     return
  //   }
  // }

  find(selector, options, callback) {
    // validate selector argument
    if (_.isFunction(selector)) {
      callback = selector;
      selector = undefined;
      options = {};
    } else if (
      !_.isUndefined(selector) &&
      !_.isBoolean(selector) &&
      !_.isNumber(selector) &&
      !_.isString(selector) &&
      !_.isDate(selector) &&
      !_.isPlainObject(selector) &&
      !_.isArray(selector) &&
      !Buffer.isBuffer(selector)
    ) {
      throw new TypeError('Invalid "selector" argument; ' +
        'expected boolean, number, string, date, object, array or buffer ' +
        `received ${type(selector)}`);
    }

    // validate options argument
    if (_.isFunction(options)) {
      callback = options;
      options = {};
    } else if (_.isUndefined(options)) {
      options = {};
    } else if (!_.isObject(options)) {
      throw new TypeError(`Invalid "options" argument; expected object, received ${type(options)}`);
    }

    // parse input
    const collection = ['COLLECTION', ['KEY', this.name]];
    const selection = this.validateSelection(this.parseSelection(selector));
    const projection = this.validateProjection(this.parseProjection(options.projection));
    const orderby = this.validateOrderBy(this.parseOrderBy(options.orderby));
    const limit = this.parseLimit(options.limit);
    const offset = this.parseOffset(options.offset);

    // compile parameterized SQL query
    const query = compileFindQuery({ collection, selection, projection, orderby, limit, offset });

    // execute query
    return this.db.execute(query).nodeify(callback);
  }

  findOne(selector, options, callback) {
    // validate options argument
    if (_.isFunction(options)) {
      callback = options;
      options = {};
    } else if (_.isUndefined(options)) {
      options = {};
    } else if (!_.isObject(options)) {
      throw new TypeError(`Invalid "options" argument; expected object, received ${type(options)}`);
    }

    // overload options argument
    options.limit = 1;

    // execute query
    return this.find(selector, options)

      .then((records) => {
        return records[0]; // return single record
      })

      .nodeify(callback);
  }

  findStream(selector, options = {}) {
    // validate selector argument
    if (
      !_.isUndefined(selector) &&
      !_.isBoolean(selector) &&
      !_.isNumber(selector) &&
      !_.isString(selector) &&
      !_.isDate(selector) &&
      !_.isPlainObject(selector) &&
      !_.isArray(selector) &&
      !Buffer.isBuffer(selector)
    ) {
      throw new TypeError('Invalid "selector" argument; ' +
        'expected boolean, number, string, date, object, array or buffer ' +
        `received ${type(selector)}`);
    }

    // validate options argument
    if (!_.isObject(options)) {
      throw new TypeError(`Invalid "options" argument; expected object, received ${type(options)}`);
    }

    // parse input
    const collection = ['COLLECTION', ['KEY', this.name]];
    const selection = this.validateSelection(this.parseSelection(selector));
    const projection = this.validateProjection(this.parseProjection(options.projection));
    const orderby = this.validateOrderBy(this.parseOrderBy(options.orderby));
    const limit = this.parseLimit(options.limit);
    const offset = this.parseOffset(options.offset);

    // compile parameterized SQL query
    const query = compileFindQuery({ collection, selection, projection, orderby, limit, offset });

    // execute query
    return this.db.executeStream(query, options);
  }

  count(selector, options, callback) {
    // validate selector
    if (_.isFunction(selector)) {
      callback = selector;
      selector = undefined;
      options = {};
    } else if (!(
      _.isUndefined(selector) ||
      _.isBoolean(selector) ||
      _.isNumber(selector) ||
      _.isString(selector) ||
      _.isDate(selector) ||
      _.isPlainObject(selector) ||
      _.isArray(selector) ||
      Buffer.isBuffer(selector)
    )) {
      throw new TypeError('Invalid "selector" argument; ' +
        'expected boolean, number, string, date, object, array or buffer ' +
        `received ${type(selector)}`);
    }

    // validate options
    if (_.isFunction(options)) {
      callback = options;
      options = {};
    } else if (_.isUndefined(options)) {
      options = {};
    } else if (!_.isObject(options)) {
      throw new TypeError(`Invalid "options" argument; expected object, received ${type(options)}`);
    }

    // parse input
    const collection = ['COLLECTION', ['KEY', this.name]];
    const selection = this.validateSelection(this.parseSelection(selector));
    const orderby = this.validateOrderBy(this.parseOrderBy(options.orderby));
    const limit = this.parseLimit(options.limit);
    const offset = this.parseOffset(options.offset);

    // compile parameterized SQL query
    const query = compileCountQuery({ collection, selection, orderby, limit, offset });

    // execute query
    return this.db.execute(query)

      .then((records) => {
        return records[0].count || 0; // return count
      })

      .nodeify(callback);
  }

  remove(selector, options, callback) {
    // validate selector
    if (_.isFunction(selector)) {
      callback = selector;
      selector = undefined;
      options = {};
    } else if (!(
      _.isUndefined(selector) ||
      _.isBoolean(selector) ||
      _.isNumber(selector) ||
      _.isString(selector) ||
      _.isDate(selector) ||
      _.isPlainObject(selector) ||
      _.isArray(selector) ||
      Buffer.isBuffer(selector)
    )) {
      throw new TypeError('Invalid "selector" argument; ' +
        'expected boolean, number, string, date, object, array or buffer ' +
        `received ${type(selector)}`);
    }

    // validate options
    if (_.isFunction(options)) {
      callback = options;
      options = {};
    } else if (_.isUndefined(options)) {
      options = {};
    } else if (!_.isObject(options)) {
      throw new TypeError(`Invalid "options" argument; expected object, received ${type(options)}`);
    }

    // parse input
    const collection = ['COLLECTION', ['KEY', this.name]];
    const selection = this.validateSelection(this.parseSelection(selector));
    const orderby = this.validateOrderBy(this.parseOrderBy(options.orderby));
    const limit = this.parseLimit(options.limit);

    // compile parameterized SQL query
    const query = compileRemoveQuery({ collection, selection, orderby, limit });

    // execute query
    return this.db.execute(query).nodeify(callback);
  }

  insert(records, options, callback) {
    let isRecordsObject = false;

    // validate records
    if (_.isPlainObject(records)) {
      isRecordsObject = true;
      records = [records]; // make sure records is array
    } else if (!_.isArray(records)) {
      throw new TypeError(`Invalid records argument; exprected object or array, received ${type(records)}`);
    }

    // validate options
    if (_.isFunction(options)) {
      callback = options;
      options = {};
    } else if (_.isUndefined(options)) {
      options = {};
    }

    // validate records content
    return Promise.map(records, (record) => this.schema.validate(record))

      // compile + run query
      .then((values) => {
        const collection = ['COLLECTION', ['KEY', this.name]];
        const keys = this.schema.getKeys();
        const ignore = options.ignore === true;

        const query = compileInsertQuery({ collection, keys, values, ignore });

        // execute query
        return this.db.execute(query)

          // return record indices in db
          .then((result) => {
            const autoinc = this.schema.hasAutoIncPrimaryKey();

            return values.map((obj, i) => {
              // check if table has simple auto-inc primary key
              if (autoinc) {
                return { [this.schema.getPrimaryKey()[0]]: result.insertId + i };
              }

              // extract primary key from obj
              return _.pick(obj, this.schema.getPrimaryKey());
            });
          })

          // return object, if object was received
          .then((arr) => {
            if (isRecordsObject) return arr[0];
            return arr;
          });
      })

      .nodeify(callback);
  }

  upsert(records, options, callback) {
    let isRecordsObject = false;

    // validate records
    if (_.isPlainObject(records)) {
      isRecordsObject = true;
      records = [records]; // make sure records is array
    } else if (!_.isArray(records)) {
      throw new TypeError(`Invalid records argument; exprected object or array, received ${type(records)}`);
    }

    // validate options
    if (_.isFunction(options)) {
      callback = options;
      options = {};
    } else if (_.isUndefined(options)) {
      options = {};
    }

    // validate records
    return Promise.map(records, (record) => this.schema.validate(record))

      // compile + run query
      .then((values) => {
        const collection = ['COLLECTION', ['KEY', this.name]];
        const keys = this.schema.getKeys();
        const updateKeys = _.difference(keys, this.schema.getPrimaryKeys());

        const query = compileUpsertQuery({ collection, keys, updateKeys, values });

        // execute query
        return this.db.execute(query)

          // return record indices in db
          .then((result) => {
            const autoinc = this.schema.hasAutoIncPrimaryKey();
            let insertedRows = 0;

            return values.map((obj) => {
              // check if obj contains primary key
              const containsPrimaryKey = this.schema.getPrimaryKey().every((k) => {
                return obj.hasOwnProperty(k);
              });

              if (containsPrimaryKey) {
                return _.pick(obj, this.schema.getPrimaryKey());
              }

              // check if table has simple auto-inc primary key
              if (autoinc) {
                return { [this.schema.getPrimaryKey()[0]]: result.insertId + (insertedRows++) };
              }

              // return empty object by default
              return {};
            });
          })

          // return object, if object was received
          .then((arr) => {
            if (isRecordsObject) return arr[0];
            return arr;
          });
      })

      .nodeify(callback);
  }

  update(selector, payload, options, callback) {
    // validate payload
    if (!_.isPlainObject(payload)) {
      throw new TypeError(`Invalid "payload" argument; exprected plain object, received ${type(payload)}`);
    }

    // validate options
    if (_.isFunction(options)) {
      callback = options;
      options = {};
    } else if (_.isUndefined(options)) {
      options = {};
    }

    // validate values
    return Promise.resolve()

      .then(() => this.schema.validate(payload, _.keys(payload)))

      // parse input + compile query
      .then((attrs) => {
        const collection = ['COLLECTION', ['KEY', this.name]];
        const selection = this.validateSelection(this.parseSelection(selector));
        const orderby = this.validateOrderBy(this.parseOrderBy(options.orderby));
        const limit = this.parseLimit(options.limit);

        return compileUpdateQuery({ attrs, collection, selection, orderby, limit });
      })

      // execute query
      .then((query) => this.db.execute(query))

      // // compile find query
      // .then(() => {
      //   const collection = ['COLLECTION', ['KEY', this.name]];
      //   const projection = this.validateProjection(['PROJECTION', null]);
      //   const selection = this.validateSelection(this.parseSelection(selector));
      //   const orderby = this.validateOrderBy(this.parseOrderBy(options.orderby));
      //   const limit = this.parseLimit(options.limit);
      //   const offset = this.parseOffset(null);

      //   return compileFindQuery({collection, selection, projection, orderby, limit, offset});
      // })

      // // execute find query
      // .then((query) => {
      //   console.log(query);
      //   return this.db.query(query.sql, query.params);
      // })

      // // return record indices in db
      // .map((record) => _.pick(record, this.schema.primaryKey()))

      // .then((results) => {
      //   console.log(results);
      //   return results;
      // })

      .nodeify(callback);
  }

  /**
   * Reverse engineers the collection's schema from metadata retrieved from the database.
   * This function will update the collection's schema.
   * @param {Function<Error>} [callback] an optional callback function.
   * @return {Promise}
   */
  reverseEngineer(callback) {
    const sql = `
      SELECT
        \`ORDINAL_POSITION\`,
        \`COLUMN_NAME\`,
        \`DATA_TYPE\`,
        \`COLUMN_TYPE\`,
        \`CHARACTER_MAXIMUM_LENGTH\`,
        \`NUMERIC_PRECISION\`,
        \`NUMERIC_SCALE\`,
        \`DATETIME_PRECISION\`
        \`IS_NULLABLE\`,
        \`EXTRA\`,
        \`COLUMN_DEFAULT\`,
        \`CHARACTER_SET_NAME\`,
        \`COLLATION_NAME\`,
        \`COLUMN_COMMENT\`
      FROM information_schema.COLUMNS
      WHERE \`TABLE_SCHEMA\` = ? AND \`TABLE_NAME\` = ?
      ORDER BY \`ORDINAL_POSITION\` ASC;
    `;
    const params = [this.db.name, this.name];

    return this.db.execute({ sql, params })
      .then((metadata) => Schema.fromMetadata(metadata))
      .nodeify(callback);
  }

}

export default MySqlCollection;
<|endoftext|>


<commit_before>angular.module('opal.services')
    .factory('episodesLoader', function($q, $window,
                                        $http,
                                        $route,
                                        EpisodeResource, Episode,
                                        listSchemaLoader) {
    return function() {
	    var deferred = $q.defer();
        var params = $route.current.params;

        if(!$route.current.params.tag){
            deferred.resolve([])
        }

	    listSchemaLoader().then(function(schema) {
            var target = '/episode/' + params.tag;
            if(params.subtag){
                target += '/' + params.subtag;
            }
            $http.get(target).then(
                function(resources) {
	                var episodes = {};
		            _.each(resources.data, function(resource) {
		                episodes[resource.id] = new Episode(resource, schema);
		            });
		            deferred.resolve(episodes);
                }, function() {
		            // handle error better
		            $window.alert('Episodes could not be loaded');
	            });
	    });
	    return deferred.promise;
    };
});
<commit_msg>load schema and episodes in parrallel<commit_after>angular.module('opal.services')
    .factory('episodesLoader', function($q, $window,
                                        $http,
                                        $route,
                                        EpisodeResource, Episode,
                                        listSchemaLoader) {
    return function() {
      "use strict";

	    var deferred = $q.defer();
      var params = $route.current.params;
      var listSchemaPromise = listSchemaLoader();
      var target = '/episode/' + params.tag;
      if(params.subtag){
          target += '/' + params.subtag;
      }

      var getEpisodePromise = $http.get(target);

      $q.all([listSchemaPromise, getEpisodePromise]).then(function(results){
          var listSchema = results[0];
          var episodesResult = results[1];
          var episodes = {};
          _.each(episodesResult.data, function(resource) {
              episodes[resource.id] = new Episode(resource, listSchema);
          });
          deferred.resolve(episodes);
      });

	    return deferred.promise;
    };
});
<|endoftext|>


<commit_before>import colors from 'colors/safe';
import {errToString} from './util';
import moment from 'moment';
import net from 'net';
import protodef from 'rethinkdb/proto-def';
import url from 'url';

// A Connection instance wraps an incoming WebSocket (likely from a browser
// using rethinkdb-websocket-client) and an outgoing RethinkDB TCP socket. Once
// start() is called, it will forward traffic in both directions until either
// side disconnects. Each incoming WebSocket spawns a new outgoing TCP socket.
export class Connection {
  constructor(queryValidator, webSocket, loggingMode) {
    this.queryValidator = queryValidator;
    this.webSocket = webSocket;
    this.loggingMode = loggingMode;
    this.remoteAddress = webSocket._socket.remoteAddress;
    this.remotePort = webSocket._socket.remotePort;
  }

  start({sessionCreator, dbHost, dbPort}) {
    const urlQueryParams = url.parse(this.webSocket.upgradeReq.url, true).query;
    this.sessionPromise = sessionCreator(urlQueryParams).catch(e => {
      this.cleanupAndLogErr('Error in sessionCreator', e);
    });
    this.wsInBuffer = new Buffer(0);
    this.handshakeComplete = false;
    this.isClosed = false;
    this.dbSocket = net.createConnection(dbPort, dbHost);
    this.setupDbSocket();
    this.setupWebSocket();
    if (this.loggingMode === 'all') {
      this.log('Connect');
    }
  }

  setupDbSocket() {
    this.dbSocket.setNoDelay();
    this.dbSocket.on('data', data => {
      if (!this.isClosed) {
        try {
          if (this.webSocket.protocol === 'base64') {
            const b64EncodedData = (new Buffer(data)).toString('base64');
            this.webSocket.send(b64EncodedData);
          } else {
            this.webSocket.send(data, {binary: true});
          }
        } catch (e) {
          this.cleanupAndLogErr('Error recv dbSocket data', e);
        }
      }
    });
    this.dbSocket.on('end', () => {
      this.cleanup();
    });
    this.dbSocket.on('close', () => {
      if (this.loggingMode === 'all') {
        this.log('dbSocket closed');
      }
      this.cleanup();
    });
    this.dbSocket.on('error', e => {
      this.cleanupAndLogErr('dbSocket error', e);
    });
  }

  setupWebSocket() {
    this.webSocket.on('message', msg => {
      if (!this.isClosed) {
        try {
          this.handleWsMessage(msg);
        } catch (e) {
          this.cleanupAndLogErr('Error recv webSocket data', e);
        }
      }
    });
    this.webSocket.on('close', () => {
      if (this.loggingMode === 'all') {
        this.log('webSocket closed');
      }
      this.cleanup();
    });
    this.webSocket.on('error', e => {
      this.cleanupAndLogErr('webSocket error', e);
    });
  }

  log(msg, token) {
    const time = colors.blue(moment().format('HH:mm:ss.SSS'));
    const addr = colors.gray(this.remoteAddress + ':' + this.remotePort);
    const tokenStr = token ? colors.yellow(`tkn=${token} `) : '';
    console.log(`${time} ${addr} ${tokenStr}${msg}`);
  }

  cleanup() {
    this.isClosed = true;
    this.dbSocket.destroy();
    this.webSocket.close();
  }

  cleanupAndLogErr(msg, error) {
    this.cleanup();
    const fullMsg = error ? msg + '\n' + errToString(error) : msg;
    this.log(colors.red(fullMsg));
  }

  processClientCommand(cmdBuf) {
    // https://github.com/rethinkdb/rethinkdb/blob/61692c0ed4/drivers/javascript/net.coffee#L269-L273
    const token = cmdBuf.readUInt32LE(0) + 0x100000000 * cmdBuf.readUInt32LE(4);
    const queryCmdBuf = cmdBuf.slice(12, cmdBuf.length);
    const logFn = this.log.bind(this);
    const validatePromise = this.sessionPromise.then(session => (
      this.queryValidator.validateQueryCmd(token, queryCmdBuf, session, logFn)
    ));
    return validatePromise.then(allow => {
      if (allow) {
        this.dbSocket.write(cmdBuf, 'binary');
      }
      // TODO It might be nice to send a response back to the client of type
      // CLIENT_ERROR. However, we'd need to parse every message back from
      // the db in order to avoid injecting our response in the middle of a
      // large response.
      //
      // Right now, the query will be silently ignored in the frontend and
      // logged in the backend. This might lead to some sort of memory leak
      // in the frontend if it's waiting for responses from queries. This is
      // not very important, because properly written frontends should avoid
      // submitting queries that are denied.
      //
      // Alternatively, we could close the connection after a denied query,
      // as an easier to debug fail-fast option.
    });
  }

  processNextMessage(buf) {
    if (!this.handshakeComplete) {
      if (buf.length >= 12) {
        const protocolVersion = buf.readUInt32LE(0);
        if (protocolVersion !== protodef.VersionDummy.Version.V0_4) {
          this.cleanupAndLogErr('Invalid protocolVersion ' + protocolVersion);
          return 0;
        }
        const keyLength = buf.readUInt32LE(4);
        if (keyLength !== 0) {
          this.cleanupAndLogErr('Auth key not supported');
          return 0;
        }
        const protocolType = buf.readUInt32LE(8 + keyLength);
        if (protocolType !== protodef.VersionDummy.Protocol.JSON) {
          this.cleanupAndLogErr('Protocol type not supported ' + protocolType);
          return 0;
        }
        const handshakeLength = 12 + keyLength;
        this.dbSocket.write(buf.slice(0, handshakeLength), 'binary');
        this.handshakeComplete = true;
        return handshakeLength;
      }
    } else {
      if (buf.length >= 12) {
        const encodedQueryLength = buf.readUInt32LE(8);
        const queryEndOffset = 12 + encodedQueryLength;
        if (queryEndOffset <= buf.length) {
          const cmdBuf = buf.slice(0, queryEndOffset);
          this.processClientCommand(cmdBuf).catch(e => {
            this.cleanupAndLogErr('Error in processClientCommand', e);
          });
          return queryEndOffset;
        }
      }
    }
    return 0;
  }

  handleWsMessage(msg) {
    const isBase64 = typeof msg === 'string' && this.webSocket.protocol === 'base64';
    const incomingBuffer = isBase64 ? new Buffer(msg, 'base64') : msg;
    this.wsInBuffer = Buffer.concat([this.wsInBuffer, incomingBuffer]);
    let keepGoing = true;
    while (keepGoing) {
      let bytesConsumed = this.processNextMessage(this.wsInBuffer);
      this.wsInBuffer = this.wsInBuffer.slice(bytesConsumed);
      keepGoing = bytesConsumed > 0;
    }
  }
}
<commit_msg>Factor out Connection.validateClientHandshake()<commit_after>import colors from 'colors/safe';
import {errToString} from './util';
import moment from 'moment';
import net from 'net';
import protodef from 'rethinkdb/proto-def';
import url from 'url';

// A Connection instance wraps an incoming WebSocket (likely from a browser
// using rethinkdb-websocket-client) and an outgoing RethinkDB TCP socket. Once
// start() is called, it will forward traffic in both directions until either
// side disconnects. Each incoming WebSocket spawns a new outgoing TCP socket.
export class Connection {
  constructor(queryValidator, webSocket, loggingMode) {
    this.queryValidator = queryValidator;
    this.webSocket = webSocket;
    this.loggingMode = loggingMode;
    this.remoteAddress = webSocket._socket.remoteAddress;
    this.remotePort = webSocket._socket.remotePort;
  }

  start({sessionCreator, dbHost, dbPort}) {
    const urlQueryParams = url.parse(this.webSocket.upgradeReq.url, true).query;
    this.sessionPromise = sessionCreator(urlQueryParams).catch(e => {
      this.cleanupAndLogErr('Error in sessionCreator', e);
    });
    this.wsInBuffer = new Buffer(0);
    this.handshakeComplete = false;
    this.isClosed = false;
    this.dbSocket = net.createConnection(dbPort, dbHost);
    this.setupDbSocket();
    this.setupWebSocket();
    if (this.loggingMode === 'all') {
      this.log('Connect');
    }
  }

  setupDbSocket() {
    this.dbSocket.setNoDelay();
    this.dbSocket.on('data', data => {
      if (!this.isClosed) {
        try {
          if (this.webSocket.protocol === 'base64') {
            const b64EncodedData = (new Buffer(data)).toString('base64');
            this.webSocket.send(b64EncodedData);
          } else {
            this.webSocket.send(data, {binary: true});
          }
        } catch (e) {
          this.cleanupAndLogErr('Error recv dbSocket data', e);
        }
      }
    });
    this.dbSocket.on('end', () => {
      this.cleanup();
    });
    this.dbSocket.on('close', () => {
      if (this.loggingMode === 'all') {
        this.log('dbSocket closed');
      }
      this.cleanup();
    });
    this.dbSocket.on('error', e => {
      this.cleanupAndLogErr('dbSocket error', e);
    });
  }

  setupWebSocket() {
    this.webSocket.on('message', msg => {
      if (!this.isClosed) {
        try {
          this.handleWsMessage(msg);
        } catch (e) {
          this.cleanupAndLogErr('Error recv webSocket data', e);
        }
      }
    });
    this.webSocket.on('close', () => {
      if (this.loggingMode === 'all') {
        this.log('webSocket closed');
      }
      this.cleanup();
    });
    this.webSocket.on('error', e => {
      this.cleanupAndLogErr('webSocket error', e);
    });
  }

  log(msg, token) {
    const time = colors.blue(moment().format('HH:mm:ss.SSS'));
    const addr = colors.gray(this.remoteAddress + ':' + this.remotePort);
    const tokenStr = token ? colors.yellow(`tkn=${token} `) : '';
    console.log(`${time} ${addr} ${tokenStr}${msg}`);
  }

  cleanup() {
    this.isClosed = true;
    this.dbSocket.destroy();
    this.webSocket.close();
  }

  cleanupAndLogErr(msg, error) {
    this.cleanup();
    const fullMsg = error ? msg + '\n' + errToString(error) : msg;
    this.log(colors.red(fullMsg));
  }

  processClientCommand(cmdBuf) {
    // https://github.com/rethinkdb/rethinkdb/blob/61692c0ed4/drivers/javascript/net.coffee#L269-L273
    const token = cmdBuf.readUInt32LE(0) + 0x100000000 * cmdBuf.readUInt32LE(4);
    const queryCmdBuf = cmdBuf.slice(12, cmdBuf.length);
    const logFn = this.log.bind(this);
    const validatePromise = this.sessionPromise.then(session => (
      this.queryValidator.validateQueryCmd(token, queryCmdBuf, session, logFn)
    ));
    return validatePromise.then(allow => {
      if (allow) {
        this.dbSocket.write(cmdBuf, 'binary');
      }
      // TODO It might be nice to send a response back to the client of type
      // CLIENT_ERROR. However, we'd need to parse every message back from
      // the db in order to avoid injecting our response in the middle of a
      // large response.
      //
      // Right now, the query will be silently ignored in the frontend and
      // logged in the backend. This might lead to some sort of memory leak
      // in the frontend if it's waiting for responses from queries. This is
      // not very important, because properly written frontends should avoid
      // submitting queries that are denied.
      //
      // Alternatively, we could close the connection after a denied query,
      // as an easier to debug fail-fast option.
    });
  }

  validateClientHandshake(buf) {
    const protocolVersion = buf.readUInt32LE(0);
    if (protocolVersion !== protodef.VersionDummy.Version.V0_4) {
      this.cleanupAndLogErr('Invalid protocolVersion ' + protocolVersion);
      return 0;
    }
    const keyLength = buf.readUInt32LE(4);
    if (keyLength !== 0) {
      this.cleanupAndLogErr('Auth key not supported');
      return 0;
    }
    const protocolType = buf.readUInt32LE(8);
    if (protocolType !== protodef.VersionDummy.Protocol.JSON) {
      this.cleanupAndLogErr('Protocol type not supported ' + protocolType);
      return 0;
    }
    return 12;
  }

  processNextMessage(buf) {
    if (!this.handshakeComplete) {
      if (buf.length >= 12) {
        const clientHandshakeLength = this.validateClientHandshake(buf);
        if (clientHandshakeLength > 0) {
          this.dbSocket.write(buf.slice(0, clientHandshakeLength), 'binary');
          this.handshakeComplete = true;
          return clientHandshakeLength;
        } else {
          return 0;
        }
      }
    } else {
      if (buf.length >= 12) {
        const encodedQueryLength = buf.readUInt32LE(8);
        const queryEndOffset = 12 + encodedQueryLength;
        if (queryEndOffset <= buf.length) {
          const cmdBuf = buf.slice(0, queryEndOffset);
          this.processClientCommand(cmdBuf).catch(e => {
            this.cleanupAndLogErr('Error in processClientCommand', e);
          });
          return queryEndOffset;
        }
      }
    }
    return 0;
  }

  handleWsMessage(msg) {
    const isBase64 = typeof msg === 'string' && this.webSocket.protocol === 'base64';
    const incomingBuffer = isBase64 ? new Buffer(msg, 'base64') : msg;
    this.wsInBuffer = Buffer.concat([this.wsInBuffer, incomingBuffer]);
    let keepGoing = true;
    while (keepGoing) {
      let bytesConsumed = this.processNextMessage(this.wsInBuffer);
      this.wsInBuffer = this.wsInBuffer.slice(bytesConsumed);
      keepGoing = bytesConsumed > 0;
    }
  }
}
<|endoftext|>


<commit_before>
"use strict";

class Model {

    constructor() {
    }

    $load(data = {}) {
        this.$data = {};
        this.$initial = data;
        this.$fields = [];
        this.$deleted = false;
        for (let key in data) {
            var props = {enumerable: true, configurable: true};
            if (!this.hasOwnProperty(key)) {
                props.get = () => {
                    return this.$data[key];
                };
                props.set = value => {
                    this.$data[key] = value;
                };
            }
            Object.defineProperty(this, key, props);
            this.$data[key] = data[key];
            this.$fields.push(key);
        }
        return this;
    }

    $update() {
        for (let field in this) {
            if (typeof this[field] == 'object' && this[field] != null) {
                if ('$update' in this[field] && this[field].$dirty) {
                    this[field].$update();
                } else if ('$delete' in this[field] && this[field].$deleted) {
                    this[field].$delete();
                } else if ('map' in this[field]) {
                    this[field].map(item => {
                        if (typeof item == 'object') {
                            if ('$update' in item && item.$dirty) {
                                item.$update();
                            } else if ('$delete' in item && item.$deleted) {
                                item.$delete();
                            }
                        }
                    });
                }
            }
        }
    }

    get $new() {
        return !('id' in this.$data && this.$data.id);
    }

    get $dirty() {
        let dirty = false;
        for (let key in this) {
            if (key.substring(0, 1) == '$') {
                continue;
            }
            if (key in this.$data && this.$data[key] != this.$initial[key]) {
                return true;
            } else if (typeof this[key] == 'object' && this[key] != null) {
                if ('map' in this[key]) {
                    this[key].map(elem => {
                        if (typeof elem == 'object' && elem.$dirty) {
                            dirty = true;
                        }
                    });
                } else if (this[key].$dirty) {
                    return true;
                }
            }
        }
        return dirty;
    }

    $pluralize(field) {
        field = field.replace(/y$/, 'ie');
        return this[field + 's'];
    }

};

export {Model};

<commit_msg>move these to constructor<commit_after>
"use strict";

class Model {

    constructor() {
        this.$data = {};
        this.$fields = [];
        this.$deleted = false;
    }

    $load(data = {}) {
        this.$initial = data;
        for (let key in data) {
            var props = {enumerable: true, configurable: true};
            if (!this.hasOwnProperty(key)) {
                props.get = () => {
                    return this.$data[key];
                };
                props.set = value => {
                    this.$data[key] = value;
                };
            }
            Object.defineProperty(this, key, props);
            this.$data[key] = data[key];
            this.$fields.push(key);
        }
        return this;
    }

    $update() {
        for (let field in this) {
            if (typeof this[field] == 'object' && this[field] != null) {
                if ('$update' in this[field] && this[field].$dirty) {
                    this[field].$update();
                } else if ('$delete' in this[field] && this[field].$deleted) {
                    this[field].$delete();
                } else if ('map' in this[field]) {
                    this[field].map(item => {
                        if (typeof item == 'object') {
                            if ('$update' in item && item.$dirty) {
                                item.$update();
                            } else if ('$delete' in item && item.$deleted) {
                                item.$delete();
                            }
                        }
                    });
                }
            }
        }
    }

    get $new() {
        return !('id' in this.$data && this.$data.id);
    }

    get $dirty() {
        let dirty = false;
        for (let key in this) {
            if (key.substring(0, 1) == '$') {
                continue;
            }
            if (key in this.$data && this.$data[key] != this.$initial[key]) {
                return true;
            } else if (typeof this[key] == 'object' && this[key] != null) {
                if ('map' in this[key]) {
                    this[key].map(elem => {
                        if (typeof elem == 'object' && elem.$dirty) {
                            dirty = true;
                        }
                    });
                } else if (this[key].$dirty) {
                    return true;
                }
            }
        }
        return dirty;
    }

    $pluralize(field) {
        field = field.replace(/y$/, 'ie');
        return this[field + 's'];
    }

};

export {Model};

<|endoftext|>


<commit_before>import React from "react";
import { Dimensions } from "react-native";

import moment from "moment";
import _ from "lodash";

import DateSliderComponent from "./DateSliderComponent";

class DateSlider extends React.Component {
  flatlist = undefined;
  selectedIndex = 0;
  viewableIndex = -1;

  constructor(props) {
    super(props);

    // The very first date in the FlatList
    const startingDate = moment().isoWeekday(1).subtract(4, "weeks");

    this.state = {
      viewWidth: Dimensions.get("window").width,
      arrDates: this._generateDates(startingDate, 9, moment()),
      selectedDate: moment().startOf("day")
    };
  }

  render() {
    return (
      <DateSliderComponent
        arrDates={this.state.arrDates}
        captureFlatListRef={this._captureFlatListRef}
        viewWidth={this.state.viewWidth}
        getItemLayout={this._getItemLayout}
        onDateSelected={this._onDateSelected}
        onEndReached={this._onEndReached}
        onEndReachedThreshold={3}
        onLayout={this._onLayout}
        onViewableItemsChanged={this._onViewableItemsChanged}
      />
    );
  }

  /**
   * Pure function to generate an array of objects that can be used by the FlatList.
   *
   * The FlatList renders a 'week' as a new item. This function
   * creates a number of entries equal to the weeks parameter
   * and starting from the fromDate paramater. The fromDate
   * needs to be set to the correct weekday used to start each WeekView
   * by the callee.
   *
   * @param {moment} fromDate
   *  The starting date for the array of weeks to be generated
   * @param {integer} weeks
   *  The number of weeks to be generated
   * @param {moment} selectedDate
   *  The date that is current selected
   * @returns {Array<Object>}
   *
   * @memberof DateSlider
   */
  _generateDates(fromDate, weeks, selectedDate) {
    const startOfSelectedWeek = selectedDate.clone().isoWeekday(1);

    return _.map(_.range(weeks), i => {
      const weekStartDate = fromDate.clone().add(i, "weeks").startOf("day");
      if (startOfSelectedWeek.isSame(weekStartDate, "day")) {
        this.selectedIndex = i;
      }
      return {
        key: weekStartDate.toISOString(),
        date: weekStartDate,
        arrSelected: this._generateArrSelected(
          weekStartDate,
          startOfSelectedWeek,
          selectedDate
        )
      };
    });
  }

  /**
   * A pure function to generate an array of boolean values
   * that specify if one of the days of the week is selected.
   *
   * This array is to be used as a prop on the WeekView component.
   *
   * @param {moment} weekStartDate
   *  The start date of the week this is to be generated
   * @param {moment} startOfSelectedWeek
   *  The start of the week that contains the selectedDate
   * @param {moment} selectedDate
   *  The date that is selected
   *
   * @returns {Array<boolean>}
   *
   * @memberof DateSlider
   */
  _generateArrSelected(weekStartDate, startOfSelectedWeek, selectedDate) {
    let result = [false, false, false, false, false, false, false];
    if (weekStartDate && weekStartDate.isSame(startOfSelectedWeek, "day")) {
      result[selectedDate.isoWeekday() - 1] = true;
    }
    return result;
  }

  /**
   * Captures the FlatList instance ref, so that
   * the scrollTo method can be called on it.
   *
   * This is populated from the DateSliderComponent
   * @param {FlatList} flatlist
   *   The FlatList instance
   *
   * @memberof DateSlider
   */
  _captureFlatListRef = flatlist => (this.flatlist = flatlist);

  /**
   * ReactNative method called by the FlatList
   * This needs to be here as it uses viewWidth held in state
   *
   * @param {Object} data
   * @param {Object} index
   *
   * @memberof DateSlider
   */
  _getItemLayout = (data, index) => {
    return {
      length: this.state.viewWidth,
      offset: this.state.viewWidth * index,
      index
    };
  };

  _onLayout = event => {
    const nativeEvent = event.nativeEvent;
    this.setState({ viewWidth: nativeEvent.layout.width });
    if (this.viewableIndex === -1) {
      this._scrollTo(this.selectedIndex);
    } else {
      this._scrollTo(this.viewableIndex);
    }
  };

  _scrollTo = index => {
    const scrollToPromise = new Promise(resolve => setTimeout(resolve, 0));
    scrollToPromise.then(() => {
      this.flatlist.scrollToIndex({
        viewPosition: 0,
        index: index,
        animated: false
      });
    });
  };

  _onEndReached = () => {
    const append1Week = this.state.arrDates[this.state.arrDates.length - 1].date
      .clone()
      .add(1, "weeks");
    const arrNewDates = this._generateDates(
      append1Week,
      4,
      this.state.selectedDate
    );
    this.setState({ arrDates: [...this.state.arrDates, ...arrNewDates] });
  };

  _onViewableItemsChanged = ({ viewableItems, changed }) => {
    if (viewableItems.length === 1) {
      this.viewableIndex = viewableItems[0].index;
    }
    //console.log("viewable " + JSON.stringify(viewableItems));
    // console.log("changed " + JSON.stringify(changed));
    if (
      _.get(changed[0], "index") === 2 &&
      _.get(viewableItems[0], "index") === 1 &&
      !_.get(changed[0], "isViewable")
    ) {
      const prepend4Weeks = this.state.arrDates[0].date
        .clone()
        .subtract(4, "weeks");
      const arrNewDates = this._generateDates(
        prepend4Weeks,
        4,
        this.state.selectedDate
      );
      this.setState({ arrDates: [...arrNewDates, ...this.state.arrDates] });
      this.selectedIndex = this.selectedIndex + 4;
      this._scrollTo(1 + 4);
    }
  };

  _onDateSelected = date => {
    const startOfSelectedWeek = date.clone().isoWeekday(1);

    let arrDates = [...this.state.arrDates];
    const prevSelected = _.findIndex(arrDates, [
      "key",
      this.state.selectedDate.clone().isoWeekday(1).toISOString()
    ]);
    const selected = _.findIndex(arrDates, [
      "key",
      startOfSelectedWeek.toISOString()
    ]);

    arrDates[prevSelected].arrSelected = this._generateArrSelected();
    arrDates[selected].arrSelected = this._generateArrSelected(
      arrDates[selected].date,
      startOfSelectedWeek,
      date
    );

    this.selectedIndex = _.findIndex(arrDates, [
      "key",
      startOfSelectedWeek.toISOString()
    ]);

    this.setState({
      arrDates: arrDates,
      selectedDate: date
    });
  };
}

export default DateSlider;
<commit_msg>Added onDateSelected and onWeekSelected props. Plus some refactoring<commit_after>import React from "react";
import { Dimensions } from "react-native";

import moment from "moment";
import _ from "lodash";

import DateSliderComponent from "./DateSliderComponent";

const END_THRESHOLD = 3;
const EXTEND_WEEKS_BY = 4;

class DateSlider extends React.Component {
  static propTypes = {
    onDateSelected: React.PropTypes.func,
    onWeekChanged: React.PropTypes.func
  };

  flatlist = undefined;
  selectedIndex = 0;
  viewableIndex = -1;

  constructor(props) {
    super(props);

    // The very first date in the FlatList
    const startingDate = moment().isoWeekday(1).subtract(4, "weeks");

    this.state = {
      viewWidth: Dimensions.get("window").width,
      arrDates: this._generateDates(startingDate, 9, moment()),
      selectedDate: moment().startOf("day")
    };
  }

  render() {
    return (
      <DateSliderComponent
        arrDates={this.state.arrDates}
        captureFlatListRef={this._captureFlatListRef}
        viewWidth={this.state.viewWidth}
        getItemLayout={this._getItemLayout}
        onDateSelected={this._onDateSelected}
        onEndReached={this._onEndReached}
        onEndReachedThreshold={END_THRESHOLD}
        onLayout={this._onLayout}
        onViewableItemsChanged={this._onViewableItemsChanged}
      />
    );
  }

  /**
   * Pure function to generate an array of objects that can be used by the FlatList.
   *
   * The FlatList renders a 'week' as a new item. This function
   * creates a number of entries equal to the weeks parameter
   * and starting from the fromDate paramater. The fromDate
   * needs to be set to the correct weekday used to start each WeekView
   * by the callee.
   *
   * @param {moment} fromDate
   *  The starting date for the array of weeks to be generated
   * @param {integer} weeks
   *  The number of weeks to be generated
   * @param {moment} selectedDate
   *  The date that is current selected
   * @returns {Array<Object>}
   *
   * @memberof DateSlider
   */
  _generateDates(fromDate, weeks, selectedDate) {
    const startOfSelectedWeek = selectedDate.clone().isoWeekday(1);

    return _.map(_.range(weeks), i => {
      const weekStartDate = fromDate.clone().add(i, "weeks").startOf("day");
      if (startOfSelectedWeek.isSame(weekStartDate, "day")) {
        this.selectedIndex = i;
      }
      return {
        key: weekStartDate.toISOString(),
        date: weekStartDate,
        arrSelected: this._generateArrSelected(
          weekStartDate,
          startOfSelectedWeek,
          selectedDate
        )
      };
    });
  }

  /**
   * A pure function to generate an array of boolean values
   * that specify if one of the days of the week is selected.
   *
   * This array is to be used as a prop on the WeekView component.
   *
   * @param {moment} weekStartDate
   *  The start date of the week this is to be generated
   * @param {moment} startOfSelectedWeek
   *  The start of the week that contains the selectedDate
   * @param {moment} selectedDate
   *  The date that is selected
   *
   * @returns {Array<boolean>}
   *
   * @memberof DateSlider
   */
  _generateArrSelected(weekStartDate, startOfSelectedWeek, selectedDate) {
    let result = [false, false, false, false, false, false, false];
    if (weekStartDate && weekStartDate.isSame(startOfSelectedWeek, "day")) {
      result[selectedDate.isoWeekday() - 1] = true;
    }
    return result;
  }

  /**
   * Captures the FlatList instance ref, so that
   * the scrollTo method can be called on it.
   * This is populated from the DateSliderComponent.
   *
   * @param {FlatList} flatlist
   *   The FlatList instance
   *
   * @memberof DateSlider
   */
  _captureFlatListRef = flatlist => (this.flatlist = flatlist);

  /**
   * ReactNative method called by the FlatList
   * This needs to be here as it uses viewWidth held in state
   *
   * @param {Object} data
   * @param {Object} index
   *
   * @memberof DateSlider
   */
  _getItemLayout = (data, index) => {
    return {
      length: this.state.viewWidth,
      offset: this.state.viewWidth * index,
      index
    };
  };

  /**
   * ReactNative method called by the view containing the FlatList
   * This needs to be here as handles setting the viewWidth
   * based on what the containing view reports.
   *
   * @param {Object} event
   *  The event generated from the RN onLayout call
   *
   * @memberof DateSlider
   */
  _onLayout = event => {
    const nativeEvent = event.nativeEvent;
    this.setState({ viewWidth: nativeEvent.layout.width });
    if (this.viewableIndex === -1) {
      this._scrollTo(this.selectedIndex);
    } else {
      this._scrollTo(this.viewableIndex);
    }
  };

  /**
   * Scrolls the flatlist to the specified index.
   * This wraps the call in a setTimeout(_,0) Promise
   * to stop issues when the index to be scrolled to has not yet been
   * rendered.
   *
   * @param {number} index
   *  The index to scroll the flatlist to
   *
   * @memberof DateSlider
   */
  _scrollTo = index => {
    const scrollToPromise = new Promise(resolve => setTimeout(resolve, 0));
    scrollToPromise.then(() => {
      this.flatlist.scrollToIndex({
        viewPosition: 0,
        index: index,
        animated: false
      });
    });
  };

  _onEndReached = () => {
    const lastDate = this.state.arrDates[this.state.arrDates.length - 1].date;
    const appendWeeksFrom = lastDate.clone().add(1, "weeks");
    const arrNewDates = this._generateDates(
      appendWeeksFrom,
      EXTEND_WEEKS_BY,
      this.state.selectedDate
    );
    this.setState({ arrDates: [...this.state.arrDates, ...arrNewDates] });
  };

  _onStartReached = () => {
    const firstDate = this.state.arrDates[0].date;
    const prependWeeksFrom = firstDate.clone().subtract(4, "weeks");
    const arrNewDates = this._generateDates(
      prependWeeksFrom,
      EXTEND_WEEKS_BY,
      this.state.selectedDate
    );
    this.setState({ arrDates: [...arrNewDates, ...this.state.arrDates] });
    this.selectedIndex = this.selectedIndex + 4;
    this._scrollTo(END_THRESHOLD - 1 + EXTEND_WEEKS_BY);
  };

  _onViewableItemsChanged = ({ viewableItems, changed }) => {
    if (viewableItems.length === 1) {
      const viewableIndex = viewableItems[0].index;
      const changedIndex = _.get(changed[0], "index");
      this.viewableIndex = viewableIndex;

      if (changedIndex === viewableIndex - 1) {
        // Week forward
        const date = this.state.arrDates[viewableIndex].date;
        this.props.onWeekChanged(date);
      }

      if (changedIndex === viewableIndex + 1) {
        // week back
        const date = this.state.arrDates[viewableIndex].date;
        this.props.onWeekChanged(date);
      }

      if (
        changedIndex === END_THRESHOLD &&
        viewableIndex === changedIndex - 1
      ) {
        this._onStartReached();
      }
    }
  };

  _onDateSelected = date => {
    this.props.onDateSelected(date);
    const startOfSelectedWeek = date.clone().isoWeekday(1);

    let arrDates = [...this.state.arrDates];
    const prevSelected = _.findIndex(arrDates, [
      "key",
      this.state.selectedDate.clone().isoWeekday(1).toISOString()
    ]);
    const selected = _.findIndex(arrDates, [
      "key",
      startOfSelectedWeek.toISOString()
    ]);

    arrDates[prevSelected].arrSelected = this._generateArrSelected();
    arrDates[selected].arrSelected = this._generateArrSelected(
      arrDates[selected].date,
      startOfSelectedWeek,
      date
    );

    this.selectedIndex = _.findIndex(arrDates, [
      "key",
      startOfSelectedWeek.toISOString()
    ]);

    this.setState({
      arrDates: arrDates,
      selectedDate: date
    });
  };
}

export default DateSlider;
<|endoftext|>