<commit_before>/* Copyright (c) 2006-2012 by OpenLayers Contributors (see authors.txt for
 * full list of contributors). Published under the 2-clause BSD license.
 * See license.txt in the OpenLayers distribution or repository for the
 * full text of the license. */

/**
 * @requires OpenLayers/Control/PanZoom.js
 */

/**
 * Class: OpenLayers.Control.PanZoomBar
 * The PanZoomBar is a visible control composed of a
 * <OpenLayers.Control.PanPanel> and a <OpenLayers.Control.ZoomBar>.
 * By default it is displayed in the upper left corner of the map as 4
 * directional arrows above a vertical slider.
 *
 * Inherits from:
 *  - <OpenLayers.Control.PanZoom>
 */
OpenLayers.Control.PanZoomBar = OpenLayers.Class(OpenLayers.Control.PanZoom, {

    /**
     * APIProperty: zoomStopWidth
     */
    zoomStopWidth: 18,

    /**
     * APIProperty: zoomStopHeight
     */
    zoomStopHeight: 11,

    /**
     * Property: slider
     */
    slider: null,

    /**
     * Property: sliderEvents
     * {<OpenLayers.Events>}
     */
    sliderEvents: null,

    /**
     * Property: zoombarDiv
     * {DOMElement}
     */
    zoombarDiv: null,

    /**
     * APIProperty: zoomWorldIcon
     * {Boolean}
     */
    zoomWorldIcon: false,

    /**
     * APIProperty: panIcons
     * {Boolean} Set this property to false not to display the pan icons. If
     * false the zoom world icon is placed under the zoom bar. Defaults to
     * true.
     */
    panIcons: true,

    /**
     * APIProperty: forceFixedZoomLevel
     * {Boolean} Force a fixed zoom level even though the map has
     *     fractionalZoom
     */
    forceFixedZoomLevel: false,

    /**
     * Property: mouseDragStart
     * {<OpenLayers.Pixel>}
     */
    mouseDragStart: null,

    /**
     * Property: deltaY
     * {Number} The cumulative vertical pixel offset during a zoom bar drag.
     */
    deltaY: null,

    /**
     * Property: zoomStart
     * {<OpenLayers.Pixel>}
     */
    zoomStart: null,

    /**
     * Constructor: OpenLayers.Control.PanZoomBar
     */

    /**
     * APIMethod: destroy
     */
    destroy: function() {

        this._removeZoomBar();

        this.map.events.un({
            "changebaselayer": this.redraw,
            "updatesize": this.redraw,
            scope: this
        });

        OpenLayers.Control.PanZoom.prototype.destroy.apply(this, arguments);

        delete this.mouseDragStart;
        delete this.zoomStart;
    },

    /**
     * Method: setMap
     *
     * Parameters:
     * map - {<OpenLayers.Map>}
     */
    setMap: function(map) {
        OpenLayers.Control.PanZoom.prototype.setMap.apply(this, arguments);
        this.map.events.on({
            "changebaselayer": this.redraw,
            "updatesize": this.redraw,
            scope: this
        });
    },

    /**
     * Method: redraw
     * clear the div and start over.
     */
    redraw: function() {
        if (this.div != null) {
            this.removeButtons();
            this._removeZoomBar();
        }
        this.draw();
    },

    /**
    * Method: draw
    *
    * Parameters:
    * px - {<OpenLayers.Pixel>}
    */
    draw: function(px) {
        // initialize our internal div
        OpenLayers.Control.prototype.draw.apply(this, arguments);
        px = this.position.clone();

        // place the controls
        this.buttons = [];

        var sz = {w: 18, h: 18};
        if (this.panIcons) {
            var centered = new OpenLayers.Pixel(px.x+sz.w/2, px.y);
            var wposition = sz.w;

            if (this.zoomWorldIcon) {
                centered = new OpenLayers.Pixel(px.x+sz.w, px.y);
            }

            this._addButton("panup", "north-mini.png", centered, sz);
            px.y = centered.y+sz.h;
            this._addButton("panleft", "west-mini.png", px, sz);
            if (this.zoomWorldIcon) {
                this._addButton("zoomworld", "zoom-world-mini.png", px.add(sz.w, 0), sz);

                wposition *= 2;
            }
            this._addButton("panright", "east-mini.png", px.add(wposition, 0), sz);
            this._addButton("pandown", "south-mini.png", centered.add(0, sz.h*2), sz);
            this._addButton("zoomin", "zoom-plus-mini.png", centered.add(0, sz.h*3+5), sz);
            centered = this._addZoomBar(centered.add(0, sz.h*4 + 5));
            this._addButton("zoomout", "zoom-minus-mini.png", centered, sz);
        }
        else {
            this._addButton("zoomin", "zoom-plus-mini.png", px, sz);
            centered = this._addZoomBar(px.add(0, sz.h));
            this._addButton("zoomout", "zoom-minus-mini.png", centered, sz);
            if (this.zoomWorldIcon) {
                centered = centered.add(0, sz.h+3);
                this._addButton("zoomworld", "zoom-world-mini.png", centered, sz);
            }
        }
        return this.div;
    },

    /**
    * Method: _addZoomBar
    *
    * Parameters:
    * centered - {<OpenLayers.Pixel>} where zoombar drawing is to start.
    */
    _addZoomBar:function(centered) {
        var imgLocation = OpenLayers.Util.getImageLocation("slider.png");
        var id = this.id + "_" + this.map.id;
        var minZoom = this.map.getMinZoom();
        var zoomsToEnd = this.map.getNumZoomLevels() - 1 - this.map.getZoom();
        var slider = OpenLayers.Util.createAlphaImageDiv(id,
                       centered.add(-1, zoomsToEnd * this.zoomStopHeight),
                       {w: 20, h: 9},
                       imgLocation,
                       "absolute");
        slider.style.cursor = "move";
        this.slider = slider;

        this.sliderEvents = new OpenLayers.Events(this, slider, null, true,
                                            {includeXY: true});
        this.sliderEvents.on({
            "touchstart": this.zoomBarDown,
            "touchmove": this.zoomBarDrag,
            "touchend": this.zoomBarUp,
            "mousedown": this.zoomBarDown,
            "mousemove": this.zoomBarDrag,
            "mouseup": this.zoomBarUp
        });

        var sz = {
            w: this.zoomStopWidth,
            h: this.zoomStopHeight * (this.map.getNumZoomLevels() - minZoom)
        };
        var imgLocation = OpenLayers.Util.getImageLocation("zoombar.png");
        var div = null;

        if (OpenLayers.Util.alphaHack()) {
            var id = this.id + "_" + this.map.id;
            div = OpenLayers.Util.createAlphaImageDiv(id, centered,
                                      {w: sz.w, h: this.zoomStopHeight},
                                      imgLocation,
                                      "absolute", null, "crop");
            div.style.height = sz.h + "px";
        } else {
            div = OpenLayers.Util.createDiv(
                        'OpenLayers_Control_PanZoomBar_Zoombar' + this.map.id,
                        centered,
                        sz,
                        imgLocation);
        }
        div.style.cursor = "pointer";
        div.className = "olButton";
        this.zoombarDiv = div;

        this.div.appendChild(div);

        this.startTop = parseInt(div.style.top);
        this.div.appendChild(slider);

        this.map.events.register("zoomend", this, this.moveZoomBar);

        centered = centered.add(0,
            this.zoomStopHeight * (this.map.getNumZoomLevels() - minZoom));
        return centered;
    },

    /**
     * Method: _removeZoomBar
     */
    _removeZoomBar: function() {
        this.sliderEvents.un({
            "touchstart": this.zoomBarDown,
            "touchmove": this.zoomBarDrag,
            "touchend": this.zoomBarUp,
            "mousedown": this.zoomBarDown,
            "mousemove": this.zoomBarDrag,
            "mouseup": this.zoomBarUp
        });
        this.sliderEvents.destroy();

        this.div.removeChild(this.zoombarDiv);
        this.zoombarDiv = null;
        this.div.removeChild(this.slider);
        this.slider = null;

        this.map.events.unregister("zoomend", this, this.moveZoomBar);
    },

    /**
     * Method: onButtonClick
     *
     * Parameters:
     * evt - {Event}
     */
    onButtonClick: function(evt) {
        OpenLayers.Control.PanZoom.prototype.onButtonClick.apply(this, arguments);
        if (evt.buttonElement === this.zoombarDiv) {
            var levels = evt.buttonXY.y / this.zoomStopHeight;
            if(this.forceFixedZoomLevel || !this.map.fractionalZoom) {
                levels = Math.floor(levels);
            }
            var zoom = (this.map.getNumZoomLevels() - 1) - levels;
            zoom = Math.min(Math.max(zoom, 0), this.map.getNumZoomLevels() - 1);
            this.map.zoomTo(zoom);
        }
    },

    /**
     * Method: passEventToSlider
     * This function is used to pass events that happen on the div, or the map,
     * through to the slider, which then does its moving thing.
     *
     * Parameters:
     * evt - {<OpenLayers.Event>}
     */
    passEventToSlider:function(evt) {
        this.sliderEvents.handleBrowserEvent(evt);
    },

    /*
     * Method: zoomBarDown
     * event listener for clicks on the slider
     *
     * Parameters:
     * evt - {<OpenLayers.Event>}
     */
    zoomBarDown:function(evt) {
        if (!OpenLayers.Event.isLeftClick(evt) && !OpenLayers.Event.isSingleTouch(evt)) {
            return;
        }
        this.map.events.on({
            "touchmove": this.passEventToSlider,
            "mousemove": this.passEventToSlider,
            "mouseup": this.passEventToSlider,
            scope: this
        });
        this.mouseDragStart = evt.xy.clone();
        this.zoomStart = evt.xy.clone();
        this.div.style.cursor = "move";
        // reset the div offsets just in case the div moved
        this.zoombarDiv.offsets = null;
        OpenLayers.Event.stop(evt);
    },

    /*
     * Method: zoomBarDrag
     * This is what happens when a click has occurred, and the client is
     * dragging.  Here we must ensure that the slider doesn't go beyond the
     * bottom/top of the zoombar div, as well as moving the slider to its new
     * visual location
     *
     * Parameters:
     * evt - {<OpenLayers.Event>}
     */
    zoomBarDrag:function(evt) {
        if (this.mouseDragStart != null) {
            var deltaY = this.mouseDragStart.y - evt.xy.y;
            var offsets = OpenLayers.Util.pagePosition(this.zoombarDiv);
            if ((evt.clientY - offsets[1]) > 0 &&
                (evt.clientY - offsets[1]) < parseInt(this.zoombarDiv.style.height) - 2) {
                var newTop = parseInt(this.slider.style.top) - deltaY;
                this.slider.style.top = newTop+"px";
                this.mouseDragStart = evt.xy.clone();
            }
            // set cumulative displacement
            this.deltaY = this.zoomStart.y - evt.xy.y;
            OpenLayers.Event.stop(evt);
        }
    },

    /*
     * Method: zoomBarUp
     * Perform cleanup when a mouseup event is received -- discover new zoom
     * level and switch to it.
     *
     * Parameters:
     * evt - {<OpenLayers.Event>}
     */
    zoomBarUp:function(evt) {
        if (!OpenLayers.Event.isLeftClick(evt) && evt.type !== "touchend") {
            return;
        }
        if (this.mouseDragStart) {
            this.div.style.cursor="";
            this.map.events.un({
                "touchmove": this.passEventToSlider,
                "mouseup": this.passEventToSlider,
                "mousemove": this.passEventToSlider,
                scope: this
            });
            var zoomLevel = this.map.zoom;
            if (!this.forceFixedZoomLevel && this.map.fractionalZoom) {
                zoomLevel += this.deltaY/this.zoomStopHeight;
                zoomLevel = Math.min(Math.max(zoomLevel, 0),
                                     this.map.getNumZoomLevels() - 1);
            } else {
                zoomLevel += this.deltaY/this.zoomStopHeight;
                zoomLevel = Math.max(Math.round(zoomLevel), 0);
            }
            this.map.zoomTo(zoomLevel);
            this.mouseDragStart = null;
            this.zoomStart = null;
            this.deltaY = 0;
            OpenLayers.Event.stop(evt);
        }
    },

    /*
    * Method: moveZoomBar
    * Change the location of the slider to match the current zoom level.
    */
    moveZoomBar:function() {
        var newTop =
            ((this.map.getNumZoomLevels()-1) - this.map.getZoom()) *
            this.zoomStopHeight + this.startTop + 1;
        this.slider.style.top = newTop + "px";
    },

    CLASS_NAME: "OpenLayers.Control.PanZoomBar"
});<commit_msg>It's fix zoom bar.<commit_after>/* Copyright (c) 2006-2012 by OpenLayers Contributors (see authors.txt for
 * full list of contributors). Published under the 2-clause BSD license.
 * See license.txt in the OpenLayers distribution or repository for the
 * full text of the license. */

/**
 * @requires OpenLayers/Control/PanZoom.js
 */

/**
 * Class: OpenLayers.Control.PanZoomBar
 * The PanZoomBar is a visible control composed of a
 * <OpenLayers.Control.PanPanel> and a <OpenLayers.Control.ZoomBar>.
 * By default it is displayed in the upper left corner of the map as 4
 * directional arrows above a vertical slider.
 *
 * Inherits from:
 *  - <OpenLayers.Control.PanZoom>
 */
OpenLayers.Control.PanZoomBar = OpenLayers.Class(OpenLayers.Control.PanZoom, {

    /**
     * APIProperty: zoomStopWidth
     */
    zoomStopWidth: 18,

    /**
     * APIProperty: zoomStopHeight
     */
    zoomStopHeight: 6,

    /**
     * Property: slider
     */
    slider: null,

    /**
     * Property: sliderEvents
     * {<OpenLayers.Events>}
     */
    sliderEvents: null,

    /**
     * Property: zoombarDiv
     * {DOMElement}
     */
    zoombarDiv: null,

    /**
     * APIProperty: zoomWorldIcon
     * {Boolean}
     */
    zoomWorldIcon: false,

    /**
     * APIProperty: panIcons
     * {Boolean} Set this property to false not to display the pan icons. If
     * false the zoom world icon is placed under the zoom bar. Defaults to
     * true.
     */
    panIcons: true,

    /**
     * APIProperty: forceFixedZoomLevel
     * {Boolean} Force a fixed zoom level even though the map has
     *     fractionalZoom
     */
    forceFixedZoomLevel: false,

    /**
     * Property: mouseDragStart
     * {<OpenLayers.Pixel>}
     */
    mouseDragStart: null,

    /**
     * Property: deltaY
     * {Number} The cumulative vertical pixel offset during a zoom bar drag.
     */
    deltaY: null,

    /**
     * Property: zoomStart
     * {<OpenLayers.Pixel>}
     */
    zoomStart: null,

    /**
     * Constructor: OpenLayers.Control.PanZoomBar
     */

    /**
     * APIMethod: destroy
     */
    destroy: function() {

        this._removeZoomBar();

        this.map.events.un({
            "changebaselayer": this.redraw,
            "updatesize": this.redraw,
            scope: this
        });

        OpenLayers.Control.PanZoom.prototype.destroy.apply(this, arguments);

        delete this.mouseDragStart;
        delete this.zoomStart;
    },

    /**
     * Method: setMap
     *
     * Parameters:
     * map - {<OpenLayers.Map>}
     */
    setMap: function(map) {
        OpenLayers.Control.PanZoom.prototype.setMap.apply(this, arguments);
        this.map.events.on({
            "changebaselayer": this.redraw,
            "updatesize": this.redraw,
            scope: this
        });
    },

    /**
     * Method: redraw
     * clear the div and start over.
     */
    redraw: function() {
        if (this.div != null) {
            this.removeButtons();
            this._removeZoomBar();
        }
        this.draw();
    },

    /**
    * Method: draw
    *
    * Parameters:
    * px - {<OpenLayers.Pixel>}
    */
    draw: function(px) {
        // initialize our internal div
        OpenLayers.Control.prototype.draw.apply(this, arguments);
        px = this.position.clone();

        // place the controls
        this.buttons = [];

        var sz = {w: 18, h: 18};
        if (this.panIcons) {
            var centered = new OpenLayers.Pixel(px.x+sz.w/2, px.y);
            var wposition = sz.w;

            if (this.zoomWorldIcon) {
                centered = new OpenLayers.Pixel(px.x+sz.w, px.y);
            }

            this._addButton("panup", "north-mini.png", centered, sz);
            px.y = centered.y+sz.h;
            this._addButton("panleft", "west-mini.png", px, sz);
            if (this.zoomWorldIcon) {
                this._addButton("zoomworld", "zoom-world-mini.png", px.add(sz.w, 0), sz);

                wposition *= 2;
            }
            this._addButton("panright", "east-mini.png", px.add(wposition, 0), sz);
            this._addButton("pandown", "south-mini.png", centered.add(0, sz.h*2), sz);
            this._addButton("zoomin", "zoom-plus-mini.png", centered.add(0, sz.h*3+5), sz);
            centered = this._addZoomBar(centered.add(0, sz.h*4 + 5));
            this._addButton("zoomout", "zoom-minus-mini.png", centered, sz);
        }
        else {
            this._addButton("zoomin", "zoom-plus-mini.png", px, sz);
            centered = this._addZoomBar(px.add(0, sz.h));
            this._addButton("zoomout", "zoom-minus-mini.png", centered, sz);
            if (this.zoomWorldIcon) {
                centered = centered.add(0, sz.h+3);
                this._addButton("zoomworld", "zoom-world-mini.png", centered, sz);
            }
        }
        return this.div;
    },

    /**
    * Method: _addZoomBar
    *
    * Parameters:
    * centered - {<OpenLayers.Pixel>} where zoombar drawing is to start.
    */
    _addZoomBar:function(centered) {
        var imgLocation = OpenLayers.Util.getImageLocation("slider.png");
        var id = this.id + "_" + this.map.id;
        var minZoom = this.map.getMinZoom();
        var zoomsToEnd = this.map.getNumZoomLevels() - 1 - this.map.getZoom();
        var slider = OpenLayers.Util.createAlphaImageDiv(id,
                       centered.add(-1, zoomsToEnd * this.zoomStopHeight),
                       {w: 20, h: 9},
                       imgLocation,
                       "absolute");

        slider.style.cursor = "move";
        slider.className = "slider";
        this.slider = slider;

        this.sliderEvents = new OpenLayers.Events(this, slider, null, true,
                                            {includeXY: true});
        this.sliderEvents.on({
            "touchstart": this.zoomBarDown,
            "touchmove": this.zoomBarDrag,
            "touchend": this.zoomBarUp,
            "mousedown": this.zoomBarDown,
            "mousemove": this.zoomBarDrag,
            "mouseup": this.zoomBarUp
        });

        var sz = {
            w: this.zoomStopWidth,
            h: this.zoomStopHeight * (this.map.getNumZoomLevels() - minZoom)
        };
        var imgLocation = OpenLayers.Util.getImageLocation("zoombar.png");
        var div = null;

        if (OpenLayers.Util.alphaHack()) {
            var id = this.id + "_" + this.map.id;
            div = OpenLayers.Util.createAlphaImageDiv(id, centered,
                                      {w: sz.w, h: this.zoomStopHeight},
                                      imgLocation,
                                      "absolute", null, "crop");
            div.style.height = sz.h + "px";
        } else {
            div = OpenLayers.Util.createDiv(
                'OpenLayers_Control_PanZoomBar_Zoombar' + this.map.id,
                centered,
                //sz,
		{'w': 3, 'h': sz.h }, // kiet: force to get thin line as track for slider.
                imgLocation,
		'static' // kiet: added to let the track to reveal the layout height.
	    );
        }
        div.style.cursor = "pointer";
        div.className = "olButton track";
        this.zoombarDiv = div;

        this.div.appendChild(div);

        this.startTop = parseInt(div.style.top);
        this.div.appendChild(slider);

        this.map.events.register("zoomend", this, this.moveZoomBar);

        centered = centered.add(0,
            this.zoomStopHeight * (this.map.getNumZoomLevels() - minZoom));
        return centered;
    },

    /**
     * Method: _removeZoomBar
     */
    _removeZoomBar: function() {
        this.sliderEvents.un({
            "touchstart": this.zoomBarDown,
            "touchmove": this.zoomBarDrag,
            "touchend": this.zoomBarUp,
            "mousedown": this.zoomBarDown,
            "mousemove": this.zoomBarDrag,
            "mouseup": this.zoomBarUp
        });
        this.sliderEvents.destroy();

        this.div.removeChild(this.zoombarDiv);
        this.zoombarDiv = null;
        this.div.removeChild(this.slider);
        this.slider = null;

        this.map.events.unregister("zoomend", this, this.moveZoomBar);
    },

    /**
     * Method: onButtonClick
     *
     * Parameters:
     * evt - {Event}
     */
    onButtonClick: function(evt) {
        OpenLayers.Control.PanZoom.prototype.onButtonClick.apply(this, arguments);
        if (evt.buttonElement === this.zoombarDiv) {
            var levels = evt.buttonXY.y / this.zoomStopHeight;
            if(this.forceFixedZoomLevel || !this.map.fractionalZoom) {
                levels = Math.floor(levels);
            }
            var zoom = (this.map.getNumZoomLevels() - 1) - levels;
            zoom = Math.min(Math.max(zoom, 0), this.map.getNumZoomLevels() - 1);
            this.map.zoomTo(zoom);
        }
    },

    /**
     * Method: passEventToSlider
     * This function is used to pass events that happen on the div, or the map,
     * through to the slider, which then does its moving thing.
     *
     * Parameters:
     * evt - {<OpenLayers.Event>}
     */
    passEventToSlider:function(evt) {
        this.sliderEvents.handleBrowserEvent(evt);
    },

    /*
     * Method: zoomBarDown
     * event listener for clicks on the slider
     *
     * Parameters:
     * evt - {<OpenLayers.Event>}
     */
    zoomBarDown:function(evt) {
        if (!OpenLayers.Event.isLeftClick(evt) && !OpenLayers.Event.isSingleTouch(evt)) {
            return;
        }
        this.map.events.on({
            "touchmove": this.passEventToSlider,
            "mousemove": this.passEventToSlider,
            "mouseup": this.passEventToSlider,
            scope: this
        });
        this.mouseDragStart = evt.xy.clone();
        this.zoomStart = evt.xy.clone();
        this.div.style.cursor = "move";
        // reset the div offsets just in case the div moved
        this.zoombarDiv.offsets = null;
        OpenLayers.Event.stop(evt);
    },

    /*
     * Method: zoomBarDrag
     * This is what happens when a click has occurred, and the client is
     * dragging.  Here we must ensure that the slider doesn't go beyond the
     * bottom/top of the zoombar div, as well as moving the slider to its new
     * visual location
     *
     * Parameters:
     * evt - {<OpenLayers.Event>}
     */
    zoomBarDrag:function(evt) {
        if (this.mouseDragStart != null) {
            var deltaY = this.mouseDragStart.y - evt.xy.y;
            var offsets = OpenLayers.Util.pagePosition(this.zoombarDiv);
            if ((evt.clientY - offsets[1]) > 0 &&
                (evt.clientY - offsets[1]) < parseInt(this.zoombarDiv.style.height) - 2) {
                var newTop = parseInt(this.slider.style.top) - deltaY;
                this.slider.style.top = newTop+"px";
                this.mouseDragStart = evt.xy.clone();
            }
            // set cumulative displacement
            this.deltaY = this.zoomStart.y - evt.xy.y;
            OpenLayers.Event.stop(evt);
        }
    },

    /*
     * Method: zoomBarUp
     * Perform cleanup when a mouseup event is received -- discover new zoom
     * level and switch to it.
     *
     * Parameters:
     * evt - {<OpenLayers.Event>}
     */
    zoomBarUp:function(evt) {
        if (!OpenLayers.Event.isLeftClick(evt) && evt.type !== "touchend") {
            return;
        }
        if (this.mouseDragStart) {
            this.div.style.cursor="";
            this.map.events.un({
                "touchmove": this.passEventToSlider,
                "mouseup": this.passEventToSlider,
                "mousemove": this.passEventToSlider,
                scope: this
            });
            var zoomLevel = this.map.zoom;
            if (!this.forceFixedZoomLevel && this.map.fractionalZoom) {
                zoomLevel += this.deltaY/this.zoomStopHeight;
                zoomLevel = Math.min(Math.max(zoomLevel, 0),
                                     this.map.getNumZoomLevels() - 1);
            } else {
                zoomLevel += this.deltaY/this.zoomStopHeight;
                zoomLevel = Math.max(Math.round(zoomLevel), 0);
            }
            this.map.zoomTo(zoomLevel);
            this.mouseDragStart = null;
            this.zoomStart = null;
            this.deltaY = 0;
            OpenLayers.Event.stop(evt);
        }
    },

    /*
    * Method: moveZoomBar
    * Change the location of the slider to match the current zoom level.
    */
    moveZoomBar:function() {
        var newTop =
            ((this.map.getNumZoomLevels()-1) - this.map.getZoom()) *
            this.zoomStopHeight + this.startTop + 1;
        this.slider.style.top = newTop + "px";
    },

    CLASS_NAME: "OpenLayers.Control.PanZoomBar"
});<|endoftext|>


<commit_before>let fs = require('../core/fs.js');
let hostedGitInfo = require('hosted-git-info');
let utils = require('./utils.js');

class TemplateGenerator {
  constructor(templateName) {
    this.templateName = templateName;
  }

  checkPathExists(fspath) {
    if (fs.existsSync(fspath)) {
      console.error(`${fspath} already exists, will not overwrite`.red);
      process.exit(1);
    }
  }

  downloadAndGenerate(uri, destinationFolder, name) {
    const fspath = utils.joinPath(destinationFolder, name);
    this.checkPathExists(fspath);
    const self = this;
    let {url, filePath, browse} = this.getExternalProject(uri);
    let tmpFilePath = fs.tmpDir(filePath);
    console.log(__('Installing template from ' + browse).green);
    console.log(__('Downloading template...').green);

    fs.mkdirpSync(utils.dirname(tmpFilePath));
    utils.downloadFile(url, tmpFilePath, (err) => {
      if (err) {
        console.error(err.red);
        console.error('Does the template really exist?'.red);
        console.error(`Embark's supported templates: https://embark.status.im/templates/`.green);
        process.exit(1);
      }
      utils.extractZip(tmpFilePath, fspath, {
        map: file => {
          let fixed_path = file.path.split('/');
          fixed_path.shift(); // remove first directory
          file.path = utils.joinPath(...fixed_path);
          return file;
        }
      }, () => {
        self.installTemplate(fspath, name, true);
      });
    });
  }

  generate(destinationFolder, name) {
    const fspath = utils.joinPath(destinationFolder, name);
    this.checkPathExists(fspath);
    console.log(__('Initializing Embark template...').green);
    let templatePath = fs.embarkPath(utils.joinPath('templates', this.templateName));
    fs.copySync(templatePath, fspath);

    this.installTemplate(
      fspath,
      name,
      (this.templateName === 'boilerplate' || this.templateName === 'demo')
    );

    if (name === 'embark_demo') {
      console.log('-------------------'.yellow);
      console.log(__('Next steps:').green);
      console.log(('-> ' + ('cd ' + fspath).bold.cyan).green);
      console.log('-> '.green + 'embark run'.bold.cyan);
      console.log(__('For more info go to http://embark.status.im').green);
    }
  }

  installTemplate(templatePath, name, installPackages) {
    utils.cd(templatePath);
    utils.sed('package.json', '%APP_NAME%', name);
    if (fs.existsSync('dot.gitignore')) {
      fs.moveSync('dot.gitignore', '.gitignore');
    }

    if (fs.existsSync('dot.gitignore')) {
      fs.moveSync('dot.gitignore', '.gitignore');
    }

    if (installPackages) {
      console.log(__('Installing packages...').green);
      utils.runCmd('npm install', null, (err) => {
        if (err) {
          return console.error(err.red);
        }
        console.log(__('Init complete').green);
        console.log('\n' + __('App ready at ').green + templatePath);
      });
    }
  }

  getExternalProject(uri) {
    let url, folder, hgi;
    try {
      hgi = hostedGitInfo.fromUrl(uri);
      if (!hgi) {
        let x = uri.split('#');
        x[0] = `embark-framework/embark-${x[0]}-template`;
        hgi = hostedGitInfo.fromUrl(x.join('#'));
      }
      if(!hgi) { throw new Error(); }
      url = hgi.tarball();
      folder = `${hgi.user}/${hgi.project}/${hgi.committish || 'master'}`;
    } catch (e) {
      console.error('Unsupported template name or git host URL'.red);
      process.exit(1);
    }

    return {
      url,
      filePath: utils.joinPath(".embark/templates/", folder, "archive.zip"),
      browse: decodeURIComponent(hgi.browse())
    };
  }
}
module.exports = TemplateGenerator;
<commit_msg>rename var<commit_after>let fs = require('../core/fs.js');
let hostedGitInfo = require('hosted-git-info');
let utils = require('./utils.js');

class TemplateGenerator {
  constructor(templateName) {
    this.templateName = templateName;
  }

  checkPathExists(fspath) {
    if (fs.existsSync(fspath)) {
      console.error(`${fspath} already exists, will not overwrite`.red);
      process.exit(1);
    }
  }

  downloadAndGenerate(uri, destinationFolder, name) {
    const fspath = utils.joinPath(destinationFolder, name);
    this.checkPathExists(fspath);
    const self = this;
    let {url, filePath, browse} = this.getExternalProject(uri);
    let tmpFilePath = fs.tmpDir(filePath);
    console.log(__('Installing template from ' + browse).green);
    console.log(__('Downloading template...').green);

    fs.mkdirpSync(utils.dirname(tmpFilePath));
    utils.downloadFile(url, tmpFilePath, (err) => {
      if (err) {
        console.error(err.red);
        console.error('Does the template really exist?'.red);
        console.error(`Embark's supported templates: https://embark.status.im/templates/`.green);
        process.exit(1);
      }
      utils.extractZip(tmpFilePath, fspath, {
        map: file => {
          let fixed_path = file.path.split('/');
          fixed_path.shift(); // remove first directory
          file.path = utils.joinPath(...fixed_path);
          return file;
        }
      }, () => {
        self.installTemplate(fspath, name, true);
      });
    });
  }

  generate(destinationFolder, name) {
    const fspath = utils.joinPath(destinationFolder, name);
    this.checkPathExists(fspath);
    console.log(__('Initializing Embark template...').green);
    let templatePath = fs.embarkPath(utils.joinPath('templates', this.templateName));
    fs.copySync(templatePath, fspath);

    this.installTemplate(
      fspath,
      name,
      (this.templateName === 'boilerplate' || this.templateName === 'demo')
    );

    if (name === 'embark_demo') {
      console.log('-------------------'.yellow);
      console.log(__('Next steps:').green);
      console.log(('-> ' + ('cd ' + fspath).bold.cyan).green);
      console.log('-> '.green + 'embark run'.bold.cyan);
      console.log(__('For more info go to http://embark.status.im').green);
    }
  }

  installTemplate(templatePath, name, installPackages) {
    utils.cd(templatePath);
    utils.sed('package.json', '%APP_NAME%', name);
    if (fs.existsSync('dot.gitignore')) {
      fs.moveSync('dot.gitignore', '.gitignore');
    }

    if (fs.existsSync('dot.gitignore')) {
      fs.moveSync('dot.gitignore', '.gitignore');
    }

    if (installPackages) {
      console.log(__('Installing packages...').green);
      utils.runCmd('npm install', null, (err) => {
        if (err) {
          return console.error(err.red);
        }
        console.log(__('Init complete').green);
        console.log('\n' + __('App ready at ').green + templatePath);
      });
    }
  }

  getExternalProject(uri) {
    let url, folder, hgi;
    try {
      hgi = hostedGitInfo.fromUrl(uri);
      if (!hgi || hgi.user.includes('#')) {
        let templateAndBranch = uri.split('#');
        templateAndBranch[0] = `embark-framework/embark-${templateAndBranch[0]}-template`;
        hgi = hostedGitInfo.fromUrl(templateAndBranch.join('#'));
      }
      if(!hgi) { throw new Error(); }
      url = hgi.tarball();
      folder = `${hgi.user}/${hgi.project}/${hgi.committish || 'master'}`;
    } catch (e) {
      console.error('Unsupported template name or git host URL'.red);
      process.exit(1);
    }

    return {
      url,
      filePath: utils.joinPath(".embark/templates/", folder, "archive.zip"),
      browse: decodeURIComponent(hgi.browse())
    };
  }
}
module.exports = TemplateGenerator;
<|endoftext|>


<commit_before>import * as React from 'react'
import _ from 'underscore'
import PropTypes from 'prop-types'
import Waypoint from 'react-waypoint'
import articlesQuery from 'desktop/apps/article2/queries/articles.js'
import colors from '@artsy/reaction-force/dist/Assets/Colors'
import positronql from 'desktop/lib/positronql.coffee'
import styled from 'styled-components'
import { Article } from '@artsy/reaction-force/dist/Components/Publishing'
import { data as sd } from 'sharify'

export default class InfiniteScrollArticle extends React.Component {
  static propTypes = {
    article: PropTypes.object,
    emailSignupUrl: PropTypes.string,
    isMobile: PropTypes.bool
  }

  constructor (props) {
    super(props)

    this.state = {
      isLoading: false,
      articles: [this.props.article],
      offset: 0,
      error: false,
      isEnabled: true
    }
  }

  fetchNextArticles = async () => {
    const { articles, offset } = this.state

    this.setState({
      isLoading: true
    })

    try {
      const data = await positronql({
        query: articlesQuery({
          offset,
          limit: 3,
          channel: sd.ARTSY_EDITORIAL_CHANNEL,
          omit: this.props.article.id
        })
      })

      // TODO:
      // At some point this could go in a query so as not to fetch unnecessary data

      // Ignore featured layouts
      const newArticles = data.articles.filter(article => article.layout !== 'feature')

      if (newArticles.length) {
        this.setState({
          articles: articles.concat(newArticles),
          isLoading: false,
          offset: offset + 3
        })
      } else {
        this.setState({
          isEnabled: false,
          isLoading: false
        })
      }
    } catch (error) {
      console.error(
        '(apps/article/InfiniteScrollArticle) Error fetching next article set: ', error
      )

      this.setState({
        isEnabled: false,
        isLoading: false,
        error: true
      })
    }
  }

  renderWaypoint = () => {
    const { isEnabled, isLoading, error } = this.state

    if (isEnabled) {
      if (!isLoading) {
        return (
          <Waypoint
            onEnter={this.fetchNextArticles}
            threshold={2.0}
          />
        )
      } else if (!error) {
        return (
          <LoadingSpinner>

          </LoadingSpinner>
        )
      }
    }
  }

  onEnter = (article, {previousPosition, currentPosition}) => {
    if (previousPosition === 'above' && currentPosition === 'inside') {
      document.title = article.thumbnail_title
      window.history.replaceState({}, article.id, `/article/${article.slug}`)
    }
  }

  onLeave = (i, {previousPosition, currentPosition}) => {
    const nextArticle = this.state.articles[i + 1]

    if (nextArticle && previousPosition === 'inside' && currentPosition === 'above') {
      document.title = nextArticle.thumbnail_title
      window.history.replaceState({}, nextArticle.id, `/article/${nextArticle.slug}`)
    }
  }

  renderContent = () => {
    return _.flatten(this.state.articles.map((article, i) => {
      return (
        <div key={`article-${i}`}>

          <Break />
          <Waypoint
            onEnter={(waypointData => this.onEnter(article, waypointData))}
            onLeave={(waypointData => this.onLeave(i, waypointData))}
          />

      )
    }))
  }

  render () {
    return (
      <InfiniteScrollContainer id='article-root'>
        {this.renderContent()}
        {this.renderWaypoint()}
      </InfiniteScrollContainer>
    )
  }
}

const LoadingSpinner = styled.div`
  position: relative;
  padding: 100px;
`
const Break = styled.div`
  border-top: 1px solid ${colors.grayRegular};
  width: 100%;
  margin-top: 80px;
  margin-bottom: -42px; // FIXME: Magic values like this to be addressed between Reaction / Force
`
const InfiniteScrollContainer = styled.div`
  margin-top: 100px;
`
<commit_msg>Update infinite scroll offset<commit_after>import * as React from 'react'
import _ from 'underscore'
import PropTypes from 'prop-types'
import Waypoint from 'react-waypoint'
import articlesQuery from 'desktop/apps/article2/queries/articles.js'
import colors from '@artsy/reaction-force/dist/Assets/Colors'
import positronql from 'desktop/lib/positronql.coffee'
import styled from 'styled-components'
import { Article } from '@artsy/reaction-force/dist/Components/Publishing'
import { data as sd } from 'sharify'

const FETCH_TOP_OFFSET = 200

export default class InfiniteScrollArticle extends React.Component {
  static propTypes = {
    article: PropTypes.object,
    emailSignupUrl: PropTypes.string,
    isMobile: PropTypes.bool
  }

  constructor (props) {
    super(props)

    this.state = {
      isLoading: false,
      articles: [this.props.article],
      offset: 0,
      error: false,
      isEnabled: true
    }
  }

  fetchNextArticles = async () => {
    const { articles, offset } = this.state

    this.setState({
      isLoading: true
    })

    try {
      const data = await positronql({
        query: articlesQuery({
          offset,
          limit: 3,
          channel: sd.ARTSY_EDITORIAL_CHANNEL,
          omit: this.props.article.id
        })
      })

      // TODO:
      // At some point this could go in a query so as not to fetch unnecessary data

      // Ignore featured layouts
      const newArticles = data.articles.filter(article => article.layout !== 'feature')

      if (newArticles.length) {
        this.setState({
          articles: articles.concat(newArticles),
          isLoading: false,
          offset: offset + 3
        })
      } else {
        this.setState({
          isEnabled: false,
          isLoading: false
        })
      }
    } catch (error) {
      console.error(
        '(apps/article/InfiniteScrollArticle) Error fetching next article set: ', error
      )

      this.setState({
        isEnabled: false,
        isLoading: false,
        error: true
      })
    }
  }

  renderWaypoint = () => {
    const { isEnabled, isLoading, error } = this.state

    if (isEnabled) {
      if (!isLoading) {
        return (
          <Waypoint
            onEnter={this.fetchNextArticles}
            threshold={2.0}
            topOffset={FETCH_TOP_OFFSET}
          />
        )
      } else if (!error) {
        return (
          <LoadingSpinner>

          </LoadingSpinner>
        )
      }
    }
  }

  onEnter = (article, {previousPosition, currentPosition}) => {
    if (previousPosition === 'above' && currentPosition === 'inside') {
      document.title = article.thumbnail_title
      window.history.replaceState({}, article.id, `/article/${article.slug}`)
    }
  }

  onLeave = (i, {previousPosition, currentPosition}) => {
    const nextArticle = this.state.articles[i + 1]

    if (nextArticle && previousPosition === 'inside' && currentPosition === 'above') {
      document.title = nextArticle.thumbnail_title
      window.history.replaceState({}, nextArticle.id, `/article/${nextArticle.slug}`)
    }
  }

  renderContent = () => {
    return _.flatten(this.state.articles.map((article, i) => {
      return (
        <div key={`article-${i}`}>

          <Break />
          <Waypoint
            onEnter={(waypointData => this.onEnter(article, waypointData))}
            onLeave={(waypointData => this.onLeave(i, waypointData))}
          />

      )
    }))
  }

  render () {
    return (
      <InfiniteScrollContainer id='article-root'>
        {this.renderContent()}
        {this.renderWaypoint()}
      </InfiniteScrollContainer>
    )
  }
}

const LoadingSpinner = styled.div`
  position: relative;
  padding: 100px;
`
const Break = styled.div`
  border-top: 1px solid ${colors.grayRegular};
  width: 100%;
  margin-top: 80px;
  margin-bottom: -42px; // FIXME: Magic values like this to be addressed between Reaction / Force
`
const InfiniteScrollContainer = styled.div`
  margin-top: 100px;
`
<|endoftext|>


<commit_before>(function (Q, $) {

/**
 * @module Streams-tools
 */

/**
 * Displays participants of a given stream in a horizontal list.
 * Each item in the list is presented with an avatar and also can have a contextual associated with it.
 * @class Streams participants
 * @constructor
 * @param {Object} options Provide options for this tool
 *   @param {String} options.publisherId The id of the publisher
 *   @param {String} options.streamName The name of the stream
 *   @param {Object} [options.invite] Pass an object here to pass as fields to
 *     Streams/participants/invite template, otherwise the invite button doesn't appear.
 *   @param {Boolean} [options.showSummary] Whether to show a summary
 *   @param {Array} [options.ordering] Array of user id's to order avatars in participants list.
 *   Only users mentioned in this array will ordered. Other users order none change.
 *   @param {Boolean} [options.showBlanks] Whether to show blank avatars in place of remaining spots
 *   @param {Number} [options.max]
 *    The number, if any, to show in the denominator of the summary
 *   @param {Number} [options.maxShow=10]
 *    The maximum number of participants to fetch for display
 *   @param {Function} [options.filter]
 *    Takes (userId, element) and can modify them.
 *    If this function returns false, the element is not appended.
 *   @param {Function} [options.hideIfNoParticipants=false] If true hide tool element when no participants and show when somebody join
 *   @param {Q.Event} [options.onRefresh] An event that occurs when the tool is refreshed
 */
Q.Tool.define("Streams/participants",

function _Streams_participants(options) {

	var tool = this;
	var state = tool.state;
	var $toolElement = $(tool.element);

	if (!state.publisherId) {
		throw new Q.Error("Streams/chat: missing publisherId option");
	}
	if (!state.streamName) {
		throw new Q.Error("Streams/chat: missing streamName option");
	}

	tool.Q.onStateChanged('count').set(function (name) {
		var c = state.count;
		tool.$count.text(c >= 100 ? '99+' : c.toString());
		if (state.showSummary) {
			tool.$summary.show().plugin('Q/textfill', 'refresh');
		} else {
			tool.$summary.hide();
		}

		if (state.hideIfNoParticipants) {
			if (c <= 0) {
				tool.cssDisplay = $toolElement.css("display");
				$toolElement.css("display", "none");
			} else {
				$toolElement.css("display", tool.cssDisplay);
			}
		}
	}, tool);

	tool.Q.onStateChanged('ordering').set(this.orderAvatars.bind(this));

	tool.refresh(function () {
		tool.forEachChild('Users/avatar', function () {
			tool.$elements[this.state.userId] = $(this.element);
		});
	});
},

{
	invite: {
		userChooser: true,
		appUrl: function () {
			return location.href;
		}
	},
	ordering: [],
	hideIfNoParticipants: false,
	maxShow: 10,
	maxLoad: 100,
	max: null,
	filter: function () { },
	avatar: {
		"short": true,
		icon: (window.devicePixelRatio > 1 ? '80' : '40'),
		reflectChanges: false
	},
	showSummary: true,
	showControls: false,
	showBlanks: false,
	onRefresh: new Q.Event(),
	onInvited: new Q.Event(),
	templates: {
		invite: {
			name: 'Streams/participants/invite',
			fields: {
				src: Q.Streams.iconUrl('labels/Streams/invited', 40),
				alt: 'Invite',
				title: 'Invite'
			}
		}
	}
},

{
	Q: {
		beforeRemove: function () {
			clearInterval(this.adjustInterval);
		}
	},
	/**
	 * Refresh the participants tool
	 * @method refresh
	 * @param {Function} callback pass a callback to be called after the refresh is done
	 */
	refresh: function (callback) {
		var tool = this;
		var state = tool.state;
		var $te = $(tool.element);
		tool.$elements = {};

		if (state.rendered) {
			tool.$count = $('.Streams_participants_count', $te);
			tool.$max = $('.Streams_participants_max', $te);
			tool.$summary = $('.Streams_participants_summary', $te);
			tool.$controls = $('.Streams_participants_controls', $te);
			tool.$pei = $('.Streams_participants_expand_img', $te);
			tool.$pet = $('.Streams_participants_expand_text', $te);
			tool.$pc = $('.Streams_participants_container', $te);
			tool.$avatars = $('.Streams_participants_avatars', $te);
			tool.$blanks = $('.Streams_participants_blanks', $te);
			_continue();
			return false;
		}

		tool.$count = $("");
		tool.$max = $("");
		tool.$pet = $("See All");
		tool.$pei = $('').attr({
			src: Q.url('{{Q}}/img/expand.png'),
			alt: "expand"
		});
		tool.$controls = $("
")
			.append(
				$("
")
				.append(tool.$pei, tool.$pet)
			).appendTo($te);
		tool.$summary = $("
")
			.append($('').append(tool.$count, tool.$max))
			.appendTo($te);
		tool.$pc = $("
")
			.appendTo($te);
		tool.$avatars = $("")
			.appendTo(tool.$pc);
		tool.$blanks = $("")
			.appendTo(tool.$pc);

		// set expand icon click event
		tool.$pei.plugin('Q/clickable').on(Q.Pointer.fastclick, function () {
			if (state.expanded) {
				tool.$blanks.show();
				$te.animate({height: state.originalHeight}, function () {
					state.expanded = false;
				});
				tool.$pei.attr({
					src: Q.url('{{Q}}/img/expand.png'),
					alt: 'expand'
				});
				tool.$pet.html('See All');
			} else {
				state.originalHeight = $te.height();
				tool.$blanks.hide();
				$te.animate({height: tool.$pc.height()}, function () {
					state.expanded = true;
				});
				tool.$pei.attr({
					src: Q.url('{{Q}}/img/collapse.png'),
					alt: 'collapse'
				});
				tool.$pet.html('Fewer');
			}
		});

		$te.addClass('Streams_participants_loading');
		Q.Streams.get(state.publisherId, state.streamName, function (err, stream, extra) {
			var fem = Q.firstErrorMessage(err);
			if (fem) {
				return console.warn("Streams/participants: " + fem);
			}
			if (!$(tool.element).closest('body').length) {
				return;
			}
			tool.stream = this;
			var i = 0, c = 0;
			$te.removeClass('Streams_participants_loading');
			Q.Tool.clear(tool.$avatars[0]);
			Q.Tool.clear(tool.$blanks[0]);
			tool.$avatars.empty();
			tool.$blanks.empty();
			Q.each(extra && extra.participants, function (userId, participant) {
				if (participant.state !== 'participating') {
					return;
				}
				++c;
				if (!state.maxShow || ++i <= state.maxShow) {
					_addAvatar(userId);
				}
			}, { sort: 'insertedTime', ascending: false });
			state.count = c;
			if (state.showBlanks) {
				Q.each(c, state.maxShow-1, 1, function () {
					_addAvatar('');
				});
			}
			_continue();

		}, {participants: state.maxLoad});
		return true;

		function _continue() {
			tool.stateChanged('count');

			tool.adjustInterval = setInterval(_adjustInterval, 500);

			if (state.max) {
				tool.$max.text('/' + state.max);
			}

			Q.Streams.retainWith(tool).get(state.publisherId, state.streamName, function () {
				var stream = this;
				stream.onMessage("Streams/join")
				.set(function (stream, message, messages) {
					_addAvatar(message.byUserId, true);
					++tool.state.count;
					tool.stateChanged('count');
				}, tool);
				stream.onMessage("Streams/leave")
				.set(function (stream, message, messages) {
					_removeAvatar(message.byUserId);
					--tool.state.count;
					tool.stateChanged('count');
				}, tool);
				var si = state.invite;
				if (!si || !stream.testAdminLevel('invite')) {
					Q.handle(callback, tool, []);
					return Q.handle(state.onRefresh, tool, []);
				}
				if (tool.$('.Streams_inviteTrigger').length) {
					return; // the invite button already rendered
				}
				Q.Text.get("Streams/content", function (err, result) {
					var text = result && result.invite;
					if (text) {
						state.templates.invite.fields.alt = text.command;
						state.templates.invite.fields.title = text.command;
					}
					Q.Template.render(
						'Streams/participants/invite',
						state.templates.invite.fields,
						function (err, html) {
							if (err) return;
							var $element = tool.$invite = $(html).insertBefore(tool.$avatars);
							var filter = '.Streams_inviteTrigger';
							$(tool.element).on(Q.Pointer.fastclick, filter, function () {
								var options = Q.extend({
									identifier: si.identifier
								}, si);
								Q.Streams.invite(
									state.publisherId,
									state.streamName,
									options,
									function (err, data) {
										state.onInvited.handle.call(tool, err, data);
									}
								);
								return false;
							}).on(Q.Pointer.click, filter, function () {
								return false;
							}).on(Q.Pointer.start.eventName, filter, function () {
								$(tool.element).addClass('Q_discouragePointerEvents');
								function _pointerEndHandler() {
									$(tool.element).removeClass('Q_discouragePointerEvents');
									$(window).off(Q.Pointer.end, _pointerEndHandler);
								}
								$(window).on(Q.Pointer.end, _pointerEndHandler);
							});

							if (si.clickable) {
								$('img', $element).plugin(
									'Q/clickable', Q.extend({
										triggers: $element
									}, si.clickable)
								);
							}
							Q.handle(callback, tool, []);
							Q.handle(state.onRefresh, tool, []);
						},
						state.templates.invite
					);
				});
			});
		}

		function _addAvatar(userId, prepend) {
			var $e = userId ? tool.$avatars : tool.$blanks;
			if (userId && $(".Users_avatar_tool[id*=" + userId + "]", $e).length) {
				return;
			}

			var $element = $(Q.Tool.setUpElement(
				'div',
				'Users/avatar',
				Q.extend({}, state.avatar, {
					userId: userId,
				}),
				userId || null,
				tool.prefix)
			);
			if (false === Q.handle(state.filter, tool, [userId, $element[0]])) {
				return;
			}

			$element[prepend?'prependTo':'appendTo']($e).activate();

			tool.orderAvatars();
		}

		function _removeAvatar(userId) {
			var $element = tool.$elements[userId];
			if ($element) {
				Q.removeElement($element[0], true);
			}
		}

		function _adjustInterval() {
			var w = $te.width();
			var pm = tool.$pc.outerWidth(true) - tool.$pc.width();
			if (state.showSummary) {
				w = w - tool.$summary.outerWidth(true);
			}
			if (state.showControls) {
				w = w - tool.$controls.outerWidth(true);
				var $pcw = tool.$pc.innerWidth();
				var avatarsWidth = 0;
				$(".Streams_participants_invite", tool.$pc).add(".Users_avatar_tool", tool.$avatars).each(function () {
					avatarsWidth += $(this).outerWidth(true);
				});
				var overflowed = ($pcw > 0 && avatarsWidth > $pcw);
				if (overflowed) {
					if (!state.overflowed) {
						$te.addClass('Q_overflowed');
					}
				} else {
					$te.removeClass('Q_overflowed');
					tool.$blanks.show();
				}
				state.overflowed = overflowed;
			}

			tool.$pc.width(w - pm);
		}
	},
	/**
	 * Reorder avatars according to state.orders array
	 * @method orderAvatars
	 */
	orderAvatars: function () {
		var tool = this;
		var state = this.state;
		var $avatars = $(".Users_avatar_tool", tool.$avatars);

		// if state.ordering array empty, this method have no sense
		if (Q.isEmpty(state.ordering)) {
			return;
		}

		$avatars.each(function () {
			var avatarTool = Q.Tool.from(this, "Users/avatar");
			if (!avatarTool) {
				return;
			}

			var userId = avatarTool.state.userId;
			var $element = $(this);

			if (state.ordering.includes(userId)) {
				var elementInsertAfter = null;
				var $avatars = $(".Users_avatar_tool", tool.$avatars);

				for (var i = 0; i < state.ordering.length; i++) {
					if (state.ordering[i] === userId) {
						elementInsertAfter ? $element.insertAfter(elementInsertAfter) : $element.prependTo(tool.$avatars);
						break;
					}

					$avatars.each(function () {
						if (Q.getObject("state.userId", Q.Tool.from(this, "Users/avatar")) === state.ordering[i]) {
							elementInsertAfter = this;
						}
					});
				}

				$element.addClass("Streams_participants_ordered");
			} else {
				var $lastOrdered = $(".Streams_participants_ordered", tool.$avatars).last();
				if ($lastOrdered.length) {
					$element.insertAfter($lastOrdered);
				} else {
					$element.prependTo(tool.$avatars);
				}
			}
		});
	}
});

Q.Template.set('Streams/participants/invite',
	'
' +
	'	' +
	'
{{& title}}
' +
	'
'
);

})(Q, jQuery);<commit_msg>Updated Streams/participants tool. If user not logged in and click invite, call login dialog.<commit_after>(function (Q, $) {

/**
 * @module Streams-tools
 */

/**
 * Displays participants of a given stream in a horizontal list.
 * Each item in the list is presented with an avatar and also can have a contextual associated with it.
 * @class Streams participants
 * @constructor
 * @param {Object} options Provide options for this tool
 *   @param {String} options.publisherId The id of the publisher
 *   @param {String} options.streamName The name of the stream
 *   @param {Object} [options.invite] Pass an object here to pass as fields to
 *     Streams/participants/invite template, otherwise the invite button doesn't appear.
 *   @param {Boolean} [options.showSummary] Whether to show a summary
 *   @param {Array} [options.ordering] Array of user id's to order avatars in participants list.
 *   Only users mentioned in this array will ordered. Other users order none change.
 *   @param {Boolean} [options.showBlanks] Whether to show blank avatars in place of remaining spots
 *   @param {Number} [options.max]
 *    The number, if any, to show in the denominator of the summary
 *   @param {Number} [options.maxShow=10]
 *    The maximum number of participants to fetch for display
 *   @param {Function} [options.filter]
 *    Takes (userId, element) and can modify them.
 *    If this function returns false, the element is not appended.
 *   @param {Function} [options.hideIfNoParticipants=false] If true hide tool element when no participants and show when somebody join
 *   @param {Q.Event} [options.onRefresh] An event that occurs when the tool is refreshed
 */
Q.Tool.define("Streams/participants",

function _Streams_participants(options) {

	var tool = this;
	var state = tool.state;
	var $toolElement = $(tool.element);

	if (!state.publisherId) {
		throw new Q.Error("Streams/chat: missing publisherId option");
	}
	if (!state.streamName) {
		throw new Q.Error("Streams/chat: missing streamName option");
	}

	tool.Q.onStateChanged('count').set(function (name) {
		var c = state.count;
		tool.$count.text(c >= 100 ? '99+' : c.toString());
		if (state.showSummary) {
			tool.$summary.show().plugin('Q/textfill', 'refresh');
		} else {
			tool.$summary.hide();
		}

		if (state.hideIfNoParticipants) {
			if (c <= 0) {
				tool.cssDisplay = $toolElement.css("display");
				$toolElement.css("display", "none");
			} else {
				$toolElement.css("display", tool.cssDisplay);
			}
		}
	}, tool);

	tool.Q.onStateChanged('ordering').set(this.orderAvatars.bind(this));

	tool.refresh(function () {
		tool.forEachChild('Users/avatar', function () {
			tool.$elements[this.state.userId] = $(this.element);
		});
	});
},

{
	invite: {
		userChooser: true,
		appUrl: function () {
			return location.href;
		}
	},
	ordering: [],
	hideIfNoParticipants: false,
	maxShow: 10,
	maxLoad: 100,
	max: null,
	filter: function () { },
	avatar: {
		"short": true,
		icon: (window.devicePixelRatio > 1 ? '80' : '40'),
		reflectChanges: false
	},
	showSummary: true,
	showControls: false,
	showBlanks: false,
	onRefresh: new Q.Event(),
	onInvited: new Q.Event(function (err, data) {
		var msg = Q.firstErrorMessage(err, data);
		if (msg) {
			if (msg.includes("not logged")) {
				Q.Users.login();
			}
		}
	}),
	templates: {
		invite: {
			name: 'Streams/participants/invite',
			fields: {
				src: Q.Streams.iconUrl('labels/Streams/invited', 40),
				alt: 'Invite',
				title: 'Invite'
			}
		}
	}
},

{
	Q: {
		beforeRemove: function () {
			clearInterval(this.adjustInterval);
		}
	},
	/**
	 * Refresh the participants tool
	 * @method refresh
	 * @param {Function} callback pass a callback to be called after the refresh is done
	 */
	refresh: function (callback) {
		var tool = this;
		var state = tool.state;
		var $te = $(tool.element);
		tool.$elements = {};

		if (state.rendered) {
			tool.$count = $('.Streams_participants_count', $te);
			tool.$max = $('.Streams_participants_max', $te);
			tool.$summary = $('.Streams_participants_summary', $te);
			tool.$controls = $('.Streams_participants_controls', $te);
			tool.$pei = $('.Streams_participants_expand_img', $te);
			tool.$pet = $('.Streams_participants_expand_text', $te);
			tool.$pc = $('.Streams_participants_container', $te);
			tool.$avatars = $('.Streams_participants_avatars', $te);
			tool.$blanks = $('.Streams_participants_blanks', $te);
			_continue();
			return false;
		}

		tool.$count = $("");
		tool.$max = $("");
		tool.$pet = $("See All");
		tool.$pei = $('').attr({
			src: Q.url('{{Q}}/img/expand.png'),
			alt: "expand"
		});
		tool.$controls = $("
")
			.append(
				$("
")
				.append(tool.$pei, tool.$pet)
			).appendTo($te);
		tool.$summary = $("
")
			.append($('').append(tool.$count, tool.$max))
			.appendTo($te);
		tool.$pc = $("
")
			.appendTo($te);
		tool.$avatars = $("")
			.appendTo(tool.$pc);
		tool.$blanks = $("")
			.appendTo(tool.$pc);

		// set expand icon click event
		tool.$pei.plugin('Q/clickable').on(Q.Pointer.fastclick, function () {
			if (state.expanded) {
				tool.$blanks.show();
				$te.animate({height: state.originalHeight}, function () {
					state.expanded = false;
				});
				tool.$pei.attr({
					src: Q.url('{{Q}}/img/expand.png'),
					alt: 'expand'
				});
				tool.$pet.html('See All');
			} else {
				state.originalHeight = $te.height();
				tool.$blanks.hide();
				$te.animate({height: tool.$pc.height()}, function () {
					state.expanded = true;
				});
				tool.$pei.attr({
					src: Q.url('{{Q}}/img/collapse.png'),
					alt: 'collapse'
				});
				tool.$pet.html('Fewer');
			}
		});

		$te.addClass('Streams_participants_loading');
		Q.Streams.get(state.publisherId, state.streamName, function (err, stream, extra) {
			var fem = Q.firstErrorMessage(err);
			if (fem) {
				return console.warn("Streams/participants: " + fem);
			}
			if (!$(tool.element).closest('body').length) {
				return;
			}
			tool.stream = this;
			var i = 0, c = 0;
			$te.removeClass('Streams_participants_loading');
			Q.Tool.clear(tool.$avatars[0]);
			Q.Tool.clear(tool.$blanks[0]);
			tool.$avatars.empty();
			tool.$blanks.empty();
			Q.each(extra && extra.participants, function (userId, participant) {
				if (participant.state !== 'participating') {
					return;
				}
				++c;
				if (!state.maxShow || ++i <= state.maxShow) {
					_addAvatar(userId);
				}
			}, { sort: 'insertedTime', ascending: false });
			state.count = c;
			if (state.showBlanks) {
				Q.each(c, state.maxShow-1, 1, function () {
					_addAvatar('');
				});
			}
			_continue();

		}, {participants: state.maxLoad});
		return true;

		function _continue() {
			tool.stateChanged('count');

			tool.adjustInterval = setInterval(_adjustInterval, 500);

			if (state.max) {
				tool.$max.text('/' + state.max);
			}

			Q.Streams.retainWith(tool).get(state.publisherId, state.streamName, function () {
				var stream = this;
				stream.onMessage("Streams/join")
				.set(function (stream, message, messages) {
					_addAvatar(message.byUserId, true);
					++tool.state.count;
					tool.stateChanged('count');
				}, tool);
				stream.onMessage("Streams/leave")
				.set(function (stream, message, messages) {
					_removeAvatar(message.byUserId);
					--tool.state.count;
					tool.stateChanged('count');
				}, tool);
				var si = state.invite;
				if (!si || !stream.testAdminLevel('invite')) {
					Q.handle(callback, tool, []);
					return Q.handle(state.onRefresh, tool, []);
				}
				if (tool.$('.Streams_inviteTrigger').length) {
					return; // the invite button already rendered
				}
				Q.Text.get("Streams/content", function (err, result) {
					var text = result && result.invite;
					if (text) {
						state.templates.invite.fields.alt = text.command;
						state.templates.invite.fields.title = text.command;
					}
					Q.Template.render(
						'Streams/participants/invite',
						state.templates.invite.fields,
						function (err, html) {
							if (err) return;
							var $element = tool.$invite = $(html).insertBefore(tool.$avatars);
							var filter = '.Streams_inviteTrigger';
							$(tool.element).on(Q.Pointer.fastclick, filter, function () {
								var options = Q.extend({
									identifier: si.identifier
								}, si);
								Q.Streams.invite(
									state.publisherId,
									state.streamName,
									options,
									function (err, data) {
										state.onInvited.handle.call(tool, err, data);
									}
								);
								return false;
							}).on(Q.Pointer.click, filter, function () {
								return false;
							}).on(Q.Pointer.start.eventName, filter, function () {
								$(tool.element).addClass('Q_discouragePointerEvents');
								function _pointerEndHandler() {
									$(tool.element).removeClass('Q_discouragePointerEvents');
									$(window).off(Q.Pointer.end, _pointerEndHandler);
								}
								$(window).on(Q.Pointer.end, _pointerEndHandler);
							});

							if (si.clickable) {
								$('img', $element).plugin(
									'Q/clickable', Q.extend({
										triggers: $element
									}, si.clickable)
								);
							}
							Q.handle(callback, tool, []);
							Q.handle(state.onRefresh, tool, []);
						},
						state.templates.invite
					);
				});
			});
		}

		function _addAvatar(userId, prepend) {
			var $e = userId ? tool.$avatars : tool.$blanks;
			if (userId && $(".Users_avatar_tool[id*=" + userId + "]", $e).length) {
				return;
			}

			var $element = $(Q.Tool.setUpElement(
				'div',
				'Users/avatar',
				Q.extend({}, state.avatar, {
					userId: userId,
				}),
				userId || null,
				tool.prefix)
			);
			if (false === Q.handle(state.filter, tool, [userId, $element[0]])) {
				return;
			}

			$element[prepend?'prependTo':'appendTo']($e).activate();

			tool.orderAvatars();
		}

		function _removeAvatar(userId) {
			var $element = tool.$elements[userId];
			if ($element) {
				Q.removeElement($element[0], true);
			}
		}

		function _adjustInterval() {
			var w = $te.width();
			var pm = tool.$pc.outerWidth(true) - tool.$pc.width();
			if (state.showSummary) {
				w = w - tool.$summary.outerWidth(true);
			}
			if (state.showControls) {
				w = w - tool.$controls.outerWidth(true);
				var $pcw = tool.$pc.innerWidth();
				var avatarsWidth = 0;
				$(".Streams_participants_invite", tool.$pc).add(".Users_avatar_tool", tool.$avatars).each(function () {
					avatarsWidth += $(this).outerWidth(true);
				});
				var overflowed = ($pcw > 0 && avatarsWidth > $pcw);
				if (overflowed) {
					if (!state.overflowed) {
						$te.addClass('Q_overflowed');
					}
				} else {
					$te.removeClass('Q_overflowed');
					tool.$blanks.show();
				}
				state.overflowed = overflowed;
			}

			tool.$pc.width(w - pm);
		}
	},
	/**
	 * Reorder avatars according to state.orders array
	 * @method orderAvatars
	 */
	orderAvatars: function () {
		var tool = this;
		var state = this.state;
		var $avatars = $(".Users_avatar_tool", tool.$avatars);

		// if state.ordering array empty, this method have no sense
		if (Q.isEmpty(state.ordering)) {
			return;
		}

		$avatars.each(function () {
			var avatarTool = Q.Tool.from(this, "Users/avatar");
			if (!avatarTool) {
				return;
			}

			var userId = avatarTool.state.userId;
			var $element = $(this);

			if (state.ordering.includes(userId)) {
				var elementInsertAfter = null;
				var $avatars = $(".Users_avatar_tool", tool.$avatars);

				for (var i = 0; i < state.ordering.length; i++) {
					if (state.ordering[i] === userId) {
						elementInsertAfter ? $element.insertAfter(elementInsertAfter) : $element.prependTo(tool.$avatars);
						break;
					}

					$avatars.each(function () {
						if (Q.getObject("state.userId", Q.Tool.from(this, "Users/avatar")) === state.ordering[i]) {
							elementInsertAfter = this;
						}
					});
				}

				$element.addClass("Streams_participants_ordered");
			} else {
				var $lastOrdered = $(".Streams_participants_ordered", tool.$avatars).last();
				if ($lastOrdered.length) {
					$element.insertAfter($lastOrdered);
				} else {
					$element.prependTo(tool.$avatars);
				}
			}
		});
	}
});

Q.Template.set('Streams/participants/invite',
	'
' +
	'	' +
	'
{{& title}}
' +
	'
'
);

})(Q, jQuery);<|endoftext|>


<commit_before>Dionysus.module('AdminCourse', function (Course, Dionysus, Backbone, Marionette, $) {
  'use strict';

  var CourseEditorView = Marionette.ItemView.extend({
    template: '#admin-course-editor-tpl',
    initialize: function(options) {
      this.categories = options.categories;
      this.consultants = options.consultants;
    },
    serializeData: function(){
      var data = this.model.toJSON();
      data.categories = this.categories.toSelection();
      data.consultants = this.consultants.toSelection();
      return data;
    },
    onRender: function() {
      this.$('[name="category"]').dropdown();
      this.$('[name="state"]').dropdown();
      this.$('[name="consultant"]').dropdown();
      this.$('#videoPart').hide();
      this.$('[name="approach"]').dropdown();
      this.$('[name="approach"]').change(function(eventObject){
        eventObject.target.value === 'VIDEO' ? $('#videoPart').show():$('#videoPart').hide()
      });
      var data = this.model.toJSON();
      this.$('ui.form').form('set values', data);
    },
    ui : {
      save : '.button.submit'
    },
    events: {
      'click @ui.save' : 'saveCourse'
    },
    saveCourse: function() {
      var json = this.$el.form('get values');
      this.trigger('course:save', json);
    }
  });

  var CourseController = Marionette.Controller.extend({

    createCourse: function() {
      var course = Dionysus.request('course:new');
      $.when(Dionysus.request('course:categories'),Dionysus.request('consultant:entities')).done(function(categories,consultants){
        console.log(consultants.toSelection());
        console.log(categories.toSelection());
        var editor = new CourseEditorView({model:course,categories:categories,consultants: consultants});
        editor.on('course:save', function(json) {
          course.save(json, {
            success: function(model, response, options){
              console.log(model);
              console.log(response);
              console.log(options);
            },
            error: function(model, response, options){
              console.log(model);
              console.log(response);
              console.log(options);
          }});
        });
        Dionysus.mainRegion.show(editor);

      });
    }
  });

  Dionysus.addInitializer(function() {
    new Marionette.AppRouter({
      appRoutes : {
        'admin/courses/create(/)': 'createCourse'
      },
      controller: new CourseController()
    });
  });

});
<commit_msg>fix every time save adding new course<commit_after>Dionysus.module('AdminCourse', function (Course, Dionysus, Backbone, Marionette, $) {
  'use strict';

  var CourseEditorView = Marionette.ItemView.extend({
    template: '#admin-course-editor-tpl',
    initialize: function(options) {
      this.categories = options.categories;
      this.consultants = options.consultants;
    },
    serializeData: function(){
      var data = this.model.toJSON();
      data.categories = this.categories.toSelection();
      data.consultants = this.consultants.toSelection();
      return data;
    },
    onRender: function() {
      this.$('[name="category"]').dropdown();
      this.$('[name="state"]').dropdown();
      this.$('[name="consultant"]').dropdown();
      this.$('#videoPart').hide();
      this.$('[name="approach"]').dropdown();
      this.$('[name="approach"]').change(function(eventObject){
        eventObject.target.value === 'VIDEO' ? $('#videoPart').show():$('#videoPart').hide()
      });
      var data = this.model.toJSON();
      this.$('ui.form').form('set values', data);
    },
    ui : {
      save : '.button.submit'
    },
    events: {
      'click @ui.save' : 'saveCourse'
    },
    saveCourse: function() {
      var json = this.$el.form('get values');
      this.trigger('course:save', json);
    }
  });

  var CourseController = Marionette.Controller.extend({

    createCourse: function() {
      var course = Dionysus.request('course:new');
      $.when(Dionysus.request('course:categories'),Dionysus.request('consultant:entities')).done(function(categories,consultants){
        var editor = new CourseEditorView({model:course,categories:categories,consultants: consultants});
        editor.on('course:save', function(json) {
          if(!course.isNew()){
            var id = course.get('id');
            course.clear();
            course.set('id',id);
          }
          course.save(json, {
            error: function(model, response, options){
              console.log(model);
              console.log(response);
              console.log(options);
          }}).done(function(){
            toastr.info('课程保存成功');
          });
        });
        Dionysus.mainRegion.show(editor);
      });
    }
  });

  Dionysus.addInitializer(function() {
    new Marionette.AppRouter({
      appRoutes : {
        'admin/courses/create(/)': 'createCourse'
      },
      controller: new CourseController()
    });
  });

});
<|endoftext|>


<commit_before>import { createValidator, required, email } from '../../utils/validation'

export default createValidator({
  first_name: [required],
  last_name: [required],
  email: [required, email],
  year: [required],
  interesting_project: [required],
  systems_hacked: [required],
  first_steps: [required],
  referer: [required]
})
<commit_msg>Application 'steps taken to start your club' question is required<commit_after>import { createValidator, required, email } from '../../utils/validation'

export default createValidator({
  first_name: [required],
  last_name: [required],
  email: [required, email],
  year: [required],
  interesting_project: [required],
  systems_hacked: [required],
  steps_taken: [required],
  referer: [required]
})
<|endoftext|>


<commit_before>var sum = 1;
var currentNumber = 1;

for (var increment = 2; currentNumber < 1002001; increment += 2) {
  for (var counter = 0; counter < 4; counter++) {
    currentNumber += increment;
    sum += currentNumber;
  }
}

console.log(sum);
<commit_msg>Refactor for more general usability<commit_after>diagonalSum = function(sideLength) {
  var sum = 1;
  var currentNumber = 1;

  for (var increment = 2; currentNumber < sideLength * sideLength; increment += 2) {
    for (var counter = 0; counter < 4; counter++) {
      currentNumber += increment;
      sum += currentNumber;
    }
  }

  return sum;
}

console.log(diagonalSum(1001));
<|endoftext|>


<commit_before>importScripts('https://unpkg.com/workbox-broadcast-cache-update@0.0.2/build/importScripts/workbox-broadcast-cache-update.dev.v0.0.2.js');

// Set up some constants used later on.
const testFileUrl = new URL('test.txt', location).toString();
const cacheName = 'workbox-broadcast-cache-update-example';

const broadcastCacheUpdate = new workbox.broadcastCacheUpdate.BroadcastCacheUpdate({
  // Choose whatever channel name you'd like. You can also set up multiple
  // instances that broadcast to different channel names.
  channelName: 'test-file-updates',
  // The default headers are ['content-length', 'etag', 'last-modified'].
  // None of those headers will be different when we refetch our test file,
  // though, so we'll check for 'date', which will change eventually.
  headersToCheck: ['date'],
  // You can use any identifying string here.
  source: 'broadcast-cache-update-example',
});

const testFileHandler = async () => {
  const networkResponse = await fetch(testFileUrl);
  const cache = await caches.open(cacheName);
  const cacheResponse = await cache.match(testFileUrl);

  // If there isn't a response already in the cache, then skip checking to see
  // if there's an update.
  if (cacheResponse) {
    broadcastCacheUpdate.notifyIfUpdated({
      first: cacheResponse,
      second: networkResponse,
      url: testFileUrl,
      cacheName,
    });
  }

  await cache.put(testFileUrl, networkResponse.clone());

  return networkResponse;
};

/**
 * Set up a fetch handler that only responds to requests for the test file.
 */
self.addEventListener('fetch', (event) => {
  if (event.request.url === testFileUrl) {
    event.respondWith(testFileHandler());
  }
});

/**
 * This is boilerplate, instructing the service worker to take control as soon
 * as it can.
 */
self.addEventListener('install', () => self.skipWaiting());
self.addEventListener('activate', () => self.clients.claim());
<commit_msg>Linting fix.<commit_after>importScripts('https://unpkg.com/workbox-broadcast-cache-update@0.0.2/build/importScripts/workbox-broadcast-cache-update.dev.v0.0.2.js');

// Set up some constants used later on.
const testFileUrl = new URL('test.txt', location).toString();
const cacheName = 'workbox-broadcast-cache-update-example';

const broadcastCacheUpdate =
  new workbox.broadcastCacheUpdate.BroadcastCacheUpdate({
  // Choose whatever channel name you'd like. You can also set up multiple
  // instances that broadcast to different channel names.
  channelName: 'test-file-updates',
  // The default headers are ['content-length', 'etag', 'last-modified'].
  // None of those headers will be different when we refetch our test file,
  // though, so we'll check for 'date', which will change eventually.
  headersToCheck: ['date'],
  // You can use any identifying string here.
  source: 'broadcast-cache-update-example',
});

const testFileHandler = async () => {
  const networkResponse = await fetch(testFileUrl);
  const cache = await caches.open(cacheName);
  const cacheResponse = await cache.match(testFileUrl);

  // If there isn't a response already in the cache, then skip checking to see
  // if there's an update.
  if (cacheResponse) {
    broadcastCacheUpdate.notifyIfUpdated({
      first: cacheResponse,
      second: networkResponse,
      url: testFileUrl,
      cacheName,
    });
  }

  await cache.put(testFileUrl, networkResponse.clone());

  return networkResponse;
};

/**
 * Set up a fetch handler that only responds to requests for the test file.
 */
self.addEventListener('fetch', (event) => {
  if (event.request.url === testFileUrl) {
    event.respondWith(testFileHandler());
  }
});

/**
 * This is boilerplate, instructing the service worker to take control as soon
 * as it can.
 */
self.addEventListener('install', () => self.skipWaiting());
self.addEventListener('activate', () => self.clients.claim());
<|endoftext|>


<commit_before>/* global $, $iq */

import { getLogger } from 'jitsi-meet-logger';
const logger = getLogger(__filename);
const XMPPEvents = require('../../service/xmpp/XMPPEvents');
const JitsiRecorderErrors = require('../../JitsiRecorderErrors');
const GlobalOnErrorHandler = require('../util/GlobalOnErrorHandler');

/**
 * Extracts the error details from given error element/node.
 *
 * @param {Element|Object} errorIqNode - Either DOM element or the structure
 * from ChatRoom packet2JSON.
 * @return {{
 *     code: string,
 *     type: string,
 *     message: string
 * }}
 */
function getJibriErrorDetails(errorIqNode) {
    if (typeof errorIqNode.querySelector === 'function') {
        const error = errorIqNode.querySelector('error');
        const errorText = error && error.querySelector('text');

        return error && {
            code: error.attributes.code && error.attributes.code.value,
            type: error.attributes.type && error.attributes.type.value,
            message: errorText && errorText.textContent
        };
    }

    let error = null;

    for (const child of errorIqNode.children) {
        if (child.tagName === 'error') {
            error = child;
            break;
        }
    }

    if (!error) {
        return null;
    }

    let errorText = null;

    for (const errorChild of error.children) {
        if (errorChild.tagName === 'text') {
            errorText = errorChild.value;
            break;
        }
    }

    return {
        code: error.attributes.code,
        type: error.attributes.type,
        message: errorText
    };
}

/* eslint-disable max-params */

/**
 *
 * @param type
 * @param eventEmitter
 * @param connection
 * @param focusMucJid
 * @param jirecon
 * @param roomjid
 */
export default function Recording(
        type,
        eventEmitter,
        connection,
        focusMucJid,
        jirecon,
        roomjid) {
    this.eventEmitter = eventEmitter;
    this.connection = connection;
    this.state = null;
    this.focusMucJid = focusMucJid;
    this.jirecon = jirecon;
    this.url = null;
    this.type = type;
    this._isSupported
        = !(
            (type === Recording.types.JIRECON && !this.jirecon)
                || (type !== Recording.types.JIBRI
                    && type !== Recording.types.JIBRI_FILE
                    && type !== Recording.types.COLIBRI));

    /**
     * The ID of the jirecon recording session. Jirecon generates it when we
     * initially start recording, and it needs to be used in subsequent requests
     * to jirecon.
     */
    this.jireconRid = null;
    this.roomjid = roomjid;
}

/* eslint-enable max-params */

Recording.types = {
    COLIBRI: 'colibri',
    JIRECON: 'jirecon',
    JIBRI: 'jibri',
    JIBRI_FILE: 'jibri_file'
};

Recording.status = {
    ON: 'on',
    OFF: 'off',
    AVAILABLE: 'available',
    UNAVAILABLE: 'unavailable',
    PENDING: 'pending',
    RETRYING: 'retrying',
    ERROR: 'error',
    BUSY: 'busy',
    FAILED: 'failed'
};

Recording.action = {
    START: 'start',
    STOP: 'stop'
};

Recording.prototype.handleJibriPresence = function(jibri) {
    const attributes = jibri.attributes;

    if (!attributes) {
        return;
    }

    const newState = attributes.status;
    const errorDetails = getJibriErrorDetails(jibri);

    logger.log(`Handle Jibri presence : ${newState}`, errorDetails);

    if (newState === this.state) {
        return;
    }

    if (newState === 'undefined') {
        this.state = Recording.status.UNAVAILABLE;
    } else if (newState === Recording.status.OFF) {
        if (!this.state
            || this.state === 'undefined'
            || this.state === Recording.status.UNAVAILABLE) {
            this.state = Recording.status.AVAILABLE;
        } else {
            this.state = Recording.status.OFF;
        }
    } else {
        this.state = newState;
    }

    this.eventEmitter.emit(XMPPEvents.RECORDER_STATE_CHANGED, this.state);
};

/* eslint-disable max-params */

Recording.prototype.setRecordingJibri = function(
        state,
        callback,
        errCallback,
        options = {}) {
    if (state === this.state) {
        errCallback(JitsiRecorderErrors.INVALID_STATE);
    }

    // FIXME jibri does not accept IQ without 'url' attribute set ?
    const iq
        = $iq({
            to: this.focusMucJid,
            type: 'set'
        })
            .c('jibri', {
                'xmlns': 'http://jitsi.org/protocol/jibri',
                'action':
                    state === Recording.status.ON
                        ? Recording.action.START
                        : Recording.action.STOP,
                'recording_mode':
                    this.type === Recording.types.JIBRI_FILE
                        ? 'file'
                        : 'stream',
                'streamid':
                    this.type === Recording.types.JIBRI
                        ? options.streamId
                        : undefined
            })
            .up();

    logger.log(`Set jibri recording: ${state}`, iq.nodeTree);
    logger.log(iq.nodeTree);
    this.connection.sendIQ(
        iq,
        result => {
            logger.log('Result', result);

            const jibri = $(result).find('jibri');

            callback(jibri.attr('state'), jibri.attr('url'));
        },
        error => {
            logger.log(
                'Failed to start recording, error: ',
                getJibriErrorDetails(error));
            errCallback(error);
        });
};

/* eslint-enable max-params */

Recording.prototype.setRecordingJirecon
    = function(state, callback, errCallback) {
        if (state === this.state) {
            errCallback(new Error('Invalid state!'));
        }

        const iq
            = $iq({
                to: this.jirecon,
                type: 'set'
            })
                .c('recording', { xmlns: 'http://jitsi.org/protocol/jirecon',
                    action: state === Recording.status.ON
                        ? Recording.action.START
                        : Recording.action.STOP,
                    mucjid: this.roomjid });

        if (state === Recording.status.OFF) {
            iq.attrs({ rid: this.jireconRid });
        }

        logger.log('Start recording');
        const self = this;

        this.connection.sendIQ(
            iq,
            result => {
                // TODO wait for an IQ with the real status, since this is
                // provisional?
                // eslint-disable-next-line newline-per-chained-call
                self.jireconRid = $(result).find('recording').attr('rid');

                const stateStr
                    = state === Recording.status.ON ? 'started' : 'stopped';

                logger.log(`Recording ${stateStr}(jirecon)${result}`);

                self.state = state;
                if (state === Recording.status.OFF) {
                    self.jireconRid = null;
                }

                callback(state);
            },
            error => {
                logger.log('Failed to start recording, error: ', error);
                errCallback(error);
            });
    };

/* eslint-disable max-params */

// Sends a COLIBRI message which enables or disables (according to 'state')
// the recording on the bridge. Waits for the result IQ and calls 'callback'
// with the new recording state, according to the IQ.
Recording.prototype.setRecordingColibri = function(
        state,
        callback,
        errCallback,
        options) {
    const elem = $iq({
        to: this.focusMucJid,
        type: 'set'
    });

    elem.c('conference', {
        xmlns: 'http://jitsi.org/protocol/colibri'
    });
    elem.c('recording', {
        state,
        token: options.token
    });

    const self = this;

    this.connection.sendIQ(
        elem,
        result => {
            logger.log('Set recording "', state, '". Result:', result);
            const recordingElem = $(result).find('>conference>recording');
            const newState = recordingElem.attr('state');

            self.state = newState;
            callback(newState);

            if (newState === 'pending') {
                self.connection.addHandler(iq => {
                    // eslint-disable-next-line newline-per-chained-call
                    const s = $(iq).find('recording').attr('state');

                    if (s) {
                        self.state = newState;
                        callback(s);
                    }
                }, 'http://jitsi.org/protocol/colibri', 'iq', null, null, null);
            }
        },
        error => {
            logger.warn(error);
            errCallback(error);
        }
    );
};

/* eslint-enable max-params */

Recording.prototype.setRecording = function(...args) {
    switch (this.type) {
    case Recording.types.JIRECON:
        this.setRecordingJirecon(...args);
        break;
    case Recording.types.COLIBRI:
        this.setRecordingColibri(...args);
        break;
    case Recording.types.JIBRI:
    case Recording.types.JIBRI_FILE:
        this.setRecordingJibri(...args);
        break;
    default: {
        const errmsg = 'Unknown recording type!';

        GlobalOnErrorHandler.callErrorHandler(new Error(errmsg));
        logger.error(errmsg);
        break;
    }
    }
};

/**
 * Starts/stops the recording.
 * @param {Object} options
 * @param {string} options.token token for authentication
 * @param {string} options.streamId the stream ID to be used with Jibri in
 * the streaming mode.
 * @param statusChangeHandler {function} receives the new status as argument.
 */
Recording.prototype.toggleRecording = function(
        options = { },
        statusChangeHandler) {
    const oldState = this.state;

    // If the recorder is currently unavailable we throw an error.
    if (oldState === Recording.status.UNAVAILABLE
        || oldState === Recording.status.FAILED) {
        statusChangeHandler(
            Recording.status.FAILED,
            JitsiRecorderErrors.RECORDER_UNAVAILABLE);
    } else if (oldState === Recording.status.BUSY) {
        statusChangeHandler(
            Recording.status.BUSY,
            JitsiRecorderErrors.RECORDER_BUSY);
    }

    // If we're about to turn ON the recording we need either a streamId or
    // an authentication token depending on the recording type. If we don't
    // have any of those we throw an error.
    if ((oldState === Recording.status.OFF
                || oldState === Recording.status.AVAILABLE)
            && ((!options.token && this.type === Recording.types.COLIBRI)
                || (!options.streamId
                    && this.type === Recording.types.JIBRI))) {
        statusChangeHandler(
            Recording.status.FAILED,
            JitsiRecorderErrors.NO_TOKEN);
        logger.error('No token passed!');

        return;
    }

    const newState
        = oldState === Recording.status.AVAILABLE
                || oldState === Recording.status.OFF
            ? Recording.status.ON
            : Recording.status.OFF;

    const self = this;

    logger.log('Toggle recording (old state, new state): ', oldState, newState);
    this.setRecording(
        newState,
        (state, url) => {
            // If the state is undefined we're going to wait for presence
            // update.
            if (state && state !== oldState) {
                self.state = state;
                self.url = url;
                statusChangeHandler(state);
            }
        },
        error => statusChangeHandler(Recording.status.FAILED, error),
        options);
};

/**
 * Returns true if the recording is supproted and false if not.
 */
Recording.prototype.isSupported = function() {
    return this._isSupported;
};

/**
 * Returns null if the recording is not supported, "on" if the recording started
 * and "off" if the recording is not started.
 */
Recording.prototype.getState = function() {
    return this.state;
};

/**
 * Returns the url of the recorded video.
 */
Recording.prototype.getURL = function() {
    return this.url;
};
<commit_msg>set jibri displayname<commit_after>/* global $, $iq */

import { getLogger } from 'jitsi-meet-logger';
const logger = getLogger(__filename);
const XMPPEvents = require('../../service/xmpp/XMPPEvents');
const JitsiRecorderErrors = require('../../JitsiRecorderErrors');
const GlobalOnErrorHandler = require('../util/GlobalOnErrorHandler');

/**
 * Extracts the error details from given error element/node.
 *
 * @param {Element|Object} errorIqNode - Either DOM element or the structure
 * from ChatRoom packet2JSON.
 * @return {{
 *     code: string,
 *     type: string,
 *     message: string
 * }}
 */
function getJibriErrorDetails(errorIqNode) {
    if (typeof errorIqNode.querySelector === 'function') {
        const error = errorIqNode.querySelector('error');
        const errorText = error && error.querySelector('text');

        return error && {
            code: error.attributes.code && error.attributes.code.value,
            type: error.attributes.type && error.attributes.type.value,
            message: errorText && errorText.textContent
        };
    }

    let error = null;

    for (const child of errorIqNode.children) {
        if (child.tagName === 'error') {
            error = child;
            break;
        }
    }

    if (!error) {
        return null;
    }

    let errorText = null;

    for (const errorChild of error.children) {
        if (errorChild.tagName === 'text') {
            errorText = errorChild.value;
            break;
        }
    }

    return {
        code: error.attributes.code,
        type: error.attributes.type,
        message: errorText
    };
}

/* eslint-disable max-params */

/**
 *
 * @param type
 * @param eventEmitter
 * @param connection
 * @param focusMucJid
 * @param jirecon
 * @param roomjid
 */
export default function Recording(
        type,
        eventEmitter,
        connection,
        focusMucJid,
        jirecon,
        roomjid) {
    this.eventEmitter = eventEmitter;
    this.connection = connection;
    this.state = null;
    this.focusMucJid = focusMucJid;
    this.jirecon = jirecon;
    this.url = null;
    this.type = type;
    this._isSupported
        = !(
            (type === Recording.types.JIRECON && !this.jirecon)
                || (type !== Recording.types.JIBRI
                    && type !== Recording.types.JIBRI_FILE
                    && type !== Recording.types.COLIBRI));

    /**
     * The ID of the jirecon recording session. Jirecon generates it when we
     * initially start recording, and it needs to be used in subsequent requests
     * to jirecon.
     */
    this.jireconRid = null;
    this.roomjid = roomjid;
}

/* eslint-enable max-params */

Recording.types = {
    COLIBRI: 'colibri',
    JIRECON: 'jirecon',
    JIBRI: 'jibri',
    JIBRI_FILE: 'jibri_file'
};

Recording.status = {
    ON: 'on',
    OFF: 'off',
    AVAILABLE: 'available',
    UNAVAILABLE: 'unavailable',
    PENDING: 'pending',
    RETRYING: 'retrying',
    ERROR: 'error',
    BUSY: 'busy',
    FAILED: 'failed'
};

Recording.action = {
    START: 'start',
    STOP: 'stop'
};

Recording.prototype.handleJibriPresence = function(jibri) {
    const attributes = jibri.attributes;

    if (!attributes) {
        return;
    }

    const newState = attributes.status;
    const errorDetails = getJibriErrorDetails(jibri);

    logger.log(`Handle Jibri presence : ${newState}`, errorDetails);

    if (newState === this.state) {
        return;
    }

    if (newState === 'undefined') {
        this.state = Recording.status.UNAVAILABLE;
    } else if (newState === Recording.status.OFF) {
        if (!this.state
            || this.state === 'undefined'
            || this.state === Recording.status.UNAVAILABLE) {
            this.state = Recording.status.AVAILABLE;
        } else {
            this.state = Recording.status.OFF;
        }
    } else {
        this.state = newState;
    }

    this.eventEmitter.emit(
        XMPPEvents.RECORDER_STATE_CHANGED, this.state, errorDetails);
};

/* eslint-disable max-params */

Recording.prototype.setRecordingJibri = function(
        state,
        callback,
        errCallback,
        options = {}) {
    if (state === this.state) {
        errCallback(JitsiRecorderErrors.INVALID_STATE);
    }

    // FIXME jibri does not accept IQ without 'url' attribute set ?
    const iq
        = $iq({
            to: this.focusMucJid,
            type: 'set'
        })
            .c('jibri', {
                'xmlns': 'http://jitsi.org/protocol/jibri',
                'action':
                    state === Recording.status.ON
                        ? Recording.action.START
                        : Recording.action.STOP,
                'recording_mode':
                    this.type === Recording.types.JIBRI_FILE
                        ? 'file'
                        : 'stream',
                'streamid':
                    this.type === Recording.types.JIBRI
                        ? options.streamId
                        : undefined,
                'displayname': options.displayname
            })
            .up();

    logger.log(`Set jibri recording: ${state}`, iq.nodeTree);
    logger.log(iq.nodeTree);
    this.connection.sendIQ(
        iq,
        result => {
            logger.log('Result', result);

            const jibri = $(result).find('jibri');

            callback(jibri.attr('state'), jibri.attr('url'));
        },
        error => {
            logger.log(
                'Failed to start recording, error: ',
                getJibriErrorDetails(error));
            errCallback(error);
        });
};

/* eslint-enable max-params */

Recording.prototype.setRecordingJirecon
    = function(state, callback, errCallback) {
        if (state === this.state) {
            errCallback(new Error('Invalid state!'));
        }

        const iq
            = $iq({
                to: this.jirecon,
                type: 'set'
            })
                .c('recording', { xmlns: 'http://jitsi.org/protocol/jirecon',
                    action: state === Recording.status.ON
                        ? Recording.action.START
                        : Recording.action.STOP,
                    mucjid: this.roomjid });

        if (state === Recording.status.OFF) {
            iq.attrs({ rid: this.jireconRid });
        }

        logger.log('Start recording');
        const self = this;

        this.connection.sendIQ(
            iq,
            result => {
                // TODO wait for an IQ with the real status, since this is
                // provisional?
                // eslint-disable-next-line newline-per-chained-call
                self.jireconRid = $(result).find('recording').attr('rid');

                const stateStr
                    = state === Recording.status.ON ? 'started' : 'stopped';

                logger.log(`Recording ${stateStr}(jirecon)${result}`);

                self.state = state;
                if (state === Recording.status.OFF) {
                    self.jireconRid = null;
                }

                callback(state);
            },
            error => {
                logger.log('Failed to start recording, error: ', error);
                errCallback(error);
            });
    };

/* eslint-disable max-params */

// Sends a COLIBRI message which enables or disables (according to 'state')
// the recording on the bridge. Waits for the result IQ and calls 'callback'
// with the new recording state, according to the IQ.
Recording.prototype.setRecordingColibri = function(
        state,
        callback,
        errCallback,
        options) {
    const elem = $iq({
        to: this.focusMucJid,
        type: 'set'
    });

    elem.c('conference', {
        xmlns: 'http://jitsi.org/protocol/colibri'
    });
    elem.c('recording', {
        state,
        token: options.token
    });

    const self = this;

    this.connection.sendIQ(
        elem,
        result => {
            logger.log('Set recording "', state, '". Result:', result);
            const recordingElem = $(result).find('>conference>recording');
            const newState = recordingElem.attr('state');

            self.state = newState;
            callback(newState);

            if (newState === 'pending') {
                self.connection.addHandler(iq => {
                    // eslint-disable-next-line newline-per-chained-call
                    const s = $(iq).find('recording').attr('state');

                    if (s) {
                        self.state = newState;
                        callback(s);
                    }
                }, 'http://jitsi.org/protocol/colibri', 'iq', null, null, null);
            }
        },
        error => {
            logger.warn(error);
            errCallback(error);
        }
    );
};

/* eslint-enable max-params */

Recording.prototype.setRecording = function(...args) {
    switch (this.type) {
    case Recording.types.JIRECON:
        this.setRecordingJirecon(...args);
        break;
    case Recording.types.COLIBRI:
        this.setRecordingColibri(...args);
        break;
    case Recording.types.JIBRI:
    case Recording.types.JIBRI_FILE:
        this.setRecordingJibri(...args);
        break;
    default: {
        const errmsg = 'Unknown recording type!';

        GlobalOnErrorHandler.callErrorHandler(new Error(errmsg));
        logger.error(errmsg);
        break;
    }
    }
};

/**
 * Starts/stops the recording.
 * @param {Object} options
 * @param {string} options.token token for authentication
 * @param {string} options.streamId the stream ID to be used with Jibri in
 * the streaming mode.
 * @param statusChangeHandler {function} receives the new status as argument.
 */
Recording.prototype.toggleRecording = function(
        options = { },
        statusChangeHandler) {
    const oldState = this.state;

    // If the recorder is currently unavailable we throw an error.
    if (oldState === Recording.status.UNAVAILABLE
        || oldState === Recording.status.FAILED) {
        statusChangeHandler(
            Recording.status.FAILED,
            JitsiRecorderErrors.RECORDER_UNAVAILABLE);
    } else if (oldState === Recording.status.BUSY) {
        statusChangeHandler(
            Recording.status.BUSY,
            JitsiRecorderErrors.RECORDER_BUSY);
    }

    // If we're about to turn ON the recording we need either a streamId or
    // an authentication token depending on the recording type. If we don't
    // have any of those we throw an error.
    if ((oldState === Recording.status.OFF
                || oldState === Recording.status.AVAILABLE)
            && ((!options.token && this.type === Recording.types.COLIBRI)
                || (!options.streamId
                    && this.type === Recording.types.JIBRI))) {
        statusChangeHandler(
            Recording.status.FAILED,
            JitsiRecorderErrors.NO_TOKEN);
        logger.error('No token passed!');

        return;
    }

    const newState
        = oldState === Recording.status.AVAILABLE
                || oldState === Recording.status.OFF
            ? Recording.status.ON
            : Recording.status.OFF;

    const self = this;

    logger.log('Toggle recording (old state, new state): ', oldState, newState);
    this.setRecording(
        newState,
        (state, url) => {
            // If the state is undefined we're going to wait for presence
            // update.
            if (state && state !== oldState) {
                self.state = state;
                self.url = url;
                statusChangeHandler(state);
            }
        },
        error => statusChangeHandler(Recording.status.FAILED, error),
        options);
};

/**
 * Returns true if the recording is supproted and false if not.
 */
Recording.prototype.isSupported = function() {
    return this._isSupported;
};

/**
 * Returns null if the recording is not supported, "on" if the recording started
 * and "off" if the recording is not started.
 */
Recording.prototype.getState = function() {
    return this.state;
};

/**
 * Returns the url of the recorded video.
 */
Recording.prototype.getURL = function() {
    return this.url;
};
<|endoftext|>