<commit_before>////////////////////////////////////////////////////////////////////////////////
// Control Base
//
// 'Abstract' base class for UI controls.
////////////////////////////////////////////////////////////////////////////////
var controls = function (controls, $) {
	var LAST_ID = 0;

	controls.id = function () {
		return LAST_ID++;
	};

	controls.base = function () {
		var self =  {
			// properties
			id: controls.id(),	// id is automatically assigned
			parent: null,				// parent control
			children: [],				// child controls

			// adds a child to the control
			append: function (child) {
				this.children.push(child);
				child.parent = this;
			},

			// removes child controls
			clear: function () {
				this.children.length = 0;
			},

			// initializes the control
			// e.g. adding data, events, etc.
			init: null,

			// produces the control's html
			html: function () {
				throw "Abstract";
			},

			// renders the control
			// -parent: parent jQuery object
			render: function (parent) {
				// initializing jQuery object from full control markup
				var elem = $(this.html()),
						target = $('#' + this.id);

				// adding control to dom
				if (target.length) {
					target.replaceWith(elem);
				} else if (parent) {
					parent.append(elem);
				}

				// walking control tree initializing nodes
				(function inner(control) {
					var selector = '#' + control.id,
							i,
					// initializing current control
					result = control.init ? control.init(elem.is(selector) ? elem : elem.find(selector)) : true;
					// initializing children
					if (control.children.length) {
						for (i = 0; i < control.children.length; i++) {
							// when a control.init() returns false, it breaks the init cycle
							if (inner(control.children[i]) === false) {
								break;
							}
						}
					}
					return result;
				}(this));

				return parent || elem;
			}
		};

		return self;
	};

	return controls;
}(controls || {},
	jQuery);
<commit_msg>added control lookup<commit_after>////////////////////////////////////////////////////////////////////////////////
// Control Base
//
// 'Abstract' base class for UI controls.
////////////////////////////////////////////////////////////////////////////////
var controls = function (controls, $) {
	var LAST_ID = 0;

	// generates a new, unique control id
	controls.id = function () {
		return LAST_ID++;
	};

	// control lookup (id -> control)
	controls.lookup = {};

	// base control class
	controls.base = function () {
		var id = controls.id(),

		self =  {
			// properties
			id: id,							// id is automatically assigned
			parent: null,				// parent control
			children: [],				// child controls
			data: [],						// data associated with control

			// adds a child to the control
			append: function (child) {
				this.children.push(child);
				child.parent = this;
			},

			// removes child controls
			clear: function () {
				this.children.length = 0;
			},

			// initializes the control
			// e.g. adding data, events, etc.
			init: null,

			// produces the control's html
			html: function () {
				throw "Abstract";
			},

			// renders the control
			// -parent: parent jQuery object
			render: function (parent) {
				// initializing jQuery object from full control markup
				var elem = $(this.html()),
						target = $('#' + this.id);

				// adding control to dom
				if (target.length) {
					target.replaceWith(elem);
				} else if (parent) {
					parent.append(elem);
				}

				// walking control tree initializing nodes
				(function inner(control) {
					var selector = '#' + control.id,
							i,
					// initializing current control
					result = control.init ? control.init(elem.is(selector) ? elem : elem.find(selector)) : true;
					// initializing children
					if (control.children.length) {
						for (i = 0; i < control.children.length; i++) {
							// when a control.init() returns false, it breaks the init cycle
							// for its siblings -> useful in conj. with jQuery.live()
							if (inner(control.children[i]) === false) {
								break;
							}
						}
					}
					return result;
				}(this));

				return parent || elem;
			}
		};

		// adding control to lookup
		controls.lookup[id] = self;

		return self;
	};

	return controls;
}(controls || {},
	jQuery);
<|endoftext|>


<commit_before>var _ = require('underscore');
var Backbone = require('backbone');
Backbone.$ = window.$; // WHHHHHHYYYYYYYY?!

var badgeBackground = new createjs.Bitmap("/static/badge-background.svg");
var coreFlag = new createjs.Bitmap("/static/badge-core.svg");
var newFlag = new createjs.Bitmap("/static/badge-new.svg");
var doneBtnSheet = new createjs.SpriteSheet({
  images: ["/static/pathway/badge-icon-checked-spritesheet-v2.svg"],
  frames: {width: 30, height: 30},
  animations: {
    unchecked: [0],
    checked: [1],
    button: ['unchecked', 'checked']
  }
});
var grayscale = new createjs.ColorMatrixFilter(new createjs.ColorMatrix(0, 0, -100));

function World () {

  Object.defineProperty(this, "columnWidth", {
    get: function () { return this.canvasWidth / this.columnCount; },
    set: function () { throw new Error('Cannot set columnWidth directly'); }
  });

  this.gridToPixel = function gridToPixel (gx, gy) {
    if (typeof gx === 'object') {
      var obj = gx;
      gx = obj.x;
      gy = obj.y;
    }
    var x = gx * this.columnWidth;
    var y = gy * this.columnWidth;
    return {
      x: x,
      y: y,
      w: this.columnWidth,
      cx: x + (this.columnWidth / 2),
      cy: y + (this.columnWidth / 2),
      bounds: [x, y, this.columnWidth, this.columnWidth]
    };
  };

  this.pixelToGrid = function pixelToGrid (x, y) {
    if (typeof x === 'object') {
      var obj = x;
      x = obj.x;
      y = obj.y;
    }
    var col = Math.floor(x / this.columnWidth);
    var row = Math.floor(y / this.columnWidth);
    return {
      x: col,
      y: row
    };
  };

  return this;
}
var world = new World();

function makePathwayItem(item) {
  var container = new createjs.Container();
  container.model = item;
  container.name = item.cid;

  container.setBounds(0, 0, world.columnWidth, world.columnWidth);

  function reemit (obj, evtName, newName) {
    obj.on(evtName, function (evt) {
      var newEvt = evt.clone();
      newEvt.type = newName;
      newEvt.nativeEvent = evt.nativeEvent;
      container.dispatchEvent(newEvt);
    });
  }

  var grabContainer = new createjs.Container();
  reemit(grabContainer, 'rollover', 'grab-rollover');
  reemit(grabContainer, 'rollout', 'grab-rollout');
  reemit(grabContainer, 'mousedown', 'grab');
  reemit(grabContainer, 'pressmove', 'move');
  reemit(grabContainer, 'pressup', 'release');
  var move = false;
  grabContainer.on('mousedown', function (evt) {
    move = false;
  });
  grabContainer.on('pressmove', function (evt) {
    move = true;
  });
  grabContainer.on('click', function () {
    if (!move) container.dispatchEvent('tile-click');
  });
  reemit(grabContainer, 'dblclick', 'tile-dblclick');

  var rect = badgeBackground.clone();
  var img = new createjs.Bitmap(item.imgSrc);
  img.image.onload = function () {
    container.layout();
    container.dispatchEvent('ready');
  };
  var title = new createjs.Text(item.name, "14px 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif");
  grabContainer.addChild(rect, img);
  container.addChild(grabContainer, title);

  var core;
  if (item.core) {
    core = coreFlag.clone();
    container.addChild(core);
  }

  var doneBtn = new createjs.Sprite(doneBtnSheet, "button");
  container.addChild(doneBtn);
  reemit(doneBtn, 'rollover', 'button-rollover');
  reemit(doneBtn, 'rollout', 'button-rollout');
  reemit(doneBtn, 'click', 'toggleComplete');

  var delBtn = new createjs.Shape();
  delBtn.graphics.beginFill('#0fa1d6').drawRoundRect(0, 0, 40, 40, 40)
    .beginStroke('white').moveTo(10, 10).lineTo(30, 30)
    .moveTo(10, 30).lineTo(30, 10);
  reemit(delBtn, 'click', 'delete');
  reemit(delBtn, 'rollover', 'button-rollover');
  reemit(delBtn, 'rollout', 'button-rollout');

  function scaleToMax (img, w, h) {
    var imgBounds = img.getBounds();
    if (imgBounds) {
      var width = Math.min(imgBounds.width, w);
      var height = Math.min(imgBounds.height, h);
      var scale = Math.min(width / imgBounds.width, height / imgBounds.height);
      img.scaleX = img.scaleY = scale;
    }
    return img.getTransformedBounds();
  }

  container.layout = function () {
    var width = world.columnWidth;
    var height = width;
    container.setBounds(0, 0, width, height);

    var margin = 10;
    var corners = 10;
    var rw = width - (2 * margin);
    var rh = height - (2 * margin);
    rect.x = rect.y = margin;
    rect.scaleX = rw / rect.getBounds().width;
    rect.scaleY = rh / rect.getBounds().height;

    doneBtn.visible = world.showProgress;
    if (doneBtn.visible) {
      doneBtn.x = doneBtn.y = margin + 2;
      if (item.complete) {
        doneBtn.gotoAndStop('checked');
        img.filters = [];
        img.cache(0, 0, img.getBounds().width, img.getBounds().height);
      }
      else {
        doneBtn.gotoAndStop('unchecked');
        img.filters = [grayscale];
        img.cache(0, 0, img.getBounds().width, img.getBounds().height);
      }
    }

    if (item.core) {
      core.x = margin + rect.getTransformedBounds().width - core.getBounds().width - 5;
      core.y = margin + 5;
    }

    var imgBounds = scaleToMax(img, rw, rh - (24 * 2));
    if (imgBounds) {
      img.x = margin + (rw / 2) - (imgBounds.width / 2);
      img.y = margin + (rh / 2) - (imgBounds.height / 2) - (24 / 2);

      title.lineWidth = rw - 20;
      title.textAlign = "center";
      title.x = margin + (rw / 2);
      title.y = img.y + imgBounds.height;
    }

    if (!item.core) {
      var idx = container.getChildIndex(delBtn);
      if (world.deletable) {
        if (idx === -1) container.addChild(delBtn);
      }
      else {
        if (idx !== -1) container.removeChildAt(idx);
      }
    }

    var coords = world.gridToPixel(item.x, item.y);
    container.x = coords.x;
    container.y = coords.y;
  };

  return container;
}

module.exports = Backbone.View.extend({
  initialize: function (opts) {
    if (!(opts && opts.canvas && opts.columns && opts.requirements))
      throw new Error('You must specify canvas, columns, and requirements options');

    var self = this;
    ["columnCount", "mode", "showProgress"].forEach(function (prop) {
      Object.defineProperty(self, prop, {
        get: function () { return world[prop]; },
        set: function (val) { world[prop] = val; }
      });
    });
    world.canvasWidth = opts.canvas.width;
    world.columnCount = opts.columns;

    _.extend(this, opts);

    this.stage = new createjs.Stage(this.canvas);
    createjs.Touch.enable(this.stage, true, true);
    if (this.isRearrangeable()) {
      var pollRate = 20;
      this.stage.enableMouseOver(pollRate);
      $(this.stage.canvas).addClass('edit-mode');
    }

    var gridLayer = this.gridLayer = new createjs.Container();
    this.stage.addChild(this.gridLayer);

    this.maxRow = 0;
    this.requirements.forEach(function (req) {
      this.addBadge(req);
    }.bind(this));

    this.listenTo(this.requirements, "add", this.addBadge, this);

    this.listenTo(this.requirements, "remove", function (req) {
      if (req.cid) {
        var item = this.stage.getChildByName(req.cid);
        this.stage.removeChild(item);
        this.refresh();
      }
    });

    var editor = this;
    gridLayer.layout = function () {
      this.removeAllChildren();
      for (var row = 0; row <= editor.maxRow + 2; row++) {
        for (var col = 0; col < world.columnCount; col++) {
          var coords = world.gridToPixel(col, row);
          var hex = new createjs.Shape();
          hex.graphics.beginFill("#CCC")
            .drawPolyStar(0, 0, 15, 6, 0, 30);
          hex.x = coords.cx;
          hex.y = coords.cy;
          hex.setBounds(-coords.w/2, -coords.w/2, coords.w, coords.w);
          this.addChild(hex);
        }
      }
    };

    this.layout();
  },
  addBadge: function (model) {
    if (model.y === undefined) {
      var row = 0;
      while (model.y === undefined) {
        var xs = _.pluck(this.requirements.where({y: row}), 'x');
        var empties = _.difference(_.range(world.columnCount), xs);
        if (empties.length) {
          model.set({
            x: empties.shift(),
            y: row
          });
          model.trigger('positioned', model);
        }
        else {
          row++;
        }
      }
    }
    this.maxRow = Math.max(this.maxRow, model.y);
    var item = makePathwayItem(model);

    if (model.newFlag) {
      var flag = newFlag.clone();
      flag.x = item.getBounds().width - flag.getBounds().width;
      flag.y = 0;
      flag.name = 'newFlag';
      item.addChild(flag);

      item.on('mousedown', function () {
        item.model.newFlag = false;
        item.removeChild(item.getChildByName('newFlag'));
        this.refresh();
      }, this);
    }

    item.on('ready', function () {
      this.stage.addChild(item);
      this.refresh();
    }, this);

    item.on('toggleComplete', function () {
      if (this.isRearrangeable()) {
        item.model.complete = !item.model.complete;
        this.refresh();
      }
    }, this);

    item.on('button-rollover', function () {
      $(this.stage.canvas).addClass('cursor-button');
    }, this);
    item.on('button-rollout', function () {
      $(this.stage.canvas).removeClass('cursor-button');
    }, this);
    item.on('delete', function (evt) {
      this.trigger('delete', evt.target.model);
    }, this);

    item.on('grab-rollover', function () {
      if (this.isRearrangeable()) $(this.stage.canvas).addClass('cursor-grab');
    }, this);
    item.on('grab', function () {
      if (this.isRearrangeable()) $(this.stage.canvas).addClass('cursor-grabbing');
    }, this);
    item.on('release', function () {
      if (this.isRearrangeable()) $(this.stage.canvas).removeClass('cursor-grabbing');
    }, this);
    item.on('grab-rollout', function () {
      if (this.isRearrangeable()) $(this.stage.canvas).removeClass('cursor-grab');
    }, this);

    item.on('move', function (evt) {
      if (this.isRearrangeable()) {
        var coords = world.pixelToGrid(this.stage.globalToLocal(evt.stageX, evt.stageY));
        item.model.set({
          x: coords.x,
          y: coords.y
        });
        evt.nativeEvent.preventDefault();
        this.refresh();
      }
    }, this);

    item.on('tile-dblclick', function (evt) {
      this.trigger('click', evt.currentTarget.model);
    }, this);
  },
  isRearrangeable: function () {
    return world.mode && (world.mode === 'edit');
  },
  isDeletable: function () {
    return world.deletable;
  },
  layout: function () {
    world.canvasWidth = this.stage.canvas.width;
    this.stage.children.forEach(function (child) {
      if (child.layout) child.layout();
    });
  },
  render: function () {
    this.stage.canvas.height = this.stage.getTransformedBounds().height;
    this.stage.update();
    return this;
  },
  refresh: function () {
    this.layout();
    this.render();
  },
  remove: function () {
    createjs.Touch.disable(this.stage);
    this.$el.remove();
    this.stopListening();
    return this;
  },
  enableDelete: function (flag) {
    world.deletable = flag;
  }
});
<commit_msg>Do a lot less layout and refresh business in editor for more responsive feel<commit_after>var _ = require('underscore');
var Backbone = require('backbone');
Backbone.$ = window.$; // WHHHHHHYYYYYYYY?!

var badgeBackground = new createjs.Bitmap("/static/badge-background.svg");
var coreFlag = new createjs.Bitmap("/static/badge-core.svg");
var newFlag = new createjs.Bitmap("/static/badge-new.svg");
var doneBtnSheet = new createjs.SpriteSheet({
  images: ["/static/pathway/badge-icon-checked-spritesheet-v2.svg"],
  frames: {width: 30, height: 30},
  animations: {
    unchecked: [0],
    checked: [1],
    button: ['unchecked', 'checked']
  }
});
var grayscale = new createjs.ColorMatrixFilter(new createjs.ColorMatrix(0, 0, -100));

function World () {

  Object.defineProperty(this, "columnWidth", {
    get: function () { return this.canvasWidth / this.columnCount; },
    set: function () { throw new Error('Cannot set columnWidth directly'); }
  });

  this.gridToPixel = function gridToPixel (gx, gy) {
    if (typeof gx === 'object') {
      var obj = gx;
      gx = obj.x;
      gy = obj.y;
    }
    var x = gx * this.columnWidth;
    var y = gy * this.columnWidth;
    return {
      x: x,
      y: y,
      w: this.columnWidth,
      cx: x + (this.columnWidth / 2),
      cy: y + (this.columnWidth / 2),
      bounds: [x, y, this.columnWidth, this.columnWidth]
    };
  };

  this.pixelToGrid = function pixelToGrid (x, y) {
    if (typeof x === 'object') {
      var obj = x;
      x = obj.x;
      y = obj.y;
    }
    var col = Math.floor(x / this.columnWidth);
    var row = Math.floor(y / this.columnWidth);
    return {
      x: col,
      y: row
    };
  };

  return this;
}
var world = new World();

function makePathwayItem(item) {
  var container = new createjs.Container();
  container.model = item;
  container.name = item.cid;

  container.setBounds(0, 0, world.columnWidth, world.columnWidth);

  function reemit (obj, evtName, newName) {
    obj.on(evtName, function (evt) {
      var newEvt = evt.clone();
      newEvt.type = newName;
      newEvt.nativeEvent = evt.nativeEvent;
      container.dispatchEvent(newEvt);
    });
  }

  var grabContainer = new createjs.Container();
  reemit(grabContainer, 'rollover', 'grab-rollover');
  reemit(grabContainer, 'rollout', 'grab-rollout');
  reemit(grabContainer, 'mousedown', 'grab');
  reemit(grabContainer, 'pressmove', 'move');
  reemit(grabContainer, 'pressup', 'release');
  var move = false;
  grabContainer.on('mousedown', function (evt) {
    move = false;
  });
  grabContainer.on('pressmove', function (evt) {
    move = true;
  });
  grabContainer.on('click', function () {
    if (!move) container.dispatchEvent('tile-click');
  });
  reemit(grabContainer, 'dblclick', 'tile-dblclick');

  var rect = badgeBackground.clone();
  var img = new createjs.Bitmap(item.imgSrc);
  img.image.onload = function () {
    container.layout();
    container.dispatchEvent('ready');
  };
  var title = new createjs.Text(item.name, "14px 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif");
  grabContainer.addChild(rect, img, title);
  container.addChild(grabContainer);

  var core;
  if (item.core) {
    core = coreFlag.clone();
    container.addChild(core);
  }

  var doneBtn = new createjs.Sprite(doneBtnSheet, "button");
  container.addChild(doneBtn);
  reemit(doneBtn, 'rollover', 'button-rollover');
  reemit(doneBtn, 'rollout', 'button-rollout');
  reemit(doneBtn, 'click', 'toggleComplete');

  var delBtn = new createjs.Shape();
  delBtn.graphics.beginFill('#0fa1d6').drawRoundRect(0, 0, 40, 40, 40)
    .beginStroke('white').moveTo(10, 10).lineTo(30, 30)
    .moveTo(10, 30).lineTo(30, 10);
  reemit(delBtn, 'click', 'delete');
  reemit(delBtn, 'rollover', 'button-rollover');
  reemit(delBtn, 'rollout', 'button-rollout');

  function scaleToMax (img, w, h) {
    var imgBounds = img.getBounds();
    if (imgBounds) {
      var width = Math.min(imgBounds.width, w);
      var height = Math.min(imgBounds.height, h);
      var scale = Math.min(width / imgBounds.width, height / imgBounds.height);
      img.scaleX = img.scaleY = scale;
    }
    return img.getTransformedBounds();
  }

  container.layout = function () {
    var width = world.columnWidth;
    var height = width;
    container.setBounds(0, 0, width, height);

    var margin = 10;
    var corners = 10;
    var rw = width - (2 * margin);
    var rh = height - (2 * margin);
    rect.x = rect.y = margin;
    rect.scaleX = rw / rect.getBounds().width;
    rect.scaleY = rh / rect.getBounds().height;

    doneBtn.visible = world.showProgress;
    if (doneBtn.visible) {
      doneBtn.x = doneBtn.y = margin + 2;
      if (item.complete) {
        doneBtn.gotoAndStop('checked');
        img.filters = [];
        img.cache(0, 0, img.getBounds().width, img.getBounds().height);
      }
      else {
        doneBtn.gotoAndStop('unchecked');
        img.filters = [grayscale];
        img.cache(0, 0, img.getBounds().width, img.getBounds().height);
      }
    }

    if (item.core) {
      core.x = margin + rect.getTransformedBounds().width - core.getBounds().width - 5;
      core.y = margin + 5;
    }

    var imgBounds = scaleToMax(img, rw, rh - (24 * 2));
    if (imgBounds) {
      img.x = margin + (rw / 2) - (imgBounds.width / 2);
      img.y = margin + (rh / 2) - (imgBounds.height / 2) - (24 / 2);

      title.lineWidth = rw - 20;
      title.textAlign = "center";
      title.x = margin + (rw / 2);
      title.y = img.y + imgBounds.height;
    }

    if (!item.core) {
      var idx = container.getChildIndex(delBtn);
      if (world.deletable) {
        if (idx === -1) container.addChild(delBtn);
      }
      else {
        if (idx !== -1) container.removeChildAt(idx);
      }
    }

    var coords = world.gridToPixel(item.x, item.y);
    container.x = coords.x;
    container.y = coords.y;
  };

  return container;
}

module.exports = Backbone.View.extend({
  initialize: function (opts) {
    if (!(opts && opts.canvas && opts.columns && opts.requirements))
      throw new Error('You must specify canvas, columns, and requirements options');

    var self = this;
    ["columnCount", "mode", "showProgress"].forEach(function (prop) {
      Object.defineProperty(self, prop, {
        get: function () { return world[prop]; },
        set: function (val) { world[prop] = val; }
      });
    });
    world.canvasWidth = opts.canvas.width;
    world.columnCount = opts.columns;

    _.extend(this, opts);

    this.stage = new createjs.Stage(this.canvas);
    createjs.Touch.enable(this.stage, true, true);
    if (this.isRearrangeable()) {
      var pollRate = 20;
      this.stage.enableMouseOver(pollRate);
      $(this.stage.canvas).addClass('edit-mode');
    }

    var gridLayer = this.gridLayer = new createjs.Container();
    this.stage.addChild(this.gridLayer);

    this.maxRow = 0;
    this.requirements.forEach(function (req) {
      this.addBadge(req);
    }.bind(this));

    this.listenTo(this.requirements, "add", this.addBadge, this);

    this.listenTo(this.requirements, "remove", function (req) {
      if (req.cid) {
        var item = this.stage.getChildByName(req.cid);
        this.stage.removeChild(item);
        this.refresh();
      }
    });

    var editor = this;
    gridLayer.layout = function () {
      this.removeAllChildren();
      for (var row = 0; row <= editor.maxRow + 2; row++) {
        for (var col = 0; col < world.columnCount; col++) {
          var coords = world.gridToPixel(col, row);
          var hex = new createjs.Shape();
          hex.graphics.beginFill("#CCC")
            .drawPolyStar(0, 0, 15, 6, 0, 30);
          hex.x = coords.cx;
          hex.y = coords.cy;
          hex.setBounds(-coords.w/2, -coords.w/2, coords.w, coords.w);
          this.addChild(hex);
        }
      }
    };

    this.layout();
  },
  addBadge: function (model) {
    if (model.y === undefined) {
      var row = 0;
      while (model.y === undefined) {
        var xs = _.pluck(this.requirements.where({y: row}), 'x');
        var empties = _.difference(_.range(world.columnCount), xs);
        if (empties.length) {
          model.set({
            x: empties.shift(),
            y: row
          });
          model.trigger('positioned', model);
        }
        else {
          row++;
        }
      }
    }
    this.maxRow = Math.max(this.maxRow, model.y);
    var item = makePathwayItem(model);

    if (model.newFlag) {
      var flag = newFlag.clone();
      flag.x = item.getBounds().width - flag.getBounds().width;
      flag.y = 0;
      flag.name = 'newFlag';
      item.addChild(flag);

      item.on('mousedown', function () {
        item.model.newFlag = false;
        item.removeChild(item.getChildByName('newFlag'));
        this.refresh();
      }, this);
    }

    item.on('ready', function () {
      this.stage.addChild(item);
      this.refresh(item);
    }, this);

    item.on('toggleComplete', function () {
      if (this.isRearrangeable()) {
        item.model.complete = !item.model.complete;
        this.refresh(item);
      }
    }, this);

    item.on('button-rollover', function () {
      $(this.stage.canvas).addClass('cursor-button');
    }, this);
    item.on('button-rollout', function () {
      $(this.stage.canvas).removeClass('cursor-button');
    }, this);
    item.on('delete', function (evt) {
      this.trigger('delete', evt.target.model);
    }, this);

    item.on('grab-rollover', function () {
      if (this.isRearrangeable()) $(this.stage.canvas).addClass('cursor-grab');
    }, this);
    item.on('grab', function () {
      if (this.isRearrangeable()) $(this.stage.canvas).addClass('cursor-grabbing');
    }, this);
    item.on('release', function () {
      if (this.isRearrangeable()) $(this.stage.canvas).removeClass('cursor-grabbing');
    }, this);
    item.on('grab-rollout', function () {
      if (this.isRearrangeable()) $(this.stage.canvas).removeClass('cursor-grab');
    }, this);

    item.on('move', function (evt) {
      if (this.isRearrangeable()) {
        var coords = world.pixelToGrid(this.stage.globalToLocal(evt.stageX, evt.stageY));
        var model = item.model.set({
          x: coords.x,
          y: coords.y
        });
        evt.nativeEvent.preventDefault();
        if (model.changedAttributes())
          this.refresh(item);
      }
    }, this);

    item.on('tile-dblclick', function (evt) {
      this.trigger('click', evt.currentTarget.model);
    }, this);
  },
  isRearrangeable: function () {
    return world.mode && (world.mode === 'edit');
  },
  isDeletable: function () {
    return world.deletable;
  },
  layout: function (item) {
    world.canvasWidth = this.stage.canvas.width;
    this.stage.children.forEach(function (child) {
      if (child.layout) child.layout();
    });
  },
  render: function (item) {
    if (!item)
      this.stage.canvas.height = this.stage.getTransformedBounds().height;
    this.stage.update();
    return this;
  },
  refresh: function (item) {
    if (item)
      item.layout();
    else
      this.layout();
    this.render(item);
  },
  remove: function () {
    createjs.Touch.disable(this.stage);
    this.$el.remove();
    this.stopListening();
    return this;
  },
  enableDelete: function (flag) {
    world.deletable = flag;
  }
});
<|endoftext|>


<commit_before>HTMLHint.addRule({
    id: 'radio-group-acceessibility',
    description: 'group of radio buttons should have role of radiogroup and aria-labelledby attribute. every radio button should have radio role',
    init: function(parser, reporter){
        var isRadiogroupContainer = function(event) {
            var classNames = HTMLHint.utils.getAttributeValue(event.attrs,"class");
            var classesArray = classNames.split(/\s+/g);
            return classesArray.some( className => className.toLowerCase() ==="radio");
        };

        var isRadioInput = function(event){
            return HTMLHint.utils.getAttributeValue(event.attrs,"type")  === 'radio';
        };

        var isRadioBindWithoutRaiogroupAccessibility = function(event){
            var bindAttributeValue = HTMLHint.utils.getAttributeValue(event.attrs,"data-bind");
            return isRadioInput(event) && (!bindAttributeValue.includes('radioGroupAccessibility'));
        };

        var prevEvent;
        var self = this;

        var isRadioContainerWithoutAriaAttribute = function(event){
            var ariaLabelAttribute = HTMLHint.utils.getAttributeValue(event.attrs,"aria-labelledby");
            return isRadiogroupContainer(event) && ariaLabelAttribute === '';
        };
        var isRadioContainerWithoutRoleAttribute = function(event){
            var roleAttribute = HTMLHint.utils.getAttributeValue(event.attrs,"role");
            return isRadiogroupContainer(event) && roleAttribute !== 'radiogroup';
        };
        var isRadioNotWrappedWithDiv = function(event, prevEvent){
            return isRadioInput(event) && prevEvent.tagName.toLowerCase() !== 'div';
        };

        parser.addListener('tagstart', function(event){
            if (isRadioContainerWithoutRoleAttribute(event))
            {
               reporter.error('radiogroup container should have role attribute with radiogroup value' + event.line , event.line, event.col, self, event.raw);
            }
            if (isRadioContainerWithoutAriaAttribute(event))
            {
               reporter.error('radiogroup container should have aria-labelledby attribute with value' + event.line , event.line, event.col, self, event.raw);
            }
            if(isRadioNotWrappedWithDiv(event, prevEvent)){
               reporter.error('radio input should be wrap with div element' + event.line , event.line, event.col, self, event.raw);
            }
            if(isRadioBindWithoutRaiogroupAccessibility(event)){
                reporter.error('radio input should have radioGroupAccessibility binding' + event.line , event.line, event.col, self, event.raw);
            }
            prevEvent = event;
        });
    }
});
<commit_msg>change radioGroupContainer class<commit_after>HTMLHint.addRule({
    id: 'radio-group-acceessibility',
    description: 'group of radio buttons should have role of radiogroup and aria-labelledby attribute. every radio button should have radio role',
    init: function(parser, reporter){
        var isRadiogroupContainer = function(event) {
            var classNames = HTMLHint.utils.getAttributeValue(event.attrs,"class");
            var classesArray = classNames.split(/\s+/g);
            return classesArray.some( className => className.toLowerCase() ==="radiogroupContainer");
        };

        var isRadioInput = function(event){
            return HTMLHint.utils.getAttributeValue(event.attrs,"type")  === 'radio';
        };

        var isRadioBindWithoutRaiogroupAccessibility = function(event){
            var bindAttributeValue = HTMLHint.utils.getAttributeValue(event.attrs,"data-bind");
            return isRadioInput(event) && (!bindAttributeValue.includes('radioGroupAccessibility'));
        };

        var prevEvent;
        var self = this;

        var isRadioContainerWithoutAriaAttribute = function(event){
            var ariaLabelAttribute = HTMLHint.utils.getAttributeValue(event.attrs,"aria-labelledby");
            return isRadiogroupContainer(event) && ariaLabelAttribute === '';
        };
        var isRadioContainerWithoutRoleAttribute = function(event){
            var roleAttribute = HTMLHint.utils.getAttributeValue(event.attrs,"role");
            return isRadiogroupContainer(event) && roleAttribute !== 'radiogroup';
        };
        var isRadioNotWrappedWithDiv = function(event, prevEvent){
            return isRadioInput(event) && prevEvent.tagName.toLowerCase() !== 'div';
        };

        parser.addListener('tagstart', function(event){
            if (isRadioContainerWithoutRoleAttribute(event))
            {
               reporter.error('radiogroup container should have role attribute with radiogroup value' + event.line , event.line, event.col, self, event.raw);
            }
            if (isRadioContainerWithoutAriaAttribute(event))
            {
               reporter.error('radiogroup container should have aria-labelledby attribute with value' + event.line , event.line, event.col, self, event.raw);
            }
            if(isRadioNotWrappedWithDiv(event, prevEvent)){
               reporter.error('radio input should be wrap with div element' + event.line , event.line, event.col, self, event.raw);
            }
            if(isRadioBindWithoutRaiogroupAccessibility(event)){
                reporter.error('radio input should have radioGroupAccessibility binding' + event.line , event.line, event.col, self, event.raw);
            }
            prevEvent = event;
        });
    }
});
<|endoftext|>


<commit_before>import rule, { ruleName, messages } from "..";

testRule(rule, {
  ruleName,
  config: [true],
  syntax: "scss",

  accept: [
    {
      code: `
      a {
       background: rgb(27,224,63);
      }
    `,
      description: "An allowed global function"
    },
    {
      code: `
      a {
       background: rgba(27,224,63, 0);
      }
    `,
      description: "An allowed global function"
    },
    {
      code: `
      a {
       background: hsla(27,224,63, 0);
      }
    `,
      description: "An allowed global function"
    },
    {
      code: `
      a {
       background: hsa(27,224,63);
      }
    `,
      description: "An allowed global function"
    },
    {
      code: `
      @use "sass:color";
      a {
       background: color.red(#6b717f);
      }
    `,
      description: "Red function"
    },
    {
      code: `
      @use "sass:color";
      a {
       background: color.blue(#6b717f);
      }
    `,
      description: "Blue function"
    },
    {
      code: `
      @use "sass:color";
      a {
       background: color.green(#6b717f);
      }
    `,
      description: "color.green function"
    },
    {
      code: `
      @use "sass:color";
      a {
       background: color.mix(#6b717f, #6b717f);
      }
    `,
      description: "color.mix"
    },
    {
      code: `
      @use "sass:color";
      a {
       background: color.hue(#6b717f);
      }
    `,
      description: "color.hue"
    },
    {
      code: `
      @use "sass:color";
      a {
       background: color.saturation(#6b717f);
      }
    `,
      description: "color.saturation"
    },
    {
      code: `
      @use "sass:color";
      a {
       background: color.lightness(#6b717f);
      }
    `,
      description: "color.lightness"
    },
    {
      code: `
      @use "sass:color";
      @debug color.alpha(#e1d7d2)`,
      description: "color.alpha"
    },
    {
      code: `
      @use "sass:color";
      a {
       background: color.adjust(#6b717f, $red: 15);
      }
    `,
      description: "color.adjust (not adjust-color)"
    },
    {
      code: `
      @use "sass:color";
      a {
       background: color.scale(#6b717f, $red: 15);
      }
    `,
      description: "color.scale (not scale-color)"
    },
    {
      code: `
      @use "sass:color";
      a {
       background: color.ie-hex-str(#6b717f);
      }
    `,
      description: "color.ie-hex-str"
    },
    {
      code: `
      @use "sass:map";
      a {b: map.get((), 1)}
    `,
      description: "map.get()"
    },
    {
      code: `
      @use "sass:map";
      $font-weights: ("regular": 400, "medium": 500, "bold": 700)
      @debug map.has-key($font-weights, "regular")
    `,
      description: "map.has-key"
    },
    {
      code: `
      @use "sass:map";
      $light-weights: ("lightest": 100, "light": 300);
      $heavy-weights: ("medium": 500, "bold": 700);

      @debug map.merge($light-weights, $heavy-weights)
    `,
      description: "map.merge"
    },
    {
      code: `
      @use "sass:map";
      $font-weights: ("regular": 400, "medium": 500, "bold": 700)

      @debug map.remove($font-weights, "regular")
    `,
      description: "map.remove"
    },
    {
      code: `
      @use "sass:map";
      $font-weights: ("regular": 400, "medium": 500, "bold": 700)

      @debug map.keys($font-weights)
    `,
      description: "map.keys"
    },
    {
      code: `
      @use "sass:map";
      $font-weights: ("regular": 400, "medium": 500, "bold": 700)

      @debug map.values($font-weights)
    `,
      description: "map.values"
    }
  ],

  reject: [
    {
      code: `
      a {
        background: red(#6b717f);
      }
    `,
      line: 3,
      message: messages.rejected("red"),
      description: "red"
    },
    {
      code: `
      a {
        background: blue(#6b717f);
      }
    `,
      line: 3,
      message: messages.rejected("blue"),
      description: "blue"
    },
    {
      code: `
      a {
        background: green(#6b717f);
      }
    `,
      line: 3,
      message: messages.rejected("green"),
      description: "green"
    },
    {
      code: `
      a {
        background: mix(#6b717f, #6b717f);
      }
    `,
      line: 3,
      message: messages.rejected("mix"),
      description: "mix"
    },
    {
      code: `
      a {
        background: hue(#6b717f);
      }
    `,
      line: 3,
      message: messages.rejected("hue"),
      description: "hue"
    },
    {
      code: `
      a {
        background: saturation(#6b717f);
      }
    `,
      line: 3,
      message: messages.rejected("saturation"),
      description: "saturation"
    },
    {
      code: `
      a {
        background: lightness(#6b717f);
      }
    `,
      line: 3,
      message: messages.rejected("lightness"),
      description: "lightness"
    },
    {
      code: `
      a {
        background: alpha(#6b717f);
      }
    `,
      line: 3,
      message: messages.rejected("alpha"),
      description: "alpha"
    },
    {
      code: `
      a {
        background: adjust-color(#6b717f, $red: 15);
      }
    `,
      line: 3,
      message: messages.rejected("adjust-color"),
      description: "adjust-color"
    },
    {
      code: `
      a {
        background: scale-color(#6b717f, $red: 15);
      }
    `,
      line: 3,
      message: messages.rejected("scale-color"),
      description: "scale-color"
    },
    {
      code: `
      a {
        background: change-color(#6b717f, $red: 15);
      }
    `,
      line: 3,
      message: messages.rejected("change-color"),
      description: "change-color"
    },
    {
      code: `
      a {
        background: ie-hex-str(#6b717f);
      }
    `,
      line: 3,
      message: messages.rejected("ie-hex-str"),
      description: "ie-hex-str"
    },
    {
      code: `
      a {b: map-get((), 1)}
    `,
      line: 2,
      message: messages.rejected("map-get"),
      description: "map-get"
    },
    {
      code: `
      $font-weights: ("regular": 400, "medium": 500, "bold": 700)
      @debug map-has-key($font-weights, "regular")
    `,
      line: 2,
      message: messages.rejected("map-has-key"),
      description: "map-has-key"
    },
    {
      code: `
      $font-weights: ("regular": 400, "medium": 500, "bold": 700)
      @debug map-remove($font-weights, "regular")
    `,
      line: 2,
      message: messages.rejected("map-remove"),
      description: "map-remove"
    },
    {
      code: `
      $font-weights: ("regular": 400, "medium": 500, "bold": 700)
      @debug map-keys($font-weights)
    `,
      line: 2,
      message: messages.rejected("map-keys"),
      description: "map-keys"
    },
    {
      code: `
      $font-weights: ("regular": 400, "medium": 500, "bold": 700)
      @debug map-values($font-weights)
    `,
      line: 2,
      message: messages.rejected("map-values"),
      description: "map-values"
    }
  ]
});
<commit_msg>commit<commit_after>import rule, { ruleName, messages } from "..";

testRule(rule, {
  ruleName,
  config: [true],
  syntax: "scss",

  accept: [
    {
      code: `
      a {
       background: rgb(27,224,63);
      }
    `,
      description: "An allowed global function"
    },
    {
      code: `
      a {
       background: rgba(27,224,63, 0);
      }
    `,
      description: "An allowed global function"
    },
    {
      code: `
      a {
       background: hsla(27,224,63, 0);
      }
    `,
      description: "An allowed global function"
    },
    {
      code: `
      a {
       background: hsa(27,224,63);
      }
    `,
      description: "An allowed global function"
    },
    {
      code: `
      @use "sass:color";
      a {
       background: color.red(#6b717f);
      }
    `,
      description: "Red function"
    },
    {
      code: `
      @use "sass:color";
      a {
       background: color.blue(#6b717f);
      }
    `,
      description: "Blue function"
    },
    {
      code: `
      @use "sass:color";
      a {
       background: color.green(#6b717f);
      }
    `,
      description: "color.green function"
    },
    {
      code: `
      @use "sass:color";
      a {
       background: color.mix(#6b717f, #6b717f);
      }
    `,
      description: "color.mix"
    },
    {
      code: `
      @use "sass:color";
      a {
       background: color.hue(#6b717f);
      }
    `,
      description: "color.hue"
    },
    {
      code: `
      @use "sass:color";
      a {
       background: color.saturation(#6b717f);
      }
    `,
      description: "color.saturation"
    },
    {
      code: `
      @use "sass:color";
      a {
       background: color.lightness(#6b717f);
      }
    `,
      description: "color.lightness"
    },
    {
      code: `
      @use "sass:color";
      @debug color.alpha(#e1d7d2)`,
      description: "color.alpha"
    },
    {
      code: `
      @use "sass:color";
      a {
       background: color.adjust(#6b717f, $red: 15);
      }
    `,
      description: "color.adjust (not adjust-color)"
    },
    {
      code: `
      @use "sass:color";
      a {
       background: color.scale(#6b717f, $red: 15);
      }
    `,
      description: "color.scale (not scale-color)"
    },
    {
      code: `
      @use "sass:color";
      a {
       background: color.ie-hex-str(#6b717f);
      }
    `,
      description: "color.ie-hex-str"
    },
    {
      code: `
      @use "sass:map";
      a {b: map.get((), 1)}
    `,
      description: "map.get()"
    },
    {
      code: `
      @use "sass:map";
      $font-weights: ("regular": 400, "medium": 500, "bold": 700)
      @debug map.has-key($font-weights, "regular")
    `,
      description: "map.has-key"
    },
    {
      code: `
      @use "sass:map";
      $light-weights: ("lightest": 100, "light": 300);
      $heavy-weights: ("medium": 500, "bold": 700);

      @debug map.merge($light-weights, $heavy-weights)
    `,
      description: "map.merge"
    },
    {
      code: `
      @use "sass:map";
      $font-weights: ("regular": 400, "medium": 500, "bold": 700)

      @debug map.remove($font-weights, "regular")
    `,
      description: "map.remove"
    },
    {
      code: `
      @use "sass:map";
      $font-weights: ("regular": 400, "medium": 500, "bold": 700)

      @debug map.keys($font-weights)
    `,
      description: "map.keys"
    },
    {
      code: `
      @use "sass:map";
      $font-weights: ("regular": 400, "medium": 500, "bold": 700)

      @debug map.values($font-weights)
    `,
      description: "map.values"
    },
    {
      code: `
      @use "sass:string"
      @debug string.quote(Helvetica);
      `,
      description: "string.quote"
    },
    {
      code: `
      @use "sass:string"
      @debug string.unquote("Helvetica");
      `,
      description: "string.unquote"
    },
    {
      code: `
      @use "sass:string"
      @debug string.length("Helvetica");
      `,
      description: "string.length"
    },
    {
      code: `
      @use "sass:string"
      @debug string.insert("Roboto Bold", " Mono", 7);
      `,
      description: "string.insert"
    },
    {
      code: `
      @use "sass:string"
      @debug string.slice("Helvetica Neue", 11);
      `,
      description: "string.slice"
    },
    {
      code: `
      @use "sass:string"
      @debug string.index("Helvetica Neue", "Helvetica");
      `,
      description: "string.index"
    },
    {
      code: `
      @use "sass:string"
      @debug string.to-upper-case("Bold");
      `,
      description: "string.to-upper-case"
    },
    {
      code: `
      @use "sass:string"
      @debug string.to-lower-case("Bold");
      `,
      description: "string.to-lower-case"
    },
    {
      code: `
      @use "sass:string"
      @debug string.unique-id()
      `,
      description: "string.unique-id"
    }
  ],

  reject: [
    {
      code: `
      a {
        background: red(#6b717f);
      }
    `,
      line: 3,
      message: messages.rejected("red"),
      description: "red"
    },
    {
      code: `
      a {
        background: blue(#6b717f);
      }
    `,
      line: 3,
      message: messages.rejected("blue"),
      description: "blue"
    },
    {
      code: `
      a {
        background: green(#6b717f);
      }
    `,
      line: 3,
      message: messages.rejected("green"),
      description: "green"
    },
    {
      code: `
      a {
        background: mix(#6b717f, #6b717f);
      }
    `,
      line: 3,
      message: messages.rejected("mix"),
      description: "mix"
    },
    {
      code: `
      a {
        background: hue(#6b717f);
      }
    `,
      line: 3,
      message: messages.rejected("hue"),
      description: "hue"
    },
    {
      code: `
      a {
        background: saturation(#6b717f);
      }
    `,
      line: 3,
      message: messages.rejected("saturation"),
      description: "saturation"
    },
    {
      code: `
      a {
        background: lightness(#6b717f);
      }
    `,
      line: 3,
      message: messages.rejected("lightness"),
      description: "lightness"
    },
    {
      code: `
      a {
        background: alpha(#6b717f);
      }
    `,
      line: 3,
      message: messages.rejected("alpha"),
      description: "alpha"
    },
    {
      code: `
      a {
        background: adjust-color(#6b717f, $red: 15);
      }
    `,
      line: 3,
      message: messages.rejected("adjust-color"),
      description: "adjust-color"
    },
    {
      code: `
      a {
        background: scale-color(#6b717f, $red: 15);
      }
    `,
      line: 3,
      message: messages.rejected("scale-color"),
      description: "scale-color"
    },
    {
      code: `
      a {
        background: change-color(#6b717f, $red: 15);
      }
    `,
      line: 3,
      message: messages.rejected("change-color"),
      description: "change-color"
    },
    {
      code: `
      a {
        background: ie-hex-str(#6b717f);
      }
    `,
      line: 3,
      message: messages.rejected("ie-hex-str"),
      description: "ie-hex-str"
    },
    {
      code: `
      a {b: map-get((), 1)}
    `,
      line: 2,
      message: messages.rejected("map-get"),
      description: "map-get"
    },
    {
      code: `
      $font-weights: ("regular": 400, "medium": 500, "bold": 700)
      @debug map-has-key($font-weights, "regular")
    `,
      line: 2,
      message: messages.rejected("map-has-key"),
      description: "map-has-key"
    },
    {
      code: `
      $font-weights: ("regular": 400, "medium": 500, "bold": 700)
      @debug map-remove($font-weights, "regular")
    `,
      line: 2,
      message: messages.rejected("map-remove"),
      description: "map-remove"
    },
    {
      code: `
      $font-weights: ("regular": 400, "medium": 500, "bold": 700)
      @debug map-keys($font-weights)
    `,
      line: 2,
      message: messages.rejected("map-keys"),
      description: "map-keys"
    },
    {
      code: `
      $font-weights: ("regular": 400, "medium": 500, "bold": 700)
      @debug map-values($font-weights)
    `,
      line: 2,
      message: messages.rejected("map-values"),
      description: "map-values"
    },
    {
      code: `
      @debug unquote("Helvetica");
      `,
      line: 2,
      message: messages.rejected("unquote"),
      description: "unquote"
    },
    {
      code: `
      @debug str-length("Helvetica");
      `,
      line: 2,
      message: messages.rejected("str-length"),
      description: "str-length"
    },
    {
      code: `
      @debug str-insert("Roboto Bold", " Mono", 7);
      `,
      line: 2,
      message: messages.rejected("str-insert"),
      description: "str-insert"
    },
    {
      code: `
      @debug str-slice("Helvetica Neue", 11);
      `,
      line: 2,
      message: messages.rejected("str-slice"),
      description: "str-slice"
    },
    {
      code: `
      @debug str-index("Helvetica Neue", "Helvetica");
      `,
      line: 2,
      message: messages.rejected("str-index"),
      description: "str-index"
    },
    {
      code: `
      @debug to-upper-case("Bold");
      `,
      line: 2,
      message: messages.rejected("to-upper-case"),
      description: "to-upper-case"
    },
    {
      code: `
      @debug to-lower-case("Bold");
      `,
      line: 2,
      message: messages.rejected("to-lower-case"),
      description: "to-lower-case"
    },
    {
      code: `
      @debug unique-id();
      `,
      line: 2,
      message: messages.rejected("unique-id"),
      description: "unique-id"
    }
  ]
});
<|endoftext|>


<commit_before>const { BadRequest } = require('@feathersjs/errors');
const request = require('request-promise-native');

const { getRequestOptions, makeStringRCConform } = require('../helpers');
const { randomSuffix } = require('../randomPass');
const { channelModel } = require('../model');
const docs = require('../docs');
const logger = require('../../../logger');

/**
 * Service that maps schulcloud teams to rocketChat groups.
 *
 * Other services should only get groups by the id of the corresponding team,
 * creation and deletion of RC groups is handled automatically by the service.
 */
class RocketChatChannel {
	constructor(options) {
		this.options = options || {};
		this.docs = docs;
	}

	generateChannelName(team) {
		// toDo: implementation with bound execution time.
		const channelName = makeStringRCConform(`${team.name}.${randomSuffix()}`);
		// toDo: check availibility in rocketChat as well.
		return channelModel.findOne({ channelName })
			.then((result) => {
				if (!result) {
					return Promise.resolve(channelName);
				} return this.generateChannelName(team);
			});
	}

	createChannel(teamId, name) {
		if (teamId === undefined) { throw new BadRequest('Missing data value.'); }

		let currentTeam;
		const internalParams = {
			query: { $populate: 'schoolId' },
		};
		return this.app.service('teams').get(teamId, internalParams)
			.then((team) => {
				currentTeam = team;
				const userNamePromises = currentTeam.userIds.map(user => this.app.service('rocketChat/user')
					.get(user.userId)
					.catch(() => Promise.resolve));
				return Promise.all(userNamePromises).then(async (users) => {
					const userNames = [];
					users.forEach((user) => {
						if (user.username) userNames.push(user.username);
					});
					const channelName = name || await this.generateChannelName(currentTeam);
					const body = {
						name: channelName,
						members: userNames,
					};
					return request(getRequestOptions('/api/v1/groups.create', body, true))
						.then((res) => {
							if (res.success === true) return res;
							return Promise.reject(new BadRequest('bad answer on group creation'));
						});
				});
			}).then((result) => {
				if (name) {
					// channel already exists in database
					return channelModel.findOne({ teamId }).lean().exec();
				}
				const channelData = {
					teamId: currentTeam._id,
					channelName: result.group.name,
				};
				return channelModel.create(channelData);
			})
			.catch((err) => {
				logger.warning(new BadRequest('Can not create RocketChat Channel', err));
				throw new BadRequest('Can not create RocketChat Channel', err);
			});
	}

	async handleChannelMissingRcSide(err, channel, teamId) {
		const rcError = err.error.errorType;
		if (rcError === 'error-room-not-found') {
			await this.createChannel(teamId, channel.channelName);
			return request(getRequestOptions(
				`/api/v1/groups.members?roomName=${channel.channelName}`, {}, true, {}, 'GET',
			));
		}
		throw err;
	}

	async ensureRcChannel(channel, params, teamId) {
		const rcAccount = await this.app.service('/rocketChat/user').get(params.account.userId);
		const rcChannelMembers = await request(getRequestOptions(
			`/api/v1/groups.members?roomName=${channel.channelName}`, {}, true, {}, 'GET',
		)).catch((err) => this.handleChannelMissingRcSide(err, channel, teamId));

		const userInChannel = !!(rcChannelMembers.find((e) => e._id === rcAccount.rcId));
		if (!userInChannel) {
			const body = {
				roomName: channel.channelName,
				userId: rcAccount.rcId,
			};
			await request(getRequestOptions('/api/v1/groups.invite', body, true));
		}
		return Promise.resolve();
	}

	async getOrCreateRocketChatChannel(teamId, params) {
		try {
			let channel = await channelModel.findOne({ teamId });
			if (!channel) {
				channel = await this.createChannel(teamId)
					.then(() => channelModel.findOne({ teamId }));
			}
			// check that channel exists in rocketchat, and user is part of it.
			await this.ensureRcChannel(channel, params, teamId);
			return {
				teamId: channel.teamId,
				channelName: channel.channelName,
			};
		} catch (err) {
			logger.warning(new BadRequest('error initializing the rocketchat channel', err));
			return new BadRequest('error initializing the rocketchat channel', err);
		}
	}

	async addUsersToChannel(userIds, teamId) {
		const rcUserNames = await this.app.service('/rocketChat/user').find({ userIds });
		const channel = await this.app.service('/rocketChat/channel').get(teamId);

		const invitationPromises = rcUserNames.map((userName) => {
			const body = {
				roomName: channel.channelName,
				username: userName,
			};
			return request(getRequestOptions('/api/v1/groups.invite', body, true))
				.catch((err) => {
					logger.warning(new BadRequest('addUsersToChannel', err));
				});
		});
		return Promise.all(invitationPromises);
	}

	async removeUsersFromChannel(userIds, teamId) {
		const rcUserNames = await this.app.service('/rocketChat/user').find({ userIds });
		const channel = await this.app.service('/rocketChat/channel').get(teamId);

		const kickPromises = rcUserNames.map((userName) => {
			const body = {
				roomName: channel.channelName,
				username: userName,
			};
			return request(getRequestOptions('/api/v1/groups.kick', body, true))
				.catch((err) => {
					logger.warning(new BadRequest('removeUsersFromChannel', err));
				});
		});
		return Promise.all(kickPromises);
	}

	/**
     * removes the channel belonging to the team given by Id
     * @param {*} teamId Id of a team in the schulcloud
     */
	static deleteChannel(teamId) {
		return channelModel.findOne({ teamId })
			.then(async (channel) => {
				if (channel) {
					await request(getRequestOptions('/api/v1/groups.delete', { roomName: channel.channelName }, true));
					await channelModel.deleteOne({ _id: channel._id });
				}
				return Promise.resolve();
			})
			.catch((err) => {
				logger.warning(new BadRequest('deleteChannel', err));
			});
	}

	static async archiveChannel(teamId) {
		const channel = await channelModel.findOne({ teamId });
		if (channel) {
			await request(getRequestOptions('/api/v1/groups.archive', { roomName: channel.channelName }, true));
		}
		return Promise.resolve();
	}

	static async unarchiveChannel(teamId) {
		const channel = await channelModel.findOne({ teamId });
		if (channel) {
			await request(getRequestOptions('/api/v1/groups.unarchive', { roomName: channel.channelName }, true));
		}
		return Promise.resolve();
	}

	async synchronizeModerators(teamId) {
		try {
			const team = await this.app.service('teams').get(teamId);
			const channel = await channelModel.findOne({ teamId });
			if (channel) {
				const rcResponse = await request(getRequestOptions(
					`/api/v1/groups.moderators?roomName=${channel.channelName}`,
					{},
					true,
					undefined,
					'GET',
				));
				let rcChannelModerators = rcResponse.moderators;
				const scModeratorPromises = [];
				team.userIds.forEach(async (user) => {
					if (this.teamModeratorRoles.includes(user.role.toString())) {
						scModeratorPromises.push(this.app.service('rocketChat/user').get(user.userId));
					}
				});
				let scModerators = await Promise.all(scModeratorPromises);
				rcChannelModerators = rcChannelModerators.map(mod => mod._id);
				scModerators = scModerators.map(mod => mod.rcId);
				const moderatorsToAdd = scModerators.filter(x => !rcChannelModerators.includes(x));
				const moderatorsToRemove = rcChannelModerators.filter(x => !scModerators.includes(x));
				moderatorsToAdd.forEach(x => request(getRequestOptions(
					'/api/v1/groups.addModerator', { roomName: channel.channelName, userId: x }, true,
				)));
				moderatorsToRemove.forEach(x => request(getRequestOptions(
					'/api/v1/groups.removeModerator', { roomName: channel.channelName, userId: x }, true,
				)));
			}
			return Promise.resolve();
		} catch (err) {
			logger.warn(`Fehler beim Synchronisieren der rocket.chat moderatoren für team ${teamId} `, err);
			return Promise.reject(err);
		}
	}

	/**
     * returns an existing or new rocketChat channel for a given Team ID
     * @param {*} teamId Id of a Team in the schulcloud
     * @param {*} params
     */
	async get(teamId, params) {
		const result = await this.getOrCreateRocketChatChannel(teamId, params);
		this.synchronizeModerators(teamId);
		return result;
	}

	async onTeamPatched(result) {
		if (result.features.includes('rocketChat')) {
			await RocketChatChannel.unarchiveChannel(result._id);
			await this.synchronizeModerators(result._id);
		} else {
			RocketChatChannel.archiveChannel(result._id);
		}
	}

	/**
     * React to event published by the Team service when users are added or
     * removed to a team.
     * @param {Object} context event context given by the Team service
     */
	onTeamUsersChanged(context) {
		const { team } = ((context || {}).additionalInfosTeam || {});
		let additionalUsers = (((context || {}).additionalInfosTeam || {}).changes || {}).add;
		let removedUsers = (((context || {}).additionalInfosTeam || {}).changes || {}).remove;

		additionalUsers = additionalUsers.map(user => user.userId);
		removedUsers = removedUsers.map(user => user.userId);

		if (additionalUsers.length > 0) this.addUsersToChannel(additionalUsers, team._id);
		if (removedUsers.length > 0) this.removeUsersFromChannel(removedUsers, team._id);
	}

	/**
     * react to a team being deleted
     * @param {*} context
     */
	static onRemoved(context) {
		RocketChatChannel.deleteChannel(context._id);
	}

	/**
     * Register methods of the service to listen to events of other services
     * @listens teams:after:usersChanged
     * @listens teams:removed
     */
	registerEventListeners() {
		this.app.on('teams:after:usersChanged', this.onTeamUsersChanged.bind(this)); // use hook to get app
		this.app.service('teams').on('removed', RocketChatChannel.onRemoved.bind(this));
		this.app.service('teams').on('patched', this.onTeamPatched.bind(this));
	}

	setup(app) {
		this.app = app;
		this.registerEventListeners();
		return app.service('roles').find({
			query: { name: { $in: ['teamowner', 'teamadministrator'] } },
		}).then((teamModeratorRoles) => {
			this.teamModeratorRoles = teamModeratorRoles.data.map(role => role._id.toString());
			return Promise.resolve();
		});
	}
}

module.exports = RocketChatChannel;
<commit_msg>make inline comment a little clearer<commit_after>const { BadRequest } = require('@feathersjs/errors');
const request = require('request-promise-native');

const { getRequestOptions, makeStringRCConform } = require('../helpers');
const { randomSuffix } = require('../randomPass');
const { channelModel } = require('../model');
const docs = require('../docs');
const logger = require('../../../logger');

/**
 * Service that maps schulcloud teams to rocketChat groups.
 *
 * Other services should only get groups by the id of the corresponding team,
 * creation and deletion of RC groups is handled automatically by the service.
 */
class RocketChatChannel {
	constructor(options) {
		this.options = options || {};
		this.docs = docs;
	}

	generateChannelName(team) {
		// toDo: implementation with bound execution time.
		const channelName = makeStringRCConform(`${team.name}.${randomSuffix()}`);
		// toDo: check availibility in rocketChat as well.
		return channelModel.findOne({ channelName })
			.then((result) => {
				if (!result) {
					return Promise.resolve(channelName);
				} return this.generateChannelName(team);
			});
	}

	createChannel(teamId, name) {
		if (teamId === undefined) { throw new BadRequest('Missing data value.'); }

		let currentTeam;
		const internalParams = {
			query: { $populate: 'schoolId' },
		};
		return this.app.service('teams').get(teamId, internalParams)
			.then((team) => {
				currentTeam = team;
				const userNamePromises = currentTeam.userIds.map(user => this.app.service('rocketChat/user')
					.get(user.userId)
					.catch(() => Promise.resolve));
				return Promise.all(userNamePromises).then(async (users) => {
					const userNames = [];
					users.forEach((user) => {
						if (user.username) userNames.push(user.username);
					});
					const channelName = name || await this.generateChannelName(currentTeam);
					const body = {
						name: channelName,
						members: userNames,
					};
					return request(getRequestOptions('/api/v1/groups.create', body, true))
						.then((res) => {
							if (res.success === true) return res;
							return Promise.reject(new BadRequest('bad answer on group creation'));
						});
				});
			}).then((result) => {
				if (name) {
					// channel already exists in database
					return channelModel.findOne({ teamId }).lean().exec();
				}
				const channelData = {
					teamId: currentTeam._id,
					channelName: result.group.name,
				};
				return channelModel.create(channelData);
			})
			.catch((err) => {
				logger.warning(new BadRequest('Can not create RocketChat Channel', err));
				throw new BadRequest('Can not create RocketChat Channel', err);
			});
	}

	async handleChannelMissingRcSide(err, channel, teamId) {
		const rcError = err.error.errorType;
		if (rcError === 'error-room-not-found') {
			await this.createChannel(teamId, channel.channelName);
			return request(getRequestOptions(
				`/api/v1/groups.members?roomName=${channel.channelName}`, {}, true, {}, 'GET',
			));
		}
		throw err;
	}

	async ensureRcChannel(channel, params, teamId) {
		const rcAccount = await this.app.service('/rocketChat/user').get(params.account.userId);
		const rcChannelMembers = await request(getRequestOptions(
			`/api/v1/groups.members?roomName=${channel.channelName}`, {}, true, {}, 'GET',
		)).catch((err) => this.handleChannelMissingRcSide(err, channel, teamId));

		const userInChannel = !!(rcChannelMembers.find((e) => e._id === rcAccount.rcId));
		if (!userInChannel) {
			const body = {
				roomName: channel.channelName,
				userId: rcAccount.rcId,
			};
			await request(getRequestOptions('/api/v1/groups.invite', body, true));
		}
		return Promise.resolve();
	}

	async getOrCreateRocketChatChannel(teamId, params) {
		try {
			let channel = await channelModel.findOne({ teamId });
			if (!channel) {
				channel = await this.createChannel(teamId)
					.then(() => channelModel.findOne({ teamId }));
			}
			// check that channel exists in rocketchat, and user is part of it.
			await this.ensureRcChannel(channel, params, teamId);
			return {
				teamId: channel.teamId,
				channelName: channel.channelName,
			};
		} catch (err) {
			logger.warning(new BadRequest('error initializing the rocketchat channel', err));
			return new BadRequest('error initializing the rocketchat channel', err);
		}
	}

	async addUsersToChannel(userIds, teamId) {
		const rcUserNames = await this.app.service('/rocketChat/user').find({ userIds });
		const channel = await this.app.service('/rocketChat/channel').get(teamId);

		const invitationPromises = rcUserNames.map((userName) => {
			const body = {
				roomName: channel.channelName,
				username: userName,
			};
			return request(getRequestOptions('/api/v1/groups.invite', body, true))
				.catch((err) => {
					logger.warning(new BadRequest('addUsersToChannel', err));
				});
		});
		return Promise.all(invitationPromises);
	}

	async removeUsersFromChannel(userIds, teamId) {
		const rcUserNames = await this.app.service('/rocketChat/user').find({ userIds });
		const channel = await this.app.service('/rocketChat/channel').get(teamId);

		const kickPromises = rcUserNames.map((userName) => {
			const body = {
				roomName: channel.channelName,
				username: userName,
			};
			return request(getRequestOptions('/api/v1/groups.kick', body, true))
				.catch((err) => {
					logger.warning(new BadRequest('removeUsersFromChannel', err));
				});
		});
		return Promise.all(kickPromises);
	}

	/**
     * removes the channel belonging to the team given by Id
     * @param {*} teamId Id of a team in the schulcloud
     */
	static deleteChannel(teamId) {
		return channelModel.findOne({ teamId })
			.then(async (channel) => {
				if (channel) {
					await request(getRequestOptions('/api/v1/groups.delete', { roomName: channel.channelName }, true));
					await channelModel.deleteOne({ _id: channel._id });
				}
				return Promise.resolve();
			})
			.catch((err) => {
				logger.warning(new BadRequest('deleteChannel', err));
			});
	}

	static async archiveChannel(teamId) {
		const channel = await channelModel.findOne({ teamId });
		if (channel) {
			await request(getRequestOptions('/api/v1/groups.archive', { roomName: channel.channelName }, true));
		}
		return Promise.resolve();
	}

	static async unarchiveChannel(teamId) {
		const channel = await channelModel.findOne({ teamId });
		if (channel) {
			await request(getRequestOptions('/api/v1/groups.unarchive', { roomName: channel.channelName }, true));
		}
		return Promise.resolve();
	}

	async synchronizeModerators(teamId) {
		try {
			const team = await this.app.service('teams').get(teamId);
			const channel = await channelModel.findOne({ teamId });
			if (channel) {
				const rcResponse = await request(getRequestOptions(
					`/api/v1/groups.moderators?roomName=${channel.channelName}`,
					{},
					true,
					undefined,
					'GET',
				));
				let rcChannelModerators = rcResponse.moderators;
				const scModeratorPromises = [];
				team.userIds.forEach(async (user) => {
					if (this.teamModeratorRoles.includes(user.role.toString())) {
						scModeratorPromises.push(this.app.service('rocketChat/user').get(user.userId));
					}
				});
				let scModerators = await Promise.all(scModeratorPromises);
				rcChannelModerators = rcChannelModerators.map(mod => mod._id);
				scModerators = scModerators.map(mod => mod.rcId);
				const moderatorsToAdd = scModerators.filter(x => !rcChannelModerators.includes(x));
				const moderatorsToRemove = rcChannelModerators.filter(x => !scModerators.includes(x));
				moderatorsToAdd.forEach(x => request(getRequestOptions(
					'/api/v1/groups.addModerator', { roomName: channel.channelName, userId: x }, true,
				)));
				moderatorsToRemove.forEach(x => request(getRequestOptions(
					'/api/v1/groups.removeModerator', { roomName: channel.channelName, userId: x }, true,
				)));
			}
			return Promise.resolve();
		} catch (err) {
			logger.warn(`Fehler beim Synchronisieren der rocket.chat moderatoren für team ${teamId} `, err);
			return Promise.reject(err);
		}
	}

	/**
     * returns an existing or new rocketChat channel for a given Team ID, and ensures validity of the channel first.
     * @param {*} teamId Id of a Team in the schulcloud
     * @param {*} params
     */
	async get(teamId, params) {
		const result = await this.getOrCreateRocketChatChannel(teamId, params);
		this.synchronizeModerators(teamId);
		return result;
	}

	async onTeamPatched(result) {
		if (result.features.includes('rocketChat')) {
			await RocketChatChannel.unarchiveChannel(result._id);
			await this.synchronizeModerators(result._id);
		} else {
			RocketChatChannel.archiveChannel(result._id);
		}
	}

	/**
     * React to event published by the Team service when users are added or
     * removed to a team.
     * @param {Object} context event context given by the Team service
     */
	onTeamUsersChanged(context) {
		const { team } = ((context || {}).additionalInfosTeam || {});
		let additionalUsers = (((context || {}).additionalInfosTeam || {}).changes || {}).add;
		let removedUsers = (((context || {}).additionalInfosTeam || {}).changes || {}).remove;

		additionalUsers = additionalUsers.map(user => user.userId);
		removedUsers = removedUsers.map(user => user.userId);

		if (additionalUsers.length > 0) this.addUsersToChannel(additionalUsers, team._id);
		if (removedUsers.length > 0) this.removeUsersFromChannel(removedUsers, team._id);
	}

	/**
     * react to a team being deleted
     * @param {*} context
     */
	static onRemoved(context) {
		RocketChatChannel.deleteChannel(context._id);
	}

	/**
     * Register methods of the service to listen to events of other services
     * @listens teams:after:usersChanged
     * @listens teams:removed
     */
	registerEventListeners() {
		this.app.on('teams:after:usersChanged', this.onTeamUsersChanged.bind(this)); // use hook to get app
		this.app.service('teams').on('removed', RocketChatChannel.onRemoved.bind(this));
		this.app.service('teams').on('patched', this.onTeamPatched.bind(this));
	}

	setup(app) {
		this.app = app;
		this.registerEventListeners();
		return app.service('roles').find({
			query: { name: { $in: ['teamowner', 'teamadministrator'] } },
		}).then((teamModeratorRoles) => {
			this.teamModeratorRoles = teamModeratorRoles.data.map(role => role._id.toString());
			return Promise.resolve();
		});
	}
}

module.exports = RocketChatChannel;
<|endoftext|>


<commit_before>// Copyright 2006 The Closure Library 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.

/**
 * @fileoverview Constant declarations for common key values.
 *
 */

goog.provide('goog.events.Keys');

/**
 * Key values for common characters.
 *
 * @see https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key
 * @enum {string}
 */
goog.events.Keys = {
  CTRL: 'Control',
  SHIFT: 'Shift',
  ALT: 'Meta',
  ALTGRAPH: 'AltGraph'
};
<commit_msg>Expand Keys enum to include: home, end, enter, and arrow keys. Also reorder enum entries alphabetically.<commit_after>// Copyright 2006 The Closure Library 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.

/**
 * @fileoverview Constant declarations for common key values.
 *
 */

goog.provide('goog.events.Keys');

/**
 * Key values for common characters.
 *
 * @see https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key
 * @enum {string}
 */
goog.events.Keys = {
  ALT: 'Meta',
  ALTGRAPH: 'AltGraph',
  CTRL: 'Control',
  DOWN: 'ArrowDown',
  END: 'End',
  ENTER: 'Enter',
  HOME: 'Home',
  LEFT: 'ArrowLeft',
  RIGHT: 'ArrowRight',
  SHIFT: 'Shift',
  UP: 'ArrowUp',
};
<|endoftext|>


<commit_before>import cleanObject from 'driveback-utils/cleanObject';
import { getProp } from 'driveback-utils/dotProp';
import {
  getEnrichableVariableMappingProps,
  extractVariableMappingValues,
} from '../IntegrationUtils';

import Integration from '../Integration';
import {
  VIEWED_PAGE,
  VIEWED_PRODUCT_DETAIL,
  VIEWED_PRODUCT_LISTING,
  ADDED_PRODUCT,
  REMOVED_PRODUCT,
  COMPLETED_TRANSACTION,
  SEARCHED_PRODUCTS,
} from '../events/semanticEvents';

const SEMANTIC_EVENTS = [
  VIEWED_PAGE,
  VIEWED_PRODUCT_DETAIL,
  VIEWED_PRODUCT_LISTING,
  ADDED_PRODUCT,
  REMOVED_PRODUCT,
  COMPLETED_TRANSACTION,
  SEARCHED_PRODUCTS,
];

class REES46 extends Integration {
  constructor(digitalData, options) {
    const optionsWithDefaults = Object.assign({
      storeKey: '',
      feedWithGroupedProducts: false,
      productVariables: {},
    }, options);

    super(digitalData, optionsWithDefaults);

    this.addTag({
      type: 'script',
      attr: {
        src: 'https://cdn.rees46.com/v3.js',
      },
    });
  }

  initialize() {
    window.r46 = window.r46 || function r46Init() {
      (window.r46.q = window.r46.q || []).push(arguments);
    };
    window.r46('init', this.getOption('storeKey'));
  }

  getSemanticEvents() {
    return SEMANTIC_EVENTS;
  }

  getEnrichableEventProps(event) {
    switch (event.name) {
      case VIEWED_PAGE: return ['user', 'website'];
      case VIEWED_PRODUCT_LISTING: return ['listing.categoryId'];
      case SEARCHED_PRODUCTS: return ['listing.query'];
      case COMPLETED_TRANSACTION: return ['transaction'];
      case VIEWED_PRODUCT_DETAIL:
        return [
          ...getEnrichableVariableMappingProps(this.getOption('productVariables')),
          'product',
        ];
      default: return [];
    }
  }

  getProductVariables(event) {
    const { product } = event;
    const mapping = this.getOption('productVariables');
    return extractVariableMappingValues(
      { event, product },
      mapping,
      { multipleScopes: true },
    );
  }

  trackEvent(event) {
    const eventMap = {
      [VIEWED_PAGE]: this.onViewedPage.bind(this),
      [VIEWED_PRODUCT_DETAIL]: this.onViewedProductDetail.bind(this),
      [VIEWED_PRODUCT_LISTING]: this.onViewedProductListing.bind(this),
      [SEARCHED_PRODUCTS]: this.onSearchedProducts.bind(this),
      [ADDED_PRODUCT]: this.onAddedProduct.bind(this),
      [REMOVED_PRODUCT]: this.onRemovedProduct.bind(this),
      [COMPLETED_TRANSACTION]: this.onCompletedTransaction.bind(this),
    };
    if (eventMap[event.name]) {
      eventMap[event.name](event);
    }
  }

  onViewedPage(event) {
    const { user } = event;
    if (!user || !user.userId || !user.email) return;
    const gender = String(user.gender).toLowerCase();

    const rees46Data = cleanObject({
      id: user.userId,
      email: user.email,
      gender: ['m', 'f'].indexOf(gender) >= 0 ? gender : undefined,
      birthday: user.birthDate,
      location: getProp(event, 'website.regionId'),
    });

    window.r46('profile', 'set', rees46Data);
  }

  onViewedProductDetail(event) {
    const product = event.product || {};
    const feedWithGroupedProducts = this.getOption('feedWithGroupedProducts');
    const productId = (feedWithGroupedProducts) ? product.skuCode : product.id;
    if (!productId) return;

    const productVariables = this.getProductVariables(event);
    const { stock } = product;
    if (stock !== undefined) {
      productVariables.stock = (stock > 0);
    }

    if (Object.keys(productVariables).length > 0) {
      window.r46('track', 'view', {
        id: productId,
        ...productVariables,
      });
    } else {
      window.r46('track', 'view', productId);
    }
  }

  onAddedProduct(event) {
    const product = event.product || {};
    const feedWithGroupedProducts = this.getOption('feedWithGroupedProducts');
    const productId = (feedWithGroupedProducts) ? product.skuCode : product.id;
    if (productId) {
      window.r46('track', 'cart', {
        id: productId,
        amount: event.quantity || 1,
      });
    }
  }

  onRemovedProduct(event) {
    const product = event.product || {};
    const feedWithGroupedProducts = this.getOption('feedWithGroupedProducts');
    const productId = (feedWithGroupedProducts) ? product.skuCode : product.id;
    if (productId) {
      window.r46('track', 'remove_from_cart', productId);
    }
  }

  onViewedProductListing(event) {
    const listing = event.listing || {};
    const { categoryId } = listing;
    if (categoryId) {
      window.r46('track', 'category', categoryId);
    }
  }

  onSearchedProducts(event) {
    const listing = event.listing || {};
    const { query } = listing;
    if (query) {
      window.r46('track', 'search', query);
    }
  }

  onCompletedTransaction(event) {
    const transaction = event.transaction || {};
    const lineItems = transaction.lineItems || [];
    const feedWithGroupedProducts = this.getOption('feedWithGroupedProducts');
    if (lineItems.length) {
      window.r46('track', 'purchase', {
        products: lineItems.map(lineItem => ({
          id: (feedWithGroupedProducts)
            ? getProp(lineItem, 'product.skuCode') : getProp(lineItem, 'product.id'),
          price: getProp(lineItem, 'product.unitSalePrice'),
          amount: lineItem.quantity || 1,
        })),
        order: transaction.orderId,
        order_price: transaction.total,
      });
    }
  }
}

export default REES46;
<commit_msg>DRI-796 fix rees46 Viewed Page event for anonimous user<commit_after>import cleanObject from 'driveback-utils/cleanObject';
import { getProp } from 'driveback-utils/dotProp';
import {
  getEnrichableVariableMappingProps,
  extractVariableMappingValues,
} from '../IntegrationUtils';

import Integration from '../Integration';
import {
  VIEWED_PAGE,
  VIEWED_PRODUCT_DETAIL,
  VIEWED_PRODUCT_LISTING,
  ADDED_PRODUCT,
  REMOVED_PRODUCT,
  COMPLETED_TRANSACTION,
  SEARCHED_PRODUCTS,
} from '../events/semanticEvents';

const SEMANTIC_EVENTS = [
  VIEWED_PAGE,
  VIEWED_PRODUCT_DETAIL,
  VIEWED_PRODUCT_LISTING,
  ADDED_PRODUCT,
  REMOVED_PRODUCT,
  COMPLETED_TRANSACTION,
  SEARCHED_PRODUCTS,
];

class REES46 extends Integration {
  constructor(digitalData, options) {
    const optionsWithDefaults = Object.assign({
      storeKey: '',
      feedWithGroupedProducts: false,
      productVariables: {},
    }, options);

    super(digitalData, optionsWithDefaults);

    this.addTag({
      type: 'script',
      attr: {
        src: 'https://cdn.rees46.com/v3.js',
      },
    });
  }

  initialize() {
    window.r46 = window.r46 || function r46Init() {
      (window.r46.q = window.r46.q || []).push(arguments);
    };
    window.r46('init', this.getOption('storeKey'));
  }

  getSemanticEvents() {
    return SEMANTIC_EVENTS;
  }

  getEnrichableEventProps(event) {
    switch (event.name) {
      case VIEWED_PAGE: return ['user', 'website'];
      case VIEWED_PRODUCT_LISTING: return ['listing.categoryId'];
      case SEARCHED_PRODUCTS: return ['listing.query'];
      case COMPLETED_TRANSACTION: return ['transaction'];
      case VIEWED_PRODUCT_DETAIL:
        return [
          ...getEnrichableVariableMappingProps(this.getOption('productVariables')),
          'product',
        ];
      default: return [];
    }
  }

  getProductVariables(event) {
    const { product } = event;
    const mapping = this.getOption('productVariables');
    return extractVariableMappingValues(
      { event, product },
      mapping,
      { multipleScopes: true },
    );
  }

  trackEvent(event) {
    const eventMap = {
      [VIEWED_PAGE]: this.onViewedPage.bind(this),
      [VIEWED_PRODUCT_DETAIL]: this.onViewedProductDetail.bind(this),
      [VIEWED_PRODUCT_LISTING]: this.onViewedProductListing.bind(this),
      [SEARCHED_PRODUCTS]: this.onSearchedProducts.bind(this),
      [ADDED_PRODUCT]: this.onAddedProduct.bind(this),
      [REMOVED_PRODUCT]: this.onRemovedProduct.bind(this),
      [COMPLETED_TRANSACTION]: this.onCompletedTransaction.bind(this),
    };
    if (eventMap[event.name]) {
      eventMap[event.name](event);
    }
  }

  onViewedPage(event) {
    const user = event.user || {};
    const location = getProp(event, 'website.regionId');
    const gender = String(user.gender).toLowerCase();

    const rees46Data = cleanObject({
      id: user.userId,
      email: user.email,
      gender: ['m', 'f'].indexOf(gender) >= 0 ? gender : undefined,
      birthday: user.birthDate,
      location,
    });

    if (Object.keys(rees46Data).length > 0) {
      window.r46('profile', 'set', rees46Data);
    }
  }

  onViewedProductDetail(event) {
    const product = event.product || {};
    const feedWithGroupedProducts = this.getOption('feedWithGroupedProducts');
    const productId = (feedWithGroupedProducts) ? product.skuCode : product.id;
    if (!productId) return;

    const productVariables = this.getProductVariables(event);
    const { stock } = product;
    if (stock !== undefined) {
      productVariables.stock = (stock > 0);
    }

    if (Object.keys(productVariables).length > 0) {
      window.r46('track', 'view', {
        id: productId,
        ...productVariables,
      });
    } else {
      window.r46('track', 'view', productId);
    }
  }

  onAddedProduct(event) {
    const product = event.product || {};
    const feedWithGroupedProducts = this.getOption('feedWithGroupedProducts');
    const productId = (feedWithGroupedProducts) ? product.skuCode : product.id;
    if (productId) {
      window.r46('track', 'cart', {
        id: productId,
        amount: event.quantity || 1,
      });
    }
  }

  onRemovedProduct(event) {
    const product = event.product || {};
    const feedWithGroupedProducts = this.getOption('feedWithGroupedProducts');
    const productId = (feedWithGroupedProducts) ? product.skuCode : product.id;
    if (productId) {
      window.r46('track', 'remove_from_cart', productId);
    }
  }

  onViewedProductListing(event) {
    const listing = event.listing || {};
    const { categoryId } = listing;
    if (categoryId) {
      window.r46('track', 'category', categoryId);
    }
  }

  onSearchedProducts(event) {
    const listing = event.listing || {};
    const { query } = listing;
    if (query) {
      window.r46('track', 'search', query);
    }
  }

  onCompletedTransaction(event) {
    const transaction = event.transaction || {};
    const lineItems = transaction.lineItems || [];
    const feedWithGroupedProducts = this.getOption('feedWithGroupedProducts');
    if (lineItems.length) {
      window.r46('track', 'purchase', {
        products: lineItems.map(lineItem => ({
          id: (feedWithGroupedProducts)
            ? getProp(lineItem, 'product.skuCode') : getProp(lineItem, 'product.id'),
          price: getProp(lineItem, 'product.unitSalePrice'),
          amount: lineItem.quantity || 1,
        })),
        order: transaction.orderId,
        order_price: transaction.total,
      });
    }
  }
}

export default REES46;
<|endoftext|>


<commit_before>import { is, isDev, check, warnDeprecated } from './utils'
import asap from './asap'
import proc from './proc'
import emitter from './emitter'
import { MONITOR_ACTION } from './monitorActions'
import SagaCancellationException from './SagaCancellationException'

export const sagaArgError = (fn, pos, saga) => (`
  ${fn} can only be called on Generator functions
  Argument ${saga} at position ${pos} is not function!
`)

export const RUN_SAGA_DYNAMIC_ERROR = 'Before running a Saga dynamically using middleware.run, you must mount the Saga middleware on the Store using applyMiddleware'

export const GET_STATE_DEPRECATED_WARNING = `
  Using the 'getState' param of Sagas to access the state is deprecated since 0.9.1
  To access the Store's state use 'yield select()' instead
  For more infos see http://yelouafi.github.io/redux-saga/docs/api/index.html#selectselector-args
`

export default function sagaMiddlewareFactory(...sagas) {
  let runSagaDynamically

  sagas.forEach((saga, idx) => check(saga, is.func, sagaArgError('createSagaMiddleware', idx, saga)))

  function sagaMiddleware({getState, dispatch}) {
    const sagaEmitter = emitter()
    const monitor = isDev ? action => asap(() => dispatch(action)) : undefined

    const getStateDeprecated = () => {
      warnDeprecated(GET_STATE_DEPRECATED_WARNING)
      return getState()
    }

    function runSaga(saga, ...args) {
      return proc(
        saga(getStateDeprecated, ...args),
        sagaEmitter.subscribe,
        dispatch,
        getState,
        monitor,
        0,
        saga.name
      )
    }

    runSagaDynamically = runSaga

    asap.suspend()
    Promise.resolve().then(asap.flush)
    sagas.forEach(runSaga)

    return next => action => {
      const result = next(action) // hit reducers
      // filter out monitor actions to avoid endless loops
      // see https://github.com/yelouafi/redux-saga/issues/61
      if(!action[MONITOR_ACTION])
        asap(() => sagaEmitter.emit(action))
      return result;
    }
  }

  sagaMiddleware.run = (saga, ...args) => {
    if(!runSagaDynamically) {
      throw new Error(RUN_SAGA_DYNAMIC_ERROR)
    }
    check(saga, is.func, sagaArgError('sagaMiddleware.run', 0, saga))

    const task = runSagaDynamically(saga, ...args)
    task.done.catch(err => {
      if(!(err instanceof SagaCancellationException))
        throw err
    })
    return task
  }

  return sagaMiddleware
}
<commit_msg>breaking change in middleware API<commit_after>import { is, isDev, check } from './utils'
import asap from './asap'
import proc from './proc'
import emitter from './emitter'
import { MONITOR_ACTION } from './monitorActions'
import SagaCancellationException from './SagaCancellationException'

export const sagaArgError = (fn, pos, saga) => (`
  ${fn} can only be called on Generator functions
  Argument ${saga} at position ${pos} is not function!
`)

export const MIDDLEWARE_NOT_CONNECTED_ERROR = 'Before running a Saga, you must mount the Saga middleware on the Store using applyMiddleware'

export default function sagaMiddlewareFactory() {
  let runSagaDynamically

  function sagaMiddleware({getState, dispatch}) {
    runSagaDynamically = runSaga
    const sagaEmitter = emitter()
    const monitor = isDev ? action => asap(() => dispatch(action)) : undefined

    function runSaga(saga, ...args) {
      return proc(
        saga(...args),
        sagaEmitter.subscribe,
        dispatch,
        getState,
        monitor,
        0,
        saga.name
      )
    }

    return next => action => {
      const result = next(action) // hit reducers
      // filter out monitor actions to avoid endless loops
      // see https://github.com/yelouafi/redux-saga/issues/61
      if(!action[MONITOR_ACTION])
        sagaEmitter.emit(action)
      return result;
    }
  }

  sagaMiddleware.run = (saga, ...args) => {
    if(!runSagaDynamically) {
      throw new Error(MIDDLEWARE_NOT_CONNECTED_ERROR)
    }
    check(saga, is.func, sagaArgError('sagaMiddleware.run', 0, saga))

    const task = runSagaDynamically(saga, ...args)
    task.done.catch(err => {
      if(!(err instanceof SagaCancellationException))
        throw err
    })
    return task
  }

  return sagaMiddleware
}
<|endoftext|>


<commit_before>/*
* mvcActionResult
* author: ronglin
* create date: 2014.6.30
*/

'use strict';

var http = require('http'),
    utils = require('./utilities'),
    mvcHelper = require('./mvcHelper');

/* baseResult
***************************************/
var baseResult = exports.baseResult = function(set) {
    var self = this;
    utils.each(set, function (key, val) {
        if (val) { self[key] = val;}
    });
};

baseResult.prototype = {
    constructor: baseResult, className: 'mvcActionResult',
    execute: function(context) {
        throw new Error('"execute" function needs override by sub classes.');
    }
};

/* emptyResult
***************************************/
var emptyResult = exports.emptyResult = function(set) {
    emptyResult.superclass.constructor.call(this, set);
};

utils.inherit(emptyResult, baseResult, {
    execute: function(context) {
        context.response.header('Content-Type', 'text/plain');
        context.response.send('');
    }
});

/* jsonResult
***************************************/
var jsonResult = exports.jsonResult = function(set) {
    jsonResult.superclass.constructor.call(this, set);
};

utils.inherit(jsonResult, baseResult, {
    data: null, contentType: 'application/json',
    execute: function(context) {
        context.response.header('Content-Type', this.contentType);
        context.response.json(this.data);
    }
});

/* jsonpResult
***************************************/
var jsonpResult = exports.jsonpResult = function(set) {
    jsonpResult.superclass.constructor.call(this, set);
};

utils.inherit(jsonpResult, baseResult, {
    data: null, contentType: 'text/javascript', callbackName: 'callback',
    execute: function(context) {
        var old = context.response.app.get('jsonp callback name');
        context.response.app.set('jsonp callback name', this.callbackName);
        context.response.header('Content-Type', this.contentType);
        context.response.jsonp(this.data);
        context.response.app.set('jsonp callback name', old);
    }
});

/* partialViewResult
***************************************/
var partialViewResult = exports.partialViewResult = function(set) {
    partialViewResult.superclass.constructor.call(this, set);
};

utils.inherit(partialViewResult, baseResult, {
    viewName: null,
    execute: function(context) {
        //TODO:
    }
});

/* viewResult
***************************************/
var viewResult = exports.viewResult = function(set) {
    viewResult.superclass.constructor.call(this, set);
};

utils.inherit(viewResult, baseResult, {
    viewName: null, model: null,
    execute: function(context) {
        //TODO:
    }
});

/* fileResult
***************************************/
var fileResult = exports.fileResult = function(set) {
    fileResult.superclass.constructor.call(this, set);
};

utils.inherit(fileResult, baseResult, {
    filePath: null, fileDownloadName: null,
    execute: function(context) {
        context.response.download(this.filePath, this.fileDownloadName);
    }
});

/* contentResult
***************************************/
var contentResult = exports.contentResult = function(set) {
    contentResult.superclass.constructor.call(this, set);
};

utils.inherit(contentResult, baseResult, {
    content: null, contentType: 'text/plain',
    execute: function(context) {
        context.response.header('Content-Type', this.contentType);
        context.response.send(this.content);
    }
});

/* httpStatusCodeResult
***************************************/
var httpStatusCodeResult = exports.httpStatusCodeResult = function(set) {
    httpStatusCodeResult.superclass.constructor.call(this, set);
};

utils.inherit(httpStatusCodeResult, baseResult, {
    statusCode: null, statusText: null,
    execute: function(context) {
        //
        var message = this.statusText;
        if (!message) { message = http.STATUS_CODES[this.statusCode];}
        //
        context.exception = new Error(message);
        context.exception.status = this.statusCode;
    }
});

/* httpNotFoundResult
***************************************/
var httpNotFoundResult = exports.httpNotFoundResult = function(set) {
    httpNotFoundResult.superclass.constructor.call(this, set);
    this.statusCode = 404;
};

utils.inherit(httpNotFoundResult, httpStatusCodeResult, {
    execute: function(context) {
        httpNotFoundResult.superclass.execute.call(this, context);
    }
});

/* redirectResult
***************************************/
var redirectResult = exports.redirectResult = function(set) {
    redirectResult.superclass.constructor.call(this, set);
};

utils.inherit(redirectResult, baseResult, {
    url: null, permanent: false,
    execute: function(context) {
        if (this.permanent) {
            context.response.redirect(this.url, 301);
        } else {
            context.response.redirect(this.url, 302);
        }
    }
});

/* redirectToRouteResult
***************************************/
var redirectToRouteResult = exports.redirectToRouteResult = function(set) {
    redirectToRouteResult.superclass.constructor.call(this, set);
};

utils.inherit(redirectToRouteResult, baseResult, {
    routeValues: null, permanent: false,
    execute: function(context) {
        var url = mvcHelper.generateUrl(null, null, this.routeValues, context.request.routeSet, context.request, false);
        if (this.permanent) {
            context.response.redirect(url, 301);
        } else {
            context.response.redirect(url, 302);
        }
    }
});

<commit_msg>fix bug<commit_after>/*
* mvcActionResult
* author: ronglin
* create date: 2014.6.30
*/

'use strict';

var http = require('http'),
    utils = require('./utilities'),
    mvcHelper = require('./mvcHelper');

/* baseResult
***************************************/
var baseResult = exports.baseResult = function(set) {
    var self = this;
    utils.each(set, function (key, val) {
        if (val) { self[key] = val;}
    });
};

baseResult.prototype = {
    constructor: baseResult, className: 'mvcActionResult',
    execute: function(context) {
        throw new Error('"execute" function needs override by sub classes.');
    }
};

/* emptyResult
***************************************/
var emptyResult = exports.emptyResult = function(set) {
    emptyResult.superclass.constructor.call(this, set);
};

utils.inherit(emptyResult, baseResult, {
    execute: function(context) {
        context.response.header('Content-Type', 'text/plain');
        context.response.send('');
    }
});

/* jsonResult
***************************************/
var jsonResult = exports.jsonResult = function(set) {
    jsonResult.superclass.constructor.call(this, set);
};

utils.inherit(jsonResult, baseResult, {
    data: null, contentType: 'application/json',
    execute: function(context) {
        context.response.header('Content-Type', this.contentType);
        context.response.json(this.data);
    }
});

/* jsonpResult
***************************************/
var jsonpResult = exports.jsonpResult = function(set) {
    jsonpResult.superclass.constructor.call(this, set);
};

utils.inherit(jsonpResult, baseResult, {
    data: null, contentType: 'text/javascript', callbackName: 'callback',
    execute: function(context) {
        var old = context.response.app.get('jsonp callback name');
        context.response.app.set('jsonp callback name', this.callbackName);
        context.response.header('Content-Type', this.contentType);
        context.response.jsonp(this.data);
        context.response.app.set('jsonp callback name', old);
    }
});

/* partialViewResult
***************************************/
var partialViewResult = exports.partialViewResult = function(set) {
    partialViewResult.superclass.constructor.call(this, set);
};

utils.inherit(partialViewResult, baseResult, {
    viewName: null,
    execute: function(context) {
        //TODO:
    }
});

/* viewResult
***************************************/
var viewResult = exports.viewResult = function(set) {
    viewResult.superclass.constructor.call(this, set);
};

utils.inherit(viewResult, baseResult, {
    viewName: null, model: null,
    execute: function(context) {
        //TODO:
    }
});

/* fileResult
***************************************/
var fileResult = exports.fileResult = function(set) {
    fileResult.superclass.constructor.call(this, set);
};

utils.inherit(fileResult, baseResult, {
    filePath: null, fileDownloadName: null,
    execute: function(context) {
        context.response.download(this.filePath, this.fileDownloadName);
    }
});

/* contentResult
***************************************/
var contentResult = exports.contentResult = function(set) {
    contentResult.superclass.constructor.call(this, set);
};

utils.inherit(contentResult, baseResult, {
    content: null, contentType: 'text/plain',
    execute: function(context) {
        context.response.header('Content-Type', this.contentType);
        context.response.send(this.content);
    }
});

/* httpStatusCodeResult
***************************************/
var httpStatusCodeResult = exports.httpStatusCodeResult = function(set) {
    httpStatusCodeResult.superclass.constructor.call(this, set);
};

utils.inherit(httpStatusCodeResult, baseResult, {
    statusCode: null, statusText: null,
    execute: function(context) {
        //
        var message = this.statusText;
        if (!message) { message = http.STATUS_CODES[this.statusCode];}
        //
        context.exception = new Error(message);
        context.exception.status = this.statusCode;
    }
});

/* httpNotFoundResult
***************************************/
var httpNotFoundResult = exports.httpNotFoundResult = function(set) {
    httpNotFoundResult.superclass.constructor.call(this, set);
    this.statusCode = 404;
};

utils.inherit(httpNotFoundResult, httpStatusCodeResult, {
    execute: function(context) {
        httpNotFoundResult.superclass.execute.call(this, context);
    }
});

/* redirectResult
***************************************/
var redirectResult = exports.redirectResult = function(set) {
    redirectResult.superclass.constructor.call(this, set);
};

utils.inherit(redirectResult, baseResult, {
    url: null, permanent: false,
    execute: function(context) {
        if (this.permanent) {
            context.response.redirect(this.url, 301);
        } else {
            context.response.redirect(this.url, 302);
        }
    }
});

/* redirectToRouteResult
***************************************/
var redirectToRouteResult = exports.redirectToRouteResult = function(set) {
    redirectToRouteResult.superclass.constructor.call(this, set);
};

utils.inherit(redirectToRouteResult, baseResult, {
    routeValues: null, permanent: false,
    execute: function(context) {
        var url = mvcHelper.generateUrl(null, null, this.routeValues, context.routeSet, context, false);
        if (this.permanent) {
            context.response.redirect(url, 301);
        } else {
            context.response.redirect(url, 302);
        }
    }
});

<|endoftext|>