<commit_before>/* jshint unused:true, undef:true, browser:true */
/* global Phaser:false */

// TODO:
// Make expanding/contracting explosion. (Graphics plus tween).
// Collide explosion with the pig.
// Make the emitter emit random colored confetti.

var Flak = function(game, position) {
    //  We call the Phaser.Sprite passing in the game reference
    //  We're giving it a random X/Y position here, just for the sake of this demo - you could also pass the x/y in the constructor
    Phaser.Sprite.call(this, game, position.x, position.y, this.spriteImage);
    this.anchor.setTo(0.5, 0.5);
    game.physics.arcade.enable(this);
    game.add.existing(this);
};
Flak.prototype = Object.create(Phaser.Sprite.prototype);
// Expanding outward unless this is true.
Flak.prototype.imploding = false;
// Pixels per frame.
Flak.prototype.sizeChangeVelocity = 20;
// How many pixels big before we implode.
// TODO: Look into how phaser handles scaling. Scaling seems off on my high rez screen.
Flak.prototype.maxSize = 600;
Flak.prototype.update = function() {
    //  Automatically called by World.update
    // Increase the size of the sprite.
    if (!this.imploding) {
        this.width += this.sizeChangeVelocity;
        this.height += this.sizeChangeVelocity;
        if (this.width > this.maxSize) {
            this.imploding = true;
        }
    } else {
        this.width -= this.sizeChangeVelocity;
        this.height -= this.sizeChangeVelocity;
        if (this.width <= 0) {
            // Kill doesn't opt for gc, just rmeoves it from render and update
            // cycle.
            //this.kill();
            // Destroy removes the object from the game.
            this.destroy();
        }
    }
};
// Call before using flak instances.
Flak.init = function(game) {
    var spriteImage = game.add.bitmapData(30, 30, "flak");
    spriteImage.circle(15, 15, 1, "#ff0000");
    spriteImage.fill(0, 0, 0, 0);
    Flak.prototype.spriteImage = spriteImage;
};

var Title = function() {};
Title.prototype = Object.create(Phaser.State);
Title.prototype.preload = function() {
    // Load all items for this game. In other games, might want to load
    // state specific items in respective stages.

    // WebGL doesn't like file:// protocol, need a server.
    this.game.load.image('pig', 'assets/sprites/pig.png');

    // Audio has some decoding helpers. See docs.
    this.game.load.audio('explosion', 'assets/sounds/explosion.wav');
};
Title.prototype.create = function() {
    this.titleText = this.game.add.text(this.game.world.centerX, this.game.world.centerY,
        "Pig In Space", {
        // These seem like canvas stylings, not CSS stylings.
        fill: "#ffffff",
		font: "bold 42px Arial",
	});
    // Anchor is how the text is centered relative to the placement point.
    this.titleText.anchor.set(0.5);

    this.game.input.onDown.add(function() {
        // console.log("click on game world");
        // This event listener gets purged when we transition to "play" state.
        game.state.start("play", true);
    }.bind(this));
};

// Play state.
var Play = function() {};
Play.prototype = Object.create(Phaser.State);
Play.prototype.preload = function() {
    // Note: I feel like I'm doing something wrong. I've tried to add the
    // bitmapData to the cache, but can't seem to make the loader obey and
    // there isn't a load.bitmapdata option.
    // ---
    // create a new bitmap data object
    this.confetti = game.add.bitmapData(10, 10, "confetti");
    // Can access canvas context wtih .ctx if needed.
    this.confetti.fill(255, 255, 255, 1);

    // Flak needs to be initialized.
    Flak.init(this.game);
};
Play.prototype.create = function() {
    // To make the sprite move we need to enable Arcade Physics
    this.game.physics.startSystem(Phaser.Physics.ARCADE);

    this.timerText = this.game.add.text(32, this.game.height - 32, "-", {
        fill: "#ffffff",
		font: "bold 16px Arial",
	});

    // Pig sprite.
    this.sprite = this.game.add.sprite(this.game.world.centerX, this.game.world.centerY, 'pig');
    this.sprite.anchor.set(0.5);

    // And enable the Sprite to have a physics body:
    this.game.physics.arcade.enable(this.sprite);

    this.emitter = game.add.emitter(0, 0, 100);
    this.emitter.makeParticles(this.confetti);
    this.emitter.gravity = 200;

    this.game.input.onDown.add(function(pointer) {
        // Simple immediate sound play.
        game.sound.play("explosion", true);

        // use the bitmap data as the texture for the sprite
        //var confetti = game.add.sprite(this.game.input.x, this.game.input.y, this.confetti);
        //game.physics.enable(confetti, Phaser.Physics.ARCADE);
        //confetti.tint = "0xFF0000";
        // Tint seems forgiving.
        //confetti.tint = Phaser.Color.getRandomColor();
        //confetti.body.gravity.set(0, 180);

        this.emitter.x = pointer.x;
        this.emitter.y = pointer.y;
        //  The first parameter sets the effect to "explode" which means all particles are emitted at once
        //  The second gives each particle a 2000ms lifespan
        //  The third is ignored when using burst/explode mode
        //  The final parameter (10) is how many particles will be emitted in this single burst
        this.emitter.start(true, 2000, null, 10);

        // Todo need to keep track of Flak.
        new Flak(game, pointer);

        // confetti.checkWorldBounds = true;
        // confetti.outOfBoundsKill = true;
        // Diagnostics. It works :)
        // confetti.events.onOutOfBounds.add(function() {
        //     console.log("out of bounds confetti");
        // });

    }.bind(this));
};
Play.prototype.update = function() {
    // If the sprite is > 8px away from the pointer then let's move to it
    if (this.game.physics.arcade.distanceToPointer(this.sprite, this.game.input.activePointer) > 8) {
        // Make the object seek to the active pointer (mouse or touch).
        this.game.physics.arcade.moveToPointer(this.sprite, 300);
    } else {
        // Otherwise turn off velocity because we're close enough to the pointer
        this.sprite.body.velocity.set(0);
    }

    this.timerText.text = "Time: " + Date.now();
};
Play.prototype.render = function() {
    // Info about input params are positioning offset.
	this.game.debug.inputInfo(32, 32);
    //this.game.debug.pointer();
    // Info about sprites.
    //game.debug.bodyInfo(sprite, 32, 32);
    //game.debug.body(sprite);
    // Other debug helpers.
    //-----
    // Num entities registered in the game.
    //console.log(game.world.children.length);
};

var game = new Phaser.Game(
    // String dimensions are considered percentages of parent container.
    "100", "100",
    // Let Phaser choose the renderer.
    Phaser.AUTO,
    // What element do we want to use as the parent.
    document.querySelector(".game-container")
);

// Move to multiple states.
game.state.add("play", Play);
game.state.add("title", Title);
game.state.start("title");
<commit_msg>Multi-colored. Neat.<commit_after>/* jshint unused:true, undef:true, browser:true */
/* global Phaser:false */

// TODO:
// Collide explosion with the pig.
// Make the emitter emit random colored confetti.

var Flak = function(position) {
    Phaser.Sprite.call(this, this.game, position.x, position.y, this.spriteImage);
    // Center flak over pointer.
    this.anchor.setTo(0.5, 0.5);
    // For collisions.
    this.game.physics.arcade.enable(this);
    this.game.add.existing(this);
};
Flak.prototype = Object.create(Phaser.Sprite.prototype);
// Expanding outward unless this is true.
Flak.prototype.imploding = false;
// Pixels per frame.
Flak.prototype.sizeChangeVelocity = 5;
// How many pixels big before we implode.
Flak.prototype.maxSize = 100;
Flak.prototype.update = function() {
    //  Automatically called by World.update
    // Increase the size of the sprite.
    if (!this.imploding) {
        this.width += this.sizeChangeVelocity;
        this.height += this.sizeChangeVelocity;
        if (this.width > this.maxSize) {
            this.imploding = true;
        }
    } else {
        this.width -= this.sizeChangeVelocity;
        this.height -= this.sizeChangeVelocity;
        if (this.width <= 0) {
            // Kill doesn't opt for gc, just rmeoves it from render and update
            // cycle.
            //this.kill();
            // Destroy removes the object from the game.
            this.destroy();
        }
    }
};
// Reference to sprite shared by all Flak instances. Initialized during init.
Flak.prototype.spriteImage = null;
// Reference to game instance using flak. Initialized during init.
Flak.prototype.game = null;
// Call before using flak instances.
Flak.init = function(game) {
    var spriteImage = game.add.bitmapData(30, 30, "flak");
    spriteImage.circle(15, 15, 7, "#ff0000");
    // Seems the default babckground is transparent.
    //spriteImage.fill(0, 0, 0, 0);
    Flak.prototype.spriteImage = spriteImage;

    Flak.prototype.game = game;
};

var Title = function() {};
Title.prototype = Object.create(Phaser.State);
Title.prototype.preload = function() {
    // Load all items for this game. In other games, might want to load
    // state specific items in respective stages.

    // WebGL doesn't like file:// protocol, need a server.
    this.game.load.image('pig', 'assets/sprites/pig.png');

    // Audio has some decoding helpers. See docs.
    this.game.load.audio('explosion', 'assets/sounds/explosion.wav');
};
Title.prototype.create = function() {
    this.titleText = this.game.add.text(this.game.world.centerX, this.game.world.centerY,
        "Pig In Space", {
        // These seem like canvas stylings, not CSS stylings.
        fill: "#ffffff",
		font: "bold 42px Arial",
	});
    // Anchor is how the text is centered relative to the placement point.
    this.titleText.anchor.set(0.5);

    this.game.input.onDown.add(function() {
        // console.log("click on game world");
        // This event listener gets purged when we transition to "play" state.
        game.state.start("play", true);
    }.bind(this));
};

// Play state.
var Play = function() {};
Play.prototype = Object.create(Phaser.State);
Play.prototype.preload = function() {
    // Note: I feel like I'm doing something wrong. I've tried to add the
    // bitmapData to the cache, but can't seem to make the loader obey and
    // there isn't a load.bitmapdata option.
    // ---
    // create a new bitmap data object
    this.confetti = game.add.bitmapData(10, 10, "confetti");
    // Can access canvas context wtih .ctx if needed.
    this.confetti.fill(255, 255, 255, 1);

    // Flak needs to be initialized.
    Flak.init(this.game);
};
Play.prototype.create = function() {
    // To make the sprite move we need to enable Arcade Physics
    this.game.physics.startSystem(Phaser.Physics.ARCADE);

    this.timerText = this.game.add.text(32, this.game.height - 32, "-", {
        fill: "#ffffff",
		font: "bold 16px Arial",
	});

    // Pig sprite.
    this.sprite = this.game.add.sprite(this.game.world.centerX, this.game.world.centerY, 'pig');
    this.sprite.anchor.set(0.5);

    // Enable the Sprite to have a physics body:
    this.game.physics.arcade.enable(this.sprite);

    this.emitter = game.add.emitter(0, 0, 100);
    this.emitter.makeParticles(this.confetti);
    // TODO: Need to find a better way to add bitmap data with customer colors
    // to the emitter, or maybe just make the random colors onload... whatever.
    this.emitter.forEach(function(p) {
        // Iterate each particle.
        p.tint = Phaser.Color.getRandomColor();
    });
    this.emitter.gravity = 200;

    this.game.input.onDown.add(function(pointer) {
        // Simple immediate sound play.
        game.sound.play("explosion", true);

        // use the bitmap data as the texture for the sprite
        //var confetti = game.add.sprite(this.game.input.x, this.game.input.y, this.confetti);
        //game.physics.enable(confetti, Phaser.Physics.ARCADE);
        //confetti.tint = "0xFF0000";
        // Tint seems forgiving.
        //confetti.tint = Phaser.Color.getRandomColor();
        //confetti.body.gravity.set(0, 180);

        this.emitter.x = pointer.x;
        this.emitter.y = pointer.y;
        //  The first parameter sets the effect to "explode" which means all particles are emitted at once
        //  The second gives each particle a 2000ms lifespan
        //  The third is ignored when using burst/explode mode
        //  The final parameter (10) is how many particles will be emitted in this single burst
        this.emitter.start(true, 2000, null, 10);
        // Better to do this above.
        //this.emitter.forEachExists(function(p) {
            //console.log("A particle:", p);
            // WARNING: this modifies all confetti in existence.
            //p.tint = Phaser.Color.getRandomColor();
        //});

        // Todo need to keep track of Flak.
        new Flak(pointer);

        // confetti.checkWorldBounds = true;
        // confetti.outOfBoundsKill = true;
        // Diagnostics. It works :)
        // confetti.events.onOutOfBounds.add(function() {
        //     console.log("out of bounds confetti");
        // });

    }.bind(this));
};
Play.prototype.update = function() {
    // If the sprite is > 8px away from the pointer then let's move to it
    if (this.game.physics.arcade.distanceToPointer(this.sprite, this.game.input.activePointer) > 8) {
        // Make the object seek to the active pointer (mouse or touch).
        this.game.physics.arcade.moveToPointer(this.sprite, 300);
    } else {
        // Otherwise turn off velocity because we're close enough to the pointer
        this.sprite.body.velocity.set(0);
    }

    this.timerText.text = "Time: " + Date.now();
};
Play.prototype.render = function() {
    // Info about input params are positioning offset.
	this.game.debug.inputInfo(32, 32);
    //this.game.debug.pointer();
    // Info about sprites.
    //game.debug.bodyInfo(sprite, 32, 32);
    //game.debug.body(sprite);
    // Other debug helpers.
    //-----
    // Num entities registered in the game.
    //console.log(game.world.children.length);
};

var game = new Phaser.Game(
    // String dimensions are considered percentages of parent container.
    "100", "100",
    // Let Phaser choose the renderer.
    Phaser.AUTO,
    // What element do we want to use as the parent.
    document.querySelector(".game-container")
);

// Move to multiple states.
game.state.add("play", Play);
game.state.add("title", Title);
game.state.start("title");
<|endoftext|>


<commit_before>/* See license.txt for terms of usage */

define([
    "firebug/firebug",
    "firebug/lib/trace",
    "firebug/lib/domplate",
    "firebug/css/cssModule",
    "firebug/editor/baseEditor",
    "firebug/editor/editor",
    "firebug/editor/sourceEditor",
],
function(Firebug, FBTrace, Domplate, CSSModule, BaseEditor, Editor, SourceEditor) {

// ********************************************************************************************* //
// Constants

var {DIV} = Domplate;

// ********************************************************************************************* //
// StyleSheetEditor

/**
 * StyleSheetEditor represents the full-sized editor used for Source/Live Edit
 * within the CSS panel.
 */
function StyleSheetEditor(doc)
{
    this.box = this.tag.replace({}, doc, this);

    this.onEditorTextChangeListener = this.onEditorTextChange.bind(this);
    var config = {
        mode: "css",
        readOnly: false,
        gutters: []
    };
    // Initialize source editor, then append to the box.
    this.editor = new SourceEditor();
    this.editor.init(this.box, config, this.onEditorInitialize.bind(this));
}

StyleSheetEditor.prototype = domplate(BaseEditor,
{
    multiLine: true,

    tag: DIV({"class": "styleSheetEditor fullPanelEditor"}),

    // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //

    onEditorInitialize: function()
    {
        this.editor.addEventListener(SourceEditor.Events.textChange,
            this.onEditorTextChangeListener);
    },

    getValue: function()
    {
        return this.editor.getText();
    },

    setValue: function(value)
    {
        return this.editor.setText(value, "css");
    },

    show: function(target, panel, value, textSize)
    {
        this.target = target;
        this.panel = panel;

        // Show the box that editor already is appended to.
        this.panel.panelNode.appendChild(this.box);
        this.editor.setText(value, "css");

        // match CSSModule.getEditorOptionKey
        var command = Firebug.chrome.$("cmd_firebug_togglecssEditMode");
        command.setAttribute("checked", true);
    },

    hide: function()
    {
        var command = Firebug.chrome.$("cmd_firebug_togglecssEditMode");
        command.setAttribute("checked", false);

        if (this.box.parentNode == this.panel.panelNode)
            this.panel.panelNode.removeChild(this.box);

        delete this.target;
        delete this.panel;
        delete this.styleSheet;
    },

    saveEdit: function(target, value, previousValue)
    {
        if (FBTrace.DBG_CSS)
            FBTrace.sysout("StyleSheetEditor.saveEdit", arguments);

        CSSModule.freeEdit(this.styleSheet, value);
    },

    beginEditing: function()
    {
        if (FBTrace.DBG_CSS)
            FBTrace.sysout("StyleSheetEditor.beginEditing", arguments);

        this.editing = true;
    },

    endEditing: function()
    {
        if (FBTrace.DBG_CSS)
            FBTrace.sysout("StyleSheetEditor.endEditing", arguments);

        this.editing = false;
        this.panel.refresh();
        return true;
    },

    // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //

    onEditorTextChange: function()
    {
        Editor.update();
    },

    scrollToLine: function(line, offset)
    {
        this.editor.scrollToLine(line);
    }
});

// ********************************************************************************************* //
// Registration

return StyleSheetEditor;

// ********************************************************************************************* //
});<commit_msg>- readded StyleSheetEditor on Firebug object because Acebug is using it<commit_after>/* See license.txt for terms of usage */

define([
    "firebug/firebug",
    "firebug/lib/trace",
    "firebug/lib/domplate",
    "firebug/css/cssModule",
    "firebug/editor/baseEditor",
    "firebug/editor/editor",
    "firebug/editor/sourceEditor",
],
function(Firebug, FBTrace, Domplate, CSSModule, BaseEditor, Editor, SourceEditor) {

// ********************************************************************************************* //
// Constants

var {DIV} = Domplate;

// ********************************************************************************************* //
// StyleSheetEditor

/**
 * StyleSheetEditor represents the full-sized editor used for Source/Live Edit
 * within the CSS panel.
 */
function StyleSheetEditor(doc)
{
    this.box = this.tag.replace({}, doc, this);

    this.onEditorTextChangeListener = this.onEditorTextChange.bind(this);
    var config = {
        mode: "css",
        readOnly: false,
        gutters: []
    };
    // Initialize source editor, then append to the box.
    this.editor = new SourceEditor();
    this.editor.init(this.box, config, this.onEditorInitialize.bind(this));
}

StyleSheetEditor.prototype = domplate(BaseEditor,
{
    multiLine: true,

    tag: DIV({"class": "styleSheetEditor fullPanelEditor"}),

    // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //

    onEditorInitialize: function()
    {
        this.editor.addEventListener(SourceEditor.Events.textChange,
            this.onEditorTextChangeListener);
    },

    getValue: function()
    {
        return this.editor.getText();
    },

    setValue: function(value)
    {
        return this.editor.setText(value, "css");
    },

    show: function(target, panel, value, textSize)
    {
        this.target = target;
        this.panel = panel;

        // Show the box that editor already is appended to.
        this.panel.panelNode.appendChild(this.box);
        this.editor.setText(value, "css");

        // match CSSModule.getEditorOptionKey
        var command = Firebug.chrome.$("cmd_firebug_togglecssEditMode");
        command.setAttribute("checked", true);
    },

    hide: function()
    {
        var command = Firebug.chrome.$("cmd_firebug_togglecssEditMode");
        command.setAttribute("checked", false);

        if (this.box.parentNode == this.panel.panelNode)
            this.panel.panelNode.removeChild(this.box);

        delete this.target;
        delete this.panel;
        delete this.styleSheet;
    },

    saveEdit: function(target, value, previousValue)
    {
        if (FBTrace.DBG_CSS)
            FBTrace.sysout("StyleSheetEditor.saveEdit", arguments);

        CSSModule.freeEdit(this.styleSheet, value);
    },

    beginEditing: function()
    {
        if (FBTrace.DBG_CSS)
            FBTrace.sysout("StyleSheetEditor.beginEditing", arguments);

        this.editing = true;
    },

    endEditing: function()
    {
        if (FBTrace.DBG_CSS)
            FBTrace.sysout("StyleSheetEditor.endEditing", arguments);

        this.editing = false;
        this.panel.refresh();
        return true;
    },

    // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //

    onEditorTextChange: function()
    {
        Editor.update();
    },

    scrollToLine: function(line, offset)
    {
        this.editor.scrollToLine(line);
    }
});

// ********************************************************************************************* //
// Registration

// used in Acebug
Firebug.StyleSheetEditor = StyleSheetEditor;

return StyleSheetEditor;

// ********************************************************************************************* //
});<|endoftext|>


<commit_before>/**
 * Copyright 2018 The AMP HTML Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS-IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {CSS} from '../../../build/amp-next-page-0.1.css';
import {MultidocManager} from '../../../src/runtime';
import {PositionObserverFidelity} from '../../../src/service/position-observer/position-observer-worker';
import {Services} from '../../../src/services';
import {dev} from '../../../src/log';
import {getAmpdoc, getServiceForDoc} from '../../../src/service';
import {
  installPositionObserverServiceForDoc,
} from '../../../src/service/position-observer/position-observer-impl';
import {installStylesForDoc} from '../../../src/style-installer';
import {layoutRectLtwh} from '../../../src/layout-rect';
import {removeElement} from '../../../src/dom';
import {setStyle} from '../../../src/style';
import {triggerAnalyticsEvent} from '../../../src/analytics';

// TODO(emarchiori): Make this a configurable parameter.
const SEPARATOR_RECOS = 3;

const MAX_ARTICLES = 2;

const PRERENDER_VIEWPORT_COUNT = 3;

const TAG = 'amp-next-page';

/**
 * @typedef {{
 *   ampUrl: string,
 *   amp: ?Object,
 *   recUnit: ?Element,
 *   cancelled: boolean
 * }}
 */
export let DocumentRef;

/**
 * Window-scoped service to handle the amp-next-page lifecycle. Handles all
 * events and page lifecycle for the first {@code AmpNextPage} element
 * registered. All subsequent registrations will be ignored.
 */
export class NextPageService {
  /**
   * Creates an instance of NextPageService.
   */
  constructor() {
    /** @private {?Window} */
    this.win_ = null;

    /** @private {?Element} */
    this.element_ = null;

    /** @private {?./config.AmpNextPageConfig} */
    this.config_ = null;

    /** @private {string} */
    this.hideSelector_;

    /** @private {?Element} */
    this.separator_ = null;

    /** @private {?../../../src/service/resources-impl.Resources} */
    this.resources_ = null;

    /** @private {?MultidocManager} */
    this.multidocManager_ = null;

    /** @private {number} */
    this.nextArticle_ = 0;

    /** @private {boolean} */
    this.documentQueued_ = false;

    /** @private {?../../../src/service/viewer-impl.Viewer} */
    this.viewer_ = null;

    /** @private {?../../../src/service/viewport/viewport-impl.Viewport} */
    this.viewport_ = null;

    /** @private {?../../../src/service/position-observer/position-observer-impl.PositionObserver} */
    this.positionObserver_ = null;

    /** @private @const {!Array<!DocumentRef>} */
    this.documentRefs_ = [];

    /** @private {?DocumentRef} */
    this.activeDocumentRef_ = null;

    /** @private {function(!Element): !Promise} */
    this.appendPageHandler_ = () => {};
  }

  /** Returns true if the service has already been initialized. */
  isActive() {
    return this.config_ !== null;
  }

  /**
   * Registers the window-scoped service against the specified {@code
   * recommendationsOb}. Only the first call to this method will be used, any
   * subsequent calls will be ignored.
   * @param {!Element} element {@link AmpNextPage} element.
   * @param {!./config.AmpNextPageConfig} config Element configuration.
   * @param {?Element} separator Separator element to display between pages. If
   *     none is specified a default hairline separator will be used.
   */
  register(element, config, separator) {
    if (this.isActive()) {
      return;
    }

    const ampDoc = getAmpdoc(element);
    const {win} = ampDoc;

    this.config_ = config;
    this.win_ = win;
    this.separator_ = separator || this.createDefaultSeparator_();
    this.element_ = element;

    if (this.config_.hideSelectors) {
      this.hideSelector_ = this.config_.hideSelectors.join(',');
    }

    this.viewer_ = Services.viewerForDoc(ampDoc);
    this.viewport_ = Services.viewportForDoc(ampDoc);
    this.resources_ = Services.resourcesForDoc(ampDoc);
    this.multidocManager_ =
        new MultidocManager(win, Services.ampdocServiceFor(win),
          Services.extensionsFor(win), Services.timerFor(win));

    installPositionObserverServiceForDoc(ampDoc);
    this.positionObserver_ = getServiceForDoc(ampDoc, 'position-observer');

    this.documentRefs_.push({
      ampUrl: win.document.location.href,
      amp: {title: win.document.title},
      recUnit: null,
      cancelled: false,
    });
    this.activeDocumentRef_ = this.documentRefs_[0];

    this.viewport_.onScroll(() => this.scrollHandler_());
    this.viewport_.onResize(() => this.scrollHandler_());

    // Check scroll position immediately to handle documents which are shorter
    // than the viewport.
    this.scrollHandler_();
  }

  /**
   * Sets the handler to be called with a
   * @param {function(!Element): !Promise} handler Handler to be called when
   *     a new page needs adding to the DOM, with a container element for that
   *     page.
   */
  setAppendPageHandler(handler) {
    this.appendPageHandler_ = handler;
  }

  /**
   * Attach a ShadowDoc using the given document.
   * @param {!Element} shadowRoot Root element to attach the shadow document to.
   * @param {!Document} doc Document to attach.
   * @return {?Object} Return value of {@link MultidocManager#attachShadowDoc}
   */
  attachShadowDoc_(shadowRoot, doc) {
    if (this.hideSelector_) {
      const elements = doc.querySelectorAll(this.hideSelector_);
      for (let i = 0; i < elements.length; i++) {
        elements[i].classList.add('i-amphtml-next-page-hidden');
      }
    }

    // Drop any amp-analytics tags from the child doc. We want to reuse the
    // parent config instead.
    const analytics = doc.querySelectorAll('amp-analytics');
    for (let i = 0; i < analytics.length; i++) {
      const item = analytics[i];
      removeElement(item);
    }

    const amp =
        this.multidocManager_.attachShadowDoc(shadowRoot, doc, '', {});
    installStylesForDoc(amp.ampdoc, CSS, null, false, TAG);

    const body = amp.ampdoc.getBody();
    body.classList.add('i-amphtml-next-page-document');

    return amp;
  }

  /**
   * Creates a default hairline separator element to go between two documents.
   * @return {!Element}
   */
  createDefaultSeparator_() {
    const separator = this.win_.document.createElement('div');
    separator.classList.add('amp-next-page-default-separator');
    return separator;
  }

  /**
   * Append a new article if still possible.
   */
  appendNextArticle_() {
    if (this.nextArticle_ < this.config_.pages.length) {
      const next = this.config_.pages[this.nextArticle_];
      const documentRef = {
        ampUrl: next.ampUrl,
        amp: null,
        recUnit: null,
        cancelled: false,
      };
      this.documentRefs_.push(documentRef);

      const container = this.win_.document.createElement('div');

      const separator = this.separator_.cloneNode(true);
      separator.removeAttribute('separator');
      container.appendChild(separator);

      const articleLinks = this.createArticleLinks_(this.nextArticle_);
      container.appendChild(articleLinks);
      documentRef.recUnit = articleLinks;

      const shadowRoot = this.win_.document.createElement('div');
      container.appendChild(shadowRoot);

      const page = this.nextArticle_;
      this.appendPageHandler_(container).then(() => {
        this.positionObserver_.observe(separator, PositionObserverFidelity.LOW,
            position => this.positionUpdate_(page, position));
        this.positionObserver_.observe(articleLinks,
            PositionObserverFidelity.LOW,
            unused => this.articleLinksPositionUpdate_(documentRef));
      });

      // Don't fetch the next article if we've rendered the maximum on screen.
      // We just want the article links.
      if (this.nextArticle_ >= MAX_ARTICLES) {
        return;
      }

      this.nextArticle_++;
      Services.xhrFor(/** @type {!Window} */ (this.win_))
          .fetchDocument(next.ampUrl, {ampCors: false})
          .then(doc => new Promise((resolve, reject) => {
            if (documentRef.cancelled) {
              // User has reached the end of the document already, don't render.
              resolve();
              return;
            }

            this.positionObserver_.unobserve(articleLinks);
            this.resources_.mutateElement(container, () => {
              try {
                const amp = this.attachShadowDoc_(shadowRoot, doc);
                documentRef.amp = amp;

                setStyle(documentRef.recUnit, 'display', 'none');
                this.documentQueued_ = false;
                resolve();
              } catch (e) {
                reject(e);
              }
            });
          }),
          e => dev().error(TAG, `failed to fetch ${next.ampUrl}`, e))
          .catch(e => dev().error(TAG,
              `failed to attach shadow document for ${next.ampUrl}`, e))
          // The new page may be short and the next may already need fetching.
          .then(() => this.scrollHandler_());
    }
  }

  /**
   * Creates a recommendation unit with links to articles, starting from a given
   * one.
   * @param {number} nextPage Index of the next unseen page to use as the first
   *     recommendation in the list.
   * @return {!Element} Container element for the recommendations.
   */
  createArticleLinks_(nextPage) {
    const doc = this.win_.document;
    const currentArticle = nextPage - 1;
    let article = nextPage;
    let currentAmpUrl = '';
    if (nextPage > 0) {
      currentAmpUrl = this.documentRefs_[currentArticle].ampUrl;
    }

    const element = doc.createElement('div');
    element.classList.add('amp-next-page-links');

    while (article < this.config_.pages.length &&
           article - nextPage < SEPARATOR_RECOS) {
      const next = this.config_.pages[article];
      article++;

      const articleHolder = doc.createElement('a');
      articleHolder.href = next.ampUrl;
      articleHolder.classList.add(
          'i-amphtml-reco-holder-article', 'amp-next-page-link');
      articleHolder.addEventListener('click', e => {
        this.triggerAnalyticsEvent_(
            'amp-next-page-click', next.ampUrl, currentAmpUrl);
        const a2a =
            this.viewer_.navigateToAmpUrl(next.ampUrl, 'content-discovery');
        if (a2a) {
          // A2A is enabled, don't navigate the browser.
          e.preventDefault();
        }
      });

      const imageElement = doc.createElement('div');
      imageElement.classList.add(
          'i-amphtml-next-article-image', 'amp-next-page-image');
      setStyle(imageElement, 'background-image', `url(${next.image})`);
      articleHolder.appendChild(imageElement);

      const titleElement = doc.createElement('div');
      titleElement.classList.add(
          'i-amphtml-next-article-title', 'amp-next-page-text');

      titleElement.textContent = next.title;
      articleHolder.appendChild(titleElement);

      element.appendChild(articleHolder);
    }

    return element;
  }

  /**
   * Handles scroll events from the viewport and appends the next document when
   * the user comes within {@code PRERENDER_VIEWPORT_COUNT} viewports of the
   * end.
   * @private
   */
  scrollHandler_() {
    if (this.documentQueued_) {
      return;
    }

    const viewportSize = this.viewport_.getSize();
    const viewportBox =
        layoutRectLtwh(0, 0, viewportSize.width, viewportSize.height);
    this.viewport_.getClientRectAsync(dev().assertElement(this.element_))
        .then(elementBox => {
          if (this.documentQueued_) {
            return;
          }

          const prerenderHeight =
              PRERENDER_VIEWPORT_COUNT * viewportSize.height;
          if (elementBox.bottom - viewportBox.bottom < prerenderHeight) {
            this.documentQueued_ = true;
            this.appendNextArticle_();
          }
        });
  }

  /**
   * Handles updates from the {@code PositionObserver} indicating a change in
   * the position of a page separator in the viewport.
   * @param {number} i Index of the documentRef this recommendation unit is
   *     attached to.
   * @param {?../../../src/service/position-observer/position-observer-worker.PositionInViewportEntryDef} position
   *     Position of the current recommendation unit in the viewport.
   */
  positionUpdate_(i, position) {
    // We're only interested when the recommendations exit the viewport
    if (!position || position.positionRect !== null) {
      return;
    }

    let documentRef;
    let analyticsEvent = '';

    if (position.relativePos === 'top') {
      documentRef = this.documentRefs_[i + 1];
      analyticsEvent = 'amp-next-page-scroll';
    } else if (position.relativePos === 'bottom') {
      documentRef = this.documentRefs_[i];
      analyticsEvent = 'amp-next-page-scroll-back';
    }

    if (documentRef && documentRef.amp) {
      this.triggerAnalyticsEvent_(analyticsEvent,
          documentRef.ampUrl, this.activeDocumentRef_.ampUrl);
      this.setActiveDocument_(documentRef);
    }
  }

  /**
   * Handles updates from the {@code PositionObserver} indicating a change in
   * the position of the recommendation unit in the viewport. Used to indicate
   * when the recommendation unit has entered the viewport, and should not be
   * hidden from view automatically.
   * @param {!DocumentRef} documentRef Reference to the active document.
   */
  articleLinksPositionUpdate_(documentRef) {
    documentRef.cancelled = true;
    if (documentRef.recUnit) {
      this.positionObserver_.unobserve(documentRef.recUnit);
    }
  }

  /**
   * Sets the specified document as active, updating the document title and URL.
   * @param {!DocumentRef} documentRef Reference to the document to set as
   *     active.
   * @private
   */
  setActiveDocument_(documentRef) {
    const {amp} = documentRef;
    this.win_.document.title = amp.title || '';
    this.activeDocumentRef_ = documentRef;
    this.setActiveDocumentInHistory_(documentRef);

    // TODO(peterjosling): Send request to viewer with title/URL
    // TODO(emarchiori): Consider updating position fixed elements.
  }

  /**
   * @param {!DocumentRef} documentRef
   * @private
   */
  setActiveDocumentInHistory_(documentRef) {
    if (!this.win_.history || !this.win_.history.replaceState) {
      return;
    }

    const urlService = Services.urlForDoc(dev().assertElement(this.element_));
    const {title} = documentRef.amp;
    const {pathname} = urlService.parse(documentRef.ampUrl);
    this.win_.history.replaceState({}, title, pathname);
  }

  /**
   * Wrapper around {@code triggerAnalyticsEvent}.
   * @param {string} eventType
   * @param {string} toURL The new url after the event.
   * @param {string=} fromURL The old URL before the event.
   */
  triggerAnalyticsEvent_(eventType, toURL, fromURL) {
    fromURL = fromURL || '';

    const vars = {toURL, fromURL};
    triggerAnalyticsEvent(dev().assertElement(this.element_), eventType, vars);
  }
}
<commit_msg>🐛 amp-next-page: Update URL with query string when switching documents (#16698)<commit_after>/**
 * Copyright 2018 The AMP HTML Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS-IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {CSS} from '../../../build/amp-next-page-0.1.css';
import {MultidocManager} from '../../../src/runtime';
import {PositionObserverFidelity} from '../../../src/service/position-observer/position-observer-worker';
import {Services} from '../../../src/services';
import {dev} from '../../../src/log';
import {getAmpdoc, getServiceForDoc} from '../../../src/service';
import {
  installPositionObserverServiceForDoc,
} from '../../../src/service/position-observer/position-observer-impl';
import {installStylesForDoc} from '../../../src/style-installer';
import {layoutRectLtwh} from '../../../src/layout-rect';
import {removeElement} from '../../../src/dom';
import {setStyle} from '../../../src/style';
import {triggerAnalyticsEvent} from '../../../src/analytics';

// TODO(emarchiori): Make this a configurable parameter.
const SEPARATOR_RECOS = 3;

const MAX_ARTICLES = 2;

const PRERENDER_VIEWPORT_COUNT = 3;

const TAG = 'amp-next-page';

/**
 * @typedef {{
 *   ampUrl: string,
 *   amp: ?Object,
 *   recUnit: ?Element,
 *   cancelled: boolean
 * }}
 */
export let DocumentRef;

/**
 * Window-scoped service to handle the amp-next-page lifecycle. Handles all
 * events and page lifecycle for the first {@code AmpNextPage} element
 * registered. All subsequent registrations will be ignored.
 */
export class NextPageService {
  /**
   * Creates an instance of NextPageService.
   */
  constructor() {
    /** @private {?Window} */
    this.win_ = null;

    /** @private {?Element} */
    this.element_ = null;

    /** @private {?./config.AmpNextPageConfig} */
    this.config_ = null;

    /** @private {string} */
    this.hideSelector_;

    /** @private {?Element} */
    this.separator_ = null;

    /** @private {?../../../src/service/resources-impl.Resources} */
    this.resources_ = null;

    /** @private {?MultidocManager} */
    this.multidocManager_ = null;

    /** @private {number} */
    this.nextArticle_ = 0;

    /** @private {boolean} */
    this.documentQueued_ = false;

    /** @private {?../../../src/service/viewer-impl.Viewer} */
    this.viewer_ = null;

    /** @private {?../../../src/service/viewport/viewport-impl.Viewport} */
    this.viewport_ = null;

    /** @private {?../../../src/service/position-observer/position-observer-impl.PositionObserver} */
    this.positionObserver_ = null;

    /** @private @const {!Array<!DocumentRef>} */
    this.documentRefs_ = [];

    /** @private {?DocumentRef} */
    this.activeDocumentRef_ = null;

    /** @private {function(!Element): !Promise} */
    this.appendPageHandler_ = () => {};
  }

  /** Returns true if the service has already been initialized. */
  isActive() {
    return this.config_ !== null;
  }

  /**
   * Registers the window-scoped service against the specified {@code
   * recommendationsOb}. Only the first call to this method will be used, any
   * subsequent calls will be ignored.
   * @param {!Element} element {@link AmpNextPage} element.
   * @param {!./config.AmpNextPageConfig} config Element configuration.
   * @param {?Element} separator Separator element to display between pages. If
   *     none is specified a default hairline separator will be used.
   */
  register(element, config, separator) {
    if (this.isActive()) {
      return;
    }

    const ampDoc = getAmpdoc(element);
    const {win} = ampDoc;

    this.config_ = config;
    this.win_ = win;
    this.separator_ = separator || this.createDefaultSeparator_();
    this.element_ = element;

    if (this.config_.hideSelectors) {
      this.hideSelector_ = this.config_.hideSelectors.join(',');
    }

    this.viewer_ = Services.viewerForDoc(ampDoc);
    this.viewport_ = Services.viewportForDoc(ampDoc);
    this.resources_ = Services.resourcesForDoc(ampDoc);
    this.multidocManager_ =
        new MultidocManager(win, Services.ampdocServiceFor(win),
          Services.extensionsFor(win), Services.timerFor(win));

    installPositionObserverServiceForDoc(ampDoc);
    this.positionObserver_ = getServiceForDoc(ampDoc, 'position-observer');

    this.documentRefs_.push({
      ampUrl: win.document.location.href,
      amp: {title: win.document.title},
      recUnit: null,
      cancelled: false,
    });
    this.activeDocumentRef_ = this.documentRefs_[0];

    this.viewport_.onScroll(() => this.scrollHandler_());
    this.viewport_.onResize(() => this.scrollHandler_());

    // Check scroll position immediately to handle documents which are shorter
    // than the viewport.
    this.scrollHandler_();
  }

  /**
   * Sets the handler to be called with a
   * @param {function(!Element): !Promise} handler Handler to be called when
   *     a new page needs adding to the DOM, with a container element for that
   *     page.
   */
  setAppendPageHandler(handler) {
    this.appendPageHandler_ = handler;
  }

  /**
   * Attach a ShadowDoc using the given document.
   * @param {!Element} shadowRoot Root element to attach the shadow document to.
   * @param {!Document} doc Document to attach.
   * @return {?Object} Return value of {@link MultidocManager#attachShadowDoc}
   */
  attachShadowDoc_(shadowRoot, doc) {
    if (this.hideSelector_) {
      const elements = doc.querySelectorAll(this.hideSelector_);
      for (let i = 0; i < elements.length; i++) {
        elements[i].classList.add('i-amphtml-next-page-hidden');
      }
    }

    // Drop any amp-analytics tags from the child doc. We want to reuse the
    // parent config instead.
    const analytics = doc.querySelectorAll('amp-analytics');
    for (let i = 0; i < analytics.length; i++) {
      const item = analytics[i];
      removeElement(item);
    }

    const amp =
        this.multidocManager_.attachShadowDoc(shadowRoot, doc, '', {});
    installStylesForDoc(amp.ampdoc, CSS, null, false, TAG);

    const body = amp.ampdoc.getBody();
    body.classList.add('i-amphtml-next-page-document');

    return amp;
  }

  /**
   * Creates a default hairline separator element to go between two documents.
   * @return {!Element}
   */
  createDefaultSeparator_() {
    const separator = this.win_.document.createElement('div');
    separator.classList.add('amp-next-page-default-separator');
    return separator;
  }

  /**
   * Append a new article if still possible.
   */
  appendNextArticle_() {
    if (this.nextArticle_ < this.config_.pages.length) {
      const next = this.config_.pages[this.nextArticle_];
      const documentRef = {
        ampUrl: next.ampUrl,
        amp: null,
        recUnit: null,
        cancelled: false,
      };
      this.documentRefs_.push(documentRef);

      const container = this.win_.document.createElement('div');

      const separator = this.separator_.cloneNode(true);
      separator.removeAttribute('separator');
      container.appendChild(separator);

      const articleLinks = this.createArticleLinks_(this.nextArticle_);
      container.appendChild(articleLinks);
      documentRef.recUnit = articleLinks;

      const shadowRoot = this.win_.document.createElement('div');
      container.appendChild(shadowRoot);

      const page = this.nextArticle_;
      this.appendPageHandler_(container).then(() => {
        this.positionObserver_.observe(separator, PositionObserverFidelity.LOW,
            position => this.positionUpdate_(page, position));
        this.positionObserver_.observe(articleLinks,
            PositionObserverFidelity.LOW,
            unused => this.articleLinksPositionUpdate_(documentRef));
      });

      // Don't fetch the next article if we've rendered the maximum on screen.
      // We just want the article links.
      if (this.nextArticle_ >= MAX_ARTICLES) {
        return;
      }

      this.nextArticle_++;
      Services.xhrFor(/** @type {!Window} */ (this.win_))
          .fetchDocument(next.ampUrl, {ampCors: false})
          .then(doc => new Promise((resolve, reject) => {
            if (documentRef.cancelled) {
              // User has reached the end of the document already, don't render.
              resolve();
              return;
            }

            this.positionObserver_.unobserve(articleLinks);
            this.resources_.mutateElement(container, () => {
              try {
                const amp = this.attachShadowDoc_(shadowRoot, doc);
                documentRef.amp = amp;

                setStyle(documentRef.recUnit, 'display', 'none');
                this.documentQueued_ = false;
                resolve();
              } catch (e) {
                reject(e);
              }
            });
          }),
          e => dev().error(TAG, `failed to fetch ${next.ampUrl}`, e))
          .catch(e => dev().error(TAG,
              `failed to attach shadow document for ${next.ampUrl}`, e))
          // The new page may be short and the next may already need fetching.
          .then(() => this.scrollHandler_());
    }
  }

  /**
   * Creates a recommendation unit with links to articles, starting from a given
   * one.
   * @param {number} nextPage Index of the next unseen page to use as the first
   *     recommendation in the list.
   * @return {!Element} Container element for the recommendations.
   */
  createArticleLinks_(nextPage) {
    const doc = this.win_.document;
    const currentArticle = nextPage - 1;
    let article = nextPage;
    let currentAmpUrl = '';
    if (nextPage > 0) {
      currentAmpUrl = this.documentRefs_[currentArticle].ampUrl;
    }

    const element = doc.createElement('div');
    element.classList.add('amp-next-page-links');

    while (article < this.config_.pages.length &&
           article - nextPage < SEPARATOR_RECOS) {
      const next = this.config_.pages[article];
      article++;

      const articleHolder = doc.createElement('a');
      articleHolder.href = next.ampUrl;
      articleHolder.classList.add(
          'i-amphtml-reco-holder-article', 'amp-next-page-link');
      articleHolder.addEventListener('click', e => {
        this.triggerAnalyticsEvent_(
            'amp-next-page-click', next.ampUrl, currentAmpUrl);
        const a2a =
            this.viewer_.navigateToAmpUrl(next.ampUrl, 'content-discovery');
        if (a2a) {
          // A2A is enabled, don't navigate the browser.
          e.preventDefault();
        }
      });

      const imageElement = doc.createElement('div');
      imageElement.classList.add(
          'i-amphtml-next-article-image', 'amp-next-page-image');
      setStyle(imageElement, 'background-image', `url(${next.image})`);
      articleHolder.appendChild(imageElement);

      const titleElement = doc.createElement('div');
      titleElement.classList.add(
          'i-amphtml-next-article-title', 'amp-next-page-text');

      titleElement.textContent = next.title;
      articleHolder.appendChild(titleElement);

      element.appendChild(articleHolder);
    }

    return element;
  }

  /**
   * Handles scroll events from the viewport and appends the next document when
   * the user comes within {@code PRERENDER_VIEWPORT_COUNT} viewports of the
   * end.
   * @private
   */
  scrollHandler_() {
    if (this.documentQueued_) {
      return;
    }

    const viewportSize = this.viewport_.getSize();
    const viewportBox =
        layoutRectLtwh(0, 0, viewportSize.width, viewportSize.height);
    this.viewport_.getClientRectAsync(dev().assertElement(this.element_))
        .then(elementBox => {
          if (this.documentQueued_) {
            return;
          }

          const prerenderHeight =
              PRERENDER_VIEWPORT_COUNT * viewportSize.height;
          if (elementBox.bottom - viewportBox.bottom < prerenderHeight) {
            this.documentQueued_ = true;
            this.appendNextArticle_();
          }
        });
  }

  /**
   * Handles updates from the {@code PositionObserver} indicating a change in
   * the position of a page separator in the viewport.
   * @param {number} i Index of the documentRef this recommendation unit is
   *     attached to.
   * @param {?../../../src/service/position-observer/position-observer-worker.PositionInViewportEntryDef} position
   *     Position of the current recommendation unit in the viewport.
   */
  positionUpdate_(i, position) {
    // We're only interested when the recommendations exit the viewport
    if (!position || position.positionRect !== null) {
      return;
    }

    let documentRef;
    let analyticsEvent = '';

    if (position.relativePos === 'top') {
      documentRef = this.documentRefs_[i + 1];
      analyticsEvent = 'amp-next-page-scroll';
    } else if (position.relativePos === 'bottom') {
      documentRef = this.documentRefs_[i];
      analyticsEvent = 'amp-next-page-scroll-back';
    }

    if (documentRef && documentRef.amp) {
      this.triggerAnalyticsEvent_(analyticsEvent,
          documentRef.ampUrl, this.activeDocumentRef_.ampUrl);
      this.setActiveDocument_(documentRef);
    }
  }

  /**
   * Handles updates from the {@code PositionObserver} indicating a change in
   * the position of the recommendation unit in the viewport. Used to indicate
   * when the recommendation unit has entered the viewport, and should not be
   * hidden from view automatically.
   * @param {!DocumentRef} documentRef Reference to the active document.
   */
  articleLinksPositionUpdate_(documentRef) {
    documentRef.cancelled = true;
    if (documentRef.recUnit) {
      this.positionObserver_.unobserve(documentRef.recUnit);
    }
  }

  /**
   * Sets the specified document as active, updating the document title and URL.
   * @param {!DocumentRef} documentRef Reference to the document to set as
   *     active.
   * @private
   */
  setActiveDocument_(documentRef) {
    const {amp} = documentRef;
    this.win_.document.title = amp.title || '';
    this.activeDocumentRef_ = documentRef;
    this.setActiveDocumentInHistory_(documentRef);

    // TODO(peterjosling): Send request to viewer with title/URL
    // TODO(emarchiori): Consider updating position fixed elements.
  }

  /**
   * @param {!DocumentRef} documentRef
   * @private
   */
  setActiveDocumentInHistory_(documentRef) {
    if (!this.win_.history || !this.win_.history.replaceState) {
      return;
    }

    const urlService = Services.urlForDoc(dev().assertElement(this.element_));
    const {title} = documentRef.amp;
    const {pathname, search} = urlService.parse(documentRef.ampUrl);
    this.win_.history.replaceState({}, title, pathname + search);
  }

  /**
   * Wrapper around {@code triggerAnalyticsEvent}.
   * @param {string} eventType
   * @param {string} toURL The new url after the event.
   * @param {string=} fromURL The old URL before the event.
   */
  triggerAnalyticsEvent_(eventType, toURL, fromURL) {
    fromURL = fromURL || '';

    const vars = {toURL, fromURL};
    triggerAnalyticsEvent(dev().assertElement(this.element_), eventType, vars);
  }
}
<|endoftext|>


<commit_before>import React, { PropTypes } from 'react';
import { Link } from 'react-router';
import { connect } from 'react-redux';

import { ApiAction, setHeader } from 'redux/actions';
const { loadProducts } = ApiAction;

import loadEntities from 'commons/redux/util/loadEntities';

import MainBanner from 'components/MainBanner';
import MainRecommendList from 'components/MainRecommendList';

const Home = React.createClass({
  propTypes: {
    loadProducts: PropTypes.func.isRequired,
    setHeader: PropTypes.func.isRequired,
  },
  componentDidMount() {
    this.props.setHeader(true, true, true, '');
    this.props.loadProducts();
  },
  render() {
    return (

        <MainBanner />

              <Link to="/categoryList">
                All Categories
              </Link>

                  <Link to="/categoryList/10001">Apparel & Accessories</Link>

                  <Link to="/categoryList/10016">Jewelry</Link>

                  <Link to="/categoryList/10005">Electronics</Link>

                  <Link to="/categoryList/10004">Beauty &Health</Link>

                <Link to="http://m.aliexpress.com/column/recommend.htm?fromapp=false">

Quality Picks

                </Link>

                <Link to="http://activities.aliexpress.com/superdeals_mobile.php">

Super Deals

                </Link>

          <MainRecommendList products={this.props.products} />

        더배워 - 주문안되면 환불되고 어쩌고

        결재방법 - visa master

        따러와 - facebook vk insta

    );
  },
});

export default connect(
  (state) => ({
    ...loadEntities(state, 'products', 'products'),
  }),
  { loadProducts, setHeader }
)(Home);
<commit_msg>mobile site - load products maximum 30<commit_after>import React, { PropTypes } from 'react';
import { Link } from 'react-router';
import { connect } from 'react-redux';

import { ApiAction, setHeader } from 'redux/actions';
const { searchProducts } = ApiAction;

import loadEntities from 'commons/redux/util/loadEntities';

import MainBanner from 'components/MainBanner';
import MainRecommendList from 'components/MainRecommendList';

const Home = React.createClass({
  propTypes: {
    searchProducts: PropTypes.func.isRequired,
    setHeader: PropTypes.func.isRequired,
  },
  getInitialState() {
    return {};
  },
  componentDidMount() {
    this.props.setHeader(true, true, true, '');
    this.props.searchProducts({
      // q: query,
      // categoryId: params.categoryId === 'all' ? undefined : params.categoryId,
      // brandId,
      from: 0,
      size: 30,
    }).then((res) => this.setState(res));
  },
  render() {
    if (!this.state.products) {
      return (

);
    }
    return (

        <MainBanner />

              <Link to="/categoryList">
                All Categories
              </Link>

                  <Link to="/categoryList/10001">Apparel & Accessories</Link>

                  <Link to="/categoryList/10016">Jewelry</Link>

                  <Link to="/categoryList/10005">Electronics</Link>

                  <Link to="/categoryList/10004">Beauty &Health</Link>

                <Link to="http://m.aliexpress.com/column/recommend.htm?fromapp=false">

Quality Picks

                </Link>

                <Link to="http://activities.aliexpress.com/superdeals_mobile.php">

Super Deals

                </Link>

          <MainRecommendList products={this.state.products} />

        더배워 - 주문안되면 환불되고 어쩌고

        결재방법 - visa master

        따러와 - facebook vk insta

    );
  },
});

export default connect(
  (state) => ({
    ...loadEntities(state, 'products', 'products'),
  }),
  { searchProducts, setHeader }
)(Home);
<|endoftext|>


<commit_before>/**
 * @module Event
 * @namespace Event
 */

var TW = TW || {};
define(['./DeviceInput', '../Utils/inherit', '../Utils/Polyfills'], function(DeviceInput, inherit) {

	TW.Event = TW.Event || {};

	/**
	 * DeviceInput using the keyboard.
	 *
	 *
	 * Each event represent a key. each key has two states: `KEY_PRESSED` or `KEY_RELEASED`
	 *
	 *
	 * ## List of keys:
	 *
	 * - All letters from `KEY_A` to `KEY_Z`
	 * - All digits from `KEY_0` to `KEY_9`
	 * - Functions Keys, from `KEY_F1` to `KEY_F12`
	 * - KEY_BACKSPACE
	 * - KEY_TAB
	 * - KEY_ENTER
	 * - KEY_SHIFT
	 * - KEY_ALT
	 * - KEY_PAUSE
	 * - KEY_CAPSLOCK
	 * - KEY_ESC
	 * - KEY_SPACE
	 * - KEY_PAGE_UP
	 * - KEY_PAGE_DOWN
	 * - KEY_END
	 * - KEY_HOME
	 * - KEY_LEFT
	 * - KEY_UP
	 * - KEY_RIGHT
	 * - KEY_DOWN
	 * - KEY_INSERT
	 * - KEY_DELETE
	 * - KEY_NUMLOCK
	 *
	 *
	 *
	 * __Note:__
	 * This class use the `keyCode` attribute from KeyboardEvent object.
	 * The code list of [current W3C standard](http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-20071221/keyset.html)
	 * is not implemented and there is differences between each browser.

	 * The [next W3C standard](http://www.w3.org/TR/DOM-Level-3-Events/#keys-keyvalues)
	 * should improve compatibility.

	 * __Currently, using Numeric key (from Keypad or not) with the Shift,
	 * CapsLock or NumLock keys is not possible.__

	 * __Use exotic keys are strongly discouraged.__

	 *

	 * For example, using Chrome 22.0.1229.94 (under Linux) with a fr keyboard, key `&` (key `1` without `Shift`)
	 * are equally to key `7`.
	 * For more information about compatibility,[this document](http://unixpapa.com/js/key.html) provide
	 * a good summary of the situation.
	 *
	 * Usage of arrows keys, and usage of controls keys with alphabetic characters are supported.
	 *
	 *
	 *
	 * @example
	 *
	 *      var keyboard = new KeyboardInput();
	 *      keyboard.addListener("KEY_SPACE", KeyboardInput.KEY_PRESSED, function(event, value, provider) {
     *          if (provider.getState("KEY_CTRL") === KeyboardInput.KEY_PRESSED) {
     *              //CTRL+space is pressed !
     *          }
     *      });
	 *
	 * @class KeyboardInput
	 * @extends Event.DeviceInput
	 * @param {HTMLElement} [target]      element to listen keypressed and keyup. default to `window.document`
	 * @constructor
	 */
	function KeyboardInput(target) {
		var i, len;

		DeviceInput.call(this);

		if (target === undefined) {
			target = window.document;
		}

		/**
		 * @event {Boolean} KEY_A
		 */
		/**
		 * @event {Boolean} KEY_B
		 */
		/**
		 * @event {Boolean} KEY_C
		 */
		/**
		 * @event {Boolean} KEY_D
		 */
		/**
		 * @event {Boolean} KEY_E
		 */
		/**
		 * @event {Boolean} KEY_F
		 */
		/**
		 * @event {Boolean} KEY_G
		 */
		/**
		 * @event {Boolean} KEY_H
		 */
		/**
		 * @event {Boolean} KEY_I
		 */
		/**
		 * @event {Boolean} KEY_J
		 */
		/**
		 * @event {Boolean} KEY_K
		 */
		/**
		 * @event {Boolean} KEY_L
		 */
		/**
		 * @event {Boolean} KEY_M
		 */
		/**
		 * @event {Boolean} KEY_N
		 */
		/**
		 * @event {Boolean} KEY_O
		 */
		/**
		 * @event {Boolean} KEY_P
		 */
		/**
		 * @event {Boolean} KEY_Q
		 */
		/**
		 * @event {Boolean} KEY_R
		 */
		/**
		 * @event {Boolean} KEY_S
		 */
		/**
		 * @event {Boolean} KEY_T
		 */
		/**
		 * @event {Boolean} KEY_U
		 */
		/**
		 * @event {Boolean} KEY_V
		 */
		/**
		 * @event {Boolean} KEY_W
		 */
		/**
		 * @event {Boolean} KEY_X
		 */
		/**
		 * @event {Boolean} KEY_Y
		 */
		/**
		 * @event {Boolean} KEY_Z
		 */
		/**
		 * @event {Boolean} KEY_1
		 */
		/**
		 * @event {Boolean} KEY_2
		 */
		/**
		 * @event {Boolean} KEY_3
		 */
		/**
		 * @event {Boolean} KEY_4
		 */
		/**
		 * @event {Boolean} KEY_5
		 */
		/**
		 * @event {Boolean} KEY_5
		 */
		/**
		 * @event {Boolean} KEY_6
		 */
		/**
		 * @event {Boolean} KEY_7
		 */
		/**
		 * @event {Boolean} KEY_8
		 */
		/**
		 * @event {Boolean} KEY_9
		 */
		/**
		 * @event {Boolean} KEY_0
		 */

		/**
		 * @event {Boolean} KEY_F1
		 */
		/**
		 * @event {Boolean} KEY_F2
		 */
		/**
		 * @event {Boolean} KEY_F3
		 */
		/**
		 * @event {Boolean} KEY_F4
		 */
		/**
		 * @event {Boolean} KEY_F5
		 */
		/**
		 * @event {Boolean} KEY_F6
		 */
		/**
		 * @event {Boolean} KEY_F7
		 */
		/**
		 * @event {Boolean} KEY_F8
		 */
		/**
		 * @event {Boolean} KEY_F9
		 */
		/**
		 * @event {Boolean} KEY_F10
		 */
		/**
		 * @event {Boolean} KEY_F11
		 */
		/**
		 * @event {Boolean} KEY_F12
		 */
		/**
		 * @event {Boolean} KEY_BACKSPACE
		 */
		/**
		 * @event {Boolean} KEY_TAB
		 */
		/**
		 * @event {Boolean} KEY_ENTER
		 */
		/**
		 * @event {Boolean} KEY_SHIFT
		 */
		/**
		 * @event {Boolean} KEY_ALT
		 */
		/**
		 * @event {Boolean} KEY_PAUSE
		 */
		/**
		 * @event {Boolean} KEY_CAPSLOCK
		 */
		/**
		 * @event {Boolean} KEY_ESC
		 */
		/**
		 * @event {Boolean} KEY_SPACE
		 */
		/**
		 * @event {Boolean} KEY_PAGE_UP
		 */
		/**
		 * @event {Boolean} KEY_PAGE_DOWN
		 */
		/**
		 * @event {Boolean} KEY_END
		 */
		/**
		 * @event {Boolean} KEY_HOME
		 */
		/**
		 * @event {Boolean} KEY_UP
		 */
		/**
		 * @event {Boolean} KEY_DOWN
		 */
		/**
		 * @event {Boolean} KEY_RIGHT
		 */
		/**
		 * @event {Boolean} KEY_LEFT
		 */
		/**
		 * @event {Boolean} KEY_INSERT
		 */
		/**
		 * @event {Boolean} KEY_DELETE
		 */
		/**
		 * @event {Boolean} KEY_NUMLOCK
		 */

		//from KEY_A to KEY_Z
		for (i = 0; i < 26; i++) {
			this.states.push('KEY_' + String.fromCharCode('A'.charCodeAt(0) + i));      // charCode MAJ
		}
		//from KEY_F1 to KEY_F12
		for (i = 0; i < 12; i++) {
			this.states.push('KEY_F' + String.fromCharCode('1'.charCodeAt(0) + i)); //      112
		}
		// KEY_0 to KEY_9
		for (i = 0; i < 10; i++) {
			this.states.push('KEY_' + String.fromCharCode('0'.charCodeAt(0) + i));  //      48
		}

		this.states.push('KEY_BACKSPACE');
		this.states.push('KEY_TAB');
		this.states.push('KEY_ENTER');
		this.states.push('KEY_SHIFT');
		this.states.push('KEY_CTRL');
		this.states.push('KEY_ALT');
		this.states.push('KEY_PAUSE');
		this.states.push('KEY_CAPSLOCK');
		this.states.push('KEY_ESC');
		this.states.push('KEY_SPACE');
		this.states.push('KEY_PAGE_UP');
		this.states.push('KEY_PAGE_DOWN');
		this.states.push('KEY_END');
		this.states.push('KEY_HOME');
		this.states.push('KEY_LEFT');
		this.states.push('KEY_UP');
		this.states.push('KEY_RIGHT');
		this.states.push('KEY_DOWN');
		this.states.push('KEY_INSERT');
		this.states.push('KEY_DELETE');
		this.states.push('KEY_NUMLOCK');

		for (i = 0, len = this.states.length; i < len; i++) {
			this._values[i] = KeyboardInput.KEY_RELEASED;
		}

		target.addEventListener("keydown", this._onKeyDown.bind(this), false);
		target.addEventListener("keyup", this._onKeyUp.bind(this), false);
	}

	inherit(KeyboardInput, DeviceInput);

	/**
	 * Represent a key pressed state
	 * @property {Boolean} KEY_PRESSED
	 * @static
	 * @readonly
	 */
	KeyboardInput.KEY_PRESSED = true;

	/**
	 *Represent a key released state
	 * @property {Boolean} KEY_RELEASED
	 * @static
	 * @readonly
	 */
	KeyboardInput.KEY_RELEASED = false;

	/**
	 * Called when a key is pressed.
	 *
	 * @method _onKeyDown
	 * @param {KeyboardEvent}  event
	 * @private
	 */
	KeyboardInput.prototype._onKeyDown = function(event) {
		this.emit(this._getAssociatedEvent(event), KeyboardInput.KEY_PRESSED);
	};

	/**
	 * Called when a key is released.
	 *
	 * @method _onKeyUp
	 * @param {KeyboardEvent}  event
	 * @private
	 */
	KeyboardInput.prototype._onKeyUp = function(event) {
		this.emit(this._getAssociatedEvent(event), KeyboardInput.KEY_RELEASED);
	};

	/**
	 * search a state corresponding to the event object
	 *
	 * @method _getAssociatedEvent
	 * @param {KeyboardEvent}   event
	 * @return {String|null}    name of state changed. null if no state is found.
	 * @private
	 */
	KeyboardInput.prototype._getAssociatedEvent = function(event) {
		if (event.keyCode >= 'A'.charCodeAt(0) && event.keyCode <= 'Z'.charCodeAt(0)) {
			return 'KEY_' + String.fromCharCode(event.keyCode);
		}

		if (event.keyCode >= '0'.charCodeAt(0) && event.keyCode <= '9'.charCodeAt(0)) {
			return 'KEY_' + String.fromCharCode(event.keyCode);
		}

		if (event.keyCode >= 112 && event.keyCode < 124) {
			return 'KEY_F' + String.fromCharCode(111 - event.keyCode);
		}

		switch (event.keyCode) {
			case 8:
				return 'KEY_BACKSPACE';
			case 9:
				return 'KEY_TAB';
			case 13:
				return 'KEY_ENTER';
			case 16:
				return 'KEY_SHIFT';
			case 17:
				return 'KEY_CTRL';
			case 18:
				return 'KEY_ALT';
			case 19:
				return 'KEY_PAUSE';
			case 20:
				return 'KEY_CAPSLOCK';
			case 27:
				return 'KEY_ESC';
			case 32:
				return 'KEY_SPACE';
			case 33:
				return 'KEY_PAGE_UP';
			case 34:
				return 'KEY_PAGE_DOWN';
			case 36:
				return 'KEY_HOME';
			case 37:
				return 'KEY_LEFT';
			case 38:
				return 'KEY_UP';
			case 39:
				return 'KEY_RIGHT';
			case 40:
				return 'KEY_DOWN';
			case 45:
				return 'KEY_INSERT';
			case 46:
				return 'KEY_DELETE';
			case 144:
				return 'KEY_NUMLOCK';
			default:
				return null;
		}
	};

	/**
	 * Predicate wich can be used for testing if a key is pressed.
	 *
	 * @method isPressed
	 * @static
	 * @return {Boolean} return `true` if the key is pressed.
	 */
	KeyboardInput.isPressed = function(event, value) {
		return value;
	};

	/**
	 * Predicate wich can be used for testing if a key is released.
	 *
	 * @method isReleased
	 * @static
	 * @return {Boolean} return `true` if the key is released.
	 */
	KeyboardInput.isReleased = function(event, value) {
		return !value;
	};

	TW.Event.KeyboardInput = KeyboardInput;
	return KeyboardInput;
});
<commit_msg>adding keyboard disable repeat key feature<commit_after>/**
 * @module Event
 * @namespace Event
 */

var TW = TW || {};
define(['./DeviceInput', '../Utils/inherit', '../Utils/Polyfills'], function(DeviceInput, inherit) {

	TW.Event = TW.Event || {};

	/**
	 * DeviceInput using the keyboard.
	 *
	 *
	 * Each event represent a key. each key has two states: `KEY_PRESSED` or `KEY_RELEASED`
	 *
	 *
	 * ## List of keys:
	 *
	 * - All letters from `KEY_A` to `KEY_Z`
	 * - All digits from `KEY_0` to `KEY_9`
	 * - Functions Keys, from `KEY_F1` to `KEY_F12`
	 * - KEY_BACKSPACE
	 * - KEY_TAB
	 * - KEY_ENTER
	 * - KEY_SHIFT
	 * - KEY_ALT
	 * - KEY_PAUSE
	 * - KEY_CAPSLOCK
	 * - KEY_ESC
	 * - KEY_SPACE
	 * - KEY_PAGE_UP
	 * - KEY_PAGE_DOWN
	 * - KEY_END
	 * - KEY_HOME
	 * - KEY_LEFT
	 * - KEY_UP
	 * - KEY_RIGHT
	 * - KEY_DOWN
	 * - KEY_INSERT
	 * - KEY_DELETE
	 * - KEY_NUMLOCK
	 *
	 *
	 *
	 * __Note:__
	 * This class use the `keyCode` attribute from KeyboardEvent object.
	 * The code list of [current W3C standard](http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-20071221/keyset.html)
	 * is not implemented and there is differences between each browser.

	 * The [next W3C standard](http://www.w3.org/TR/DOM-Level-3-Events/#keys-keyvalues)
	 * should improve compatibility.

	 * __Currently, using Numeric key (from Keypad or not) with the Shift,
	 * CapsLock or NumLock keys is not possible.__

	 * __Use exotic keys are strongly discouraged.__

	 *

	 * For example, using Chrome 22.0.1229.94 (under Linux) with a fr keyboard, key `&` (key `1` without `Shift`)
	 * are equally to key `7`.
	 * For more information about compatibility,[this document](http://unixpapa.com/js/key.html) provide
	 * a good summary of the situation.
	 *
	 * Usage of arrows keys, and usage of controls keys with alphabetic characters are supported.
	 *
	 *
	 *
	 * @example
	 *
	 *      var keyboard = new KeyboardInput();
	 *      keyboard.addListener("KEY_SPACE", KeyboardInput.KEY_PRESSED, function(event, value, provider) {
     *          if (provider.getState("KEY_CTRL") === KeyboardInput.KEY_PRESSED) {
     *              //CTRL+space is pressed !
     *          }
     *      });
	 *
	 * @class KeyboardInput
	 * @extends Event.DeviceInput
	 * @param {HTMLElement} [target]      element to listen keypressed and keyup. default to `window.document`
	 * @constructor
	 */
	function KeyboardInput(target) {
		var i, len;

		DeviceInput.call(this);

		if (target === undefined) {
			target = window.document;
		}

		/**
		 * @event {Boolean} KEY_A
		 */
		/**
		 * @event {Boolean} KEY_B
		 */
		/**
		 * @event {Boolean} KEY_C
		 */
		/**
		 * @event {Boolean} KEY_D
		 */
		/**
		 * @event {Boolean} KEY_E
		 */
		/**
		 * @event {Boolean} KEY_F
		 */
		/**
		 * @event {Boolean} KEY_G
		 */
		/**
		 * @event {Boolean} KEY_H
		 */
		/**
		 * @event {Boolean} KEY_I
		 */
		/**
		 * @event {Boolean} KEY_J
		 */
		/**
		 * @event {Boolean} KEY_K
		 */
		/**
		 * @event {Boolean} KEY_L
		 */
		/**
		 * @event {Boolean} KEY_M
		 */
		/**
		 * @event {Boolean} KEY_N
		 */
		/**
		 * @event {Boolean} KEY_O
		 */
		/**
		 * @event {Boolean} KEY_P
		 */
		/**
		 * @event {Boolean} KEY_Q
		 */
		/**
		 * @event {Boolean} KEY_R
		 */
		/**
		 * @event {Boolean} KEY_S
		 */
		/**
		 * @event {Boolean} KEY_T
		 */
		/**
		 * @event {Boolean} KEY_U
		 */
		/**
		 * @event {Boolean} KEY_V
		 */
		/**
		 * @event {Boolean} KEY_W
		 */
		/**
		 * @event {Boolean} KEY_X
		 */
		/**
		 * @event {Boolean} KEY_Y
		 */
		/**
		 * @event {Boolean} KEY_Z
		 */
		/**
		 * @event {Boolean} KEY_1
		 */
		/**
		 * @event {Boolean} KEY_2
		 */
		/**
		 * @event {Boolean} KEY_3
		 */
		/**
		 * @event {Boolean} KEY_4
		 */
		/**
		 * @event {Boolean} KEY_5
		 */
		/**
		 * @event {Boolean} KEY_5
		 */
		/**
		 * @event {Boolean} KEY_6
		 */
		/**
		 * @event {Boolean} KEY_7
		 */
		/**
		 * @event {Boolean} KEY_8
		 */
		/**
		 * @event {Boolean} KEY_9
		 */
		/**
		 * @event {Boolean} KEY_0
		 */

		/**
		 * @event {Boolean} KEY_F1
		 */
		/**
		 * @event {Boolean} KEY_F2
		 */
		/**
		 * @event {Boolean} KEY_F3
		 */
		/**
		 * @event {Boolean} KEY_F4
		 */
		/**
		 * @event {Boolean} KEY_F5
		 */
		/**
		 * @event {Boolean} KEY_F6
		 */
		/**
		 * @event {Boolean} KEY_F7
		 */
		/**
		 * @event {Boolean} KEY_F8
		 */
		/**
		 * @event {Boolean} KEY_F9
		 */
		/**
		 * @event {Boolean} KEY_F10
		 */
		/**
		 * @event {Boolean} KEY_F11
		 */
		/**
		 * @event {Boolean} KEY_F12
		 */
		/**
		 * @event {Boolean} KEY_BACKSPACE
		 */
		/**
		 * @event {Boolean} KEY_TAB
		 */
		/**
		 * @event {Boolean} KEY_ENTER
		 */
		/**
		 * @event {Boolean} KEY_SHIFT
		 */
		/**
		 * @event {Boolean} KEY_ALT
		 */
		/**
		 * @event {Boolean} KEY_PAUSE
		 */
		/**
		 * @event {Boolean} KEY_CAPSLOCK
		 */
		/**
		 * @event {Boolean} KEY_ESC
		 */
		/**
		 * @event {Boolean} KEY_SPACE
		 */
		/**
		 * @event {Boolean} KEY_PAGE_UP
		 */
		/**
		 * @event {Boolean} KEY_PAGE_DOWN
		 */
		/**
		 * @event {Boolean} KEY_END
		 */
		/**
		 * @event {Boolean} KEY_HOME
		 */
		/**
		 * @event {Boolean} KEY_UP
		 */
		/**
		 * @event {Boolean} KEY_DOWN
		 */
		/**
		 * @event {Boolean} KEY_RIGHT
		 */
		/**
		 * @event {Boolean} KEY_LEFT
		 */
		/**
		 * @event {Boolean} KEY_INSERT
		 */
		/**
		 * @event {Boolean} KEY_DELETE
		 */
		/**
		 * @event {Boolean} KEY_NUMLOCK
		 */

		//from KEY_A to KEY_Z
		for (i = 0; i < 26; i++) {
			this.states.push('KEY_' + String.fromCharCode('A'.charCodeAt(0) + i));      // charCode MAJ
		}
		//from KEY_F1 to KEY_F12
		for (i = 0; i < 12; i++) {
			this.states.push('KEY_F' + String.fromCharCode('1'.charCodeAt(0) + i)); //      112
		}
		// KEY_0 to KEY_9
		for (i = 0; i < 10; i++) {
			this.states.push('KEY_' + String.fromCharCode('0'.charCodeAt(0) + i));  //      48
		}

		this.states.push('KEY_BACKSPACE');
		this.states.push('KEY_TAB');
		this.states.push('KEY_ENTER');
		this.states.push('KEY_SHIFT');
		this.states.push('KEY_CTRL');
		this.states.push('KEY_ALT');
		this.states.push('KEY_PAUSE');
		this.states.push('KEY_CAPSLOCK');
		this.states.push('KEY_ESC');
		this.states.push('KEY_SPACE');
		this.states.push('KEY_PAGE_UP');
		this.states.push('KEY_PAGE_DOWN');
		this.states.push('KEY_END');
		this.states.push('KEY_HOME');
		this.states.push('KEY_LEFT');
		this.states.push('KEY_UP');
		this.states.push('KEY_RIGHT');
		this.states.push('KEY_DOWN');
		this.states.push('KEY_INSERT');
		this.states.push('KEY_DELETE');
		this.states.push('KEY_NUMLOCK');

		for (i = 0, len = this.states.length; i < len; i++) {
			this._values[i] = KeyboardInput.KEY_RELEASED;
		}

		/**
		 * enable or not the key repeat.
		 *
		 * @property {Boolean} enableKeyRepeat
		 * @defualt false
		 */
		this.enableKeyRepeat = false;

		target.addEventListener("keydown", this._onKeyDown.bind(this), false);
		target.addEventListener("keyup", this._onKeyUp.bind(this), false);
	}

	inherit(KeyboardInput, DeviceInput);

	/**
	 * Represent a key pressed state
	 * @property {Boolean} KEY_PRESSED
	 * @static
	 * @readonly
	 */
	KeyboardInput.KEY_PRESSED = true;

	/**
	 *Represent a key released state
	 * @property {Boolean} KEY_RELEASED
	 * @static
	 * @readonly
	 */
	KeyboardInput.KEY_RELEASED = false;

	/**
	 * Called when a key is pressed.
	 *
	 * @method _onKeyDown
	 * @param {KeyboardEvent}  event
	 * @private
	 */
	KeyboardInput.prototype._onKeyDown = function(event) {
		var name = this._getAssociatedEvent(event);
		if (this.enableKeyRepeat || this.get(name) !== KeyboardInput.KEY_PRESSED) {
			this.emit(name, KeyboardInput.KEY_PRESSED);
		}
	};

	/**
	 * Called when a key is released.
	 *
	 * @method _onKeyUp
	 * @param {KeyboardEvent}  event
	 * @private
	 */
	KeyboardInput.prototype._onKeyUp = function(event) {
		var name = this._getAssociatedEvent(event);
		if (this.enableKeyRepeat || this.get(name) !== KeyboardInput.KEY_RELEASED) {
			this.emit(name, KeyboardInput.KEY_RELEASED);
		}
	};

	/**
	 * search a state corresponding to the event object
	 *
	 * @method _getAssociatedEvent
	 * @param {KeyboardEvent}   event
	 * @return {String|null}    name of state changed. null if no state is found.
	 * @private
	 */
	KeyboardInput.prototype._getAssociatedEvent = function(event) {
		if (event.keyCode >= 'A'.charCodeAt(0) && event.keyCode <= 'Z'.charCodeAt(0)) {
			return 'KEY_' + String.fromCharCode(event.keyCode);
		}

		if (event.keyCode >= '0'.charCodeAt(0) && event.keyCode <= '9'.charCodeAt(0)) {
			return 'KEY_' + String.fromCharCode(event.keyCode);
		}

		if (event.keyCode >= 112 && event.keyCode < 124) {
			return 'KEY_F' + String.fromCharCode(111 - event.keyCode);
		}

		switch (event.keyCode) {
			case 8:
				return 'KEY_BACKSPACE';
			case 9:
				return 'KEY_TAB';
			case 13:
				return 'KEY_ENTER';
			case 16:
				return 'KEY_SHIFT';
			case 17:
				return 'KEY_CTRL';
			case 18:
				return 'KEY_ALT';
			case 19:
				return 'KEY_PAUSE';
			case 20:
				return 'KEY_CAPSLOCK';
			case 27:
				return 'KEY_ESC';
			case 32:
				return 'KEY_SPACE';
			case 33:
				return 'KEY_PAGE_UP';
			case 34:
				return 'KEY_PAGE_DOWN';
			case 36:
				return 'KEY_HOME';
			case 37:
				return 'KEY_LEFT';
			case 38:
				return 'KEY_UP';
			case 39:
				return 'KEY_RIGHT';
			case 40:
				return 'KEY_DOWN';
			case 45:
				return 'KEY_INSERT';
			case 46:
				return 'KEY_DELETE';
			case 144:
				return 'KEY_NUMLOCK';
			default:
				return null;
		}
	};

	/**
	 * Predicate wich can be used for testing if a key is pressed.
	 *
	 * @method isPressed
	 * @static
	 * @return {Boolean} return `true` if the key is pressed.
	 */
	KeyboardInput.isPressed = function(event, value) {
		return value;
	};

	/**
	 * Predicate wich can be used for testing if a key is released.
	 *
	 * @method isReleased
	 * @static
	 * @return {Boolean} return `true` if the key is released.
	 */
	KeyboardInput.isReleased = function(event, value) {
		return !value;
	};

	TW.Event.KeyboardInput = KeyboardInput;
	return KeyboardInput;
});
<|endoftext|>


<commit_before>/*
    Copyright 2010 Emilis Dambauskas

    This file is part of PolicyFeed module.

    PolicyFeed is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    PolicyFeed is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with PolicyFeed.  If not, see <http://www.gnu.org/licenses/>.
*/

module.shared = true;

var UrlErrors = require("PolicyFeed/UrlErrors");
var JsonStorage = require("ctl/JsonStorage");

/**
 * 1 second delay between requests for the same host.
 */
var DEFAULT_DELAY = 1000;

/**
 * Storage path on disk:
 */
var STORAGE_PATH = "/PolicyFeed/UrlQueue/";

//----------------------------------------------------------------------------

/**
 * Queue for urls to be done as soon as possible (lower priority).
 */
var fifo = [];

/**
 * Saves fifo queue to disk.
 */
Object.defineProperty(fifo, "save", {enumerable:false, value: function() {
    JsonStorage.write(STORAGE_PATH + "fifo", this);
}});

/**
 * Loads fifo queue from disk.
 */
Object.defineProperty(fifo, "load", {enumerable:false, value: function() {
    this.length = 0;
    var stored = JsonStorage.read(STORAGE_PATH + "fifo");
    if (stored && stored.length) {
        for each (var url in stored) {
            if (typeof(url) == "object")
                this.push(url);
        }
    }
}});

//----------------------------------------------------------------------------

/**
 * Queue for urls to be done on scheduled time (higher priority).
 */
var schedule = [];

/**
 * Saves schedule queue to disk.
 */
Object.defineProperty(schedule, "save", {enumerable:false, value: function() {
    JsonStorage.write(STORAGE_PATH + "schedule", this);
}});

/**
 * Loads schedule queue from disk.
 */
Object.defineProperty(schedule, "load", {enumerable:false, value: function() {
    this.length = 0;
    var stored = JsonStorage.read(STORAGE_PATH + "schedule");
    if (stored && stored.length) {
        for each (var url in stored) {
            if (typeof(url) == "object")
                this.push(url);
        }
    }
    this.sort(time_sort);
}});

/**
 * Function for sorting the schedule queue.
 */
var time_sort = function(a, b) {
    return a.time > b.time;
}

//----------------------------------------------------------------------------

/**
 * Locked url's by requesting process id.
 */
var locks = {};

/**
 * Locks url for processing.
 */
var lockUrl = function(pid, url) {
    locks[pid] = url;

    var count = 0;
    for each (var u in locks) {
        if (u === url) {
            count++;
            if (count > 1) {
                locks[pid] = undefined;
                //saveLocks();
                return false;
            }
        }
    }

    //saveLocks();
    return count == 1;
}

/**
 * Removes a locked url for a given pid.
 */
var unlockPid = function(pid) {
    locks[pid] = undefined;
    //saveLocks();
}

/**
 * Saves locks to disk.
 */
var saveLocks = function() {
    JsonStorage.write(STORAGE_PATH + "locks", locks);
}

/**
 * Loads locks from disk.
 */
var loadLocks = function() {
    locks = JsonStorage.read(STORAGE_PATH + "locks") || {};
}

//----------------------------------------------------------------------------

/**
 * Register of last request time and delay for domains.
 */
var domains = {};

/**
 *
 */
var initDomains = function(domainList) {
    domains = domainList;
    for (var name in domains) {
        if (!domains[name].delay)
            domains[name].delay = DEFAULT_DELAY;
        domains[name].last = 0;
    }
}

/**
 *
 */
exports.requestDomain = function requestDomain(domain) {
    var d = domains[domain];
    if (!d)
        throw Error("Invalid domain requested: " + uneval(domain));
    var t = new Date().getTime();
    if (t > (d.last + d.delay)) {
        d.last = t;
        return true;
    }
    else
        return false;
}

//----------------------------------------------------------------------------

/**
 *
 */
exports.init = function(options) {
    options = options || {};
    var {domainList} = options;

    initDomains(domainList);

    loadLocks();
    schedule.load();
    fifo.load();
}

/**
 *
 */
exports.getDomainFromUrl = function(url) {
    var start = url.indexOf("/") + 2;
    var end = url.indexOf("/", start) - start;
    if (end < 0)
        end = url.length - start;
    return url.substr(start, end);
}

/**
 *
 */
exports.makeUrl = function(url, time) {

    if (typeof(url) == "string" || url instanceof String)
        url = { url: url };

    if (!url.domain)
        url.domain = this.getDomainFromUrl(url.url);

    if (time)
        url.time = time;

    return url;
}

/**
 *
 */
exports.addUrl = function(url) {
    fifo.push(this.makeUrl(url));
    fifo.save();
}

/**
 *
 */
exports.scheduleUrl = function(url, time) {
    if (!time)
        time = new Date();
    schedule.push(this.makeUrl(url, time));
    schedule.sort(time_sort);
    schedule.save();

}

/**
 *
 */
exports.isUrlScheduled = function(the_url, time) {
    for each (var url in schedule) {
        if (url.url == the_url) {
            if (time !== undefined || time > url.time)
                return true;
        }
    }
    return false;
}

/**
 *
 */
exports.getUrl = function(pid) {
    // check schedule:
    var t = new Date();
    for each (var url in schedule) {
        if (url.time > t)
            break; // exit loop when we reach urls in the future.
        else if (this.requestDomain(url.domain) && lockUrl(pid, url)) {
            schedule.remove(url);
            schedule.save();
            return url;
        }
        // else continue with next url
    }

    // check fifo:
    for each (var url in fifo) {
        if (this.requestDomain(url.domain) && lockUrl(pid, url)) {
            fifo.remove(url);
            fifo.save();
            return url;
        }
        // else continue with next url
    }

    return null;
}

/**
 *
 */
exports.failedUrl = function(pid, url) {
    UrlErrors.addUrl(url);
    unlockPid(pid);
}

/**
 * Re-schedules a locked URL for later.
 */
exports.rescheduleUrl = function(pid, after) {
    this.scheduleUrl(locks[pid], new Date().getTime() + after);
    unlockPid(pid);
}

/**
 *
 */
exports.doneUrl = function(pid) {
    unlockPid(pid);
}

/**
 *
 */
exports.getStatus = function() {
    return {
        fifo: fifo,
        schedule: schedule,
        locks: locks,
        domains: domains
    };
}
<commit_msg>Turned on lock saving and loding to PolicyFeed/UrlQueue.<commit_after>/*
    Copyright 2010 Emilis Dambauskas

    This file is part of PolicyFeed module.

    PolicyFeed is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    PolicyFeed is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with PolicyFeed.  If not, see <http://www.gnu.org/licenses/>.
*/

module.shared = true;

var UrlErrors = require("PolicyFeed/UrlErrors");
var JsonStorage = require("ctl/JsonStorage");

/**
 * 1 second delay between requests for the same host.
 */
var DEFAULT_DELAY = 1000;

/**
 * Storage path on disk:
 */
var STORAGE_PATH = "/PolicyFeed/UrlQueue/";

//----------------------------------------------------------------------------

/**
 * Queue for urls to be done as soon as possible (lower priority).
 */
var fifo = [];

/**
 * Saves fifo queue to disk.
 */
Object.defineProperty(fifo, "save", {enumerable:false, value: function() {
    JsonStorage.write(STORAGE_PATH + "fifo", this);
}});

/**
 * Loads fifo queue from disk.
 */
Object.defineProperty(fifo, "load", {enumerable:false, value: function() {
    this.length = 0;
    var stored = JsonStorage.read(STORAGE_PATH + "fifo");
    if (stored && stored.length) {
        for each (var url in stored) {
            if (typeof(url) == "object")
                this.push(url);
        }
    }
}});

//----------------------------------------------------------------------------

/**
 * Queue for urls to be done on scheduled time (higher priority).
 */
var schedule = [];

/**
 * Saves schedule queue to disk.
 */
Object.defineProperty(schedule, "save", {enumerable:false, value: function() {
    JsonStorage.write(STORAGE_PATH + "schedule", this);
}});

/**
 * Loads schedule queue from disk.
 */
Object.defineProperty(schedule, "load", {enumerable:false, value: function() {
    this.length = 0;
    var stored = JsonStorage.read(STORAGE_PATH + "schedule");
    if (stored && stored.length) {
        for each (var url in stored) {
            if (typeof(url) == "object")
                this.push(url);
        }
    }
    this.sort(time_sort);
}});

/**
 * Function for sorting the schedule queue.
 */
var time_sort = function(a, b) {
    return a.time > b.time;
}

//----------------------------------------------------------------------------

/**
 * Locked url's by requesting process id.
 */
var locks = {};

/**
 * Locks url for processing.
 */
var lockUrl = function(pid, url) {
    locks[pid] = url;

    var count = 0;
    for each (var u in locks) {
        if (u === url) {
            count++;
            if (count > 1) {
                unlockPid(pid);
                return false;
            }
        }
    }

    saveLocks();
    return count == 1;
}

/**
 * Removes a locked url for a given pid.
 */
var unlockPid = function(pid) {
    locks[pid] = undefined;
    saveLocks();
}

/**
 * Saves locks to disk.
 */
var saveLocks = function() {
    JsonStorage.write(STORAGE_PATH + "locks", locks);
}

/**
 * Loads locks from disk.
 */
var loadLocks = function() {
    locks = JsonStorage.read(STORAGE_PATH + "locks") || {};
    for (var pid in locks)
        exports.rescheduleUrl(pid, 0);
}

//----------------------------------------------------------------------------

/**
 * Register of last request time and delay for domains.
 */
var domains = {};

/**
 *
 */
var initDomains = function(domainList) {
    domains = domainList;
    for (var name in domains) {
        if (!domains[name].delay)
            domains[name].delay = DEFAULT_DELAY;
        domains[name].last = 0;
    }
}

/**
 *
 */
exports.requestDomain = function requestDomain(domain) {
    var d = domains[domain];
    if (!d)
        throw Error("Invalid domain requested: " + uneval(domain));
    var t = new Date().getTime();
    if (t > (d.last + d.delay)) {
        d.last = t;
        return true;
    }
    else
        return false;
}

//----------------------------------------------------------------------------

/**
 *
 */
exports.init = function(options) {
    options = options || {};
    var {domainList} = options;

    initDomains(domainList);

    loadLocks();
    schedule.load();
    fifo.load();
}

/**
 *
 */
exports.getDomainFromUrl = function(url) {
    var start = url.indexOf("/") + 2;
    var end = url.indexOf("/", start) - start;
    if (end < 0)
        end = url.length - start;
    return url.substr(start, end);
}

/**
 *
 */
exports.makeUrl = function(url, time) {

    if (typeof(url) == "string" || url instanceof String)
        url = { url: url };

    if (!url.domain)
        url.domain = this.getDomainFromUrl(url.url);

    if (time)
        url.time = time;

    return url;
}

/**
 *
 */
exports.addUrl = function(url) {
    fifo.push(this.makeUrl(url));
    fifo.save();
}

/**
 *
 */
exports.scheduleUrl = function(url, time) {
    if (!time)
        time = new Date();
    schedule.push(this.makeUrl(url, time));
    schedule.sort(time_sort);
    schedule.save();

}

/**
 *
 */
exports.isUrlScheduled = function(the_url, time) {
    for each (var url in schedule) {
        if (url.url == the_url) {
            if (time !== undefined || time > url.time)
                return true;
        }
    }
    return false;
}

/**
 *
 */
exports.getUrl = function(pid) {
    // check schedule:
    var t = new Date();
    for each (var url in schedule) {
        if (url.time > t)
            break; // exit loop when we reach urls in the future.
        else if (this.requestDomain(url.domain) && lockUrl(pid, url)) {
            schedule.remove(url);
            schedule.save();
            return url;
        }
        // else continue with next url
    }

    // check fifo:
    for each (var url in fifo) {
        if (this.requestDomain(url.domain) && lockUrl(pid, url)) {
            fifo.remove(url);
            fifo.save();
            return url;
        }
        // else continue with next url
    }

    return null;
}

/**
 *
 */
exports.failedUrl = function(pid, url) {
    UrlErrors.addUrl(url);
    unlockPid(pid);
}

/**
 * Re-schedules a locked URL for later.
 */
exports.rescheduleUrl = function(pid, after) {
    this.scheduleUrl(locks[pid], new Date().getTime() + after);
    unlockPid(pid);
}

/**
 *
 */
exports.doneUrl = function(pid) {
    unlockPid(pid);
}

/**
 *
 */
exports.getStatus = function() {
    return {
        fifo: fifo,
        schedule: schedule,
        locks: locks,
        domains: domains
    };
}
<|endoftext|>


<commit_before>export * from './src/directives/for';
export * from './src/directives/if';
export * from './src/directives/non_bindable';
export * from './src/directives/switch';
<commit_msg>feat: add class directive to a list of directives<commit_after>export * from './src/directives/class';
export * from './src/directives/for';
export * from './src/directives/if';
export * from './src/directives/non_bindable';
export * from './src/directives/switch';
<|endoftext|>


<commit_before>var express = require('express');
var DataStore = require('./regard-data-store.js');
var Investigation = require('../schemas/investigation.js');

var router = express.Router();

function getDataStore(id) {
  return Investigation.findById(id).exec().then(function (investigation) {
    if (!investigation.organization) {
      throw new Error('investigation is missing organization');
    }

    if (!investigation.product) {
      throw new Error('investigation is missing product');
    }

    return new DataStore(investigation.organization, investigation.product);
  });
}

router.get('/chartdata/:id', function (req, res, next) {
  var id = req.params.id;

  getDataStore(id).then(function (dataStore) {
    dataStore.runQuery(id).then(function (result) {
      res.json(JSON.parse(result).Results);
    }, next);
  }, ne);
});

function isJSON(jsonString) {
  try {
    var o = JSON.parse(jsonString);

    if (o && typeof o === "object" && o !== null) {
      return true;
    }
  } catch (e) {}
  return false;
}

router.put('/investigations/:id', function (req, res, next) {
  var queryName = req.params.id;
  var queryDefinition = req.body.investigation.queryDefinition;

  getDataStore(queryName).then(function (dataStore) {
    dataStore.registerQuery(queryName, queryDefinition).done(function (response) {
      console.dir("register query: " + response);
      next();
    }, next);
  }, next);
});

module.exports = router;<commit_msg>Interesting brackets bug<commit_after>var express = require('express');
var DataStore = require('./regard-data-store.js');
var Investigation = require('../schemas/investigation.js');

var router = express.Router();

function getDataStore(id) {
  return Investigation.findById(id).exec().then(function (investigation) {
    if (!investigation.organization) {
      throw new Error('investigation is missing organization');
    }

    if (!investigation.product) {
      throw new Error('investigation is missing product');
    }

    return new DataStore(investigation.organization, investigation.product);
  });
}

router.get('/chartdata/:id', function (req, res, next) {
  var id = req.params.id;

  getDataStore(id).then(function (dataStore) {
    dataStore.runQuery(id).then(function (result) {
      res.json(JSON.parse(result).Results);
    }, next);
  }, next);
});

function isJSON(jsonString) {
  try {
    var o = JSON.parse(jsonString);

    if (o && typeof o === "object" && o !== null) {
      return true;
    }
  } catch (e) {}
  return false;
}

router.put('/investigations/:id', function (req, res, next) {
  var queryName = req.params.id;
  var queryDefinition = req.body.investigation.queryDefinition;

  getDataStore(queryName).then(function (dataStore) {
    dataStore.registerQuery(queryName, queryDefinition).done(function (response) {
      console.dir("register query: " + response);
      next();
    }, next);
  }, next);
});

module.exports = router;<|endoftext|>