<commit_before>var mongoose = require('mongoose');
require('../models/IpAnswers');
var Deferred = require('promised-io/promise').Deferred;
var Question;

/**
 * Comment schema
 */
var commentSchema = mongoose.Schema({
  text: {
    type: String,
    required: '{PATH} is required'
  },
  author: {
    type: mongoose.Schema.Types.ObjectId,
    reference: 'User',
    required: '{PATH} is required'
  },
  upvotes: {
    type: Number,
    default: 0
  },
  date: {
    type: Date,
    default: Date.now
  }
});

/**
 * Question schema
 */
var questionSchema = mongoose.Schema({
  title: {
    type: String
  },
  description: {
    type: String
  },
  text: {
    type: String,
    required: '{PATH} is required'
  },
  answers: [{
    text: {
      type: String,
      required: '{PATH} is required'
    },
    votes: {
      type: Number,
      default: 0
    }
  }],
  published: {
    type: Date,
    default: Date.now
  },
  upvotes: {
    type: Number,
    default: 0
  },
  tags: {
    type: [String]
  },
  comments: [commentSchema],
  author: {
    type: mongoose.Schema.Types.ObjectId,
    reference: 'User'
  },
  status: {
    type: Number,
    default: 0
  }
});

var IpAnswers = mongoose.model('IpAnswers');

/**
 * Question schema methods
 */
questionSchema.methods = {
  hasBeenAnswered: function(user, ip) {
    var self = this;
    var q = new Deferred();
    var i;
    if (!user) { // Anonymous mode
      IpAnswers.find({ip: ip}, function(err, ipAnswers) {
        if (err) { // Error case : resolve with true
          q.resolve(true);
        } else {
          if (ipAnswers.length === 0) { // New user, we save it and initialize its IpAnswers entry
            IpAnswers.create({
              ip: ip,
              answers: []
            }, function(err) {
              if (err) {
                q.resolve(true);
              } else {
                q.resolve(false);
              }
            });
          } else if (ipAnswers.length === 1) { // Known user, we will work with his/her answers
            var answers = ipAnswers[0].answers;
            if (answers.length === 0) {
              q.resolve(false);
            }
            for (i = 0; i < answers.length; i++) {
              if (answers[i].question.equals(self._id)) {
                q.resolve(true);
                break;
              }
              if (i === answers.length - 1) {
                q.resolve(false);
              }
            }
          }
        }
      });
    } else { // Authenticated mode
      if (user.answers.length === 0) {
        q.resolve(false);
      }
      for (i = 0; i <  user.answers.length; i++) {
        if (user.answers[i].question._id.equals(self._id)) {
          q.resolve(true);
          break;
        }
        if (i === user.answers.length - 1) {
          q.resolve(false);
        }
      }
    }
    return q.promise;
  },
  populateQuestion: function() {
    var question = this;
    var q = new Deferred();
    Question.populate(question, [{
      path: 'comments.author',
      select: 'username answers',
      model: 'User'
    }, {
      path: 'author',
      select: 'username',
      model: 'User'
    }], function(err) {
      if (err) {
        q.reject();
      } else {
        q.resolve(question);
      }
    });
    return q.promise;
  }
};

/**
 * Get a random question
 */
questionSchema.statics.random = function(callback) {
  this.count({status: 1}, function(err, count) {
    if (err) {
      return callback(err);
    }
    var rand = Math.floor(Math.random() * count);
    this.findOne({status: 1}).skip(rand).exec(callback);
  }.bind(this));
};

Question = mongoose.model('Question', questionSchema);

/**
 * Create default questions in the db
 */
exports.createDefaultEntries = function() {
  Question.find({}).exec(function(err, collection) {
    if (err) {
      return;
    }
    if (collection.length === 0) {
      Question.create({
        text: 'Would you rather',
        answers: [{
          text: 'eat beef',
          votes: 10
        }, {
          text: 'eat chicken'
        }],
        published: new Date('1/1/2015'),
        tags: ['food', 'preferences']
      });
      Question.create({
        text: 'Do you prefer',
        answers: [{
          text: 'green'
        }, {
          text: 'blue',
          votes: 2
        }],
        published: new Date('1/1/2015'),
        tags: ['color']
      });
      Question.create({
        text: 'Would you rather',
        answers: [{
          text: 'go to Italy',
          votes: 10
        }, {
          text: 'go to France',
          votes: 15
        }],
        published: new Date('1/1/2015')
      });
      Question.create({
        text: 'Would you rather',
        answers: [{
          text: 'play football'
        }, {
          text: 'play rugby'
        }],
        published: new Date('1/1/2015')
      });
      Question.create({
        text: 'Do you prefer',
        answers: [{
          text: 'Manchester United'
        }, {
          text: 'Manchester City'
        }]
      });
      Question.create({
        text: 'Would you rather',
        answers: [{
          text: 'be immportal'
        }, {
          text: 'be mortal'
        }]
      });
      Question.create({
        text: 'Do you prefer',
        answers: [{
          text: 'men'
        }, {
          text: 'women'
        }]
      });
      Question.create({
        text: 'Do you prefer',
        answers: [{
          text: 'bar'
        }, {
          text: 'clubs'
        }]
      });
      Question.create({
        text: 'Do you prefer',
        answers: [{
          text: 'apples'
        }, {
          text: 'oranges'
        }]
      });
      Question.create({
        text: 'Do you prefer',
        answers: [{
          text: 'mathematics'
        }, {
          text: 'history'
        }]
      });
    }
    console.log('Questions collection has ' + collection.length + ' entries');
  });
};
<commit_msg>Add i18n support for Question model<commit_after>var mongoose = require('mongoose');
require('../models/IpAnswers');
var Deferred = require('promised-io/promise').Deferred;
var Question;

/**
 * Comment schema
 */
var commentSchema = mongoose.Schema({
  text: {
    type: String,
    required: '{PATH} is required'
  },
  author: {
    type: mongoose.Schema.Types.ObjectId,
    reference: 'User',
    required: '{PATH} is required'
  },
  upvotes: {
    type: Number,
    default: 0
  },
  date: {
    type: Date,
    default: Date.now
  }
});

/**
 * Question schema
 */
var questionSchema = mongoose.Schema({
  title: {
    en: {
      type: String
    },
    fr: {
      type: String
    }
  },
  description: {
    en: {
      type: String
    },
    fr: {
      type: String
    }
  },
  text: {
    en: {
      type: String,
      required: '{PATH} is required'
    },
    fr: {
      type: String,
      required: '{PATH} is required'
    }
  },
  answers: [{
    text: {
      en: {
        type: String,
        required: '{PATH} is required'
      },
      fr: {
        type: String,
        required: '{PATH} is required'
      }
    },
    votes: {
      type: Number,
      default: 0
    }
  }],
  published: {
    type: Date,
    default: Date.now
  },
  upvotes: {
    type: Number,
    default: 0
  },
  tags: {
    type: [String]
  },
  comments: [commentSchema],
  author: {
    type: mongoose.Schema.Types.ObjectId,
    reference: 'User'
  },
  status: {
    type: Number,
    default: 0
  }
});

var IpAnswers = mongoose.model('IpAnswers');

/**
 * Question schema methods
 */
questionSchema.methods = {
  hasBeenAnswered: function(user, ip) {
    var self = this;
    var q = new Deferred();
    var i;
    if (!user) { // Anonymous mode
      IpAnswers.find({ip: ip}, function(err, ipAnswers) {
        if (err) { // Error case : resolve with true
          q.resolve(true);
        } else {
          if (ipAnswers.length === 0) { // New user, we save it and initialize its IpAnswers entry
            IpAnswers.create({
              ip: ip,
              answers: []
            }, function(err) {
              if (err) {
                q.resolve(true);
              } else {
                q.resolve(false);
              }
            });
          } else if (ipAnswers.length === 1) { // Known user, we will work with his/her answers
            var answers = ipAnswers[0].answers;
            if (answers.length === 0) {
              q.resolve(false);
            }
            for (i = 0; i < answers.length; i++) {
              if (answers[i].question.equals(self._id)) {
                q.resolve(true);
                break;
              }
              if (i === answers.length - 1) {
                q.resolve(false);
              }
            }
          }
        }
      });
    } else { // Authenticated mode
      if (user.answers.length === 0) {
        q.resolve(false);
      }
      for (i = 0; i <  user.answers.length; i++) {
        if (user.answers[i].question._id.equals(self._id)) {
          q.resolve(true);
          break;
        }
        if (i === user.answers.length - 1) {
          q.resolve(false);
        }
      }
    }
    return q.promise;
  },
  populateQuestion: function() {
    var question = this;
    var q = new Deferred();
    Question.populate(question, [{
      path: 'comments.author',
      select: 'username answers',
      model: 'User'
    }, {
      path: 'author',
      select: 'username',
      model: 'User'
    }], function(err) {
      if (err) {
        q.reject();
      } else {
        q.resolve(question);
      }
    });
    return q.promise;
  }
};

/**
 * Get a random question
 */
questionSchema.statics.random = function(callback) {
  this.count({status: 1}, function(err, count) {
    if (err) {
      return callback(err);
    }
    var rand = Math.floor(Math.random() * count);
    this.findOne({status: 1}).skip(rand).exec(callback);
  }.bind(this));
};

Question = mongoose.model('Question', questionSchema);

/**
 * Create default questions in the db
 */
exports.createDefaultEntries = function() {
  Question.find({}).exec(function(err, collection) {
    if (err) {
      return;
    }
    if (collection.length === 0) {
      Question.create({
        text: {
          en: 'Would you rather',
          fr: 'Préférez-vous'
        },
        answers: [{
          text: {
            en: 'eat beef',
            fr: 'manger du boeuf'
          },
          votes: 10
        }, {
          text: {
            en: 'eat chicken',
            fr: 'manger du poulet'
          }
        }],
        published: new Date('1/1/2015'),
        tags: ['food', 'preferences'],
        status: 1
      });
      Question.create({
        text: {
          en: 'Do you prefer',
          fr: 'Préférez-vous'
        },
        answers: [{
          text: {
            en: 'green',
            fr: 'vert'
          }
        }, {
          text: {
            en: 'blue',
            fr: 'bleu'
          },
          votes: 2
        }],
        published: new Date('1/1/2015'),
        tags: ['color'],
        status: 1
      });
      Question.create({
        text: {
          en: 'Would you rather',
          fr: 'Préférez-vous'
        },
        answers: [{
          text: {
            en: 'go to Italy',
            fr: 'aller en Italie'
          },
          votes: 10
        }, {
          text: {
            en: 'go to France',
            fr: 'aller en France'
          },
          votes: 15
        }],
        published: new Date('1/1/2015')
      });
    }
    console.log('Questions collection has ' + collection.length + ' entries');
  });
};
<|endoftext|>


<commit_before>function SwatSearchEntry(id)
{
	this.id = id;
	this.input = document.getElementById(this.id);

	var labels = document.getElementsByTagName('label');
	var label = null;

	for (var i = 0; i < labels.length; i++) {
		if (labels[i].htmlFor == this.id) {
			label = labels[i];
			break;
		}
	}

	if (label != null) {
		this.label_text =
			(label.innerText) ? label.innerText : label.textContent;

		this.input_name = this.input.getAttribute('name');
		this.input_value = this.input.value;

		label.style.display = 'none';

		YAHOO.util.Event.addListener(this.input, 'focus', this.handleFocus,
			this, true);

		YAHOO.util.Event.addListener(this.input, 'blur', this.handleBlur,
			this, true);

		if (this.input.value == '')
			this.showLabelText();
		else
			this.hideLabelText();
	}
}

SwatSearchEntry.prototype.handleFocus = function(e)
{
	this.hideLabelText();
}

SwatSearchEntry.prototype.handleBlur = function(e)
{
	if (this.input.value == '')
		this.showLabelText();
}

SwatSearchEntry.prototype.showLabelText = function()
{
	YAHOO.util.Dom.addClass(this.input, 'swat-search-entry-empty');
	this.input.removeAttribute('name');
	this.input_value = this.input.value;
	this.input.value = this.label_text;
}

SwatSearchEntry.prototype.hideLabelText = function()
{
	if (!this.input.getAttribute('name')) {
		this.input.setAttribute('name', this.input_name);
		this.input.value = this.input_value;
		YAHOO.util.Dom.removeClass(this.input, 'swat-search-entry-empty');
	}
}
<commit_msg>Add 93 lines to make the standard 4 lines work in IE and make esc in Firefox not undo the searchbox clear. Urge to kill rising.<commit_after>function SwatSearchEntry(id)
{
	this.id = id;
	this.input = document.getElementById(this.id);

	var labels = document.getElementsByTagName('label');
	var label = null;

	for (var i = 0; i < labels.length; i++) {
		if (labels[i].htmlFor == this.id) {
			label = labels[i];
			break;
		}
	}

	if (label != null) {
		this.label_text =
			(label.innerText) ? label.innerText : label.textContent;

		this.input_name = this.input.getAttribute('name');
		this.input_value = this.input.value;

		label.style.display = 'none';

		YAHOO.util.Event.addListener(this.input, 'focus', this.handleFocus,
			this, true);

		YAHOO.util.Event.addListener(this.input, 'blur', this.handleBlur,
			this, true);

		YAHOO.util.Event.onDOMReady(this.onLoad, this, true);
	}
}

SwatSearchEntry.prototype.onLoad = function()
{
	if (this.input.value == '') {
		this.showLabelText();
	} else {
		this.hideLabelText();
	}
}

SwatSearchEntry.prototype.handleKeyDown = function(e)
{
	// prevent esc from undoing the clearing of label text in Firefox
	if (e.keyCode == 27) {
		this.input.value = '';
	}

	YAHOO.util.Event.removeListener(this.input, 'keypress', this.handleKeyDown);
}

SwatSearchEntry.prototype.handleFocus = function(e)
{
	this.hideLabelText();
	this.input.focus(); // IE hack to focus
}

SwatSearchEntry.prototype.handleBlur = function(e)
{
	if (this.input.value == '')
		this.showLabelText();

	YAHOO.util.Event.removeListener(this.input, 'keypress', this.handleKeyDown);
}

SwatSearchEntry.prototype.showLabelText = function()
{
	YAHOO.util.Dom.addClass(this.input, 'swat-search-entry-empty');

	if (this.input.hasAttribute) {
		this.input.removeAttribute('name');
	} else {
		// IE can't set name attribute at runtime and doesn't have
		// hasAttribute method. Unbelievable but it's true.
		if (this.input.name) {
			// remove name attribute
			var outer_html = this.input.outerHTML.replace(
				'name=' + this.input_name, '');

			var old_input = this.input;
			this.input = document.createElement(outer_html);

			// replace old input with new one
			old_input.parentNode.insertBefore(this.input, old_input);

			// prevent IE memory leaks
			YAHOO.util.Event.purgeElement(old_input);
			old_input.parentNode.removeChild(old_input);

			// add event handlers back
			YAHOO.util.Event.addListener(this.input, 'focus', this.handleFocus,
				this, true);

			YAHOO.util.Event.addListener(this.input, 'blur', this.handleBlur,
				this, true);
		}
	}

	this.input_value = this.input.value;
	this.input.value = this.label_text;
}

SwatSearchEntry.prototype.hideLabelText = function()
{
	var hide = false;

	if (this.input.hasAttribute) {
		if (!this.input.hasAttribute('name')) {
			this.input.setAttribute('name', this.input_name);
			hide = true;
		}
	} else {
		// IE hack - seriously, unbelievable.
		if (!this.input.getAttribute('name')) {

			// we want the same input with a name attribute
			var outer_html = this.input.outerHTML.replace(
				'id=' + this.id,
				'id=' + this.id + ' name=' + this.input_name);

			var old_input = this.input;
			this.input = document.createElement(outer_html);

			// add event handlers back
			YAHOO.util.Event.addListener(this.input, 'focus', this.handleFocus,
				this, true);

			YAHOO.util.Event.addListener(this.input, 'blur', this.handleBlur,
				this, true);

			// replace old input with new one
			old_input.parentNode.insertBefore(this.input, old_input);

			// prevent IE memory leaks
			YAHOO.util.Event.purgeElement(old_input);
			old_input.parentNode.removeChild(old_input);

			hide = true;
		}
	}

	if (hide) {
		this.input.value = this.input_value;
		YAHOO.util.Dom.removeClass(this.input, 'swat-search-entry-empty');
		YAHOO.util.Event.addListener(this.input, 'keypress',
			this.handleKeyDown, this, true);
	}
}
<|endoftext|>


<commit_before>const util = require('util')
const unixtimeFromDate = require('../lib/unixtime-from-date')
const sha256 = require('../lib/hash').sha256
const Badges = require('../models/badge')
const ClaimCodes = require('../models/claim-codes')
const Webhooks = require('../models/webhook')
const BadgeInstances = require('../models/badge-instance')
const errorHelper = require('../lib/error-helper')
const middleware = require('../lib/middleware')
const log = require('../lib/logger')

const findSystemBadge = [
  middleware.findSystem(),
  middleware.findBadge({
    relationships: true,
    where: {systemId: ['system', 'id']},
  }),
]
const findIssuerBadge = [
  middleware.findSystem(),
  middleware.findIssuer({where: {systemId: ['system', 'id']}}),
  middleware.findBadge({
    relationships: true,
    where: {
      systemId: ['system', 'id'],
      issuerId: ['issuer', 'id'],
    },
  }),
]
const findProgramBadge = [
  middleware.findSystem(),
  middleware.findIssuer({where: {systemId: ['system', 'id']}}),
  middleware.findProgram({where: {issuerId: ['issuer', 'id']}}),
  middleware.findBadge({
    relationships: true,
    where: {
      systemId: ['system', 'id'],
      issuerId: ['issuer', 'id'],
      programId: ['program', 'id'],
    },
  }),
]

const prefix = {
  system: '/systems/:systemSlug',
  issuer: '/systems/:systemSlug/issuers/:issuerSlug',
  program: '/systems/:systemSlug/issuers/:issuerSlug/programs/:programSlug',
}
const publicPrefix = {
  system: '/public/' + prefix.system,
  issuer: '/public/' + prefix.issuer,
  program: '/public/' + prefix.program,
}

exports = module.exports = function applyBadgeRoutes (server) {
  const createNewInstanceSuffix = '/badges/:badgeSlug/instances'
  server.post(prefix.system + createNewInstanceSuffix,
              findSystemBadge, createNewInstance)

  server.post(prefix.issuer + createNewInstanceSuffix,
              findIssuerBadge, createNewInstance)

  server.post(prefix.program + createNewInstanceSuffix,
              findProgramBadge, createNewInstance)

  function createNewInstance(req, res, next) {
    const row = BadgeInstances.formatUserInput(req.body)
    row.badgeId = req.badge.id

    const errs = BadgeInstances.validateRow(row)
    if (errs.length)
      return res.send(400, errorHelper.validation(errs));

    if (row.claimCode) {
      const query = {code: row.claimCode, badgeId: row.badgeId}
      ClaimCodes.getOne(query, function (err, code) {
        if (err) {
          log.error(err, 'error trying to find claim code')
          return next(err)
        }

        if (code.claimed && !code.multiuse) {
          return res.send(400, {
            code: 'CodeAlreadyUsed',
            message: 'Claim code `'+code.code+'` has already been claimed'
          })
        }

        code.claimed = true
        code.email = row.email

        ClaimCodes.put(code, function (err, result) {
          if (err) {
            log.error(err, 'error trying to find claim code')
            return next(err)
          }
          return saveBadgeInstance()
        })
      })
    }
    else saveBadgeInstance()

    function saveBadgeInstance() {
      BadgeInstances.put(row, function (err, result) {
        if (err) {
          log.error(err, 'error trying to save badge instance')
          return next(err)
        }
        const instance = result.row
        const relativeAssertionUrl = '/public/assertions/' + instance.slug
        const assertionUrl = req.resolvePath(relativeAssertionUrl)

        res.header('Location', relativeAssertionUrl)
        res.send(201, {
          status: 'created',
          location: assertionUrl,
        })

        // Webhook stuff shouldn't hold up the request, so we call
        // `next()` before looking up any hooks we have to send off
        next()

        const system = req.system
        const hookData = {
          action: 'award',
          uid: instance.slug,
          email: instance.email,
          assertionUrl: assertionUrl,
          issuedOn: unixtimeFromDate(instance.issuedOn),
        }
        Webhooks.getOne({systemId: system.id}, function (err, hook) {
          if (err)
            return log.error(err, 'Could not retrieve webhook')

          if (!hook)
            return log.info({code: 'WebhookNotFound', system: system}, 'Webhook not found for system')

          hook.call(hookData, function (err, res, body) {
            if (res.statusCode != 200)
              return log.warn({code: 'WebhookBadResponse', status: res.statusCode, body: body})
          })
        })
      })
    }
  }

  server.get('/public/assertions/:instanceSlug', getAssertion)
  function getAssertion(req, res, next) {
    const instanceSlug = req.params.instanceSlug
    const query = {slug: instanceSlug}
    const options = {relationships: true}
    BadgeInstances.getOne(query, options, function (err, instance) {
      if (!instance)
        return next(errorHelper.notFound('Could not find badge instance'))

      // get fully hydrated badge class
      const query = {id: instance.badge.id}
      const options = {relationships: true}
      Badges.getOne(query, options, function (err, badge) {
        if (err) return next(err)
        instance.badge = badge

        const assertion = makeAssertion(instance, req)
        res.send(200, assertion)
        return next()
      })
    })
  }
  function makeAssertion(instance, req) {
    const badge = instance.badge
    return {
      uid: instance.slug,
      recipient: {
        identity: 'sha256$' + sha256(instance.email),
        type: 'email',
        hashed: true,
      },
      badge: req.resolvePath(makeBadgeClassUrl(badge)),
      verify: {
        url: req.resolvePath('/public/assertions/' + instance.slug),
        type: 'hosted',
      },
      issuedOn: unixtimeFromDate(instance.issuedOn),
      expires: unixtimeFromDate(instance.expires),
    }
  }
  function makeBadgeClassUrl(badge) {
    const badgeSlug = badge.slug
    const programSlug = badge.program && badge.program.slug
    const issuerSlug = badge.issuer && badge.issuer.slug
    const systemSlug = badge.system && badge.system.slug

    if (programSlug && issuerSlug && systemSlug)
      return util.format('/public/systems/%s/issuers/%s/programs/%s/badges/%s',
                        systemSlug, issuerSlug, programSlug, badgeSlug)

    if (!programSlug && issuerSlug && systemSlug)
      return util.format('/public/systems/%s/issuers/%s/badges/%s',
                        systemSlug, issuerSlug, badgeSlug)

    if (!programSlug && !issuerSlug && systemSlug)
      return util.format('/public/systems/%s/badges/%s',
                        systemSlug, badgeSlug)

    log.error({badge: badge}, 'badge has incomplete parantage – sending broken assertion')
    return '/public'
  }

  server.get(publicPrefix.system +'/badges/:badgeSlug',
              findSystemBadge, getBadgeClass)

  server.get(publicPrefix.issuer +'/badges/:badgeSlug',
              findIssuerBadge, getBadgeClass)

  server.get(publicPrefix.program+'/badges/:badgeSlug',
              findProgramBadge, getBadgeClass)
  function getBadgeClass(req, res, next) {
    const badgeClass = makeBadgeClass(req.badge, req)
    res.send(200, badgeClass)
    return next()
  }
  function makeBadgeClass(badge, req) {
    // #TODO: alignment urls, tags
    var imageUrl = badge.image.toUrl()
    if (!/^http/.test(imageUrl))
      imageUrl = req.resolvePath(imageUrl)
    return {
      name: badge.name,
      description: badge.consumerDescription,
      image: badge.image.toUrl(),
      criteria: badge.criteriaUrl,
      issuer: req.resolvePath(publicIssuerUrl(badge)),
    }
  }
  function publicIssuerUrl(badge) {
    const system = badge.system
    const issuer = badge.issuer
    const program = badge.program
    if (program && program.slug)
      return util.format('/public/systems/%s/issuers/%s/programs/%s',
                         system.slug, issuer.slug, program.slug)
    if (issuer && issuer.slug)
      return util.format('/public/systems/%s/issuers',
                         system.slug, issuer.slug)
    if (badge.system && badge.system.slug)
      return util.format('/public/systems/%s', system.slug)
  }

  server.get('/public/systems/:systemSlug', [
    middleware.findSystem({relationships: true}),
    getIssuerClass,
  ])
  server.get('/public/systems/:systemSlug/issuers/:issuerSlug', [
    middleware.findSystem(),
    middleware.findIssuer({
      relationships: true,
      where: {systemId: ['system', 'id']},
    }),
    getIssuerClass,
  ])
  server.get('/public/systems/:systemSlug/issuers/:issuerSlug/programs/:programSlug', [
    middleware.findSystem(),
    middleware.findIssuer({where: {systemId: ['system', 'id']}}),
    middleware.findProgram({
      relationships: true,
      where: {issuerId: ['issuer', 'id']},
    }),
    getIssuerClass,
  ])
  function getIssuerClass(req, res, next) {
    return res.send(200, makeIssuerOrganization(
      req.program, req.issuer, req.system
    ))
  }
  function makeIssuerOrganization(program, issuer, system) {
    const lookup = findFirstKeyIn([program, issuer, system])
    return {
      name: lookup('name'),
      url: lookup('url'),
      description: lookup('description'),
      email: lookup('email'),
    }
  }
  function findFirstKeyIn(things) {
    things = things.map(function (o) { return o || {} })
    return function lookup(key) {
      for (var i = 0; i < things.length; i++)
        if (things[i][key]) return things[i][key]
    }
  }
}
<commit_msg>Flip conditional for early return.<commit_after>const util = require('util')
const unixtimeFromDate = require('../lib/unixtime-from-date')
const sha256 = require('../lib/hash').sha256
const Badges = require('../models/badge')
const ClaimCodes = require('../models/claim-codes')
const Webhooks = require('../models/webhook')
const BadgeInstances = require('../models/badge-instance')
const errorHelper = require('../lib/error-helper')
const middleware = require('../lib/middleware')
const log = require('../lib/logger')

const findSystemBadge = [
  middleware.findSystem(),
  middleware.findBadge({
    relationships: true,
    where: {systemId: ['system', 'id']},
  }),
]
const findIssuerBadge = [
  middleware.findSystem(),
  middleware.findIssuer({where: {systemId: ['system', 'id']}}),
  middleware.findBadge({
    relationships: true,
    where: {
      systemId: ['system', 'id'],
      issuerId: ['issuer', 'id'],
    },
  }),
]
const findProgramBadge = [
  middleware.findSystem(),
  middleware.findIssuer({where: {systemId: ['system', 'id']}}),
  middleware.findProgram({where: {issuerId: ['issuer', 'id']}}),
  middleware.findBadge({
    relationships: true,
    where: {
      systemId: ['system', 'id'],
      issuerId: ['issuer', 'id'],
      programId: ['program', 'id'],
    },
  }),
]

const prefix = {
  system: '/systems/:systemSlug',
  issuer: '/systems/:systemSlug/issuers/:issuerSlug',
  program: '/systems/:systemSlug/issuers/:issuerSlug/programs/:programSlug',
}
const publicPrefix = {
  system: '/public/' + prefix.system,
  issuer: '/public/' + prefix.issuer,
  program: '/public/' + prefix.program,
}

exports = module.exports = function applyBadgeRoutes (server) {
  const createNewInstanceSuffix = '/badges/:badgeSlug/instances'
  server.post(prefix.system + createNewInstanceSuffix,
              findSystemBadge, createNewInstance)

  server.post(prefix.issuer + createNewInstanceSuffix,
              findIssuerBadge, createNewInstance)

  server.post(prefix.program + createNewInstanceSuffix,
              findProgramBadge, createNewInstance)

  function createNewInstance(req, res, next) {
    const row = BadgeInstances.formatUserInput(req.body)
    row.badgeId = req.badge.id

    const errs = BadgeInstances.validateRow(row)
    if (errs.length)
      return res.send(400, errorHelper.validation(errs));

    if (!row.claimCode)
      return saveBadgeInstance()

    const query = {code: row.claimCode, badgeId: row.badgeId}
    ClaimCodes.getOne(query, function (err, code) {
      if (err) {
        log.error(err, 'error trying to find claim code')
        return next(err)
      }

      if (code.claimed && !code.multiuse) {
        return res.send(400, {
          code: 'CodeAlreadyUsed',
          message: 'Claim code `'+code.code+'` has already been claimed'
        })
      }

      code.claimed = true
      code.email = row.email

      ClaimCodes.put(code, function (err, result) {
        if (err) {
          log.error(err, 'error trying to find claim code')
          return next(err)
        }
        return saveBadgeInstance()
      })
    })

    function saveBadgeInstance() {
      BadgeInstances.put(row, function (err, result) {
        if (err) {
          log.error(err, 'error trying to save badge instance')
          return next(err)
        }
        const instance = result.row
        const relativeAssertionUrl = '/public/assertions/' + instance.slug
        const assertionUrl = req.resolvePath(relativeAssertionUrl)

        res.header('Location', relativeAssertionUrl)
        res.send(201, {
          status: 'created',
          location: assertionUrl,
        })

        // Webhook stuff shouldn't hold up the request, so we call
        // `next()` before looking up any hooks we have to send off
        next()

        const system = req.system
        const hookData = {
          action: 'award',
          uid: instance.slug,
          email: instance.email,
          assertionUrl: assertionUrl,
          issuedOn: unixtimeFromDate(instance.issuedOn),
        }
        Webhooks.getOne({systemId: system.id}, function (err, hook) {
          if (err)
            return log.error(err, 'Could not retrieve webhook')

          if (!hook)
            return log.info({code: 'WebhookNotFound', system: system}, 'Webhook not found for system')

          hook.call(hookData, function (err, res, body) {
            if (res.statusCode != 200)
              return log.warn({code: 'WebhookBadResponse', status: res.statusCode, body: body})
          })
        })
      })
    }
  }

  server.get('/public/assertions/:instanceSlug', getAssertion)
  function getAssertion(req, res, next) {
    const instanceSlug = req.params.instanceSlug
    const query = {slug: instanceSlug}
    const options = {relationships: true}
    BadgeInstances.getOne(query, options, function (err, instance) {
      if (!instance)
        return next(errorHelper.notFound('Could not find badge instance'))

      // get fully hydrated badge class
      const query = {id: instance.badge.id}
      const options = {relationships: true}
      Badges.getOne(query, options, function (err, badge) {
        if (err) return next(err)
        instance.badge = badge

        const assertion = makeAssertion(instance, req)
        res.send(200, assertion)
        return next()
      })
    })
  }
  function makeAssertion(instance, req) {
    const badge = instance.badge
    return {
      uid: instance.slug,
      recipient: {
        identity: 'sha256$' + sha256(instance.email),
        type: 'email',
        hashed: true,
      },
      badge: req.resolvePath(makeBadgeClassUrl(badge)),
      verify: {
        url: req.resolvePath('/public/assertions/' + instance.slug),
        type: 'hosted',
      },
      issuedOn: unixtimeFromDate(instance.issuedOn),
      expires: unixtimeFromDate(instance.expires),
    }
  }
  function makeBadgeClassUrl(badge) {
    const badgeSlug = badge.slug
    const programSlug = badge.program && badge.program.slug
    const issuerSlug = badge.issuer && badge.issuer.slug
    const systemSlug = badge.system && badge.system.slug

    if (programSlug && issuerSlug && systemSlug)
      return util.format('/public/systems/%s/issuers/%s/programs/%s/badges/%s',
                        systemSlug, issuerSlug, programSlug, badgeSlug)

    if (!programSlug && issuerSlug && systemSlug)
      return util.format('/public/systems/%s/issuers/%s/badges/%s',
                        systemSlug, issuerSlug, badgeSlug)

    if (!programSlug && !issuerSlug && systemSlug)
      return util.format('/public/systems/%s/badges/%s',
                        systemSlug, badgeSlug)

    log.error({badge: badge}, 'badge has incomplete parantage – sending broken assertion')
    return '/public'
  }

  server.get(publicPrefix.system +'/badges/:badgeSlug',
              findSystemBadge, getBadgeClass)

  server.get(publicPrefix.issuer +'/badges/:badgeSlug',
              findIssuerBadge, getBadgeClass)

  server.get(publicPrefix.program+'/badges/:badgeSlug',
              findProgramBadge, getBadgeClass)
  function getBadgeClass(req, res, next) {
    const badgeClass = makeBadgeClass(req.badge, req)
    res.send(200, badgeClass)
    return next()
  }
  function makeBadgeClass(badge, req) {
    // #TODO: alignment urls, tags
    var imageUrl = badge.image.toUrl()
    if (!/^http/.test(imageUrl))
      imageUrl = req.resolvePath(imageUrl)
    return {
      name: badge.name,
      description: badge.consumerDescription,
      image: badge.image.toUrl(),
      criteria: badge.criteriaUrl,
      issuer: req.resolvePath(publicIssuerUrl(badge)),
    }
  }
  function publicIssuerUrl(badge) {
    const system = badge.system
    const issuer = badge.issuer
    const program = badge.program
    if (program && program.slug)
      return util.format('/public/systems/%s/issuers/%s/programs/%s',
                         system.slug, issuer.slug, program.slug)
    if (issuer && issuer.slug)
      return util.format('/public/systems/%s/issuers',
                         system.slug, issuer.slug)
    if (badge.system && badge.system.slug)
      return util.format('/public/systems/%s', system.slug)
  }

  server.get('/public/systems/:systemSlug', [
    middleware.findSystem({relationships: true}),
    getIssuerClass,
  ])
  server.get('/public/systems/:systemSlug/issuers/:issuerSlug', [
    middleware.findSystem(),
    middleware.findIssuer({
      relationships: true,
      where: {systemId: ['system', 'id']},
    }),
    getIssuerClass,
  ])
  server.get('/public/systems/:systemSlug/issuers/:issuerSlug/programs/:programSlug', [
    middleware.findSystem(),
    middleware.findIssuer({where: {systemId: ['system', 'id']}}),
    middleware.findProgram({
      relationships: true,
      where: {issuerId: ['issuer', 'id']},
    }),
    getIssuerClass,
  ])
  function getIssuerClass(req, res, next) {
    return res.send(200, makeIssuerOrganization(
      req.program, req.issuer, req.system
    ))
  }
  function makeIssuerOrganization(program, issuer, system) {
    const lookup = findFirstKeyIn([program, issuer, system])
    return {
      name: lookup('name'),
      url: lookup('url'),
      description: lookup('description'),
      email: lookup('email'),
    }
  }
  function findFirstKeyIn(things) {
    things = things.map(function (o) { return o || {} })
    return function lookup(key) {
      for (var i = 0; i < things.length; i++)
        if (things[i][key]) return things[i][key]
    }
  }
}
<|endoftext|>


<commit_before>/**
 * Manages an IFRAME instance and provides helper functions for it.
 * (it is supposed to run only in a browser)
 */
registerModule(function (module, require){
	var exports=module.exports;

	var cbs=[],
		EventEmitter;
	__adaptor__.requireLib('events', function (exports){
		EventEmitter=exports.EventEmitter;
		while(cbs.length)
			cbs.shift()();
		cbs=null;
	});

	var Frame=function (cfg){
		$.extend(true, this, cfg); //		EventEmitter.call(this, arguments);
		this.initialize();
	};
	$.extend(Frame.prototype, {
		initialize:function (){
			this.el=$(document.createElement('iframe')).appendTo(document.body);
		},
		load:function (url){
			$(this.el).attr('src', url);
		}
	});

	exports.createFrame=function (cb){
		var obj;
		if(EventEmitter){
			obj=new Frame();
			cb&&setTimeout(function (){
				cb(obj);
			}, 15);
			return obj;
		}else if(cb)
			cbs.push(function (){
				cb(new Frame());
			});
	}

});
<commit_msg>added TO-DO note<commit_after>/**
 * Manages an IFRAME instance and provides helper functions for it.
 * (it is supposed to run only in a browser)
 */
/**
 * TO-DO: add the option to overlay the IFRAME so the user will not be able to click and interfere
 */
registerModule(function (module, require){
	var exports=module.exports;

	var cbs=[],
		EventEmitter;
	__adaptor__.requireLib('events', function (exports){
		EventEmitter=exports.EventEmitter;
		while(cbs.length)
			cbs.shift()();
		cbs=null;
	});

	var Frame=function (cfg){
		$.extend(true, this, cfg); //		EventEmitter.call(this, arguments);
		this.initialize();
	};
	$.extend(Frame.prototype, {
		initialize:function (){
			this.el=$(document.createElement('iframe')).appendTo(document.body);
		},
		load:function (url){
			$(this.el).attr('src', url);
		}
	});

	exports.createFrame=function (cb){
		var obj;
		if(EventEmitter){
			obj=new Frame();
			cb&&setTimeout(function (){
				cb(obj);
			}, 15);
			return obj;
		}else if(cb)
			cbs.push(function (){
				cb(new Frame());
			});
	}

});
<|endoftext|>


<commit_before>/*!
 * mailgun-js
 * Copyright(c) 2012 OneLobby <bojan@onelobby.com>
 * MIT Licensed
 */

var request = require('request');

/**
 * Creates new Mailgun object
 * @param api_key {String} the API Key for the mailgun account
 * @param domain {String} the mailgun domain
 * @type {Function}
 */
var Mailgun = module.exports = function (api_key, domain) {
  this.username = 'api'
  this.api_key = api_key;
  this.domain = domain;
  this.protocol = 'https://';
  this.host = 'api.mailgun.net';
  this.endpoint = '/v2';

  this.headers = {};
  var b = new Buffer([this.username, this.api_key].join(':'));
  this.headers['Authorization'] = "Basic " + b.toString('base64');
};

/**
 * Creates a new email message and sends it using mailgun
 * @param data {Object} the object containing the data to be sent.
 *                      Required parameters are 'to', 'from', and 'text' or 'html'
 * @param cb {Function} callback function accepting error, response and body
 * @type {Function}
 */
Mailgun.prototype.sendMessage = function (data, cb) {
  if (!data.to) {
    return cb(new Error('You must include a \'to\' number.'));
  }
  if (!data.from) {
    return cb(new Error('You must include a \'from\' number.'));
  }
  if (!data.text && !data.html) {
    return cb(new Error('You must include a \'text\' or \'html\' parameter.'));
  }

  return this.request('POST', '/messages', data, cb);
};

/**
 * Lists mailboxes associated with the mailgun account
 * @param data {Object} the optional object containing the GET options 'limit' and 'skip'
 * @param cb {Function} callback function accepting error, response and body
 * @type {Function}
 */
Mailgun.prototype.getMailboxes = function (data, cb) {
  return this.request('GET', '/mailboxes', data, cb);
};

/**
 * Creates a new mailbox associated with the mailgun account
 * @param data {Object} the object containing the new mailbox name ('mailbox') and new password ('password')
 * @param cb {Function} callback function accepting error, response and body
 * @type {Function}
 */
Mailgun.prototype.createMailbox = function (data, cb) {
  if (!data.mailbox) {
    return cb(new Error('You must include name of the mailbox in the \'mailbox\' parameter.'));
  }
  if (!data.password) {
    return cb(new Error('You must include a password for the new mailbox.'));
  }

  return this.request('POST', '/mailboxes', data, cb);
};

/**
 * Deletes a mailbox associated with the mailgun account
 * @param mailbox {String} the mailbox to be deleted
 * @param cb {Function} callback function accepting error, response and body
 * @type {Function}
 */
Mailgun.prototype.deleteMailbox = function (mailbox, cb) {
  if (!mailbox) {
    return cb(new Error('You must include name of the mailbox.'));
  }

  return this.request('DELETE', '/mailboxes/' + mailbox, cb);
};

/**
 * Updates the mailbox associated with the mailgun account
 * @param data {Object} the object containing the mailbox name ('mailbox') and new password ('password')
 * @param cb {Function} callback function accepting error, response and body
 * @type {Function}
 */
Mailgun.prototype.updateMailbox = function (data, cb) {
  if (!data.mailbox) {
    return cb(new Error('You must include name of the mailbox in the \'mailbox\' parameter.'));
  }
  if (!data.password) {
    return cb(new Error('You must include a password for the mailbox.'));
  }

  var mailbox = data.mailbox;
  delete data.mailbox;

  return this.request('PUT', '/mailboxes/' + mailbox, data, cb);
};

/**
 * Lists routes associated with the mailgun account
 * @param data {Object} the optional object containing the GET options 'limit' and 'skip'
 * @param cb {Function} callback function accepting error, response and body
 * @type {Function}
 */
Mailgun.prototype.getRoutes = function (data, cb) {
  return this.request('GET', '/routes', data, cb);
};

/**
 * Returns a single route object based on its ID.
 * @param id {String} the route ID
 * @param cb {Function} callback function accepting error, response and body
 * @type {Function}
 */
Mailgun.prototype.getRoute = function (id, cb) {
  if (!id) {
    return cb(new Error('You must include id of the route.'));
  }

  return this.request('GET', '/routes/' + id, cb);
};

/**
 * Creates a new route
 * @param data {Object} the object containing the priority, description, expression and action as strings
 * @param cb {Function} callback function accepting error, response and body
 * @type {Function}
 */
Mailgun.prototype.createRoute = function (data, cb) {
  if (!data.expression) {
    return cb(new Error('You must include route expression.'));
  }
  if (!data.action) {
    return cb(new Error('You must include route action.'));
  }

  return this.request('POST', '/routes', data, cb);
};

/**
 * Updates a given route by ID. All data parameters optional:
 * this API call only updates the specified fields leaving others unchanged.
 * @param id {String} the route ID
 * @param data {Object} the object containing the priority, description, expression and action as strings
 * @param cb {Function} callback function accepting error, response and body
 * @type {Function}
 */
Mailgun.prototype.updateRoute = function (id, data, cb) {
  if (!id) {
    return cb(new Error('You must include id of the route.'));
  }

  return this.request('PUT', '/routes/' + id, data, cb);
};

/**
 * Deletes a route based on the id.
 * @param id {String} the id of the route to be deleted
 * @param cb {Function} callback function accepting error, response and body
 * @type {Function}
 */
Mailgun.prototype.deleteRoute = function (id, cb) {
  if (!id) {
    return cb(new Error('You must include the ID of the mailbox.'));
  }

  return this.request('DELETE', '/routes/' + id, cb);
};

/**
 * The main function that does all the work. The client really shouldn't call this.
 * @param method {String} HTTP method
 * @param resource {String} the resource
 * @param data {Object} the data to be sent in the request
 * @param cb {Function} callback for the request
 * @return
 */

Mailgun.prototype.request = function (method, resource, data, cb) {

  var self = this;

  if (typeof data === 'function' && !cb) {
    cb = data;
    data = {};
  }

  var getDomain = function() {
    var d = '/' + self.domain;
    //filter out API calls that do not require a domain specified
    if (resource.indexOf('route') >= 0) {
      d = '';
    }
    return d;
  };

  var url = '';
  url = url.concat(
    this.protocol,
    this.username, ':', this.api_key,
    '@',
    this.host,
    this.endpoint,
    getDomain(),
    resource);

  console.log(url);

  var opts = {
    url: url,
    method: method,
    headers: this.headers,
    form: data
  };

  var responseCb = function (error, response, body) {
    if (response && (response.headers['content-type'] === 'application/json')) {
      try {
        body = JSON.parse(body);

        if (!error && response.statusCode !== 200) {
          error = new Error(body.message);
        }
      } catch (e) {

      }
    }

    return cb(error, response, body);
  };

  return request(opts, responseCb);
};<commit_msg>removed a console.log<commit_after>/*!
 * mailgun-js
 * Copyright(c) 2012 OneLobby <bojan@onelobby.com>
 * MIT Licensed
 */

var request = require('request');

/**
 * Creates new Mailgun object
 * @param api_key {String} the API Key for the mailgun account
 * @param domain {String} the mailgun domain
 * @type {Function}
 */
var Mailgun = module.exports = function (api_key, domain) {
  this.username = 'api'
  this.api_key = api_key;
  this.domain = domain;
  this.protocol = 'https://';
  this.host = 'api.mailgun.net';
  this.endpoint = '/v2';

  this.headers = {};
  var b = new Buffer([this.username, this.api_key].join(':'));
  this.headers['Authorization'] = "Basic " + b.toString('base64');
};

/**
 * Creates a new email message and sends it using mailgun
 * @param data {Object} the object containing the data to be sent.
 *                      Required parameters are 'to', 'from', and 'text' or 'html'
 * @param cb {Function} callback function accepting error, response and body
 * @type {Function}
 */
Mailgun.prototype.sendMessage = function (data, cb) {
  if (!data.to) {
    return cb(new Error('You must include a \'to\' number.'));
  }
  if (!data.from) {
    return cb(new Error('You must include a \'from\' number.'));
  }
  if (!data.text && !data.html) {
    return cb(new Error('You must include a \'text\' or \'html\' parameter.'));
  }

  return this.request('POST', '/messages', data, cb);
};

/**
 * Lists mailboxes associated with the mailgun account
 * @param data {Object} the optional object containing the GET options 'limit' and 'skip'
 * @param cb {Function} callback function accepting error, response and body
 * @type {Function}
 */
Mailgun.prototype.getMailboxes = function (data, cb) {
  return this.request('GET', '/mailboxes', data, cb);
};

/**
 * Creates a new mailbox associated with the mailgun account
 * @param data {Object} the object containing the new mailbox name ('mailbox') and new password ('password')
 * @param cb {Function} callback function accepting error, response and body
 * @type {Function}
 */
Mailgun.prototype.createMailbox = function (data, cb) {
  if (!data.mailbox) {
    return cb(new Error('You must include name of the mailbox in the \'mailbox\' parameter.'));
  }
  if (!data.password) {
    return cb(new Error('You must include a password for the new mailbox.'));
  }

  return this.request('POST', '/mailboxes', data, cb);
};

/**
 * Deletes a mailbox associated with the mailgun account
 * @param mailbox {String} the mailbox to be deleted
 * @param cb {Function} callback function accepting error, response and body
 * @type {Function}
 */
Mailgun.prototype.deleteMailbox = function (mailbox, cb) {
  if (!mailbox) {
    return cb(new Error('You must include name of the mailbox.'));
  }

  return this.request('DELETE', '/mailboxes/' + mailbox, cb);
};

/**
 * Updates the mailbox associated with the mailgun account
 * @param data {Object} the object containing the mailbox name ('mailbox') and new password ('password')
 * @param cb {Function} callback function accepting error, response and body
 * @type {Function}
 */
Mailgun.prototype.updateMailbox = function (data, cb) {
  if (!data.mailbox) {
    return cb(new Error('You must include name of the mailbox in the \'mailbox\' parameter.'));
  }
  if (!data.password) {
    return cb(new Error('You must include a password for the mailbox.'));
  }

  var mailbox = data.mailbox;
  delete data.mailbox;

  return this.request('PUT', '/mailboxes/' + mailbox, data, cb);
};

/**
 * Lists routes associated with the mailgun account
 * @param data {Object} the optional object containing the GET options 'limit' and 'skip'
 * @param cb {Function} callback function accepting error, response and body
 * @type {Function}
 */
Mailgun.prototype.getRoutes = function (data, cb) {
  return this.request('GET', '/routes', data, cb);
};

/**
 * Returns a single route object based on its ID.
 * @param id {String} the route ID
 * @param cb {Function} callback function accepting error, response and body
 * @type {Function}
 */
Mailgun.prototype.getRoute = function (id, cb) {
  if (!id) {
    return cb(new Error('You must include id of the route.'));
  }

  return this.request('GET', '/routes/' + id, cb);
};

/**
 * Creates a new route
 * @param data {Object} the object containing the priority, description, expression and action as strings
 * @param cb {Function} callback function accepting error, response and body
 * @type {Function}
 */
Mailgun.prototype.createRoute = function (data, cb) {
  if (!data.expression) {
    return cb(new Error('You must include route expression.'));
  }
  if (!data.action) {
    return cb(new Error('You must include route action.'));
  }

  return this.request('POST', '/routes', data, cb);
};

/**
 * Updates a given route by ID. All data parameters optional:
 * this API call only updates the specified fields leaving others unchanged.
 * @param id {String} the route ID
 * @param data {Object} the object containing the priority, description, expression and action as strings
 * @param cb {Function} callback function accepting error, response and body
 * @type {Function}
 */
Mailgun.prototype.updateRoute = function (id, data, cb) {
  if (!id) {
    return cb(new Error('You must include id of the route.'));
  }

  return this.request('PUT', '/routes/' + id, data, cb);
};

/**
 * Deletes a route based on the id.
 * @param id {String} the id of the route to be deleted
 * @param cb {Function} callback function accepting error, response and body
 * @type {Function}
 */
Mailgun.prototype.deleteRoute = function (id, cb) {
  if (!id) {
    return cb(new Error('You must include the ID of the mailbox.'));
  }

  return this.request('DELETE', '/routes/' + id, cb);
};

/**
 * The main function that does all the work. The client really shouldn't call this.
 * @param method {String} HTTP method
 * @param resource {String} the resource
 * @param data {Object} the data to be sent in the request
 * @param cb {Function} callback for the request
 * @return
 */

Mailgun.prototype.request = function (method, resource, data, cb) {

  var self = this;

  if (typeof data === 'function' && !cb) {
    cb = data;
    data = {};
  }

  var getDomain = function() {
    var d = '/' + self.domain;
    //filter out API calls that do not require a domain specified
    if (resource.indexOf('route') >= 0) {
      d = '';
    }
    return d;
  };

  var url = '';
  url = url.concat(
    this.protocol,
    this.username, ':', this.api_key,
    '@',
    this.host,
    this.endpoint,
    getDomain(),
    resource);

  var opts = {
    url: url,
    method: method,
    headers: this.headers,
    form: data
  };

  var responseCb = function (error, response, body) {
    if (response && (response.headers['content-type'] === 'application/json')) {
      try {
        body = JSON.parse(body);

        if (!error && response.statusCode !== 200) {
          error = new Error(body.message);
        }
      } catch (e) {

      }
    }

    return cb(error, response, body);
  };

  return request(opts, responseCb);
};<|endoftext|>


<commit_before>/*
 * weh - WebExtensions Helper
 *
 * @summary workflow and base code for developing WebExtensions browser add-ons
 * @author Michel Gutierrez
 * @link https://github.com/mi-g/weh
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

var weh = require('weh');
var browser = weh.browser;

/* extracting running parameters from URL */
var urlParams = typeof _wehPanelName !== "undefined" && { panel: _wehPanelName } || function () {
	var m = /^([^\?]*)(?:\?(.*))?$/.exec(window.location.href);
	var params = {};
	if (m[2]) m[2].split("&").forEach(function (paramExpr) {
		var terms = paramExpr.split("=");
		params[terms[0]] = decodeURIComponent(terms[1]);
	});
	return params;
}();

if (!urlParams.panel) throw new Error("Panel name not defined in URL");

weh.uiName = urlParams.panel;
var usePrefs = !urlParams.noprefs;

/* setting up RPC */
weh.rpc = require('weh-rpc');
//weh.rpc.setDebug(2);
weh.rpc.listen({
	close: () => {
		window.close()
	}
})

/* connecting communication port with background */
var port = browser.runtime.connect({ name: "weh:" + browser.runtime.id + ":" + weh.uiName });
weh.rpc.setPost(port.postMessage.bind(port));
port.onMessage.addListener((message) => {
	weh.rpc.receive(message, port.postMessage.bind(port));
});

/* notify background app is started */
weh.rpc.call("appStarted", {
		uiName: weh.uiName,
		usePrefs: usePrefs
	}).then(function () {
	}).catch(function (err) {
		console.info("appStarted failed", err);
	});

/* initializing */
var readyPromises = [
	new Promise((resolve,reject)=>{
		window.addEventListener("DOMContentLoaded", function () {
			resolve();
		});

	})
];

if(usePrefs) {
	let wehPrefs = require('weh-prefs');
	weh.prefs = wehPrefs;
	let initialPrefs = {};
	try {
		let prefsStr = localStorage.getItem("weh-prefs");
		if(prefsStr)
			JSON.parse(prefsStr).forEach((entry)=>{
				initialPrefs[entry.name] = entry.value;
			});
	} catch(e) {}
	wehPrefs.assign(initialPrefs);
	wehPrefs.on("", {
		pack: true
	}, function (newPrefs, oldPrefs) {
		weh.rpc.call("prefsSet",newPrefs);
	});
	readyPromises.push(new Promise((resolve,reject)=>{
		weh.rpc.call("prefsGetSpecs")
			.then(function (specs) {
				wehPrefs.declare(specs);
				return weh.rpc.call("prefsGetAll");
			})
			.then((allPrefs)=>{
				wehPrefs.assign(allPrefs);
				wehPrefs.forceNotify(false);
				resolve();
			})
		.catch(reject);
	}));
	weh.rpc.listen({
		setPrefs: (prefs) => {
			wehPrefs.assign(prefs);
		}
	});

}

/* notifies app ready: DOM and prefs, if used, are loaded */
Promise.all(readyPromises)
	.then(()=>{
		return weh.rpc.call("appReady",{
			uiName: weh.uiName
		});
	})
	.catch((err)=>{
		console.error("app not ready:",err);
	});

/* setting up translation */
weh._ = require("weh-i18n").getMessage;

/* utility functions */
weh.copyToClipboard = function (data, mimeType) {
	mimeType = mimeType || "text/plain";
	document.oncopy = function (event) {
		event.clipboardData.setData(mimeType, data);
		event.preventDefault();
	};
	document.execCommand("Copy", false, null);
};
weh.setPageTitle = function (title) {
	var titleElement = document.querySelector("head title");
	if (!titleElement) {
		titleElement = document.createElement("title");
		document.head.appendChild(titleElement);
	} else while (titleElement.firstChild)
		titleElement.removeChild(titleElement.firstChild);
	titleElement.appendChild(document.createTextNode(title));
};

weh.trigger = function (result) {
	return weh.rpc.call("trigger",weh.uiName,result)
		.catch(()=>{});
}

module.exports = weh;<commit_msg>ensures trigger after appReady<commit_after>/*
 * weh - WebExtensions Helper
 *
 * @summary workflow and base code for developing WebExtensions browser add-ons
 * @author Michel Gutierrez
 * @link https://github.com/mi-g/weh
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

var weh = require('weh');
var browser = weh.browser;

/* extracting running parameters from URL */
var urlParams = typeof _wehPanelName !== "undefined" && { panel: _wehPanelName } || function () {
	var m = /^([^\?]*)(?:\?(.*))?$/.exec(window.location.href);
	var params = {};
	if (m[2]) m[2].split("&").forEach(function (paramExpr) {
		var terms = paramExpr.split("=");
		params[terms[0]] = decodeURIComponent(terms[1]);
	});
	return params;
}();

if (!urlParams.panel) throw new Error("Panel name not defined in URL");

weh.uiName = urlParams.panel;
var usePrefs = !urlParams.noprefs;

/* setting up RPC */
weh.rpc = require('weh-rpc');
//weh.rpc.setDebug(2);
weh.rpc.listen({
	close: () => {
		window.close()
	}
})

/* connecting communication port with background */
var port = browser.runtime.connect({ name: "weh:" + browser.runtime.id + ":" + weh.uiName });
weh.rpc.setPost(port.postMessage.bind(port));
port.onMessage.addListener((message) => {
	weh.rpc.receive(message, port.postMessage.bind(port));
});

/* notify background app is started */
weh.rpc.call("appStarted", {
		uiName: weh.uiName,
		usePrefs: usePrefs
	}).catch(function (err) {
		console.info("appStarted failed", err);
	});

/* initializing */
var readyPromises = [
	new Promise((resolve,reject)=>{
		window.addEventListener("DOMContentLoaded", function () {
			resolve();
		});

	})
];

if(usePrefs) {
	let wehPrefs = require('weh-prefs');
	weh.prefs = wehPrefs;
	let initialPrefs = {};
	try {
		let prefsStr = localStorage.getItem("weh-prefs");
		if(prefsStr)
			JSON.parse(prefsStr).forEach((entry)=>{
				initialPrefs[entry.name] = entry.value;
			});
	} catch(e) {}
	wehPrefs.assign(initialPrefs);
	wehPrefs.on("", {
		pack: true
	}, function (newPrefs, oldPrefs) {
		weh.rpc.call("prefsSet",newPrefs);
	});
	readyPromises.push(new Promise((resolve,reject)=>{
		weh.rpc.call("prefsGetSpecs")
			.then(function (specs) {
				wehPrefs.declare(specs);
				return weh.rpc.call("prefsGetAll");
			})
			.then((allPrefs)=>{
				wehPrefs.assign(allPrefs);
				wehPrefs.forceNotify(false);
				resolve();
			})
		.catch(reject);
	}));
	weh.rpc.listen({
		setPrefs: (prefs) => {
			wehPrefs.assign(prefs);
		}
	});

}

/* notifies app ready: DOM and prefs, if used, are loaded */
Promise.all(readyPromises)
	.then(()=>{
		return weh.rpc.call("appReady",{
			uiName: weh.uiName
		});
	}).then(function () {
		if(triggerRequested) {
			let result = triggerArgs;
			triggerArgs = undefined;
			triggerRequested = false;
			weh.doTrigger(result);
		}
	})
	.catch((err)=>{
		console.error("app not ready:",err);
	});

var triggerRequested = false;
var triggerArgs = undefined;
var appStarted = false;

weh.doTrigger = function (result) {
	return weh.rpc.call("trigger",weh.uiName,result)
		.catch(()=>{});
}

weh.trigger = function (result) {
	if(appStarted)
		return weh.doTrigger(result);
	else {
		triggerArgs = result;
		triggerRequested = true;
	}
}

/* setting up translation */
weh._ = require("weh-i18n").getMessage;

/* utility functions */
weh.copyToClipboard = function (data, mimeType) {
	mimeType = mimeType || "text/plain";
	document.oncopy = function (event) {
		event.clipboardData.setData(mimeType, data);
		event.preventDefault();
	};
	document.execCommand("Copy", false, null);
};
weh.setPageTitle = function (title) {
	var titleElement = document.querySelector("head title");
	if (!titleElement) {
		titleElement = document.createElement("title");
		document.head.appendChild(titleElement);
	} else while (titleElement.firstChild)
		titleElement.removeChild(titleElement.firstChild);
	titleElement.appendChild(document.createTextNode(title));
};

module.exports = weh;<|endoftext|>


<commit_before>import PropTypes from 'prop-types'
import { Component, createElement } from 'react'

/**
 * Public API
 */

export function createProvide(store) {
  return function provide(WrappedComponent, keys) {
    if ('function' === typeof WrappedComponent)
      return createAlfaProvidedComponent(store, WrappedComponent, keys,
        isReactComponent(WrappedComponent) && 'component')
    else
      throw new Error('alfa.provide only accepts function or class.')
  }
}

export function createSubscribe(store) {
  return function subscribe(WrappedComponent, keys) {
    if ('function' === typeof WrappedComponent)
      return createAlfaSubscribedComponent(store, WrappedComponent, keys)
    else
      throw new Error('alfa.subscribe only accepts function or class.')
  }
}

/**
 * Private functions
 */

function isReactComponent(Component) {
  return Component.prototype && Component.prototype.isReactComponent
}

function createAlfaProvidedComponent(store, WrappedComponent, keys, type) {
  // Keep the name of the orginal component which makes debugging logs easier
  // to understand.
  var componentName = WrappedComponent.name || 'AlfaProvidedComponent'

  // Solution 1
  var wrapper = {
    [componentName]: function(props, context, updater) {
      // See if we have an alternative alfa store to use.
      store = context && context.alfaStore ? context.alfaStore : store
      // Props passed in directly to constructor has higher priority than keys
      // injected from the store.
      props = {
        ...store.get(keys),
        ...props || {}
      }

      if ('component' === type)
        // Create an element if it's react component.
        return createElement(WrappedComponent, props)
      else
        // Otherwise, call the original function.
        return WrappedFunction(props, context, updater)
    }
  }

  wrapper[componentName].contextTypes = {
    alfaStore: PropTypes.object
  }

  return wrapper[componentName]
}

function createAlfaSubscribedComponent(store, WrappedComponent, keys) {
  var subFunc

  class AlfaSubscribedComponent extends Component {
    // Keep the name of the orginal component which makes debugging logs easier
    // to understand.
    static get name() {
      return WrappedComponent.name
    }

    static contextTypes = {
      alfaStore: PropTypes.object
    }

    constructor(props, context, updater) {
      // Call the original constructor.
      super(props, context, updater)
      // See if we have an alternative alfa store to use.
      store = context && context.alfaStore ? context.alfaStore : store

      // Inject all keys as state.
      this.state = store.get(keys)

      // Call `setState` when subscribed keys changed.
      if ('function' === typeof this.setState) {
        // Make sure we use the correct store for unsubscribe.
        this.store = store
        subFunc = this.setState.bind(this)
        store.subscribe(keys, subFunc)
      }
    }

    componentWillUnmount() {
      'function' === typeof subFunc && this.store.unsubscribe(subFunc)
    }

    render() {
      // State injected may change during normal component lifecycle.
      // So in this case it has higher priority than props.
      var _props = {
        ...this.props || {},
        ...this.state
      }

      // State and props are merged and passed to wrapped component as props.
      return createElement(WrappedComponent, _props)
    }
  }

  return AlfaSubscribedComponent
}
<commit_msg>Fix a typo.<commit_after>import PropTypes from 'prop-types'
import { Component, createElement } from 'react'

/**
 * Public API
 */

export function createProvide(store) {
  return function provide(WrappedComponent, keys) {
    if ('function' === typeof WrappedComponent)
      return createAlfaProvidedComponent(store, WrappedComponent, keys,
        isReactComponent(WrappedComponent) && 'component')
    else
      throw new Error('alfa.provide only accepts function or class.')
  }
}

export function createSubscribe(store) {
  return function subscribe(WrappedComponent, keys) {
    if ('function' === typeof WrappedComponent)
      return createAlfaSubscribedComponent(store, WrappedComponent, keys)
    else
      throw new Error('alfa.subscribe only accepts function or class.')
  }
}

/**
 * Private functions
 */

function isReactComponent(Component) {
  return Component.prototype && Component.prototype.isReactComponent
}

function createAlfaProvidedComponent(store, WrappedComponent, keys, type) {
  // Keep the name of the orginal component which makes debugging logs easier
  // to understand.
  var componentName = WrappedComponent.name || 'AlfaProvidedComponent'

  var wrapper = {
    [componentName]: function(props, context, updater) {
      // See if we have an alternative alfa store to use.
      store = context && context.alfaStore ? context.alfaStore : store
      // Props passed in directly to constructor has higher priority than keys
      // injected from the store.
      props = {
        ...store.get(keys),
        ...props || {}
      }

      if ('component' === type)
        // Create an element if it's react component.
        return createElement(WrappedComponent, props)
      else
        // Otherwise, call the original function.
        return WrappedComponent(props, context, updater)
    }
  }

  wrapper[componentName].contextTypes = {
    alfaStore: PropTypes.object
  }

  return wrapper[componentName]
}

function createAlfaSubscribedComponent(store, WrappedComponent, keys) {
  var subFunc

  class AlfaSubscribedComponent extends Component {
    // Keep the name of the orginal component which makes debugging logs easier
    // to understand.
    static get name() {
      return WrappedComponent.name
    }

    static contextTypes = {
      alfaStore: PropTypes.object
    }

    constructor(props, context, updater) {
      // Call the original constructor.
      super(props, context, updater)
      // See if we have an alternative alfa store to use.
      store = context && context.alfaStore ? context.alfaStore : store

      // Inject all keys as state.
      this.state = store.get(keys)

      // Call `setState` when subscribed keys changed.
      if ('function' === typeof this.setState) {
        // Make sure we use the correct store for unsubscribe.
        this.store = store
        subFunc = this.setState.bind(this)
        store.subscribe(keys, subFunc)
      }
    }

    componentWillUnmount() {
      'function' === typeof subFunc && this.store.unsubscribe(subFunc)
    }

    render() {
      // State injected may change during normal component lifecycle.
      // So in this case it has higher priority than props.
      var _props = {
        ...this.props || {},
        ...this.state
      }

      // State and props are merged and passed to wrapped component as props.
      return createElement(WrappedComponent, _props)
    }
  }

  return AlfaSubscribedComponent
}
<|endoftext|>


<commit_before>(function(angular){
    'use strict';

    var app = angular.module('activities.controllers', []);

    app.controller('PHPCtrl', ['$scope', '$sce', 'Progress',
        function ($scope, $sce, Progress) {

            $scope.disableResultTab = true;
            $scope.cm_refresh = 0;
            $scope.iframeRefresh = 0;

            $scope.refresh  = function() {
                $scope.cm_refresh += 1;
            };

            $scope.answer.$promise.finally(function() {
                if (!$scope.answer.id) {
                    $scope.answer.given = $scope.currentActivity.data;
                }
                $scope.answer.given[0].active = true;
                $scope.refresh();
            });

             $scope.sendPhpAnswer = function() {
                 $scope.answer.activity = $scope.currentActivity.id;
                 $scope.answer.$update({activityId: $scope.answer.activity}, function () {
                     $scope.disableResultTab = false;
                     $scope.resultUrl = $sce.trustAsResourceUrl('http://php' + $scope.answer.user_id + '.timtec.com.br/');
                     $scope.iframeRefresh += 1;
                 }).then(function(d){
//                    ga('send', 'event', 'activity', 'result', '', d.correct);
                     return Progress.getProgressByUnitId($scope.currentUnit.id);
                 }).then(function(progress){
                     $scope.currentUnit.progress = progress;
                 });
//                ga('send', 'event', 'activity', 'submit');

            };

            $scope.codemirrorLoaded = function(cm){
                // FIXME refactor this.
                var pid = setInterval(function(){
                    if ($scope.cm_refresh < 20) {
                        $scope.refresh();
                    } else
                        clearInterval(pid);
                }, 500);
            };

            $scope.codemirrorConfig = {
                        lineNumbers:true,
                        theme:'monokai',
                        matchTags: {bothTags: true},
                        matchBrackets: true,
                        extraKeys: {'Ctrl-J': 'toMatchingTag',
                                    'Ctrl-Space': 'autocomplete'},
                        mode:'php',
                        onLoad : $scope.codemirrorLoaded
            };
        }
    ]);

    app.controller('RelationshipCtrl', ['$scope',
        function ($scope) {
            $scope.possibleAnswers = [];
            angular.forEach($scope.currentActivity.expected, function(value) {
                this.push(value);
            }, $scope.possibleAnswers);
        }
    ]);
})(angular);
<commit_msg>now we don't give the order of the answers :)<commit_after>(function(angular){
    'use strict';

    var app = angular.module('activities.controllers', []);

    app.controller('PHPCtrl', ['$scope', '$sce', 'Progress',
        function ($scope, $sce, Progress) {

            $scope.disableResultTab = true;
            $scope.cm_refresh = 0;
            $scope.iframeRefresh = 0;

            $scope.refresh  = function() {
                $scope.cm_refresh += 1;
            };

            $scope.answer.$promise.finally(function() {
                if (!$scope.answer.id) {
                    $scope.answer.given = $scope.currentActivity.data;
                }
                $scope.answer.given[0].active = true;
                $scope.refresh();
            });

             $scope.sendPhpAnswer = function() {
                 $scope.answer.activity = $scope.currentActivity.id;
                 $scope.answer.$update({activityId: $scope.answer.activity}, function () {
                     $scope.disableResultTab = false;
                     $scope.resultUrl = $sce.trustAsResourceUrl('http://php' + $scope.answer.user_id + '.timtec.com.br/');
                     $scope.iframeRefresh += 1;
                 }).then(function(d){
//                    ga('send', 'event', 'activity', 'result', '', d.correct);
                     return Progress.getProgressByUnitId($scope.currentUnit.id);
                 }).then(function(progress){
                     $scope.currentUnit.progress = progress;
                 });
//                ga('send', 'event', 'activity', 'submit');

            };

            $scope.codemirrorLoaded = function(cm){
                // FIXME refactor this.
                var pid = setInterval(function(){
                    if ($scope.cm_refresh < 20) {
                        $scope.refresh();
                    } else
                        clearInterval(pid);
                }, 500);
            };

            $scope.codemirrorConfig = {
                        lineNumbers:true,
                        theme:'monokai',
                        matchTags: {bothTags: true},
                        matchBrackets: true,
                        extraKeys: {'Ctrl-J': 'toMatchingTag',
                                    'Ctrl-Space': 'autocomplete'},
                        mode:'php',
                        onLoad : $scope.codemirrorLoaded
            };
        }
    ]);

    app.controller('RelationshipCtrl', ['$scope',
        function ($scope) {
            $scope.possibleAnswers = $scope.currentActivity.expected.slice(0);
            function compareNumbers(a, b) {
              return a - b;
            }
            $scope.possibleAnswers.sort(compareNumbers);
        }
    ]);
})(angular);
<|endoftext|>


<commit_before>import Message from './EmailMessage';

const PRIVATE = Symbol();

export default class Folder {
  static Inbox() {
    return new Folder('inbox', { type: 'distinguished' });
  }
  static Sent() {
    return new Folder('sentitems', { type: 'distinguished' });
  }
  constructor(name, { type } = {}) {
    this[PRIVATE] = {};
    this[PRIVATE].name = name;
    this[PRIVATE].type = type;
  }

  get list() {
    const folder = this;
    return function ({
      maxEntries = 30,
      offset = 0,
      basePoint = 'Beginning',
      allDetails = false,
    } = {}) {
      return new Promise((resolve, reject) => {
        this.client.FindItem({
          attributes: {Traversal: 'Shallow'},
          ItemShape: {
            BaseShape: allDetails ? 'AllProperties' : 'Default',
          },
          IndexedPageItemView: {
            attributes: {
              MaxEntriesReturned: maxEntries,
              BasePoint: basePoint,
              Offset: offset,
            },
          },
          ParentFolderIds: folder.asEws(),
        }, (err, resp) => {
          if (err) {
            const err2 = new Error(`Network error fetching folder '${folder[PRIVATE].name}'`);
            err2.original = err;
            return reject(err2);
          }
          const {
            ResponseMessages: {
              FindItemResponseMessage: {
                RootFolder: {
                  Items: items
                }
              }
            }
          } = resp;
          const {
            Message: messages,
          } = items;
          resolve({
            messages: messages.map(Message.fromResponse),
          });
        });

      });
    }
  }

  asEws() {
    if (this[PRIVATE].type === 'distinguished') {
      return {
        DistinguishedFolderId: {
          attributes: {
            Id: this[PRIVATE].name,
          },
        },
      };
    }
    throw new Error(`Unrecognized folder type ${this[PRIVATE].type}`);
  }
}
<commit_msg>Handle non-special folders and add more special ones<commit_after>import Message from './EmailMessage';

const PRIVATE = Symbol();

export default class Folder {
  static Inbox() {
    return new Folder('inbox', { type: 'distinguished' });
  }
  static Sent() {
    return new Folder('sentitems', { type: 'distinguished' });
  }
  constructor(name, { type, id } = {}) {
    this[PRIVATE] = { type, id };
  }

  get list() {
    const folder = this;
    return function ({
      maxEntries = 30,
      offset = 0,
      basePoint = 'Beginning',
      allDetails = false,
    } = {}) {
      return new Promise((resolve, reject) => {
        this.client.FindItem({
          attributes: {Traversal: 'Shallow'},
          ItemShape: {
            BaseShape: allDetails ? 'AllProperties' : 'Default',
          },
          IndexedPageItemView: {
            attributes: {
              MaxEntriesReturned: maxEntries,
              BasePoint: basePoint,
              Offset: offset,
            },
          },
          ParentFolderIds: folder.asEws(),
        }, (err, resp) => {
          if (err) {
            const err2 = new Error(`Network error fetching folder '${folder[PRIVATE].name}'`);
            err2.original = err;
            return reject(err2);
          }
          const {
            ResponseMessages: {
              FindItemResponseMessage: {
                RootFolder: {
                  Items: items
                }
              }
            }
          } = resp;
          const {
            Message: messages,
          } = items;
          resolve({
            messages: messages.map(Message.fromResponse),
          });
        });

      });
    }
  }

  asEws() {
    if (this[PRIVATE].type === 'distinguished') {
      return {
        DistinguishedFolderId: {
          attributes: this[PRIVATE].id || {
            Id: this[PRIVATE].name,
          },
        },
      };
    }
    return {
      FolderId: {
        attributes: this[PRIVATE].id,
      },
    };
  }

  static fromResponse(r) {
    const {
      FolderId: id,
      DisplayName: name,
      TotalCount: count,
      ChildFolderCount: childFolderCount,
      UnreadCount: unreadCount,
    } = r;
    const folder = new Folder(name, { id });
    Object.assign(folder[PRIVATE], { count, childFolderCount, unreadCount });
    return folder;
  }
}

const SPECIAL_FOLDERS = {
  Inbox: 'inbox',
  Sent: 'sentitems',
  Calendar: 'calendar',
  Outbox: 'outbox',
  Root: 'root',
  InstantMessageContacts: 'imcontactlist',
  Favorites: 'favorites',
  Junk: 'junkemail',
  Contacts: 'contacts',
  Drafts: 'drafts',
  Tasks: 'tasks',
  Trash: 'recoverableitemsroot',
  Archive: 'archiveroot',
  ArchiveInbox: 'archiveinbox',
}

Object.keys(SPECIAL_FOLDERS).forEach(k => {
  const f = function () {
    return new Folder(SPECIAL_FOLDERS[k], { type: 'distinguished' });
  };
  f.name = k;
  return f;
});
<|endoftext|>


<commit_before>import { isElementInViewport, findParentWithClass } from '~/utils/dom'

var tooltipNode;
var $highlightedPost;
var modifiedQuotelinks = [];

const { headerHeight } = Lurka.settings;

const highlightClass = 'highlight';
const animateClass = ' ' + 'animate';
const tooltipClass = 'ThreadPost tip';

// Space between the tooltip and the quote link
const tooltipMargin = 5

// When a person quotes multiple posts, what text should be used to
// distinguish the post you're on from the other posts
// e.g. hover over a quote, a tooltip pops up with multple quotes. Which one is
// replying to the post you're on?
const quoteDistinguisher = " (This)";

// Distance from the right side of the screen. When to render the tooltip on
// the left instead of the right
const rightThreshold = 200;

export const createTooltipCreator = ($thread) => {
    return function (event) {

        const target = event.target,
              href = target.getAttribute('href'),
              $post = $thread.find(href),
              linkPos = target.getBoundingClientRect()  // abs position of link

        highlightQuotedIDIfMultiple($post, target);

        if (isElementInViewport($post[0])) {
            $post.addClass(highlightClass)
            $highlightedPost = $post
            return
        }

        console.groupCollapsed('%c Tooltip', 'color:gold');

        let left, top, el = document.createElement('div')

        el.innerHTML = $post.html()
        el.className = tooltipClass

        document.body.appendChild(el)

        // Set distance from left == to the hovered link
        left = linkPos.left

        // Set top to render above the link
        top = linkPos.top - el.offsetHeight - tooltipMargin

        // Check if would render out of page vertically
        if (top < headerHeight) {
            // top of element overflowed
            console.log("top overflowed")

            // render below
            top = linkPos.bottom + tooltipMargin

            // check if bottom will overflow
            if (top + el.offsetHeight > window.innerHeight) {
                // center element around link
                console.warn("bottom overflowed")

                top = linkPos.top - (el.offsetHeight / 2) - target.offsetHeight / 2
                left = linkPos.right + tooltipMargin

                if (top < headerHeight || top + el.offsetHeight > window.innerHeight) {
                    // element overflows; render at top of window
                    console.warn("Can't center. Placed at top.")
                    top = headerHeight+2 // 2 == header margin
                }
            }
        }

        // Check if right side overflows
        if (left + el.offsetWidth > window.innerWidth - rightThreshold) {
            left -= el.offsetWidth/2 - 26;
        }

        // Overflows on left when screen width is small
        if (left <= 0 || left + el.offsetWidth >= window.innerWidth) {
            left = 0
        }

        console.log(`top: ${top}, left: ${left}, el.offsetHeight: ${el.offsetHeight} linkPos.top: ${linkPos.top}`);
        console.groupEnd()

        el.style.top  = top + 'px'
        el.style.left = left + 'px'

        el.className += animateClass

        tooltipNode = el
    }
}

export const destroyTooltip = () => {
    if (tooltipNode) {
        document.body.removeChild(tooltipNode);
        tooltipNode = null;
    } else if ($highlightedPost) {
        $highlightedPost.removeClass(highlightClass);
        $highlightedPost = null;
    }

    if (modifiedQuotelinks.length) {
        for (let i = 0; i < modifiedQuotelinks.length; i++) {
            modifiedQuotelinks[i].text =
                modifiedQuotelinks[i].text.replace(quoteDistinguisher, "");
        }

        modifiedQuotelinks = []
    }
}

function highlightQuotedIDIfMultiple($post, target) {
    const id = findParentWithClass(target, 'ThreadPost').id.replace('p', '');
    const $quotes = $post.find('blockquote .quotelink');

    if ($quotes.length <= 1) {
        console.info("Tooltip contained single quote");
        return
    }

    $quotes.each( function( ) {
        if (this.text.includes(id)) {
            this.text += quoteDistinguisher;
            modifiedQuotelinks.push(this);
        } else {
            console.info(`${id} not in ${this.text}`)
        }
    });
}

<commit_msg>docs(Thread): Update tooltip documentation<commit_after>import { isElementInViewport, findParentWithClass } from '~/utils/dom'

var tooltipNode;
var $highlightedPost;
var modifiedQuotelinks = [];

const { headerHeight } = Lurka.settings;

const highlightClass = 'highlight';
const animateClass = ' ' + 'animate';
const tooltipClass = 'ThreadPost tip';

// Space between the tooltip and the quote link
const tooltipMargin = 5

// When someone quotes multiple posts, this is appended to the current quote
// to distinguish it from the others.
const quoteDistinguisher = " (This)";

// Distance from the right side of the screen. Used to determine when to render
// the tooltip on the left instead of the right
const rightThreshold = 200;

export const createTooltipCreator = ($thread) => {
    return function (event) {

        const target = event.target,
              href = target.getAttribute('href'),
              $post = $thread.find(href),
              linkPos = target.getBoundingClientRect()  // abs position of link

        console.groupCollapsed('%cQuotelink Tooltip', 'color:gold');

        highlightQuotedIDIfMultiple($post, target);

        if (isElementInViewport($post[0])) {
            $post.addClass(highlightClass)
            $highlightedPost = $post
            return
        }

        let left, top, el = document.createElement('div')

        el.innerHTML = $post.html()
        el.className = tooltipClass

        document.body.appendChild(el)

        // Set distance from left == to the hovered link
        left = linkPos.left

        // Set top to render above the link
        top = linkPos.top - el.offsetHeight - tooltipMargin

        // Check if would render out of page vertically
        if (top < headerHeight) {
            // top of element overflowed
            console.log("top overflowed")

            // render below
            top = linkPos.bottom + tooltipMargin

            // check if bottom will overflow
            if (top + el.offsetHeight > window.innerHeight) {
                // center element around link
                console.warn("bottom overflowed")

                top = linkPos.top - (el.offsetHeight / 2) - target.offsetHeight / 2
                left = linkPos.right + tooltipMargin

                if (top < headerHeight || top + el.offsetHeight > window.innerHeight) {
                    // element overflows; render at top of window
                    console.warn("Can't center. Placed at top.")
                    top = headerHeight+2 // 2 == header margin
                }
            }
        }

        // Check if right side overflows
        if (left + el.offsetWidth > window.innerWidth - rightThreshold) {
            left -= el.offsetWidth/2 - 26;
        }

        // Overflows on left when screen width is small
        if (left <= 0 || left + el.offsetWidth >= window.innerWidth) {
            left = 0
        }

        console.log(`top: ${top}, left: ${left}, el.offsetHeight: ${el.offsetHeight} linkPos.top: ${linkPos.top}`);
        console.groupEnd()

        el.style.top  = top + 'px'
        el.style.left = left + 'px'

        el.className += animateClass

        tooltipNode = el
    }
}

export const destroyTooltip = () => {
    if (tooltipNode) {
        document.body.removeChild(tooltipNode);
        tooltipNode = null;
    } else if ($highlightedPost) {
        $highlightedPost.removeClass(highlightClass);
        $highlightedPost = null;
    }

    if (modifiedQuotelinks.length) {
        for (let i = 0; i < modifiedQuotelinks.length; i++) {
            modifiedQuotelinks[i].text =
                modifiedQuotelinks[i].text.replace(quoteDistinguisher, "");
        }

        modifiedQuotelinks = []
    }
}

function highlightQuotedIDIfMultiple($post, target) {
    const id = findParentWithClass(target, 'ThreadPost').id.replace('p', '');
    const $quotes = $post.find('blockquote .quotelink');

    if ($quotes.length == 1) {
        // Tooltip only has 1 quote. No need to highlight it.
        return
    }

    $quotes.each(function() {
        if (this.text.includes(id)) {
            this.text += quoteDistinguisher;
            modifiedQuotelinks.push(this);
        }
    });
}

<|endoftext|>