<commit_before>(function($) {

  function TypeSuggest(el, data, options, callback){
    var defaults = {
      min_characters: 2,
      display_key: undefined,
      search_key: undefined,
      ignored: [
        ' ',
        ',',
        '.'
      ],
      ajax: false,
      url: '',
      param: 'q',
      beforeSend: undefined
    };

    this.config = $.extend({}, defaults, options);

    // set parent element
    this.$el = $(el).addClass("typesuggest");

    this.data = data;

    // set callback
    this.callback = typeof callback === "function" && callback || false;

    this.init();
    this.events();

  }

  TypeSuggest.prototype.init = function(){
    var self = this;

    self.$fields = self.$el.find("input").addClass("typesuggest-input").attr('autocomplete', 'off');
    self.$suggest = $('
').appendTo(self.$el);
  }

  TypeSuggest.prototype.events = function(){
    var self = this;

    self.$fields.on('keydown', function(e){
      var $field = $(this);
      var key = e.keyCode || e.which;

      // if up or down arrows, scroll thru list. if enter key, select item
      if(key == 40 || key == 38 || key == 13){

        if(e.keyCode == 40){ // down arrow

          self.$suggest.addClass("selecting");
          self.move(+1);

          // prevent caret from moving
          return false;

        }else if(e.keyCode == 38){ // up arrow

          self.$suggest.addClass("selecting");
          self.move(-1);

          // prevent caret from moving
          return false;

        }else if(e.keyCode == 13){ // enter

          var $selectedItem = self.$suggest.find(".selected");
          var data = $selectedItem.data();

          self.selectItem(data);

          // prevent caret from moving
          return false;

        }else{ // everything else

          self.$suggest.removeClass("selecting");

        }

      }

    })

    self.$fields.on('keyup', function(e){
      var $field = $(this);
      var key = e.keyCode || e.which;

      self.$currentField = $field;

      // if not up or down arrows or enter key, fill suggestion
      if(key !== 40 && key !== 38 && key !== 13){

        if(!self.debounce){

          // get value
          var val = $field.val();

          self.options = [];

          if(val.length && val.length >= self.config.min_characters){

            self.startDebounce();

            if(self.config.ajax){
              if(self.config.beforeSend){
                val = self.config.beforeSend(val);
              }

              if(self.config.url.indexOf('?') > -1){
                var url = self.config.url + "&" + self.config.param + "=" + val;
              }else{
                var url = self.config.url + "?" + self.config.param + "=" + val;
              }

              $.ajax({
                url: url,
                type: 'GET',
                dataType: 'json',
                success: function(data, textStatus, xhr){
                  self.options = data[self.config.results_key];

                  self.updateList();
                }
              })
            }else{
              // compare value to provided list
              $.each(self.data, function(i, item){
                var valLower = val.toLowerCase();
                var itemLower;

                if(self.config.search_key){
                  itemLower = item[self.config.search_key].toLowerCase();
                }else{
                  itemLower = item.toLowerCase();
                }

                if(itemLower.search(valLower) > -1){
                  self.options.push(item);
                }
              })

            }

          }

          self.updateList();
        }
      }else{
        // prevent caret from moving
        return false;
      }
    })

    self.$suggest.on('click', 'li', function(){
      var data = $(this).data();

      self.selectItem(data);
      self.callback && self.callback(data);
    })

    self.$suggest.on('mouseover', 'li', function(){
      var $li = $(this);

      self.$suggest.find("li").removeClass("selected");
      $li.addClass("selected");
    })
  }

  TypeSuggest.prototype.move = function(where){
    var self = this;
    var $selectedItem = self.$suggest.find(".selected");
    var firstSelected = self.$suggest.find("li:first").hasClass("selected");
    var lastSelected = self.$suggest.find("li:last").hasClass("selected");

    if($selectedItem.length && where > 0){

      if(lastSelected){
        // select first
        $selectedItem.removeClass("selected");
        self.$suggest.find("li:first").addClass("selected");
      }else{
        // selection exists, go to next
        $selectedItem.removeClass("selected").next("li").addClass("selected")
      }

    }
    else if($selectedItem.length && where < 0){

      if(firstSelected){
        // select last
        $selectedItem.removeClass("selected");
        self.$suggest.find("li:last").addClass("selected");
      }else{
        // selection exists, go to prev
        $selectedItem.removeClass("selected").prev("li").addClass("selected")
      }

    }
    else{

      // select first
      self.$suggest.find("li:first").addClass("selected");

    }
  }

  TypeSuggest.prototype.updateList = function(){
    var self = this;
    var $list = $('
');

    if(self.options.length){
      self.$suggest.addClass("selecting");

      var options = self.options;
      $.each(options, function(i, option){
        var $listItem = $("
").data("option", option);

        if(self.config.display_keys){
          var output = '';

          $.each(self.config.display_keys, function(i, key){
            output += '' + option[key] + ''
          })

          $listItem.html(output);
        }else{
          $listItem.html(option);
        }

        $list.append($listItem);
      })

      self.$suggest.html($list);

    }else{
      self.$suggest.removeClass("selecting")
    }
  }

  TypeSuggest.prototype.selectItem = function(data){
    var self = this;

    self.options = [];
    self.updateList();

    if(!self.config.display_keys){
      self.$currentField.val(data.option);
    }

    self.callback && self.callback(data);
  }

  TypeSuggest.prototype.startDebounce = function(){
    var self = this;

    self.debounce = true;

    setTimeout(function(){
      self.debounce = false;
    }, 250)
  }

  $.fn.suggest = function(data, options, callback){
    var suggest = new TypeSuggest(this, data, options, callback);
    return this;
  }

})(jQuery);<commit_msg>hide when clicked outside<commit_after>(function($) {

  function TypeSuggest(el, data, options, callback){
    var defaults = {
      min_characters: 2,
      display_key: undefined,
      search_key: undefined,
      ignored: [
        ' ',
        ',',
        '.'
      ],
      ajax: false,
      url: '',
      param: 'q',
      beforeSend: undefined
    };

    this.config = $.extend({}, defaults, options);

    // set parent element
    this.$el = $(el).addClass("typesuggest");

    this.data = data;

    // set callback
    this.callback = typeof callback === "function" && callback || false;

    this.init();
    this.events();

  }

  TypeSuggest.prototype.init = function(){
    var self = this;

    self.$fields = self.$el.find("input").addClass("typesuggest-input").attr('autocomplete', 'off');
    self.$suggest = $('
').appendTo(self.$el);
  }

  TypeSuggest.prototype.events = function(){
    var self = this;

    self.$fields.on('keydown', function(e){
      var $field = $(this);
      var key = e.keyCode || e.which;

      // if up or down arrows, scroll thru list. if enter key, select item
      if(key == 40 || key == 38 || key == 13){

        if(e.keyCode == 40){ // down arrow

          self.$suggest.addClass("selecting");
          self.move(+1);

          // prevent caret from moving
          return false;

        }else if(e.keyCode == 38){ // up arrow

          self.$suggest.addClass("selecting");
          self.move(-1);

          // prevent caret from moving
          return false;

        }else if(e.keyCode == 13){ // enter

          var $selectedItem = self.$suggest.find(".selected");
          var data = $selectedItem.data();

          self.selectItem(data);

          // prevent caret from moving
          return false;

        }else{ // everything else

          self.$suggest.removeClass("selecting");

        }

      }

    })

    self.$fields.on('keyup', function(e){
      var $field = $(this);
      var key = e.keyCode || e.which;

      self.$currentField = $field;

      // if not up or down arrows or enter key, fill suggestion
      if(key !== 40 && key !== 38 && key !== 13){

        if(!self.debounce){

          // get value
          var val = $field.val();

          self.options = [];

          if(val.length && val.length >= self.config.min_characters){

            self.startDebounce();

            if(self.config.ajax){
              if(self.config.beforeSend){
                val = self.config.beforeSend(val);
              }

              if(self.config.url.indexOf('?') > -1){
                var url = self.config.url + "&" + self.config.param + "=" + val;
              }else{
                var url = self.config.url + "?" + self.config.param + "=" + val;
              }

              $.ajax({
                url: url,
                type: 'GET',
                dataType: 'json',
                success: function(data, textStatus, xhr){
                  self.options = data[self.config.results_key];

                  self.updateList();
                }
              })
            }else{
              // compare value to provided list
              $.each(self.data, function(i, item){
                var valLower = val.toLowerCase();
                var itemLower;

                if(self.config.search_key){
                  itemLower = item[self.config.search_key].toLowerCase();
                }else{
                  itemLower = item.toLowerCase();
                }

                if(itemLower.search(valLower) > -1){
                  self.options.push(item);
                }
              })

            }

          }

          self.updateList();
        }
      }else{
        // prevent caret from moving
        return false;
      }
    })

    self.$suggest.on('click', 'li', function(){
      var data = $(this).data();

      self.selectItem(data);
      self.callback && self.callback(data);
    })

    self.$suggest.on('mouseover', 'li', function(){
      var $li = $(this);

      self.$suggest.find("li").removeClass("selected");
      $li.addClass("selected");
    })

    $('html').on('click', function(){
      self.updateList(true);
    })
  }

  TypeSuggest.prototype.move = function(where){
    var self = this;
    var $selectedItem = self.$suggest.find(".selected");
    var firstSelected = self.$suggest.find("li:first").hasClass("selected");
    var lastSelected = self.$suggest.find("li:last").hasClass("selected");

    if($selectedItem.length && where > 0){

      if(lastSelected){
        // select first
        $selectedItem.removeClass("selected");
        self.$suggest.find("li:first").addClass("selected");
      }else{
        // selection exists, go to next
        $selectedItem.removeClass("selected").next("li").addClass("selected")
      }

    }
    else if($selectedItem.length && where < 0){

      if(firstSelected){
        // select last
        $selectedItem.removeClass("selected");
        self.$suggest.find("li:last").addClass("selected");
      }else{
        // selection exists, go to prev
        $selectedItem.removeClass("selected").prev("li").addClass("selected")
      }

    }
    else{

      // select first
      self.$suggest.find("li:first").addClass("selected");

    }
  }

  TypeSuggest.prototype.updateList = function(clear){
    var self = this;
    var $list = $('
');

    if(self.options.length && !clear){
      self.$suggest.addClass("selecting");

      var options = self.options;
      $.each(options, function(i, option){
        var $listItem = $("
").data("option", option);

        if(self.config.display_keys){
          var output = '';

          $.each(self.config.display_keys, function(i, key){
            output += '' + option[key] + ''
          })

          $listItem.html(output);
        }else{
          $listItem.html(option);
        }

        $list.append($listItem);
      })

      self.$suggest.html($list);

    }else{
      self.$suggest.removeClass("selecting").empty();;
    }
  }

  TypeSuggest.prototype.selectItem = function(data){
    var self = this;

    self.options = [];
    self.updateList(true);

    if(!self.config.display_keys){
      self.$currentField.val(data.option);
    }

    self.callback && self.callback(data);
  }

  TypeSuggest.prototype.startDebounce = function(){
    var self = this;

    self.debounce = true;

    setTimeout(function(){
      self.debounce = false;
    }, 250)
  }

  $.fn.suggest = function(data, options, callback){
    var suggest = new TypeSuggest(this, data, options, callback);
    return this;
  }

})(jQuery);<|endoftext|>


<commit_before>var global = require('../../global');

module.exports = function (unitReceiptNote) {

    var items = [].concat.apply([], unitReceiptNote.items);

    var iso = "FM.FP-GB-15-005/R2";
    var number = unitReceiptNote.no;

    var locale = 'id-ID';
    var dateLocaleOptions = {
        year: 'numeric',
        month: 'long',
        day: 'numeric'
    };
    var dateFormat = "DD-MMMM-YYYY";

    var moment = require('moment');
    moment.locale(locale);

    var numberLocaleOptions = {
        style: 'decimal',
        maximumFractionDigits: 4
    };

    var header = [{
        columns: [{
            columns: [{
                width: '*',
                stack: [{
                    text: 'PT DAN LIRIS',
                    style: ['size15', 'bold']
                }, {
                        text: 'BANARAN SUKOHARJO',
                        style: ['size09']
                    }]
            }]

        },
            {
                columns: [{
                    width: '*',
                    stack: [{
                        alignment: "right",
                        text: ' ',
                        style: ['size08', 'bold']
                    },{
                        alignment: "right",
                        text: iso,
                        style: ['size08', 'bold']
                    }, {
                            alignment: "right",
                            text: 'BON PENERIMAAN BARANG',
                            style: ['size09','bold']
                        }]
                }]

            }]
    }];

    var subHeader = [{
        columns: [
            {
                width: '40%',
                columns: [{
                    width: '35%',
                    stack: ['Tanggal', 'Diterima dari']
                }, {
                        width: '5%',
                        stack: [':', ':']
                    }, {
                        width: '*',
                        stack: [`${moment(unitReceiptNote.date).format(dateFormat)}`, unitReceiptNote.supplier.name]
                    }],
                style: ['size08']

            },
            {
                width: '30%',
                text: ''
            },
            {
                width: '30%',
                columns: [{
                    width: '25%',
                    stack: ['Bagian', 'No.']
                }, {
                        width: '5%',
                        stack: [':', ':']
                    }, {
                        width: '*',
                        stack: [unitReceiptNote.unit.subDivision, unitReceiptNote.no]
                    }],
                style: ['size08']
            }
        ]
    }, '\n'];

    var line = [{
        canvas: [{
    	       type: 'line',
            x1: 0,
            y1: 5,
            x2: 378,
            y2: 5,
            lineWidth: 0.5
        }
        ]
    }, '\n'];

    var thead = [{
            text: 'No.',
            style: 'tableHeader'
        }, {
            text: 'Nama barang',
            style: 'tableHeader'
        }, {
            text: 'Jumlah',
            style: 'tableHeader'
        }, {
            text: 'Satuan',
            style: 'tableHeader'
        }, {
            text: 'Keterangan',
            style: 'tableHeader'
        }];

    var tbody = items.map(function(item, index) {
        return [{
                    text: (index+1).toString() || '',
                    style: ['size07', 'center']
                },{
                    text: item.product.code +" - "+item.product.name,
                    style: ['size07', 'left']
                }, {
                    text: parseFloat(item.deliveredQuantity).toLocaleString(locale, numberLocaleOptions),
                    style: ['size07', 'center']
                }, {
                    text: item.deliveredUom.unit,
                    style: ['size07', 'center']
                }, {
                    text: item.remark || '',
                    style: ['size07', 'left']
                }];
    });

    tbody = tbody.length > 0 ? tbody : [
        [{
            text: "tidak ada barang",
            style: ['size08', 'center'],
            colSpan: 5
        }, "", "", "", ""]
    ];

    var table = [{
        table: {
            widths: ['5%', '40%', '20%', '10%', '25%'],
            headerRows: 1,
            body: [].concat([thead],tbody)
        }
    }];

    var footer = [
        '\n', {
            stack: [{
                text: `Sukoharjo, ${moment(unitReceiptNote.date).format(dateFormat)}`,
                alignment: "right"
            }, {
                    columns: [{
                            width: '35%',
                            stack: ['Mengetahui\n\n\n\n\n', '(_______________________)'],
                            style: 'center'
                        }, {
                            width: '30%',
                            text: ''
                        }, {
                            width: '35%',
                            stack: ['Yang Menerima\n\n\n\n\n', '(_______________________)'],
                            style: 'center'
                        },]
                }
            ],
            style: ['size08']
        }
    ];

    var dd = {
        pageSize: 'A5',
        pageOrientation: 'portrait',
        pageMargins: 20,
        content: [].concat(header, line, subHeader, table, footer),
        styles: {
            size06: {
                fontSize: 6
            },
            size07: {
                fontSize: 7
            },
            size08: {
                fontSize: 8
            },
            size09: {
                fontSize: 9
            },
            size10: {
                fontSize: 10
            },
            size15: {
                fontSize: 15
            },
            bold: {
                bold: true
            },
            center: {
                alignment: 'center'
            },
            left: {
                alignment: 'left'
            },
            right: {
                alignment: 'right'
            },
            justify: {
                alignment: 'justify'
            },
            tableHeader: {
                bold: true,
                fontSize: 8,
                color: 'black',
                alignment: 'center'
            }
        }
    };

    return dd;
}<commit_msg>change content pdf call variabel global in  unit-receipt-note<commit_after>var global = require('../../global');

module.exports = function (unitReceiptNote) {

    var items = [].concat.apply([], unitReceiptNote.items);

    var iso = "FM.FP-GB-15-005/R2";
    var number = unitReceiptNote.no;

    var locale = global.config.locale;

    var moment = require('moment');
    moment.locale(locale.name);

    var header = [{
        columns: [{
            columns: [{
                width: '*',
                stack: [{
                    text: 'PT DAN LIRIS',
                    style: ['size15', 'bold']
                }, {
                        text: 'BANARAN SUKOHARJO',
                        style: ['size09']
                    }]
            }]

        },
            {
                columns: [{
                    width: '*',
                    stack: [{
                        alignment: "right",
                        text: ' ',
                        style: ['size08', 'bold']
                    },{
                        alignment: "right",
                        text: iso,
                        style: ['size08', 'bold']
                    }, {
                            alignment: "right",
                            text: 'BON PENERIMAAN BARANG',
                            style: ['size09','bold']
                        }]
                }]

            }]
    }];

    var subHeader = [{
        columns: [
            {
                width: '40%',
                columns: [{
                    width: '35%',
                    stack: ['Tanggal', 'Diterima dari']
                }, {
                        width: '5%',
                        stack: [':', ':']
                    }, {
                        width: '*',
                        stack: [`${moment(unitReceiptNote.date).format(locale.date.format)}`, unitReceiptNote.supplier.name]
                    }],
                style: ['size08']

            },
            {
                width: '30%',
                text: ''
            },
            {
                width: '30%',
                columns: [{
                    width: '25%',
                    stack: ['Bagian', 'No.']
                }, {
                        width: '5%',
                        stack: [':', ':']
                    }, {
                        width: '*',
                        stack: [unitReceiptNote.unit.subDivision, unitReceiptNote.no]
                    }],
                style: ['size08']
            }
        ]
    }, '\n'];

    var line = [{
        canvas: [{
    	       type: 'line',
            x1: 0,
            y1: 5,
            x2: 378,
            y2: 5,
            lineWidth: 0.5
        }
        ]
    }, '\n'];

    var thead = [{
            text: 'No.',
            style: 'tableHeader'
        }, {
            text: 'Nama barang',
            style: 'tableHeader'
        }, {
            text: 'Jumlah',
            style: 'tableHeader'
        }, {
            text: 'Satuan',
            style: 'tableHeader'
        }, {
            text: 'Keterangan',
            style: 'tableHeader'
        }];

    var tbody = items.map(function(item, index) {
        return [{
                    text: (index+1).toString() || '',
                    style: ['size07', 'center']
                },{
                    text: item.product.code +" - "+item.product.name,
                    style: ['size07', 'left']
                }, {
                    text: parseFloat(item.deliveredQuantity).toLocaleString(locale, locale.decimal),
                    style: ['size07', 'center']
                }, {
                    text: item.deliveredUom.unit,
                    style: ['size07', 'center']
                }, {
                    text: item.remark || '',
                    style: ['size07', 'left']
                }];
    });

    tbody = tbody.length > 0 ? tbody : [
        [{
            text: "tidak ada barang",
            style: ['size08', 'center'],
            colSpan: 5
        }, "", "", "", ""]
    ];

    var table = [{
        table: {
            widths: ['5%', '40%', '20%', '10%', '25%'],
            headerRows: 1,
            body: [].concat([thead],tbody)
        }
    }];

    var footer = [
        '\n', {
            stack: [{
                text: `Sukoharjo, ${moment(unitReceiptNote.date).format(locale.date.format)}`,
                alignment: "right"
            }, {
                    columns: [{
                            width: '35%',
                            stack: ['Mengetahui\n\n\n\n\n', '(_______________________)'],
                            style: 'center'
                        }, {
                            width: '30%',
                            text: ''
                        }, {
                            width: '35%',
                            stack: ['Yang Menerima\n\n\n\n\n', '(_______________________)'],
                            style: 'center'
                        },]
                }
            ],
            style: ['size08']
        }
    ];

    var dd = {
        pageSize: 'A5',
        pageOrientation: 'portrait',
        pageMargins: 20,
        content: [].concat(header, line, subHeader, table, footer),
        styles: {
            size06: {
                fontSize: 6
            },
            size07: {
                fontSize: 7
            },
            size08: {
                fontSize: 8
            },
            size09: {
                fontSize: 9
            },
            size10: {
                fontSize: 10
            },
            size15: {
                fontSize: 15
            },
            bold: {
                bold: true
            },
            center: {
                alignment: 'center'
            },
            left: {
                alignment: 'left'
            },
            right: {
                alignment: 'right'
            },
            justify: {
                alignment: 'justify'
            },
            tableHeader: {
                bold: true,
                fontSize: 8,
                color: 'black',
                alignment: 'center'
            }
        }
    };

    return dd;
}<|endoftext|>


<commit_before>// Copyright 2002-2013, University of Colorado Boulder

/**
 * A DerivedProperty is computed based on other properties.  This implementation inherits from Property to (a) simplify
 * implementation and (b) ensure it remains consistent. Note that the setters should not be called directly, so the
 * setters (set, reset and es5 setter) throw an error if used directly.
 *
 * @author Sam Reid
 */

define( function( require ) {
  'use strict';

  var Property = require( 'AXON/Property' );
  var axon = require( 'AXON/axon' );
  var inherit = require( 'PHET_CORE/inherit' );

  /**
   * @param {Property[]} dependencies properties that this property's value is derived from
   * @param {function} derivation function that derives this property's value, expects args in the same order as dependencies
   * @constructor
   */
  axon.DerivedProperty = function DerivedProperty( dependencies, derivation ) {
    this.dependencies = dependencies;

    //Keep track of each dependency and only update the changed value, for speed
    this.dependencyValues = dependencies.map( function( property ) {return property.get();} );

    var initialValue = derivation.apply( null, this.dependencyValues );
    Property.call( this, initialValue );

    var derivedProperty = this;

    //Keep track of listeners so they can be detached
    this.dependencyListeners = [];

    for ( var i = 0; i < dependencies.length; i++ ) {
      var dependency = dependencies[ i ];
      (function( dependency, i ) {
        var listener = function( newValue ) {
          derivedProperty.dependencyValues[ i ] = newValue;
          Property.prototype.set.call( derivedProperty, derivation.apply( null, derivedProperty.dependencyValues ) );
        };
        derivedProperty.dependencyListeners.push( listener );
        dependency.lazyLink( listener );
      })( dependency, i );
    }
  };

  return inherit( Property, axon.DerivedProperty, {

      // Detaches this derived property from its dependencies.
      detach: function() {
        for ( var i = 0; i < this.dependencies.length; i++ ) {
          var dependency = this.dependencies[ i ];
          dependency.unlink( this.dependencyListeners[ i ] );
        }
        this.dependencies = null;
        this.dependencyListeners = null;
        this.dependencyValues = null;
      },

      //Override the mutators to provide an error message.  These should not be called directly, the value should only be modified when the dependencies change
      set: function( value ) { throw new Error( 'Cannot set values directly to a derived property, tried to set: ' + value ); },

      //Override the mutators to provide an error message.  These should not be called directly, the value should only be modified when the dependencies change
      //Keep the newValue output in the string so the argument won't be stripped by minifier (which would cause crashes like https://github.com/phetsims/axon/issues/15)
      set value( newValue ) { throw new Error( 'Cannot es5-set values directly to a derived property, tried to set: ' + newValue ); },

      //Override get value as well to satisfy the linter which wants get/set pairs (even though it just uses the same code as the superclass).
      get value() {return Property.prototype.get.call( this );},

      //Override the mutators to provide an error message.  These should not be called directly, the value should only be modified when the dependencies change
      reset: function() { throw new Error( 'Cannot reset a derived property directly' ); }
    }
  );
} );<commit_msg>Added options for DerivedProperty to support propertyID<commit_after>// Copyright 2002-2013, University of Colorado Boulder

/**
 * A DerivedProperty is computed based on other properties.  This implementation inherits from Property to (a) simplify
 * implementation and (b) ensure it remains consistent. Note that the setters should not be called directly, so the
 * setters (set, reset and es5 setter) throw an error if used directly.
 *
 * @author Sam Reid
 */

define( function( require ) {
  'use strict';

  var Property = require( 'AXON/Property' );
  var axon = require( 'AXON/axon' );
  var inherit = require( 'PHET_CORE/inherit' );

  /**
   * @param {Property[]} dependencies properties that this property's value is derived from
   * @param {function} derivation function that derives this property's value, expects args in the same order as dependencies
   * @param {object} [options] - see code
   * @constructor
   */
  axon.DerivedProperty = function DerivedProperty( dependencies, derivation, options ) {
    this.dependencies = dependencies;

    //Keep track of each dependency and only update the changed value, for speed
    this.dependencyValues = dependencies.map( function( property ) {return property.get();} );

    var initialValue = derivation.apply( null, this.dependencyValues );
    Property.call( this, initialValue, options );

    var derivedProperty = this;

    //Keep track of listeners so they can be detached
    this.dependencyListeners = [];

    for ( var i = 0; i < dependencies.length; i++ ) {
      var dependency = dependencies[ i ];
      (function( dependency, i ) {
        var listener = function( newValue ) {
          derivedProperty.dependencyValues[ i ] = newValue;
          Property.prototype.set.call( derivedProperty, derivation.apply( null, derivedProperty.dependencyValues ) );
        };
        derivedProperty.dependencyListeners.push( listener );
        dependency.lazyLink( listener );
      })( dependency, i );
    }
  };

  return inherit( Property, axon.DerivedProperty, {

      // Detaches this derived property from its dependencies.
      detach: function() {
        for ( var i = 0; i < this.dependencies.length; i++ ) {
          var dependency = this.dependencies[ i ];
          dependency.unlink( this.dependencyListeners[ i ] );
        }
        this.dependencies = null;
        this.dependencyListeners = null;
        this.dependencyValues = null;
      },

      //Override the mutators to provide an error message.  These should not be called directly, the value should only be modified when the dependencies change
      set: function( value ) { throw new Error( 'Cannot set values directly to a derived property, tried to set: ' + value ); },

      //Override the mutators to provide an error message.  These should not be called directly, the value should only be modified when the dependencies change
      //Keep the newValue output in the string so the argument won't be stripped by minifier (which would cause crashes like https://github.com/phetsims/axon/issues/15)
      set value( newValue ) { throw new Error( 'Cannot es5-set values directly to a derived property, tried to set: ' + newValue ); },

      //Override get value as well to satisfy the linter which wants get/set pairs (even though it just uses the same code as the superclass).
      get value() {return Property.prototype.get.call( this );},

      //Override the mutators to provide an error message.  These should not be called directly, the value should only be modified when the dependencies change
      reset: function() { throw new Error( 'Cannot reset a derived property directly' ); }
    }
  );
} );<|endoftext|>


<commit_before>const debug = require('debug')
const fetch = require('node-fetch')
const log = debug('app:provider:youtube')
const Scanner = require('../Scanner')
const Media = require('../../Media')
const parseArtistTitle = require('../../lib/parseArtistTitle')
const { parse, toSeconds } = require('iso8601-duration')

class YouTubeScanner extends Scanner {
  constructor (prefs) {
    super()
    this.prefs = prefs

    if (!prefs.apiKey) {
      throw new Error('Please set your YouTube API key')
    }
  }

  async scan () {
    let items = []
    let validIds = [] // mediaIds for cleanup

    // list all videos from all channels
    for (const channel of this.prefs.channels) {
      if (this.isCanceling) {
        break
      }

      this.emitStatus(`Listing "${channel}"`, 0)

      try {
        const res = await this.getPlaylistItems(channel)
        items = items.concat(res)
      } catch (err) {
        log('  => %s', err)
      }
    } // end for

    if (this.isCanceling) {
      log('Canceling scan (user requested)')
      return this.emitDone()
    }

    for (let i = 0; i < items.length; i++) {
      this.emitStatus(`Processing videos (${i} of ${items.length})`, (i / items.length) * 100)

      try {
        const mediaId = await process(items[i])
        validIds.push(mediaId)
      } catch (err) {
        log(err.message)
      }
    } // end for

    return this.emitDone()
  }

  /**
   * List all videos in a channel (using its "uploads" playlist)
   *
   * @param  {string}  channel YT channel or username
   * @return {Promise} array of video items
   */
  async getPlaylistItems (channel) {
    let playlistId, playlist
    let items = []

    // get channel/playlist info for youtube user
    try {
      let res = await this.callApi(`channels?forUsername=${channel}&part=contentDetails`)
      playlistId = res.items[0].contentDetails.relatedPlaylists.uploads

      if (typeof playlistId !== 'string') {
        throw new Error('invalid playlist id')
      }
    } catch (err) {
      log(err.message)
      return Promise.reject(err)
    }

    if (this.isCanceling) {
      return
    }

    // get playlist page by page
    do {
      let url = `playlistItems?playlistId=${playlistId}&maxResults=50&part=snippet`

      if (playlist && playlist.nextPageToken) {
        url += '&pageToken=' + playlist.nextPageToken
      }

      try {
        playlist = await this.callApi(url)
      } catch (err) {
        log(err.message)
        return Promise.reject(err)
      }

      // get durations for playlist items
      let videoIds = playlist.items.map(item => item.snippet.resourceId.videoId)
      let details

      try {
        details = await this.callApi(`videos?part=contentDetails&id=` + videoIds.join(','))
      } catch (err) {
        log(err.message)
        return Promise.reject(err)
      }

      if (this.isCanceling) {
        break
      }

      // merge snippet and contentDetails data into one
      // object and add it to our final array of items
      playlist.items.forEach((item, i) => {
        items.push(Object.assign({
          channel,
        }, playlist.items[i], details.items[i]))
      })

      this.emitStatus(`Listing "${channel}" (${items.length} of ${playlist.pageInfo.totalResults})`,
        (items.length / playlist.pageInfo.totalResults) * 100)

      log('got %s of %s items', items.length, playlist.pageInfo.totalResults)
    } while (playlist.nextPageToken && !this.isCanceling)

    return items
  }

  /**
   * Process a video item
   *
   * @param  {object} item YT composite (snippet + details) video item
   * @return {number} mediaId
   */
  async process (item) {
    log('processing: %s', JSON.stringify({
      title: item.snippet.title,
      videoId: item.id,
      duration: item.contentDetails.duration,
    }))

    // is video already in the db?
    // try {
    //   let res = await getLibrary({ providerData: { videoId: item.id } })
    //   // @todo: check mtime and title for updates
    //   if (res.songs.result.length) {
    //     log('song is in library (same videoId)')
    //     stats.ok++
    //     return Promise.resolve(res.songs.result[0])
    //   }
    // } catch (err) {
    //   log(err.message)
    //   return Promise.reject(err)
    // }

    // new song
    const { artist, title } = parseArtistTitle(item.snippet.title)

    if (!artist || !title) {
      log(' => skipping: couldn\'t parse artist/title from video title')
      return Promise.reject(new Error('couldn\'t parse artist/title'))
    }

    const song = {
      artist,
      title,
      provider: 'youtube',
      duration: toSeconds(parse(item.contentDetails.duration)),
      providerData: {
        videoId: item.id,
        channel: item.username,
        publishedAt: item.snippet.publishedAt,
      }
    }

    try {
      const mediaId = await Media.add(song)
      return Promise.resolve(mediaId)
    } catch (err) {
      log(err.message)
      return Promise.reject(err)
    }
  }

  /**
  * Make an API call
  *
  * @param  {string}  params Appended to the base URL
  * @return {Promise}        Decoded JSON response
  */
  async callApi (params) {
    const API = 'https://www.googleapis.com/youtube/v3/'
    const url = API + params + `&key=${this.prefs.apiKey}`
    log('request: %s', API + params.substr(0, params.indexOf('?')))

    try {
      let res = await fetch(url)
      let decoded = await res.json()
      return Promise.resolve(decoded)
    } catch (err) {
      log(err.message)
      return Promise.reject(err)
    }
  }
}

module.exports = YouTubeScanner

// function parseArtistTitle (str) {
//   let title, artist
//   const phrases = [
//     'karaoke video version with lyrics',
//     'karaoke version with lyrics',
//     'karaoke video with lyrics',
//     'karaoke version with lyrics',
//     'karaoke video',
//     'karaoke version',
//     'with lyrics',
//     'no lead vocal',
//     'singalong',
//   ]
//
//   // remove anything after last |
//   if (str.lastIndexOf('|') !== -1) {
//     str = str.substring(0, str.lastIndexOf('|'))
//   }
//
//   // remove anything in parantheses
//   str = str.replace(/ *\([^)]*\) */g, '')
//
//   // de-"in the style of"
//   let delim = str.toLowerCase().indexOf('in the style of')
//   if (delim !== -1) {
//     let parts = str.split(str.substring(delim, delim + 15))
//     str = parts[1] + '-' + parts[0]
//   }
//
//   // split into song title and artist
//   let parts = str.split('-')
//
//   if (parts.length < 2) {
//     return false
//   }
//
//   return { artist : parts[0].trim(), title : parts[1].trim() }
// }
<commit_msg>Improve cancelation check placement<commit_after>const debug = require('debug')
const fetch = require('node-fetch')
const log = debug('app:provider:youtube')
const Scanner = require('../Scanner')
const Media = require('../../Media')
const parseArtistTitle = require('../../lib/parseArtistTitle')
const { parse, toSeconds } = require('iso8601-duration')

class YouTubeScanner extends Scanner {
  constructor (prefs) {
    super()
    this.prefs = prefs

    if (!prefs.apiKey) {
      throw new Error('Please set your YouTube API key')
    }
  }

  async scan () {
    let items = []
    let validIds = [] // mediaIds for cleanup

    // list all videos from all channels
    for (const channel of this.prefs.channels) {
      if (this.isCanceling) {
        break
      }

      this.emitStatus(`Listing "${channel}"`, 0)

      try {
        const res = await this.getPlaylistItems(channel)
        items = items.concat(res)
      } catch (err) {
        log('  => %s', err)
      }
    } // end for

    if (this.isCanceling) {
      log('Canceling scan (user requested)')
      return this.emitDone()
    }

    for (let i = 0; i < items.length; i++) {
      this.emitStatus(`Processing videos (${i} of ${items.length})`, (i / items.length) * 100)

      try {
        const mediaId = await process(items[i])
        validIds.push(mediaId)
      } catch (err) {
        log(err.message)
      }
    } // end for

    return this.emitDone()
  }

  /**
   * List all videos in a channel (using its "uploads" playlist)
   *
   * @param  {string}  channel YT channel or username
   * @return {Promise} array of video items
   */
  async getPlaylistItems (channel) {
    let playlistId, playlist
    let items = []

    // get channel/playlist info for youtube user
    try {
      let res = await this.callApi(`channels?forUsername=${channel}&part=contentDetails`)
      playlistId = res.items[0].contentDetails.relatedPlaylists.uploads

      if (typeof playlistId !== 'string') {
        throw new Error('invalid playlist id')
      }
    } catch (err) {
      log(err.message)
      return Promise.reject(err)
    }

    if (this.isCanceling) {
      return
    }

    // get playlist page by page
    do {
      let url = `playlistItems?playlistId=${playlistId}&maxResults=50&part=snippet`

      if (playlist && playlist.nextPageToken) {
        url += '&pageToken=' + playlist.nextPageToken
      }

      try {
        playlist = await this.callApi(url)
      } catch (err) {
        log(err.message)
        return Promise.reject(err)
      }

      if (this.isCanceling) {
        break
      }

      // get durations for playlist items
      let videoIds = playlist.items.map(item => item.snippet.resourceId.videoId)
      let details

      try {
        details = await this.callApi(`videos?part=contentDetails&id=` + videoIds.join(','))
      } catch (err) {
        log(err.message)
        return Promise.reject(err)
      }

      // merge snippet and contentDetails data into one
      // object and add it to our final array of items
      playlist.items.forEach((item, i) => {
        items.push(Object.assign({
          channel,
        }, playlist.items[i], details.items[i]))
      })

      this.emitStatus(`Listing "${channel}" (${items.length} of ${playlist.pageInfo.totalResults})`,
        (items.length / playlist.pageInfo.totalResults) * 100)

      log('got %s of %s items', items.length, playlist.pageInfo.totalResults)
    } while (playlist.nextPageToken && !this.isCanceling)

    return items
  }

  /**
   * Process a video item
   *
   * @param  {object} item YT composite (snippet + details) video item
   * @return {number} mediaId
   */
  async process (item) {
    log('processing: %s', JSON.stringify({
      title: item.snippet.title,
      videoId: item.id,
      duration: item.contentDetails.duration,
    }))

    // is video already in the db?
    // try {
    //   let res = await getLibrary({ providerData: { videoId: item.id } })
    //   // @todo: check mtime and title for updates
    //   if (res.songs.result.length) {
    //     log('song is in library (same videoId)')
    //     stats.ok++
    //     return Promise.resolve(res.songs.result[0])
    //   }
    // } catch (err) {
    //   log(err.message)
    //   return Promise.reject(err)
    // }

    // new song
    const { artist, title } = parseArtistTitle(item.snippet.title)

    if (!artist || !title) {
      log(' => skipping: couldn\'t parse artist/title from video title')
      return Promise.reject(new Error('couldn\'t parse artist/title'))
    }

    const song = {
      artist,
      title,
      provider: 'youtube',
      duration: toSeconds(parse(item.contentDetails.duration)),
      providerData: {
        videoId: item.id,
        channel: item.username,
        publishedAt: item.snippet.publishedAt,
      }
    }

    try {
      const mediaId = await Media.add(song)
      return Promise.resolve(mediaId)
    } catch (err) {
      log(err.message)
      return Promise.reject(err)
    }
  }

  /**
  * Make an API call
  *
  * @param  {string}  params Appended to the base URL
  * @return {Promise}        Decoded JSON response
  */
  async callApi (params) {
    const API = 'https://www.googleapis.com/youtube/v3/'
    const url = API + params + `&key=${this.prefs.apiKey}`
    log('request: %s', API + params.substr(0, params.indexOf('?')))

    try {
      let res = await fetch(url)
      let decoded = await res.json()
      return Promise.resolve(decoded)
    } catch (err) {
      log(err.message)
      return Promise.reject(err)
    }
  }
}

module.exports = YouTubeScanner

// function parseArtistTitle (str) {
//   let title, artist
//   const phrases = [
//     'karaoke video version with lyrics',
//     'karaoke version with lyrics',
//     'karaoke video with lyrics',
//     'karaoke version with lyrics',
//     'karaoke video',
//     'karaoke version',
//     'with lyrics',
//     'no lead vocal',
//     'singalong',
//   ]
//
//   // remove anything after last |
//   if (str.lastIndexOf('|') !== -1) {
//     str = str.substring(0, str.lastIndexOf('|'))
//   }
//
//   // remove anything in parantheses
//   str = str.replace(/ *\([^)]*\) */g, '')
//
//   // de-"in the style of"
//   let delim = str.toLowerCase().indexOf('in the style of')
//   if (delim !== -1) {
//     let parts = str.split(str.substring(delim, delim + 15))
//     str = parts[1] + '-' + parts[0]
//   }
//
//   // split into song title and artist
//   let parts = str.split('-')
//
//   if (parts.length < 2) {
//     return false
//   }
//
//   return { artist : parts[0].trim(), title : parts[1].trim() }
// }
<|endoftext|>


<commit_before>var global = require('../../global');

module.exports = function (unitReceiptNote) {

    var items = [].concat.apply([], unitReceiptNote.items);

    var iso = "FM-PB-00-06-010";
    var number = unitReceiptNote.no;

    var locale = global.config.locale;

    var moment = require('moment');
    moment.locale(locale.name);

    var header = [{
        alignment: "center",
        text: 'BON PENERIMAAN BARANG',
        style: ['size10', 'bold']
    }, {
            columns: [
                {
                    columns: [{
                        width: '*',
                        stack: [{
                            text: 'PT DAN LIRIS',
                            style: ['size15', 'bold']
                        }, {
                                text: 'BANARAN, GROGOL, SUKOHARJO',
                                style: ['size09']
                            }]
                    }]

                },
                {
                    columns: [{
                        width: '*',
                        stack: [{
                            alignment: "right",
                            text: ' ',
                            style: ['size15', 'bold']
                        }, {
                                alignment: "right",
                                text: iso,
                                style: ['size08', 'bold']
                            }]
                    }]

                }]
        }];

    var subHeader = [{
        columns: [
            {
                width: '60%',
                stack: [{
                    columns: [{
                        width: '30%',
                        text: 'Tanggal'
                    }, {
                            width: '5%',
                            text: ':'
                        }, {
                            width: '*',
                            text: `${moment(unitReceiptNote.date).format(locale.date.format)}`
                        }]
                }, {
                        columns: [{
                            width: '30%',
                            text: 'Diterima dari'
                        }, {
                                width: '5%',
                                text: ':'
                            }, {
                                width: '*',
                                text: unitReceiptNote.supplier.name
                            }]
                    }
                ],
                style: ['size08']
            },
            {
                width: '10%',
                text: ''
            },
            {
                width: '30%',
                stack: [{
                    columns: [{
                        width: '25%',
                        text: 'Bagian'
                    }, {
                            width: '5%',
                            text: ':'
                        }, {
                            width: '*',
                            text: unitReceiptNote.unit.name
                        }]
                }, {
                        columns: [{
                            width: '25%',
                            text: 'No.'
                        }, {
                                width: '5%',
                                text: ':'
                            }, {
                                width: '*',
                                text: unitReceiptNote.no
                            }]
                    }],
                style: ['size08']
            }
        ]
    }, '\n'];

    var line = [{
        canvas: [{
            type: 'line',
            x1: 0,
            y1: 5,
            x2: 378,
            y2: 5,
            lineWidth: 0.5
        }
        ]
    }, '\n'];

    var thead = [{
        text: 'No.',
        style: 'tableHeader'
    }, {
            text: 'Nama barang',
            style: 'tableHeader'
        }, {
            text: 'Jumlah',
            style: 'tableHeader'
        }, {
            text: 'Satuan',
            style: 'tableHeader'
        }, {
            text: 'Keterangan',
            style: 'tableHeader'
        }];

    var tbody = items.map(function (item, index) {
        return [{
            text: (index + 1).toString() || '',
            style: ['size08', 'center']
        }, {
                text: item.product.code + " - " + item.product.name,
                style: ['size08', 'left']
            }, {
                text: parseFloat(item.deliveredQuantity).toLocaleString(locale, locale.decimal),
                style: ['size08', 'center']
            }, {
                text: item.deliveredUom.unit,
                style: ['size08', 'center']
            }, {
                text: item.remark || '',
                style: ['size08', 'left']
            }];
    });

    tbody = tbody.length > 0 ? tbody : [
        [{
            text: "tidak ada barang",
            style: ['size08', 'center'],
            colSpan: 5
        }, "", "", "", ""]
    ];

    var table = [{
        table: {
            widths: ['5%', '40%', '20%', '10%', '25%'],
            headerRows: 1,
            body: [].concat([thead], tbody)
        }
    }];

    var footer = [
        '\n', {
            stack: [{
                text: `Sukoharjo, ${moment(unitReceiptNote.date).format(locale.date.format)}`,
                alignment: "right"
            }, {
                    columns: [{
                        width: '35%',
                        stack: ['Mengetahui\n\n\n\n\n', '(_______________________)'],
                        style: 'center'
                    }, {
                            width: '30%',
                            text: ''
                        }, {
                            width: '35%',
                            stack: ['Yang Menerima\n\n\n\n\n', '(_______________________)'],
                            style: 'center'
                        }]
                }
            ],
            style: ['size08']
        }
    ];

    var dd = {
        pageSize: 'A6',
        pageOrientation: 'landscape',
        pageMargins: 20,
        content: [].concat(header, line, subHeader, table, footer),
        styles: {
            size06: {
                fontSize: 6
            },
            size07: {
                fontSize: 7
            },
            size08: {
                fontSize: 8
            },
            size09: {
                fontSize: 9
            },
            size10: {
                fontSize: 10
            },
            size15: {
                fontSize: 15
            },
            bold: {
                bold: true
            },
            center: {
                alignment: 'center'
            },
            left: {
                alignment: 'left'
            },
            right: {
                alignment: 'right'
            },
            justify: {
                alignment: 'justify'
            },
            tableHeader: {
                bold: true,
                fontSize: 8,
                color: 'black',
                alignment: 'center'
            }
        }
    };

    return dd;
}<commit_msg>fix file def<commit_after>var global = require('../../global');

module.exports = function (unitReceiptNote) {

    var items = [].concat.apply([], unitReceiptNote.items);

    var iso = "FM-PB-00-06-010";
    var number = unitReceiptNote.no;

    var locale = global.config.locale;

    var moment = require('moment');
    moment.locale(locale.name);

    var header = [{
        alignment: "center",
        text: 'BON PENERIMAAN BARANG',
        style: ['size10', 'bold']
    }, {
            columns: [
                {
                    columns: [{
                        width: '*',
                        stack: [{
                            text: 'PT DAN LIRIS',
                            style: ['size15', 'bold']
                        }, {
                                text: 'BANARAN, GROGOL, SUKOHARJO',
                                style: ['size09']
                            }]
                    }]

                },
                {
                    columns: [{
                        width: '*',
                        stack: [{
                            alignment: "right",
                            text: ' ',
                            style: ['size15', 'bold']
                        }, {
                                alignment: "right",
                                text: iso,
                                style: ['size08', 'bold']
                            }]
                    }]

                }]
        }];

    var subHeader = [{
        columns: [
            {
                width: '50%',
                stack: [{
                    columns: [{
                        width: '25%',
                        text: 'Tanggal'
                    }, {
                            width: '5%',
                            text: ':'
                        }, {
                            width: '*',
                            text: `${moment(unitReceiptNote.date).format(locale.date.format)}`
                        }]
                }, {
                        columns: [{
                            width: '25%',
                            text: 'Diterima dari'
                        }, {
                                width: '5%',
                                text: ':'
                            }, {
                                width: '*',
                                text: unitReceiptNote.supplier.name
                            }]
                    }
                ],
                style: ['size08']
            },
            {
                width: '10%',
                text: ''
            },
            {
                width: '40%',
                stack: [{
                    columns: [{
                        width: '25%',
                        text: 'Bagian'
                    }, {
                            width: '5%',
                            text: ':'
                        }, {
                            width: '*',
                            text: unitReceiptNote.unit.name
                        }]
                }, {
                        columns: [{
                            width: '25%',
                            text: 'No.'
                        }, {
                                width: '5%',
                                text: ':'
                            }, {
                                width: '*',
                                text: unitReceiptNote.no
                            }]
                    }],
                style: ['size08']
            }
        ]
    }, '\n'];

    var line = [{
        canvas: [{
            type: 'line',
            x1: 0,
            y1: 5,
            x2: 378,
            y2: 5,
            lineWidth: 0.5
        }
        ]
    }, '\n'];

    var thead = [{
        text: 'No.',
        style: 'tableHeader'
    }, {
            text: 'Nama barang',
            style: 'tableHeader'
        }, {
            text: 'Jumlah',
            style: 'tableHeader'
        }, {
            text: 'Satuan',
            style: 'tableHeader'
        }, {
            text: 'Keterangan',
            style: 'tableHeader'
        }];

    var tbody = items.map(function (item, index) {
        return [{
            text: (index + 1).toString() || '',
            style: ['size08', 'center']
        }, {
                text: item.product.code + " - " + item.product.name,
                style: ['size08', 'left']
            }, {
                text: parseFloat(item.deliveredQuantity).toLocaleString(locale, locale.decimal),
                style: ['size08', 'center']
            }, {
                text: item.deliveredUom.unit,
                style: ['size08', 'center']
            }, {
                text: item.remark || '',
                style: ['size08', 'left']
            }];
    });

    tbody = tbody.length > 0 ? tbody : [
        [{
            text: "tidak ada barang",
            style: ['size08', 'center'],
            colSpan: 5
        }, "", "", "", ""]
    ];

    var table = [{
        table: {
            widths: ['5%', '40%', '20%', '10%', '25%'],
            headerRows: 1,
            body: [].concat([thead], tbody)
        }
    }];

    var footer = [
        '\n', {
            stack: [{
                text: `Sukoharjo, ${moment(unitReceiptNote.date).format(locale.date.format)}`,
                alignment: "right"
            }, {
                    columns: [{
                        width: '35%',
                        stack: ['Mengetahui\n\n\n\n\n', '(_______________________)'],
                        style: 'center'
                    }, {
                            width: '30%',
                            text: ''
                        }, {
                            width: '35%',
                            stack: ['Yang Menerima\n\n\n\n\n', '(_______________________)'],
                            style: 'center'
                        }]
                }
            ],
            style: ['size08']
        }
    ];

    var dd = {
        pageSize: 'A6',
        pageOrientation: 'landscape',
        pageMargins: 20,
        content: [].concat(header, line, subHeader, table, footer),
        styles: {
            size06: {
                fontSize: 6
            },
            size07: {
                fontSize: 7
            },
            size08: {
                fontSize: 8
            },
            size09: {
                fontSize: 9
            },
            size10: {
                fontSize: 10
            },
            size15: {
                fontSize: 15
            },
            bold: {
                bold: true
            },
            center: {
                alignment: 'center'
            },
            left: {
                alignment: 'left'
            },
            right: {
                alignment: 'right'
            },
            justify: {
                alignment: 'justify'
            },
            tableHeader: {
                bold: true,
                fontSize: 8,
                color: 'black',
                alignment: 'center'
            }
        }
    };

    return dd;
}<|endoftext|>


<commit_before>// Copyright 2018, University of Colorado Boulder

/**
 * Slider that shows a spectrum of colors for selecting a frequency.
 *
 * @author Sam Reid (PhET Interactive Simulations)
 */
define( function( require ) {
  'use strict';

  // modules
  var inherit = require( 'PHET_CORE/inherit' );
  var sceneryPhet = require( 'SCENERY_PHET/sceneryPhet' );
  var SpectrumSlider = require( 'SCENERY_PHET/SpectrumSlider' );
  var StringUtils = require( 'PHETCOMMON/util/StringUtils' );
  var Util = require( 'DOT/Util' );
  var VisibleColor = require( 'SCENERY_PHET/VisibleColor' );

  // strings
  var frequencyUnitsPatternString = require( 'string!SCENERY_PHET/frequencyUnitsPattern' );

  // constants
  var TERA = 1E12; // by default, the FrequencySlider uses TeraHz (THz), which is 1E12 Hz

  /**
   * @param {Property.<number>} frequencyProperty - frequency in Hz
   * @param {Object} [options]
   * @constructor
   */
  function FrequencySlider( frequencyProperty, options ) {

    // options that are specific to this type
    options = _.extend( {

      minFrequency: VisibleColor.MIN_FREQUENCY,
      maxFrequency: VisibleColor.MAX_FREQUENCY,

      // By default the values are displayed in tera Hz (THz)
      valueToString: function( value ) {
        return StringUtils.fillIn( frequencyUnitsPatternString, {
          frequency: Util.toFixed( value / TERA, 0 )
        } );
      },
      valueToColor: function( value ) {
        return VisibleColor.frequencyToColor( value );
      },

      tweakerValueDelta: TERA // Since the value is shown in TeraHz
    }, options );
    assert && assert( options.minValue === undefined, 'minValue is supplied by FrequencySlider' );
    assert && assert( options.maxValue === undefined, 'maxValue is supplied by FrequencySlider' );
    options.minValue = options.minFrequency;
    options.maxValue = options.maxFrequency;

    SpectrumSlider.call( this, frequencyProperty, options );
  }

  sceneryPhet.register( 'FrequencySlider', FrequencySlider );

  return inherit( SpectrumSlider, FrequencySlider );
} );<commit_msg>don't defined any acronym using another acronym https://github.com/phetsims/scenery-phet/issues/371<commit_after>// Copyright 2018, University of Colorado Boulder

/**
 * Slider that shows a spectrum of colors for selecting a frequency.
 *
 * @author Sam Reid (PhET Interactive Simulations)
 */
define( function( require ) {
  'use strict';

  // modules
  var inherit = require( 'PHET_CORE/inherit' );
  var sceneryPhet = require( 'SCENERY_PHET/sceneryPhet' );
  var SpectrumSlider = require( 'SCENERY_PHET/SpectrumSlider' );
  var StringUtils = require( 'PHETCOMMON/util/StringUtils' );
  var Util = require( 'DOT/Util' );
  var VisibleColor = require( 'SCENERY_PHET/VisibleColor' );

  // strings
  var frequencyUnitsPatternString = require( 'string!SCENERY_PHET/frequencyUnitsPattern' );

  // constants
  var TERA = 1E12; // by default, the FrequencySlider uses terahertz (THz), which is 1E12 Hz

  /**
   * @param {Property.<number>} frequencyProperty - frequency in Hz
   * @param {Object} [options]
   * @constructor
   */
  function FrequencySlider( frequencyProperty, options ) {

    // options that are specific to this type
    options = _.extend( {

      minFrequency: VisibleColor.MIN_FREQUENCY,
      maxFrequency: VisibleColor.MAX_FREQUENCY,

      // By default the values are displayed in tera Hz (THz)
      valueToString: function( value ) {
        return StringUtils.fillIn( frequencyUnitsPatternString, {
          frequency: Util.toFixed( value / TERA, 0 )
        } );
      },
      valueToColor: function( value ) {
        return VisibleColor.frequencyToColor( value );
      },

      tweakerValueDelta: TERA // Since the value is shown in TeraHz
    }, options );
    assert && assert( options.minValue === undefined, 'minValue is supplied by FrequencySlider' );
    assert && assert( options.maxValue === undefined, 'maxValue is supplied by FrequencySlider' );
    options.minValue = options.minFrequency;
    options.maxValue = options.maxFrequency;

    SpectrumSlider.call( this, frequencyProperty, options );
  }

  sceneryPhet.register( 'FrequencySlider', FrequencySlider );

  return inherit( SpectrumSlider, FrequencySlider );
} );<|endoftext|>


<commit_before>'use strict';

/**
 *  server
 **/

/**
 *  server.admin
 **/

/**
 *  server.admin.settings
 **/

/**
 *  server.admin.settings.global
 **/

/**
 *  server.admin.settings.global.group
 **/

/*global nodeca*/

/**
 *  server.admin.settings.global.group.show(params, callback) -> Void
 *
 *  Show group settings
 **/
module.exports = function (params, next) {
  var data = this.response.data;
  var group_name = params.id;

  data.group_name = group_name;
  data.group = nodeca.settings.global.fetchSettingsByGroup(group_name);

  next();
};
<commit_msg>added missed params schemas<commit_after>'use strict';

/**
 *  server
 **/

/**
 *  server.admin
 **/

/**
 *  server.admin.settings
 **/

/**
 *  server.admin.settings.global
 **/

/**
 *  server.admin.settings.global.group
 **/

/*global nodeca*/

// Validate input parameters
//
var params_schema = {
  // thread id
  id: {
    type: 'string',
    required: true
  }
}
nodeca.validate(params_schema);

/**
 *  server.admin.settings.global.group.show(params, callback) -> Void
 *
 *  Show group settings
 **/
module.exports = function (params, next) {
  var data = this.response.data;
  var group_name = params.id;

  data.group_name = group_name;
  data.group = nodeca.settings.global.fetchSettingsByGroup(group_name);

  next();
};
<|endoftext|>


<commit_before>// Copyright 2013-2020, University of Colorado Boulder

/**
 * An observable array of items which sends notifications when items are added or removed.
 *
 * Because the array is observable, we must be careful about the possibility of concurrent-modification errors.
 * Any time we iterate over the array, we must iterate over a copy, because callback may be modifying the array.
 *
 * @author Sam Reid (PhET Interactive Simulations)
 * @author Chris Malley (PixelZoom, Inc.)
 */

import inherit from '../../phet-core/js/inherit.js';
import merge from '../../phet-core/js/merge.js';
import PhetioObject from '../../tandem/js/PhetioObject.js';
import Tandem from '../../tandem/js/Tandem.js';
import ObjectIO from '../../tandem/js/types/ObjectIO.js';
import axon from './axon.js';
import NumberProperty from './NumberProperty.js';
import ObservableArrayIO from './ObservableArrayIO.js';

// Factor out to reduce memory footprint, see https://github.com/phetsims/tandem/issues/71
const DefaultObservableArrayIOType = ObservableArrayIO( ObjectIO );

/**
 * @param {Object} [options]
 * @constructor
 */
function ObservableArray( options ) {
  assert && assert( !Array.isArray( options ), 'ObservableArray cannot be initialized with values' );

  options = merge( {
    allowDuplicates: false, // are duplicate items allowed in the array?
    phetioType: DefaultObservableArrayIOType,
    tandem: Tandem.OPTIONAL
  }, options );

  assert && assert( options.phetioType && options.phetioType.parameterTypes, 'ObservableArray\'s phetioType should have a parameterType' );
  assert && assert( options.phetioType && options.phetioType.parameterTypes.length === 1, 'ObservableArray\'s phetioType should have a parameterType' );
  assert && assert( options.phetioType && options.phetioType.parameterTypes[ 0 ], 'ObservableArray\'s phetioType should have a parameterType' );

  this.allowDuplicates = options.allowDuplicates; // @private

  this._options = options; // @private, for creating internal ObservableArray copies, see map/filter

  this._array = []; // @private internal, do not access directly
  this._addedListeners = []; // @private listeners called when an item is added
  this._removedListeners = []; // @private listeners called when an item is removed

  // @public (read-only) observe this, but don't set it
  this.lengthProperty = new NumberProperty( this._array.length, {
    numberType: 'Integer',
    tandem: options.tandem.createTandem( 'lengthProperty' ),
    phetioReadOnly: true
  } );

  PhetioObject.call( this, options );
}

axon.register( 'ObservableArray', ObservableArray );

inherit( PhetioObject, ObservableArray, {

  // @public
  dispose: function() {
    this.lengthProperty.dispose();
    PhetioObject.prototype.dispose.call( this );
  },

  /**
   * Restore the array back to its initial state
   * Note: if an item is in the current array and original array, it is removed and added back
   * This may or may not change in the future, see #4
   * @public
   */
  reset: function() {
    this.clear();
  },

  // @public
  get length() { return this._array.length; },

  /**
   * Adds a listener that will be notified when an item is added to the list.
   * @param listener function( item, observableArray )
   * @public
   */
  addItemAddedListener: function( listener ) {
    assert && assert( this._addedListeners.indexOf( listener ) === -1, 'listener is already registered' );
    this._addedListeners.push( listener );
  },

  /**
   * Removes a listener that was added via addItemAddedListener.
   * @param listener
   * @public
   */
  removeItemAddedListener: function( listener ) {
    const index = this._addedListeners.indexOf( listener );
    assert && assert( index !== -1, 'listener is not registered' );
    this._addedListeners.splice( index, 1 );
  },

  /**
   * Adds a listener that will be notified when an item is removed from the list.
   * @param listener function( item, observableArray )
   * @public
   */
  addItemRemovedListener: function( listener ) {
    assert && assert( this._removedListeners.indexOf( listener ) === -1, 'listener is already registered' );
    this._removedListeners.push( listener );
  },

  /**
   * Removes a listener that was added via addItemRemovedListener.
   * @param listener
   * @public
   */
  removeItemRemovedListener: function( listener ) {
    const index = this._removedListeners.indexOf( listener );
    assert && assert( index !== -1, 'listener is not registered' );
    this._removedListeners.splice( index, 1 );
  },

  // @private called when an item is added.
  _fireItemAdded: function( item ) {
    const self = this;
    this.phetioStartEvent( 'itemAdded', {
      getData: function() {
        return self.phetioType.parameterTypes[ 0 ].toStateObject( item );
      }
    } );

    //Signify that an item was added to the list
    const copy = this._addedListeners.slice( 0 ); // operate on a copy, firing could result in the listeners changing
    for ( let i = 0; i < copy.length; i++ ) {
      copy[ i ]( item, this );
    }

    this.phetioEndEvent();
  },

  // @private called when an item is removed.
  _fireItemRemoved: function( item ) {
    const self = this;
    this.phetioStartEvent( 'itemRemoved', {
      getData: function() {
        return self.phetioType.parameterTypes[ 0 ].toStateObject( item );
      }
    } );

    //Signify that an item was removed from the list
    const copy = this._removedListeners.slice( 0 ); // operate on a copy, firing could result in the listeners changing
    for ( let i = 0; i < copy.length; i++ ) {
      copy[ i ]( item, this );
    }

    this.phetioEndEvent();
  },

  /**
   * Adds an item to the end of the array.
   * This is a convenience function, and is the same as push.
   * @param item
   * @public
   */
  add: function( item ) {
    this.push( item );
  },

  /**
   * Add items to the end of the array.
   * This is a convenience function, and is the same as push.
   * @param {Array} items
   * @public
   */
  addAll: function( items ) {
    for ( let i = 0; i < items.length; i++ ) {
      this.add( items[ i ] );
    }
  },

  /**
   * Removes the first occurrence of an item from the array.
   * If duplicates are allowed (see options.allowDuplicates) you may need to call this multiple
   * times to totally purge item from the array.
   * @param item
   * @public
   */
  remove: function( item ) {
    const index = this._array.indexOf( item );
    if ( index !== -1 ) {
      this._array.splice( index, 1 );
      this.lengthProperty.set( this._array.length );
      this._fireItemRemoved( item );
    }
  },

  /**
   * Removes the first occurrence of each item in the specified array.
   * @param {Array} array a list of items to remove
   * @see ObservableArray.remove
   * @public
   */
  removeAll: function( array ) {
    assert && assert( _.isArray( array ), 'array should be an array' );
    for ( let i = 0; i < array.length; i++ ) {
      this.remove( array[ i ] );
    }
  },

  /**
   * Pushes an item onto the end of the array.
   * @param item
   * @throws Error if duplicates are not allowed (see options.allowDuplicates) and item is already in the array
   * @public
   */
  push: function( item ) {
    if ( !this.allowDuplicates && this.contains( item ) ) {
      throw new Error( 'duplicates are not allowed' );
    }
    this._array.push( item );
    this.lengthProperty.set( this._array.length );
    this._fireItemAdded( item );
  },

  /**
   * Removes an item from the end of the array and returns it.
   * @returns {*}
   * @public
   */
  pop: function() {
    assert && assert( this.length > 0, 'cannot pop an empty ObservableArray' );

    const item = this._array.pop();
    if ( item !== undefined ) {
      this.lengthProperty.set( this._array.length );
      this._fireItemRemoved( item );
    }
    return item;
  },

  /**
   * Removes an item from the beginning of the array and returns it.
   * @returns {*}
   * @public
   */
  shift: function() {
    assert && assert( this.length > 0, 'cannot shift an empty ObservableArray' );

    const item = this._array.shift();
    if ( item !== undefined ) {
      this.lengthProperty.set( this._array.length );
      this._fireItemRemoved( item );
    }
    return item;
  },

  /**
   * Does the array contain the specified item?
   * @param item
   * @returns {boolean}
   * @public
   */
  contains: function( item ) {
    return this.indexOf( item ) !== -1;
  },

  /**
   * Gets an item at the specified index.
   * @param index
   * @returns {*} the item
   * @throws assertion Error if the index is out of bounds
   * @public
   */
  get: function( index ) {
    assert && assert( index >= 0 && index < this.length, `index out of bounds: ${index}` );
    return this._array[ index ];
  },

  /**
   * Gets the index of a specified item.
   * @param item
   * @returns {*} -1 if item is not in the array
   * @public
   */
  indexOf: function( item ) {
    return this._array.indexOf( item );
  },

  /**
   * Removes all items from the array.
   * @public
   */
  clear: function() {
    while ( this.length > 0 ) {
      this.pop();
    }
  },

  /**
   * Applies a callback function to each item in the array
   * @param callback function(item)
   * @public
   */
  forEach: function( callback ) {

    // TODO: don't slice on forEach, see https://github.com/phetsims/axon/issues/283
    this._array.slice().forEach( callback ); // do this on a copy of the array, in case callbacks involve array modification
  },

  /**
   * Maps the values in this ObservableArray using the specified function, and returns a new ObservableArray for chaining.
   * @param {function(element:*):*} mapFunction - from Observable array element to something else
   * @returns {Object[]}
   * @public
   */
  map: function( mapFunction ) {
    return this._array.map( mapFunction );
  },

  /**
   * Filters the values in this ObservableArray using the predicate function, and returns a new ObservableArray for chaining.
   * @param {function(element):boolean} predicate
   * @returns {Object[]}
   * @public
   */
  filter: function( predicate ) {
    return this._array.filter( predicate );
  },

  /**
   * Count the number of items in this ObservableArray that satisfy the given Predicate.
   * @param {function} predicate
   * @returns {number}
   * @public
   */
  count: function( predicate ) {
    let count = 0;
    for ( let i = 0; i < this._array.length; i++ ) {
      if ( predicate( this._array[ i ] ) ) {
        count++;
      }
    }
    return count;
  },

  /**
   * Find the first element that matches the given predicate.
   * @param {function} predicate
   * @param {number} [fromIndex] - optional start index for the search
   * @returns {*} the first element that matches the given predicate, or undefined if no matching item can be found
   * @throws assertion Error if the fromIndex is specified and out of range
   * @public
   */
  find: function( predicate, fromIndex ) {
    assert && ( fromIndex !== undefined ) && assert( typeof fromIndex === 'number', 'fromIndex must be numeric, if provided' );
    assert && ( typeof fromIndex === 'number' ) && assert( fromIndex >= 0 && fromIndex < this.length,
      `fromIndex out of bounds: ${fromIndex}` );
    return _.find( this._array, predicate, fromIndex );
  },

  /**
   * Returns true if some element in this ObservableArray matches the predicate.
   * @param {function} predicate - the function to test elements
   * @returns {boolean}
   */
  some: function( predicate ) {
    return _.some( this._array, predicate );
  },

  /**
   * Starting with the initial value, combine values from this ObservableArray to come up with a composite result.
   * Same as foldLeft.  In underscore this is called _.reduce aka _.fold or _.inject
   * @param value
   * @param combiner
   * @returns {*}
   * @public
   */
  reduce: function( value, combiner ) {
    for ( let i = 0; i < this._array.length; i++ ) {
      value = combiner( value, this._array[ i ] );
    }
    return value;
  },

  /**
   * Return the underlying array.  Most clients should be able to use the ObservableArray functions for array access
   * or filtering, mapping, etc. instead of this function.  If it is necessary to access the array directly (say, for
   * performance reasons), please document why it is necessary. Also note this is not a defensive copy, so the client
   * will need to take care not to disturb the elements of the array.
   * @returns {Array}
   * @public
   */
  getArray: function() {
    return this._array;
  },

  /**
   * Like getArray, but returns a copy of the array. Use this if you will be modifying the collection.
   * @returns {Array}
   * @public
   */
  getArrayCopy: function() {
    return this._array.slice();
  },

  /**
   * Add/remove elements from any point in the array
   * @param {number} start - the index to start adding/removing items
   * @param {number} deleteCount - the number of items to delete
   * @param {Object} [item1] - an item to add
   * @param {Object} [item2] - an item to add
   * @param {Object} [etc] - varargs items to add etc.
   * @returns {Object[]} the items that were deleted.
   * @public
   */
  splice: function( start, deleteCount, item1, item2, etc ) {
    const deleted = this._array.splice.apply( this._array, arguments );
    const args = Array.prototype.slice.call( arguments );
    for ( let i = 0; i < deleted.length; i++ ) {
      this._fireItemRemoved( deleted[ i ] );
    }

    for ( let k = 2; k < args.length; k++ ) {
      this._fireItemAdded( args[ k ] );
    }
    return deleted;
  },

  /**
   * Changes the ordering of elements in the array.  Requires a Random source so that shuffles can be reproducible.
   * No items are added or removed, and this method does not send out any notifications.
   * @param {Random} random - from dot
   */
  shuffle: function( random ) {
    assert && assert( random, 'random must be supplied' );

    // preserve the same _array reference in case any clients got a reference to it with getArray()
    const shuffled = random.shuffle( this._array );
    this._array.length = 0;
    Array.prototype.push.apply( this._array, shuffled );
  }
} );

export default ObservableArray;<commit_msg>ObservableArray to run on Emitter, https://github.com/phetsims/axon/issues/270<commit_after>// Copyright 2013-2020, University of Colorado Boulder

/**
 * An observable array of items which sends notifications when items are added or removed.
 *
 * Because the array is observable, we must be careful about the possibility of concurrent-modification errors.
 * Any time we iterate over the array, we must iterate over a copy, because callback may be modifying the array.
 *
 * @author Sam Reid (PhET Interactive Simulations)
 * @author Chris Malley (PixelZoom, Inc.)
 */

import inherit from '../../phet-core/js/inherit.js';
import merge from '../../phet-core/js/merge.js';
import PhetioObject from '../../tandem/js/PhetioObject.js';
import Tandem from '../../tandem/js/Tandem.js';
import ObjectIO from '../../tandem/js/types/ObjectIO.js';
import axon from './axon.js';
import Emitter from './Emitter.js';
import NumberProperty from './NumberProperty.js';
import ObservableArrayIO from './ObservableArrayIO.js';

// Factor out to reduce memory footprint, see https://github.com/phetsims/tandem/issues/71
const DefaultObservableArrayIOType = ObservableArrayIO( ObjectIO );

/**
 * @param {Object} [options]
 * @constructor
 */
function ObservableArray( options ) {
  assert && assert( !Array.isArray( options ), 'ObservableArray cannot be initialized with values' );

  options = merge( {
    allowDuplicates: false, // are duplicate items allowed in the array?
    phetioType: DefaultObservableArrayIOType,
    tandem: Tandem.OPTIONAL
  }, options );

  assert && assert( options.phetioType && options.phetioType.parameterTypes, 'ObservableArray\'s phetioType should have a parameterType' );
  assert && assert( options.phetioType && options.phetioType.parameterTypes.length === 1, 'ObservableArray\'s phetioType should have a parameterType' );
  assert && assert( options.phetioType && options.phetioType.parameterTypes[ 0 ], 'ObservableArray\'s phetioType should have a parameterType' );

  this.allowDuplicates = options.allowDuplicates; // @private

  this._options = options; // @private, for creating internal ObservableArray copies, see map/filter

  this._array = []; // @private internal, do not access directly

  // @private
  this.itemAddedEmitter = new Emitter( { parameters: [ { isValidValue: _.stubTrue }, { valueType: ObservableArray } ] } );
  this.itemRemovedEmitter = new Emitter( { parameters: [ { isValidValue: _.stubTrue }, { valueType: ObservableArray } ] } );

  // @public (read-only) observe this, but don't set it
  this.lengthProperty = new NumberProperty( this._array.length, {
    numberType: 'Integer',
    tandem: options.tandem.createTandem( 'lengthProperty' ),
    phetioReadOnly: true
  } );

  PhetioObject.call( this, options );
}

axon.register( 'ObservableArray', ObservableArray );

inherit( PhetioObject, ObservableArray, {

  // @public
  dispose: function() {
    this.lengthProperty.dispose();
    PhetioObject.prototype.dispose.call( this );
  },

  /**
   * Restore the array back to its initial state
   * Note: if an item is in the current array and original array, it is removed and added back
   * This may or may not change in the future, see #4
   * @public
   */
  reset: function() {
    this.clear();
  },

  // @public
  get length() { return this._array.length; },

  /**
   * Adds a listener that will be notified when an item is added to the list.
   * @param {function(item:*, ObservableArray)} listener
   * @public
   */
  addItemAddedListener: function( listener ) {
    assert && assert( !this.itemAddedEmitter.hasListener( listener ), 'listener is already registered' );
    this.itemAddedEmitter.addListener( listener );
  },

  /**
   * Removes a listener that was added via addItemAddedListener.
   * @param {function(item:*, ObservableArray)} listener
   * @public
   */
  removeItemAddedListener: function( listener ) {
    assert && assert( this.itemAddedEmitter.hasListener( listener ), 'listener is not registered' );
    this.itemAddedEmitter.removeListener( listener );
  },

  /**
   * Adds a listener that will be notified when an item is removed from the list.
   * @param listener function( item, observableArray )
   * @public
   */
  addItemRemovedListener: function( listener ) {
    assert && assert( !this.itemRemovedEmitter.hasListener( listener ), 'listener is already registered' );
    this.itemRemovedEmitter.addListener( listener );
  },

  /**
   * Removes a listener that was added via addItemRemovedListener.
   * @param listener
   * @public
   */
  removeItemRemovedListener: function( listener ) {
    assert && assert( this.itemRemovedEmitter.hasListener( listener ), 'listener is not registered' );
    this.itemRemovedEmitter.removeListener( listener );
  },

  /**
   * called when an item is added
   * @param {*} item
   * @private
   */
  _fireItemAdded: function( item ) {
    const self = this;
    this.phetioStartEvent( 'itemAdded', {
      getData: function() {
        return self.phetioType.parameterTypes[ 0 ].toStateObject( item );
      }
    } );

    this.itemAddedEmitter.emit( item, this );

    this.phetioEndEvent();
  },

  /**
   * called when an item is added
   * @param {*} item
   * @private
   */
  _fireItemRemoved: function( item ) {
    const self = this;
    this.phetioStartEvent( 'itemRemoved', {
      getData: function() {
        return self.phetioType.parameterTypes[ 0 ].toStateObject( item );
      }
    } );

    this.itemRemovedEmitter.emit( item, this );

    this.phetioEndEvent();
  },

  /**
   * Adds an item to the end of the array.
   * This is a convenience function, and is the same as push.
   * @param item
   * @public
   */
  add: function( item ) {
    this.push( item );
  },

  /**
   * Add items to the end of the array.
   * This is a convenience function, and is the same as push.
   * @param {Array} items
   * @public
   */
  addAll: function( items ) {
    for ( let i = 0; i < items.length; i++ ) {
      this.add( items[ i ] );
    }
  },

  /**
   * Removes the first occurrence of an item from the array.
   * If duplicates are allowed (see options.allowDuplicates) you may need to call this multiple
   * times to totally purge item from the array.
   * @param item
   * @public
   */
  remove: function( item ) {
    const index = this._array.indexOf( item );
    if ( index !== -1 ) {
      this._array.splice( index, 1 );
      this.lengthProperty.set( this._array.length );
      this._fireItemRemoved( item );
    }
  },

  /**
   * Removes the first occurrence of each item in the specified array.
   * @param {Array} array a list of items to remove
   * @see ObservableArray.remove
   * @public
   */
  removeAll: function( array ) {
    assert && assert( _.isArray( array ), 'array should be an array' );
    for ( let i = 0; i < array.length; i++ ) {
      this.remove( array[ i ] );
    }
  },

  /**
   * Pushes an item onto the end of the array.
   * @param item
   * @throws Error if duplicates are not allowed (see options.allowDuplicates) and item is already in the array
   * @public
   */
  push: function( item ) {
    if ( !this.allowDuplicates && this.contains( item ) ) {
      throw new Error( 'duplicates are not allowed' );
    }
    this._array.push( item );
    this.lengthProperty.set( this._array.length );
    this._fireItemAdded( item );
  },

  /**
   * Removes an item from the end of the array and returns it.
   * @returns {*}
   * @public
   */
  pop: function() {
    assert && assert( this.length > 0, 'cannot pop an empty ObservableArray' );

    const item = this._array.pop();
    if ( item !== undefined ) {
      this.lengthProperty.set( this._array.length );
      this._fireItemRemoved( item );
    }
    return item;
  },

  /**
   * Removes an item from the beginning of the array and returns it.
   * @returns {*}
   * @public
   */
  shift: function() {
    assert && assert( this.length > 0, 'cannot shift an empty ObservableArray' );

    const item = this._array.shift();
    if ( item !== undefined ) {
      this.lengthProperty.set( this._array.length );
      this._fireItemRemoved( item );
    }
    return item;
  },

  /**
   * Does the array contain the specified item?
   * @param item
   * @returns {boolean}
   * @public
   */
  contains: function( item ) {
    return this.indexOf( item ) !== -1;
  },

  /**
   * Gets an item at the specified index.
   * @param index
   * @returns {*} the item
   * @throws assertion Error if the index is out of bounds
   * @public
   */
  get: function( index ) {
    assert && assert( index >= 0 && index < this.length, `index out of bounds: ${index}` );
    return this._array[ index ];
  },

  /**
   * Gets the index of a specified item.
   * @param item
   * @returns {*} -1 if item is not in the array
   * @public
   */
  indexOf: function( item ) {
    return this._array.indexOf( item );
  },

  /**
   * Removes all items from the array.
   * @public
   */
  clear: function() {
    while ( this.length > 0 ) {
      this.pop();
    }
  },

  /**
   * Applies a callback function to each item in the array
   * @param callback function(item)
   * @public
   */
  forEach: function( callback ) {

    // TODO: don't slice on forEach, see https://github.com/phetsims/axon/issues/283
    this._array.slice().forEach( callback ); // do this on a copy of the array, in case callbacks involve array modification
  },

  /**
   * Maps the values in this ObservableArray using the specified function, and returns a new ObservableArray for chaining.
   * @param {function(element:*):*} mapFunction - from Observable array element to something else
   * @returns {Object[]}
   * @public
   */
  map: function( mapFunction ) {
    return this._array.map( mapFunction );
  },

  /**
   * Filters the values in this ObservableArray using the predicate function, and returns a new ObservableArray for chaining.
   * @param {function(element):boolean} predicate
   * @returns {Object[]}
   * @public
   */
  filter: function( predicate ) {
    return this._array.filter( predicate );
  },

  /**
   * Count the number of items in this ObservableArray that satisfy the given Predicate.
   * @param {function} predicate
   * @returns {number}
   * @public
   */
  count: function( predicate ) {
    let count = 0;
    for ( let i = 0; i < this._array.length; i++ ) {
      if ( predicate( this._array[ i ] ) ) {
        count++;
      }
    }
    return count;
  },

  /**
   * Find the first element that matches the given predicate.
   * @param {function} predicate
   * @param {number} [fromIndex] - optional start index for the search
   * @returns {*} the first element that matches the given predicate, or undefined if no matching item can be found
   * @throws assertion Error if the fromIndex is specified and out of range
   * @public
   */
  find: function( predicate, fromIndex ) {
    assert && ( fromIndex !== undefined ) && assert( typeof fromIndex === 'number', 'fromIndex must be numeric, if provided' );
    assert && ( typeof fromIndex === 'number' ) && assert( fromIndex >= 0 && fromIndex < this.length,
      `fromIndex out of bounds: ${fromIndex}` );
    return _.find( this._array, predicate, fromIndex );
  },

  /**
   * Returns true if some element in this ObservableArray matches the predicate.
   * @param {function} predicate - the function to test elements
   * @returns {boolean}
   */
  some: function( predicate ) {
    return _.some( this._array, predicate );
  },

  /**
   * Starting with the initial value, combine values from this ObservableArray to come up with a composite result.
   * Same as foldLeft.  In underscore this is called _.reduce aka _.fold or _.inject
   * @param value
   * @param combiner
   * @returns {*}
   * @public
   */
  reduce: function( value, combiner ) {
    for ( let i = 0; i < this._array.length; i++ ) {
      value = combiner( value, this._array[ i ] );
    }
    return value;
  },

  /**
   * Return the underlying array.  Most clients should be able to use the ObservableArray functions for array access
   * or filtering, mapping, etc. instead of this function.  If it is necessary to access the array directly (say, for
   * performance reasons), please document why it is necessary. Also note this is not a defensive copy, so the client
   * will need to take care not to disturb the elements of the array.
   * @returns {Array}
   * @public
   */
  getArray: function() {
    return this._array;
  },

  /**
   * Like getArray, but returns a copy of the array. Use this if you will be modifying the collection.
   * @returns {Array}
   * @public
   */
  getArrayCopy: function() {
    return this._array.slice();
  },

  /**
   * Add/remove elements from any point in the array
   * @param {number} start - the index to start adding/removing items
   * @param {number} deleteCount - the number of items to delete
   * @param {Object} [item1] - an item to add
   * @param {Object} [item2] - an item to add
   * @param {Object} [etc] - varargs items to add etc.
   * @returns {Object[]} the items that were deleted.
   * @public
   */
  splice: function( start, deleteCount, item1, item2, etc ) {
    const deleted = this._array.splice.apply( this._array, arguments );
    const args = Array.prototype.slice.call( arguments );
    for ( let i = 0; i < deleted.length; i++ ) {
      this._fireItemRemoved( deleted[ i ] );
    }

    for ( let k = 2; k < args.length; k++ ) {
      this._fireItemAdded( args[ k ] );
    }
    return deleted;
  },

  /**
   * Changes the ordering of elements in the array.  Requires a Random source so that shuffles can be reproducible.
   * No items are added or removed, and this method does not send out any notifications.
   * @param {Random} random - from dot
   */
  shuffle: function( random ) {
    assert && assert( random, 'random must be supplied' );

    // preserve the same _array reference in case any clients got a reference to it with getArray()
    const shuffled = random.shuffle( this._array );
    this._array.length = 0;
    Array.prototype.push.apply( this._array, shuffled );
  }
} );

export default ObservableArray;<|endoftext|>