<commit_before>/* 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/. */

/* eslint-disable no-console */

'use strict'
const MailParser = require('mailparser').MailParser
const simplesmtp = require('simplesmtp')
const P = require('../lib/promise')

const config = require('../config').getProperties()

const TEMPLATES_WITH_NO_CODE = new Set([
  'passwordResetEmail'
])

// SMTP half

var users = {}

function emailName(emailAddress) {
  var utf8Address = Buffer.from(emailAddress, 'binary').toString('utf-8')
  return utf8Address.split('@')[0]
}

module.exports = (printLogs) => {
  printLogs = printLogs || process.env.MAIL_HELPER_LOGS
  const console = printLogs ? global.console : {
    log() {},
    error() {}
  }
  return new P((resolve, reject) => {
    const smtp = simplesmtp.createSimpleServer(
      {
        SMTPBanner: 'FXATEST'
      },
      function (req) {
        var mp = new MailParser({ defaultCharset: 'utf-8' })
        mp.on('end',
          function (mail) {
            var link = mail.headers['x-link']
            var rc = mail.headers['x-recovery-code']
            var rul = mail.headers['x-report-signin-link']
            var uc = mail.headers['x-unblock-code']
            var vc = mail.headers['x-verify-code']
            var sc = mail.headers['x-signin-verify-code']
            var template = mail.headers['x-template-name']

            var smsLink
            if (/MockNexmo\.message\.sendSms/.test(mail.subject)) {
              const smsUrlMatch = /(https?:\/\/.*$)/.exec(mail.text)
              smsLink = smsUrlMatch && smsUrlMatch[1]
            }

            var name = emailName(mail.headers.to)
            if (vc) {
              console.log('\x1B[32m', link, '\x1B[39m')
            }
            else if (sc) {
              console.log('\x1B[32mToken code: ', sc, '\x1B[39m')
            }
            else if (rc) {
              console.log('\x1B[34m', link, '\x1B[39m')
            }
            else if (uc) {
              console.log('\x1B[36mUnblock code:', uc, '\x1B[39m')
              console.log('\x1B[36mReport link:', rul, '\x1B[39m')
            }
            else if (smsLink) {
              console.log('\x1B[36mSMS link:', smsLink, '\x1B[39m')
            }
            else if (TEMPLATES_WITH_NO_CODE.has(template)) {
              console.log(`Notification email: ${template}`)
            }
            else {
              console.error('\x1B[31mNo verify code match\x1B[39m')
              console.error(mail)
            }
            if (users[name]) {
              users[name].push(mail)
            } else {
              users[name] = [mail]
            }

            if (mail.headers.cc) {
              // Support for CC headers
              var ccName = emailName(mail.headers.cc)

              if (users[ccName]) {
                users[ccName].push(mail)
              } else {
                users[ccName] = [mail]
              }
            }
          }
        )
        req.pipe(mp)
        req.accept()
      }
    )

    smtp.listen(config.smtp.port, function(err) {
      if (! err) {
        console.log(`Local SMTP server listening on port ${config.smtp.port}`)
      } else {
        console.log('Error starting SMTP server...')
        console.log(err.message)
      }
    })

    // HTTP half

    var hapi = require('hapi')
    var api = new hapi.Server({
      host: config.smtp.api.host,
      port: config.smtp.api.port
    })

    function loop(email, cb) {
      var mail = users[email]
      if (! mail) {
        return setTimeout(loop.bind(null, email, cb), 50)
      }
      cb(mail)
    }

    api.route(
      [
        {
          method: 'GET',
          path: '/mail/{email}',
          handler: async function (request) {
            const emailLoop = function () {
              return new P((resolve) => {
                loop(
                  decodeURIComponent(request.params.email),
                  function (emailData) {
                    resolve(emailData)
                  }
                )
              })
            }

            return emailLoop().then((emailData) => {
               return emailData;
            })
          }
        },
        {
          method: 'DELETE',
          path: '/mail/{email}',
          handler: async function (request) {
            delete users[decodeURIComponent(request.params.email)]
            return {}
          }
        }
      ]
    )

    api.start().then(() => {
      console.log('mail_helper started...')

      return resolve({
        close() {
          return new P((resolve, reject) => {
            let smtpClosed = false
            let apiClosed = false
            smtp.server.end(() => {
              smtpClosed = true
              if (apiClosed) {
                resolve()
              }
            })
            api.stop().then(() => {
              apiClosed = true
              if (smtpClosed) {
                resolve()
              }
            })
          })
        }
      })
    })
  })
}

if (require.main === module) {
  module.exports(true)
}
<commit_msg>fix(mail_helper): remove '<' from to headers in email service requests (#2595) r=@vladikoff<commit_after>/* 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/. */

/* eslint-disable no-console */

'use strict'
const MailParser = require('mailparser').MailParser
const simplesmtp = require('simplesmtp')
const P = require('../lib/promise')

const config = require('../config').getProperties()

const TEMPLATES_WITH_NO_CODE = new Set([
  'passwordResetEmail'
])

// SMTP half

var users = {}

function emailName(emailAddress) {
  var utf8Address = Buffer.from(emailAddress, 'binary').toString('utf-8')
  return utf8Address.split('@')[0]
}

module.exports = (printLogs) => {
  printLogs = printLogs || process.env.MAIL_HELPER_LOGS
  const console = printLogs ? global.console : {
    log() {},
    error() {}
  }
  return new P((resolve, reject) => {
    const smtp = simplesmtp.createSimpleServer(
      {
        SMTPBanner: 'FXATEST'
      },
      function (req) {
        var mp = new MailParser({ defaultCharset: 'utf-8' })
        mp.on('end',
          function (mail) {
            var link = mail.headers['x-link']
            var rc = mail.headers['x-recovery-code']
            var rul = mail.headers['x-report-signin-link']
            var uc = mail.headers['x-unblock-code']
            var vc = mail.headers['x-verify-code']
            var sc = mail.headers['x-signin-verify-code']
            var template = mail.headers['x-template-name']

            var smsLink
            if (/MockNexmo\.message\.sendSms/.test(mail.subject)) {
              const smsUrlMatch = /(https?:\/\/.*$)/.exec(mail.text)
              smsLink = smsUrlMatch && smsUrlMatch[1]
            }

            // Workaround because the email service wraps this header in `< >`.
            // See: https://github.com/mozilla/fxa-content-server/pull/6470#issuecomment-415224438
            var name = emailName(mail.headers.to.replace(/\<(.*?)\>/g, '$1'))

            if (vc) {
              console.log('\x1B[32m', link, '\x1B[39m')
            }
            else if (sc) {
              console.log('\x1B[32mToken code: ', sc, '\x1B[39m')
            }
            else if (rc) {
              console.log('\x1B[34m', link, '\x1B[39m')
            }
            else if (uc) {
              console.log('\x1B[36mUnblock code:', uc, '\x1B[39m')
              console.log('\x1B[36mReport link:', rul, '\x1B[39m')
            }
            else if (smsLink) {
              console.log('\x1B[36mSMS link:', smsLink, '\x1B[39m')
            }
            else if (TEMPLATES_WITH_NO_CODE.has(template)) {
              console.log(`Notification email: ${template}`)
            }
            else {
              console.error('\x1B[31mNo verify code match\x1B[39m')
              console.error(mail)
            }
            if (users[name]) {
              users[name].push(mail)
            } else {
              users[name] = [mail]
            }

            if (mail.headers.cc) {
              // Support for CC headers
              var ccName = emailName(mail.headers.cc)

              if (users[ccName]) {
                users[ccName].push(mail)
              } else {
                users[ccName] = [mail]
              }
            }
          }
        )
        req.pipe(mp)
        req.accept()
      }
    )

    smtp.listen(config.smtp.port, function(err) {
      if (! err) {
        console.log(`Local SMTP server listening on port ${config.smtp.port}`)
      } else {
        console.log('Error starting SMTP server...')
        console.log(err.message)
      }
    })

    // HTTP half

    var hapi = require('hapi')
    var api = new hapi.Server({
      host: config.smtp.api.host,
      port: config.smtp.api.port
    })

    function loop(email, cb) {
      var mail = users[email]
      if (! mail) {
        return setTimeout(loop.bind(null, email, cb), 50)
      }
      cb(mail)
    }

    api.route(
      [
        {
          method: 'GET',
          path: '/mail/{email}',
          handler: async function (request) {
            const emailLoop = function () {
              return new P((resolve) => {
                loop(
                  decodeURIComponent(request.params.email),
                  function (emailData) {
                    resolve(emailData)
                  }
                )
              })
            }

            return emailLoop().then((emailData) => {
               return emailData;
            })
          }
        },
        {
          method: 'DELETE',
          path: '/mail/{email}',
          handler: async function (request) {
            delete users[decodeURIComponent(request.params.email)]
            return {}
          }
        }
      ]
    )

    api.start().then(() => {
      console.log('mail_helper started...')

      return resolve({
        close() {
          return new P((resolve, reject) => {
            let smtpClosed = false
            let apiClosed = false
            smtp.server.end(() => {
              smtpClosed = true
              if (apiClosed) {
                resolve()
              }
            })
            api.stop().then(() => {
              apiClosed = true
              if (smtpClosed) {
                resolve()
              }
            })
          })
        }
      })
    })
  })
}

if (require.main === module) {
  module.exports(true)
}
<|endoftext|>


<commit_before>var AUTH = { // Default Auth Object
    "+": "v",
    "%": "h",
    "@": "o",
    "&": "a",
    "~": "q"
}
// Object types
var Channel = function Channel(name) {
    if (!name) return null;
    var channel = Object.create(Channel.prototype);
    channel.name = name;
    channel.users = {};
    channel.topic = "";
    channel.topicHistory = []; // [Current, Previous, ... , First Known Topic]

    channel.banList = {};
    channel.exceptList = {};
    channel.inviteList = {};

    channel.modes = {
        v: {},
        h: {},
        o: {},
        a: {},
        q: {},
        b: {},
        e: {},
        I: {}
    };

    return channel;
}

Channel.prototype = {
    toString: function () {
        return channel.name;
    },

    addUser: function (nick) {
        var authModes = [];
        while (nick[0] in AUTH) {
            authModes.push(AUTH[nick[0]]);
            nick = nick.slice(1);
        }
        this.users[nick] = nick;
        var addAuthToNick = function (mode) {
            this.updateModes({mode: mode, parameter: nick, set: true});
        };
        authModes.forEach(addAuthToNick.bind(this));
    },

    removeUser: function (nick) {
        delete this.users[nick];
        for (var symbol in AUTH) {
            if (this.modes[AUTH[symbol]][nick]) this.updateModes({mode: AUTH[symbol], parameter: nick, set: false});
        }
    },

    renameUser: function (nick, newNick) {
        this.users[newNick] = this.users[nick];
        for (var symbol in AUTH) {
            if (this.modes[AUTH[symbol]][nick]) this.updateModes({mode: AUTH[symbol], parameter: newNick}, this.modes[AUTH[symbol]][nick]);
        }
        this.removeUser(nick);
    },

    getUsers: function () {
        return Object.keys(this.users);
    },

    forEachUser: function (fn) { // Iterate over user nicks
        if (typeof fn !== "function") return null;
        this.getUsers().forEach(fn);
        return true;
    },

    updateTopic: function (topic) {
        this.topic = topic;
        this.topicHistory.unshift({topic:topic});
    },

    updateTopicInfo: function (editedBy, lastEdited) {
        this.topicHistory[0].editedBy = editedBy;
        this.topicHistory[0].lastEdited = lastEdited;
    },

    updateModes: function (modeObj, sender, time) {
        time = Number(time) || Date.now();
        var mode = modeObj.mode, set = !!modeObj.set, parameter = modeObj.parameter || false;
        var listModes = {"v":"+","h":"%","o":"@","a":"&","q":"~","b":"bans","e":"excepts","I":"invited"};
        if (mode in listModes) {
            if (!this.modes[mode]) this.modes[mode] = {};
            if (set) {
                this.modes[mode][parameter] = sender || undefined;
            } else {
                delete this.modes[mode][parameter];
            }
        } else {
            if (set) {
                this.modes[mode] = parameter || true;
            } else {
                client.debug("Removing "+mode+" mode.");
                delete this.modes[mode];
            }
        }
        // Additional Mode-specific Handling
        switch (mode) {
            case "b":
                this.addBan(parameter, sender, time);
                break;
            case "e":
                this.addExcept(parameter, sender, time);
                break;
            case "I":
                this.addInvite(parameter, sender, time);
                break;
        }
    },

    // If auth is a valid user mode string (v/h/o/a/q and their respective sigils)
    //   return a boolean if the user is at or above that auth level.
    // Else Returns the highest user mode q > a > o > h > v
    highestAuth: function (nick, auth) {
        nick = nick || "";
        if (!this.users[nick]) return null;
        if (auth in AUTH) auth = AUTH[auth];
        var authLevels = ["q", "a", "o", "h", "v"];
        auth = authLevels.indexOf(auth) > -1 ? auth : false;
        var nickAuth = "";
        for (var mode in authModes) {
            if (this.modes[mode][nick]) {
                if (auth) {
                    if (authLevels.indexOf(mode) <= authLevels.indexOf(auth)) {
                        nickAuth = true;
                        break;
                    }
                } else {
                    nickAuth += mode;
                    continue;
                }
            }
        }
        if (auth && !nickAuth) return false;
        return nickAuth;
    },
    addBan: function (target, setBy, setTime, reason) {
        if (!target) return null;
        setBy = setBy || "";
        setTime = Number(setTime) || Date.now();
        reason = reason || "";
        this.banList[target] = {
            setBy: setBy,
            setTime: setTime,
            reason: reason
        };
        return true;
    },
    addExcept: function (target, setBy, setTime) {
        if (!target) return null;
        setBy = setBy || "";
        setTime = Number(setTime) || Date.now();
        this.exceptList[target] = {
            setBy: setBy,
            setTime: setTime
        };
        return true;
    },
    addInvite: function (target, setBy, setTime) {
        if (!target) return null;
        setBy = setBy || "";
        setTime = Number(setTime) || Date.now();
        this.inviteList[target] = {
            setBy: setBy,
            setTime: setTime
        };
        return true;
    }
};

var channel_plugin = {
    name: "channels",
    requires: ["messages"],
    init: function (client, imports) {
        var Channels = function Channels(name) {
            if (typeof name === "undefined") return Channels.chans();
            if (typeof name === "string") return Channels.get(name);
        }
        Channels.channels = {};
        Channels.get = function (name) {
            return Channels.channels[name];
        }
        Channels.all = function (sortFn) {
            return Object.keys(Channels.channels).sort(sortFn)
        }
        Channels.chans = function (sortFn) {
            return Object.keys(Channels.channels).map(Channels.get).sort(sortFn);
        }

        const addChannel = function (name) {
            Channels.channels[name] = Channel(name);
        };

        const removeChannel = function (name) {
            delete Channels.channels[name];
        }

        var handles = {
            "join": function (msg) { // client.on("join") Add a user or channel
                var isSelf = client.nickname() === msg.nickname;
                if (isSelf) {
                    addChannel(msg.channel);
                    client.mode(msg.channel, "b"); // Get bans
                    client.mode(msg.channel, "e"); // Get excepts
                    client.mode(msg.channel, "I"); // Get invites
                } else {
                    Channels(msg.channel).addUser(msg.nickname);
                }
            },
            "quit": function (msg) { // client.on("quit") Remove user from all channels and from user list
                var nick = msg.nickname;
                var reason = msg.reason;
                Channels.chans().forEach(function (chan) {
                    if (nick in chan.users) chan.removeUser(nick);
                });
            },
            "part": function (msg) { // client.on("part") Remove user from parted channel
                var isSelf = client.nickname() === msg.nickname;
                if (isSelf) {
                    delete removeChannel(msg.channel);
                } else {
                    Channels(msg.channel).removeUser(msg.nickname);
                }
            },
            "kick": function (msg) { // client.on("kick") Remove kicked user from channel
                var isSelf = client.nickname() === msg.kicked;
                if (isSelf) {
                    delete removeChannel(msg.channel);
                } else {
                    Channels(msg.channel).removeUser(msg.kicked);
                }
            },
            "nick": function (msg) { // client.on("nick") Update nick across channels
                var nick = msg.old;
                var newNick = msg.new;
                Channels.chans().forEach(function (chan) {
                    if (nick in chan.users) chan.renameUser(nick, newNick);
                });
            },
            "332" : function (msg) { // client.on("332") Update Topic
                var channel = msg.channel;
                Channels(channel).updateTopic(msg.topic);
            },
            "333": function (msg) { // client.on("333") Update Topic Info
                Channels(msg.channel).updateTopicInfo(msg.who, msg.timestamp);
            },
            "353" : function (msg) { // client.on("353") Update channel with users
                var channel = Channels(msg.channel);
                msg.nicknames.forEach(channel.addUser.bind(channel));
            },
            // Lists
            // Banlist
            "367": function (msg) {
                var channel = Channels(msg.channel);
                if (!channel) return;
                channel.updateModes({mode: "b", parameter: msg.hostmaskPattern, set: true}, msg.setter, msg.timestamp);
            },
            // exceptList
            "346": function (msg) {
                var channel = Channels(msg.channel);
                channel.updateModes({mode: "e", parameter: msg.hostmaskPattern, set: true}, msg.setter, msg.timestamp);
            },
            // inviteList
            "348": function (msg) {
                var channel = Channels(msg.channel);
                channel.updateModes({mode: "I", parameter: msg.hostmaskPattern, set: true}, msg.setter, msg.timestamp);
            },
            "mode": function (msg) {
                if (msg.channel.charAt(0) !== "#") return;
                var handleMode = (function (modeObj) {
                    Channels(msg.channel).updateModes(modeObj, msg.nickname);
                }).bind(Channels(msg.channel));
                msg.modes.forEach(handleMode);
            }
        };
        return {
            handlers: handles,
            help: {},
            exports: Channels,
            commands: [],
       };
    }
}

module.exports = channel_plugin;
<commit_msg>Bugfix: Another channel plugin typo<commit_after>var AUTH = { // Default Auth Object
    "+": "v",
    "%": "h",
    "@": "o",
    "&": "a",
    "~": "q"
}
// Object types
var Channel = function Channel(name) {
    if (!name) return null;
    var channel = Object.create(Channel.prototype);
    channel.name = name;
    channel.users = {};
    channel.topic = "";
    channel.topicHistory = []; // [Current, Previous, ... , First Known Topic]

    channel.banList = {};
    channel.exceptList = {};
    channel.inviteList = {};

    channel.modes = {
        v: {},
        h: {},
        o: {},
        a: {},
        q: {},
        b: {},
        e: {},
        I: {}
    };

    return channel;
}

Channel.prototype = {
    toString: function () {
        return this.name;
    },

    addUser: function (nick) {
        var authModes = [];
        while (nick[0] in AUTH) {
            authModes.push(AUTH[nick[0]]);
            nick = nick.slice(1);
        }
        this.users[nick] = nick;
        var addAuthToNick = function (mode) {
            this.updateModes({mode: mode, parameter: nick, set: true});
        };
        authModes.forEach(addAuthToNick.bind(this));
    },

    removeUser: function (nick) {
        delete this.users[nick];
        for (var symbol in AUTH) {
            if (this.modes[AUTH[symbol]][nick]) this.updateModes({mode: AUTH[symbol], parameter: nick, set: false});
        }
    },

    renameUser: function (nick, newNick) {
        this.users[newNick] = this.users[nick];
        for (var symbol in AUTH) {
            if (this.modes[AUTH[symbol]][nick]) this.updateModes({mode: AUTH[symbol], parameter: newNick}, this.modes[AUTH[symbol]][nick]);
        }
        this.removeUser(nick);
    },

    getUsers: function () {
        return Object.keys(this.users);
    },

    forEachUser: function (fn) { // Iterate over user nicks
        if (typeof fn !== "function") return null;
        this.getUsers().forEach(fn);
        return true;
    },

    updateTopic: function (topic) {
        this.topic = topic;
        this.topicHistory.unshift({topic:topic});
    },

    updateTopicInfo: function (editedBy, lastEdited) {
        this.topicHistory[0].editedBy = editedBy;
        this.topicHistory[0].lastEdited = lastEdited;
    },

    updateModes: function (modeObj, sender, time) {
        time = Number(time) || Date.now();
        var mode = modeObj.mode, set = !!modeObj.set, parameter = modeObj.parameter || false;
        var listModes = {"v":"+","h":"%","o":"@","a":"&","q":"~","b":"bans","e":"excepts","I":"invited"};
        if (mode in listModes) {
            if (!this.modes[mode]) this.modes[mode] = {};
            if (set) {
                this.modes[mode][parameter] = sender || undefined;
            } else {
                delete this.modes[mode][parameter];
            }
        } else {
            if (set) {
                this.modes[mode] = parameter || true;
            } else {
                client.debug("Removing "+mode+" mode.");
                delete this.modes[mode];
            }
        }
        // Additional Mode-specific Handling
        switch (mode) {
            case "b":
                this.addBan(parameter, sender, time);
                break;
            case "e":
                this.addExcept(parameter, sender, time);
                break;
            case "I":
                this.addInvite(parameter, sender, time);
                break;
        }
    },

    // If auth is a valid user mode string (v/h/o/a/q and their respective sigils)
    //   return a boolean if the user is at or above that auth level.
    // Else Returns the highest user mode q > a > o > h > v
    highestAuth: function (nick, auth) {
        nick = nick || "";
        if (!this.users[nick]) return null;
        if (auth in AUTH) auth = AUTH[auth];
        var authLevels = ["q", "a", "o", "h", "v"];
        auth = authLevels.indexOf(auth) > -1 ? auth : false;
        var nickAuth = "";
        for (var mode in authModes) {
            if (this.modes[mode][nick]) {
                if (auth) {
                    if (authLevels.indexOf(mode) <= authLevels.indexOf(auth)) {
                        nickAuth = true;
                        break;
                    }
                } else {
                    nickAuth += mode;
                    continue;
                }
            }
        }
        if (auth && !nickAuth) return false;
        return nickAuth;
    },
    addBan: function (target, setBy, setTime, reason) {
        if (!target) return null;
        setBy = setBy || "";
        setTime = Number(setTime) || Date.now();
        reason = reason || "";
        this.banList[target] = {
            setBy: setBy,
            setTime: setTime,
            reason: reason
        };
        return true;
    },
    addExcept: function (target, setBy, setTime) {
        if (!target) return null;
        setBy = setBy || "";
        setTime = Number(setTime) || Date.now();
        this.exceptList[target] = {
            setBy: setBy,
            setTime: setTime
        };
        return true;
    },
    addInvite: function (target, setBy, setTime) {
        if (!target) return null;
        setBy = setBy || "";
        setTime = Number(setTime) || Date.now();
        this.inviteList[target] = {
            setBy: setBy,
            setTime: setTime
        };
        return true;
    }
};

var channel_plugin = {
    name: "channels",
    requires: ["messages"],
    init: function (client, imports) {
        var Channels = function Channels(name) {
            if (typeof name === "undefined") return Channels.chans();
            if (typeof name === "string") return Channels.get(name);
        }
        Channels.channels = {};
        Channels.get = function (name) {
            return Channels.channels[name];
        }
        Channels.all = function (sortFn) {
            return Object.keys(Channels.channels).sort(sortFn)
        }
        Channels.chans = function (sortFn) {
            return Object.keys(Channels.channels).map(Channels.get).sort(sortFn);
        }

        const addChannel = function (name) {
            Channels.channels[name] = Channel(name);
        };

        const removeChannel = function (name) {
            delete Channels.channels[name];
        }

        var handles = {
            "join": function (msg) { // client.on("join") Add a user or channel
                var isSelf = client.nickname() === msg.nickname;
                if (isSelf) {
                    addChannel(msg.channel);
                    client.mode(msg.channel, "b"); // Get bans
                    client.mode(msg.channel, "e"); // Get excepts
                    client.mode(msg.channel, "I"); // Get invites
                } else {
                    Channels(msg.channel).addUser(msg.nickname);
                }
            },
            "quit": function (msg) { // client.on("quit") Remove user from all channels and from user list
                var nick = msg.nickname;
                var reason = msg.reason;
                Channels.chans().forEach(function (chan) {
                    if (nick in chan.users) chan.removeUser(nick);
                });
            },
            "part": function (msg) { // client.on("part") Remove user from parted channel
                var isSelf = client.nickname() === msg.nickname;
                if (isSelf) {
                    delete removeChannel(msg.channel);
                } else {
                    Channels(msg.channel).removeUser(msg.nickname);
                }
            },
            "kick": function (msg) { // client.on("kick") Remove kicked user from channel
                var isSelf = client.nickname() === msg.kicked;
                if (isSelf) {
                    delete removeChannel(msg.channel);
                } else {
                    Channels(msg.channel).removeUser(msg.kicked);
                }
            },
            "nick": function (msg) { // client.on("nick") Update nick across channels
                var nick = msg.old;
                var newNick = msg.new;
                Channels.chans().forEach(function (chan) {
                    if (nick in chan.users) chan.renameUser(nick, newNick);
                });
            },
            "332" : function (msg) { // client.on("332") Update Topic
                var channel = msg.channel;
                Channels(channel).updateTopic(msg.topic);
            },
            "333": function (msg) { // client.on("333") Update Topic Info
                Channels(msg.channel).updateTopicInfo(msg.who, msg.timestamp);
            },
            "353" : function (msg) { // client.on("353") Update channel with users
                var channel = Channels(msg.channel);
                msg.nicknames.forEach(channel.addUser.bind(channel));
            },
            // Lists
            // Banlist
            "367": function (msg) {
                var channel = Channels(msg.channel);
                if (!channel) return;
                channel.updateModes({mode: "b", parameter: msg.hostmaskPattern, set: true}, msg.setter, msg.timestamp);
            },
            // exceptList
            "346": function (msg) {
                var channel = Channels(msg.channel);
                channel.updateModes({mode: "e", parameter: msg.hostmaskPattern, set: true}, msg.setter, msg.timestamp);
            },
            // inviteList
            "348": function (msg) {
                var channel = Channels(msg.channel);
                channel.updateModes({mode: "I", parameter: msg.hostmaskPattern, set: true}, msg.setter, msg.timestamp);
            },
            "mode": function (msg) {
                if (msg.channel.charAt(0) !== "#") return;
                var handleMode = (function (modeObj) {
                    Channels(msg.channel).updateModes(modeObj, msg.nickname);
                }).bind(Channels(msg.channel));
                msg.modes.forEach(handleMode);
            }
        };
        return {
            handlers: handles,
            help: {},
            exports: Channels,
            commands: [],
       };
    }
}

module.exports = channel_plugin;
<|endoftext|>


<commit_before>const { h } = require('preact')
const Plugin = require('../../core/Plugin')
const Translator = require('../../core/Translator')
const {
  getFileTypeExtension,
  canvasToBlob
} = require('../../core/Utils')
const supportsMediaRecorder = require('./supportsMediaRecorder')
const WebcamIcon = require('./WebcamIcon')
const CameraScreen = require('./CameraScreen')
const PermissionsScreen = require('./PermissionsScreen')

// Setup getUserMedia, with polyfill for older browsers
// Adapted from: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
function getMediaDevices () {
  if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
    return navigator.mediaDevices
  }

  const getUserMedia = navigator.mozGetUserMedia || navigator.webkitGetUserMedia
  if (!getUserMedia) {
    return null
  }

  return {
    getUserMedia (opts) {
      return new Promise((resolve, reject) => {
        getUserMedia.call(navigator, opts, resolve, reject)
      })
    }
  }
}

/**
 * Webcam
 */
module.exports = class Webcam extends Plugin {
  constructor (uppy, opts) {
    super(uppy, opts)
    this.mediaDevices = getMediaDevices()
    this.supportsUserMedia = !!this.mediaDevices
    this.protocol = location.protocol.match(/https/i) ? 'https' : 'http'
    this.id = this.opts.id || 'Webcam'
    this.title = 'Webcam'
    this.type = 'acquirer'
    this.icon = WebcamIcon

    const defaultLocale = {
      strings: {
        smile: 'Smile!'
      }
    }

    // set default options
    const defaultOptions = {
      onBeforeSnapshot: () => Promise.resolve(),
      countdown: false,
      locale: defaultLocale,
      mirror: true,
      facingMode: 'user',
      modes: [
        'video-audio',
        'video-only',
        'audio-only',
        'picture'
      ]
    }

    // merge default options with the ones set by user
    this.opts = Object.assign({}, defaultOptions, opts)

    this.locale = Object.assign({}, defaultLocale, this.opts.locale)
    this.locale.strings = Object.assign({}, defaultLocale.strings, this.opts.locale.strings)

    // i18n
    this.translator = new Translator({locale: this.locale})
    this.i18n = this.translator.translate.bind(this.translator)

    this.install = this.install.bind(this)
    this.setPluginState = this.setPluginState.bind(this)

    this.render = this.render.bind(this)

    // Camera controls
    this.start = this.start.bind(this)
    this.stop = this.stop.bind(this)
    this.takeSnapshot = this.takeSnapshot.bind(this)
    this.startRecording = this.startRecording.bind(this)
    this.stopRecording = this.stopRecording.bind(this)
    this.oneTwoThreeSmile = this.oneTwoThreeSmile.bind(this)
    this.focus = this.focus.bind(this)

    this.webcamActive = false

    if (this.opts.countdown) {
      this.opts.onBeforeSnapshot = this.oneTwoThreeSmile
    }
  }

  isSupported () {
    return !!this.mediaDevices
  }

  getConstraints () {
    const acceptsAudio = this.opts.modes.indexOf('video-audio') !== -1 ||
      this.opts.modes.indexOf('audio-only') !== -1
    const acceptsVideo = this.opts.modes.indexOf('video-audio') !== -1 ||
      this.opts.modes.indexOf('video-only') !== -1 ||
      this.opts.modes.indexOf('picture') !== -1

    return {
      audio: acceptsAudio,
      video: acceptsVideo ? { facingMode: this.opts.facingMode } : false
    }
  }

  start () {
    if (!this.isSupported()) {
      return Promise.reject(new Error('Webcam access not supported'))
    }

    this.webcamActive = true

    const constraints = this.getConstraints()

    // ask user for access to their camera
    return this.mediaDevices.getUserMedia(constraints)
      .then((stream) => {
        this.stream = stream
        // this.streamSrc = URL.createObjectURL(this.stream)
        this.setPluginState({
          cameraReady: true
        })
      })
      .catch((err) => {
        this.setPluginState({
          cameraError: err
        })
      })
  }

  startRecording () {
    // TODO We can check here if any of the mime types listed in the
    // mimeToExtensions map in Utils.js are supported, and prefer to use one of
    // those.
    // Right now we let the browser pick a type that it deems appropriate.
    this.recorder = new MediaRecorder(this.stream)
    this.recordingChunks = []
    this.recorder.addEventListener('dataavailable', (event) => {
      this.recordingChunks.push(event.data)
    })
    this.recorder.start()

    this.setPluginState({
      isRecording: true
    })
  }

  stopRecording () {
    const stopped = new Promise((resolve, reject) => {
      this.recorder.addEventListener('stop', () => {
        resolve()
      })
      this.recorder.stop()
    })

    return stopped.then(() => {
      this.setPluginState({
        isRecording: false
      })
      return this.getVideo()
    })
    .then(this.uppy.addFile)
    .then(() => {
      this.recordingChunks = null
      this.recorder = null
      const dashboard = this.uppy.getPlugin('Dashboard')
      if (dashboard) dashboard.hideAllPanels()
    }, (error) => {
      this.recordingChunks = null
      this.recorder = null
      throw error
    })
  }

  stop () {
    this.stream.getAudioTracks().forEach((track) => {
      track.stop()
    })
    this.stream.getVideoTracks().forEach((track) => {
      track.stop()
    })
    this.webcamActive = false
    this.stream = null
  }

  getVideoElement () {
    return this.el.querySelector('.uppy-Webcam-video')
  }

  oneTwoThreeSmile () {
    return new Promise((resolve, reject) => {
      let count = this.opts.countdown

      let countDown = setInterval(() => {
        if (!this.webcamActive) {
          clearInterval(countDown)
          this.captureInProgress = false
          return reject(new Error('Webcam is not active'))
        }

        if (count > 0) {
          this.uppy.info(`${count}...`, 'warning', 800)
          count--
        } else {
          clearInterval(countDown)
          this.uppy.info(this.i18n('smile'), 'success', 1500)
          setTimeout(() => resolve(), 1500)
        }
      }, 1000)
    })
  }

  takeSnapshot () {
    if (this.captureInProgress) return
    this.captureInProgress = true

    this.opts.onBeforeSnapshot().catch((err) => {
      const message = typeof err === 'object' ? err.message : err
      this.uppy.info(message, 'error', 5000)
      return Promise.reject(new Error(`onBeforeSnapshot: ${message}`))
    }).then(() => {
      return this.getImage()
    }).then((tagFile) => {
      this.captureInProgress = false
      this.uppy.addFile(tagFile)
      const dashboard = this.uppy.getPlugin('Dashboard')
      if (dashboard) dashboard.hideAllPanels()
    }, (error) => {
      this.captureInProgress = false
      throw error
    })
  }

  getImage () {
    const video = this.getVideoElement()
    if (!video) {
      return Promise.reject(new Error('No video element found, likely due to the Webcam tab being closed.'))
    }

    const name = `webcam-${Date.now()}.jpg`
    const mimeType = 'image/jpeg'

    const width = video.videoWidth
    const height = video.videoHeight

    const scaleH = this.opts.mirror ? -1 : 1 // Set horizontal scale to -1 if flip horizontal
    const scaleV = 1
    const posX = this.opts.mirror ? width * -1 : 0 // Set x position to -100% if flip horizontal
    const posY = 0

    const canvas = document.createElement('canvas')
    canvas.width = width
    canvas.height = height
    const ctx = canvas.getContext('2d')
    ctx.save() // Save the current state
    ctx.scale(scaleH, scaleV) // Set scale to flip the image
    ctx.drawImage(video, posX, posY, width, height) // draw the image
    ctx.restore() // Restore the last saved state

    return canvasToBlob(canvas, mimeType).then((blob) => {
      return {
        source: this.id,
        name: name,
        data: new File([blob], name, { type: mimeType }),
        type: mimeType
      }
    })
  }

  getVideo () {
    const mimeType = this.recordingChunks[0].type
    const fileExtension = getFileTypeExtension(mimeType)

    if (!fileExtension) {
      return Promise.reject(new Error(`Could not retrieve recording: Unsupported media type "${mimeType}"`))
    }

    const name = `webcam-${Date.now()}.${fileExtension}`
    const blob = new Blob(this.recordingChunks, { type: mimeType })
    const file = {
      source: this.id,
      name: name,
      data: new File([blob], name, { type: mimeType }),
      type: mimeType
    }

    return Promise.resolve(file)
  }

  focus () {
    if (this.opts.countdown) return
    setTimeout(() => {
      this.uppy.info(this.i18n('smile'), 'success', 1500)
    }, 1000)
  }

  render (state) {
    if (!this.webcamActive) {
      this.start()
    }

    const webcamState = this.getPluginState()

    if (!webcamState.cameraReady) {
      return PermissionsScreen(webcamState)
    }

    return h(CameraScreen, Object.assign({}, webcamState, {
      onSnapshot: this.takeSnapshot,
      onStartRecording: this.startRecording,
      onStopRecording: this.stopRecording,
      onFocus: this.focus,
      onStop: this.stop,
      modes: this.opts.modes,
      supportsRecording: supportsMediaRecorder(),
      recording: webcamState.isRecording,
      mirror: this.opts.mirror,
      src: this.stream
    }))
  }

  install () {
    this.setPluginState({
      cameraReady: false
    })

    const target = this.opts.target
    if (target) {
      this.mount(target, this)
    }
  }

  uninstall () {
    if (this.stream) {
      this.stop()
    }

    this.unmount()
  }
}
<commit_msg>Don’t flip the resulting image when mirroring, just the preview<commit_after>const { h } = require('preact')
const Plugin = require('../../core/Plugin')
const Translator = require('../../core/Translator')
const {
  getFileTypeExtension,
  canvasToBlob
} = require('../../core/Utils')
const supportsMediaRecorder = require('./supportsMediaRecorder')
const WebcamIcon = require('./WebcamIcon')
const CameraScreen = require('./CameraScreen')
const PermissionsScreen = require('./PermissionsScreen')

// Setup getUserMedia, with polyfill for older browsers
// Adapted from: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
function getMediaDevices () {
  if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
    return navigator.mediaDevices
  }

  const getUserMedia = navigator.mozGetUserMedia || navigator.webkitGetUserMedia
  if (!getUserMedia) {
    return null
  }

  return {
    getUserMedia (opts) {
      return new Promise((resolve, reject) => {
        getUserMedia.call(navigator, opts, resolve, reject)
      })
    }
  }
}

/**
 * Webcam
 */
module.exports = class Webcam extends Plugin {
  constructor (uppy, opts) {
    super(uppy, opts)
    this.mediaDevices = getMediaDevices()
    this.supportsUserMedia = !!this.mediaDevices
    this.protocol = location.protocol.match(/https/i) ? 'https' : 'http'
    this.id = this.opts.id || 'Webcam'
    this.title = 'Webcam'
    this.type = 'acquirer'
    this.icon = WebcamIcon

    const defaultLocale = {
      strings: {
        smile: 'Smile!'
      }
    }

    // set default options
    const defaultOptions = {
      onBeforeSnapshot: () => Promise.resolve(),
      countdown: false,
      locale: defaultLocale,
      mirror: true,
      facingMode: 'user',
      modes: [
        'video-audio',
        'video-only',
        'audio-only',
        'picture'
      ]
    }

    // merge default options with the ones set by user
    this.opts = Object.assign({}, defaultOptions, opts)

    this.locale = Object.assign({}, defaultLocale, this.opts.locale)
    this.locale.strings = Object.assign({}, defaultLocale.strings, this.opts.locale.strings)

    // i18n
    this.translator = new Translator({locale: this.locale})
    this.i18n = this.translator.translate.bind(this.translator)

    this.install = this.install.bind(this)
    this.setPluginState = this.setPluginState.bind(this)

    this.render = this.render.bind(this)

    // Camera controls
    this.start = this.start.bind(this)
    this.stop = this.stop.bind(this)
    this.takeSnapshot = this.takeSnapshot.bind(this)
    this.startRecording = this.startRecording.bind(this)
    this.stopRecording = this.stopRecording.bind(this)
    this.oneTwoThreeSmile = this.oneTwoThreeSmile.bind(this)
    this.focus = this.focus.bind(this)

    this.webcamActive = false

    if (this.opts.countdown) {
      this.opts.onBeforeSnapshot = this.oneTwoThreeSmile
    }
  }

  isSupported () {
    return !!this.mediaDevices
  }

  getConstraints () {
    const acceptsAudio = this.opts.modes.indexOf('video-audio') !== -1 ||
      this.opts.modes.indexOf('audio-only') !== -1
    const acceptsVideo = this.opts.modes.indexOf('video-audio') !== -1 ||
      this.opts.modes.indexOf('video-only') !== -1 ||
      this.opts.modes.indexOf('picture') !== -1

    return {
      audio: acceptsAudio,
      video: acceptsVideo ? { facingMode: this.opts.facingMode } : false
    }
  }

  start () {
    if (!this.isSupported()) {
      return Promise.reject(new Error('Webcam access not supported'))
    }

    this.webcamActive = true

    const constraints = this.getConstraints()

    // ask user for access to their camera
    return this.mediaDevices.getUserMedia(constraints)
      .then((stream) => {
        this.stream = stream
        // this.streamSrc = URL.createObjectURL(this.stream)
        this.setPluginState({
          cameraReady: true
        })
      })
      .catch((err) => {
        this.setPluginState({
          cameraError: err
        })
      })
  }

  startRecording () {
    // TODO We can check here if any of the mime types listed in the
    // mimeToExtensions map in Utils.js are supported, and prefer to use one of
    // those.
    // Right now we let the browser pick a type that it deems appropriate.
    this.recorder = new MediaRecorder(this.stream)
    this.recordingChunks = []
    this.recorder.addEventListener('dataavailable', (event) => {
      this.recordingChunks.push(event.data)
    })
    this.recorder.start()

    this.setPluginState({
      isRecording: true
    })
  }

  stopRecording () {
    const stopped = new Promise((resolve, reject) => {
      this.recorder.addEventListener('stop', () => {
        resolve()
      })
      this.recorder.stop()
    })

    return stopped.then(() => {
      this.setPluginState({
        isRecording: false
      })
      return this.getVideo()
    })
    .then(this.uppy.addFile)
    .then(() => {
      this.recordingChunks = null
      this.recorder = null
      const dashboard = this.uppy.getPlugin('Dashboard')
      if (dashboard) dashboard.hideAllPanels()
    }, (error) => {
      this.recordingChunks = null
      this.recorder = null
      throw error
    })
  }

  stop () {
    this.stream.getAudioTracks().forEach((track) => {
      track.stop()
    })
    this.stream.getVideoTracks().forEach((track) => {
      track.stop()
    })
    this.webcamActive = false
    this.stream = null
  }

  getVideoElement () {
    return this.el.querySelector('.uppy-Webcam-video')
  }

  oneTwoThreeSmile () {
    return new Promise((resolve, reject) => {
      let count = this.opts.countdown

      let countDown = setInterval(() => {
        if (!this.webcamActive) {
          clearInterval(countDown)
          this.captureInProgress = false
          return reject(new Error('Webcam is not active'))
        }

        if (count > 0) {
          this.uppy.info(`${count}...`, 'warning', 800)
          count--
        } else {
          clearInterval(countDown)
          this.uppy.info(this.i18n('smile'), 'success', 1500)
          setTimeout(() => resolve(), 1500)
        }
      }, 1000)
    })
  }

  takeSnapshot () {
    if (this.captureInProgress) return
    this.captureInProgress = true

    this.opts.onBeforeSnapshot().catch((err) => {
      const message = typeof err === 'object' ? err.message : err
      this.uppy.info(message, 'error', 5000)
      return Promise.reject(new Error(`onBeforeSnapshot: ${message}`))
    }).then(() => {
      return this.getImage()
    }).then((tagFile) => {
      this.captureInProgress = false
      this.uppy.addFile(tagFile)
      const dashboard = this.uppy.getPlugin('Dashboard')
      if (dashboard) dashboard.hideAllPanels()
    }, (error) => {
      this.captureInProgress = false
      throw error
    })
  }

  getImage () {
    const video = this.getVideoElement()
    if (!video) {
      return Promise.reject(new Error('No video element found, likely due to the Webcam tab being closed.'))
    }

    const name = `webcam-${Date.now()}.jpg`
    const mimeType = 'image/jpeg'

    const width = video.videoWidth
    const height = video.videoHeight

    // const scaleH = this.opts.mirror ? -1 : 1 // Set horizontal scale to -1 if flip horizontal
    // const scaleV = 1
    // const posX = this.opts.mirror ? width * -1 : 0 // Set x position to -100% if flip horizontal
    // const posY = 0

    const canvas = document.createElement('canvas')
    canvas.width = width
    canvas.height = height
    const ctx = canvas.getContext('2d')
    ctx.drawImage(video, 0, 0)
    // ctx.save() // Save the current state
    // ctx.scale(scaleH, scaleV) // Set scale to flip the image
    // ctx.drawImage(video, posX, posY, width, height) // draw the image
    // ctx.restore() // Restore the last saved state

    return canvasToBlob(canvas, mimeType).then((blob) => {
      return {
        source: this.id,
        name: name,
        data: new File([blob], name, { type: mimeType }),
        type: mimeType
      }
    })
  }

  getVideo () {
    const mimeType = this.recordingChunks[0].type
    const fileExtension = getFileTypeExtension(mimeType)

    if (!fileExtension) {
      return Promise.reject(new Error(`Could not retrieve recording: Unsupported media type "${mimeType}"`))
    }

    const name = `webcam-${Date.now()}.${fileExtension}`
    const blob = new Blob(this.recordingChunks, { type: mimeType })
    const file = {
      source: this.id,
      name: name,
      data: new File([blob], name, { type: mimeType }),
      type: mimeType
    }

    return Promise.resolve(file)
  }

  focus () {
    if (this.opts.countdown) return
    setTimeout(() => {
      this.uppy.info(this.i18n('smile'), 'success', 1500)
    }, 1000)
  }

  render (state) {
    if (!this.webcamActive) {
      this.start()
    }

    const webcamState = this.getPluginState()

    if (!webcamState.cameraReady) {
      return PermissionsScreen(webcamState)
    }

    return h(CameraScreen, Object.assign({}, webcamState, {
      onSnapshot: this.takeSnapshot,
      onStartRecording: this.startRecording,
      onStopRecording: this.stopRecording,
      onFocus: this.focus,
      onStop: this.stop,
      modes: this.opts.modes,
      supportsRecording: supportsMediaRecorder(),
      recording: webcamState.isRecording,
      mirror: this.opts.mirror,
      src: this.stream
    }))
  }

  install () {
    this.setPluginState({
      cameraReady: false
    })

    const target = this.opts.target
    if (target) {
      this.mount(target, this)
    }
  }

  uninstall () {
    if (this.stream) {
      this.stop()
    }

    this.unmount()
  }
}
<|endoftext|>


<commit_before>function bcbCountdown(element, config, theme) {
    var themes = 12;
    try {
        theme = theme == "0" ? 0 : parseInt(theme || '') || -1;
        var event = (Array.isArray(config) ? config : config.split("\n")).map(function(line) {
            var parts = line.split('~');
            return { time: new Date(parts.pop()), title: parts.shift(), subtitle: parts.join('~') };
        }).filter(function(event) { return event.time.valueOf() > Date.now(); })[0];
        if (!event) return $(element).slideUp();
        var $timeLeft = $('
').addClass('timeLeft');
        if (theme < 0 || theme >= themes) theme = Math.floor(Math.random() * themes);
        $(element).addClass('bcbCountdown').append($('
').addClass('title').text(event.title || "New Episode"))
            .append($('
').addClass('subtitle').text(event.subtitle || "The Feels Awaken"))
            .append($timeLeft).addClass('theme' + theme);
        function updateTime() {
            var duration = (event.time.valueOf() - Date.now()) / 1000;
            if (duration < 0) {
                clearInterval(interval);
                $(element).find('.title, .subtitle, .timeLeft').hide();
                return bcbCountdown(element, config);
            }
            var timeSpans = [{ i: 24, d: "Days" }, { i: 60, d: "Hours" }, { i: 60, d: "Minutes" }, { i: 1, d: "Seconds" }].map(function(num, base, segments) {
                for (var d = duration, i = base; i < segments.length; i++) d /= segments[i].i;
                if (base) d %= segments[base - 1].i;
                return $("").attr('data-label', num.d).html((d < 10 ? "0" : "") + Math.floor(d));
            });
            if ($timeLeft.find("span").length < timeSpans.length)
                timeSpans.forEach(function(e, i) {
                    if (i) $timeLeft.append(":");
                    $timeLeft.append(e);
                });
            else
                $timeLeft.find("span").each(function(i) {
                    $(this).html($(timeSpans[i]).html());
                });
        }
        updateTime();
        var interval = setInterval(updateTime, 1000), pressed = [], unlocked = false;
        $(window).keydown(function(e) {
            pressed.push(String.fromCharCode(e.which).toUpperCase());
            if (pressed.length > 5) pressed.shift();
            if (pressed.join('') == "SUGAR") unlocked = true;
            if (pressed.join('') == "RIDOT") sneaky();
            if (unlocked && [37, 39].indexOf(e.which) >= 0) {
                $(element).removeClass("theme" + theme);
                theme += e.which - 38;
                theme %= themes;
                while (theme < 0) theme += themes;
                $(element).addClass("theme" + theme);
            }
        });
    }
    catch (e) {
        if (typeof (debug) !== "undefined" && debug) {
            console.error(e);
        }
    }
    function sneaky() {
        $('#nav-bar').addClass('egg');
        (function() {
            return $.when(
                (function() {
                    var d = $.Deferred();
                    $("").attr('src', 'http://i.imgur.com/mN9KVNe.png').on('load', d.resolve);
                    return d;
                })(),
                (function() {
                    var d = $.Deferred();
                    $("").attr('src', 'http://i.imgur.com/wvghkvg.png').on('load', d.resolve);
                    return d;
                })());
        })().then(function() {
            var $peek = $("
").appendTo($("#nav-bar")).hide().slideDown(1000);
            function lookAway() {
                if ($(window).scrollTop() > 600) {
                    setTimeout(peridot, 0);
                    $peek.hide().remove();
                    $(window).unbind('scroll', lookAway);
                }
            }
            $(window).bind('scroll', lookAway);
        });
    }
    function peridot() {
        var $peridot = $('
').mousemove(function() {
            $peridot.unbind('mousemove');
            var i = 2;
            window.requestAnimationFrame = window.requestAnimationFrame || function(c) { setTimeout(c, 1000 / 60); };
            (function animate() {
                $peridot.css('background-position', '0 -' + Math.floor(i / 2) + "em");
                if (i / 2 == 4) $('html, body').animate({ scrollTop: $(document).height() }, $(document).height() / 10, "easeInCubic");
                if (i++ / 2 < 20) return requestAnimationFrame(animate);
                $peridot.fadeOut(function() { $peridot.remove(); });
            })();
        });
        $("#nav-bar").append($peridot);
    }
    jQuery.easing['easeInCubic'] = function(x, t, b, c, d) { return c * (t /= d) * t * t + b; };
    var d = new Date();
    if (d.getMonth() == 3 && d.getDate() == 1) setTimeout(sneaky, 60000 * Math.random());
}<commit_msg>kept Peridot from doubling<commit_after>function bcbCountdown(element, config, theme) {
    var themes = 12;
    try {
        theme = theme == "0" ? 0 : parseInt(theme || '') || -1;
        var event = (Array.isArray(config) ? config : config.split("\n")).map(function(line) {
            var parts = line.split('~');
            return { time: new Date(parts.pop()), title: parts.shift(), subtitle: parts.join('~') };
        }).filter(function(event) { return event.time.valueOf() > Date.now(); })[0];
        if (!event) return $(element).slideUp();
        var $timeLeft = $('
').addClass('timeLeft');
        if (theme < 0 || theme >= themes) theme = Math.floor(Math.random() * themes);
        $(element).addClass('bcbCountdown').append($('
').addClass('title').text(event.title || "New Episode"))
            .append($('
').addClass('subtitle').text(event.subtitle || "The Feels Awaken"))
            .append($timeLeft).addClass('theme' + theme);
        function updateTime() {
            var duration = (event.time.valueOf() - Date.now()) / 1000;
            if (duration < 0) {
                clearInterval(interval);
                $(element).find('.title, .subtitle, .timeLeft').hide();
                return bcbCountdown(element, config);
            }
            var timeSpans = [{ i: 24, d: "Days" }, { i: 60, d: "Hours" }, { i: 60, d: "Minutes" }, { i: 1, d: "Seconds" }].map(function(num, base, segments) {
                for (var d = duration, i = base; i < segments.length; i++) d /= segments[i].i;
                if (base) d %= segments[base - 1].i;
                return $("").attr('data-label', num.d).html((d < 10 ? "0" : "") + Math.floor(d));
            });
            if ($timeLeft.find("span").length < timeSpans.length)
                timeSpans.forEach(function(e, i) {
                    if (i) $timeLeft.append(":");
                    $timeLeft.append(e);
                });
            else
                $timeLeft.find("span").each(function(i) {
                    $(this).html($(timeSpans[i]).html());
                });
        }
        updateTime();
        var interval = setInterval(updateTime, 1000), pressed = [], unlocked = false;
        $(window).keydown(function(e) {
            pressed.push(String.fromCharCode(e.which).toUpperCase());
            if (pressed.length > 5) pressed.shift();
            if (pressed.join('') == "SUGAR") unlocked = true;
            if (pressed.join('') == "RIDOT") sneaky();
            if (unlocked && [37, 39].indexOf(e.which) >= 0) {
                $(element).removeClass("theme" + theme);
                theme += e.which - 38;
                theme %= themes;
                while (theme < 0) theme += themes;
                $(element).addClass("theme" + theme);
            }
        });
    }
    catch (e) {
        if (typeof (debug) !== "undefined" && debug) {
            console.error(e);
        }
    }
    function sneaky() {
        if ($('#nav-bar .peridot-egg, #nav-bar .peridot-peek').is('*')) return;
        $('#nav-bar').addClass('egg');
        (function() {
            return $.when(
                (function() {
                    var d = $.Deferred();
                    $("").attr('src', 'http://i.imgur.com/mN9KVNe.png').on('load', d.resolve);
                    return d;
                })(),
                (function() {
                    var d = $.Deferred();
                    $("").attr('src', 'http://i.imgur.com/wvghkvg.png').on('load', d.resolve);
                    return d;
                })());
        })().then(function() {
            var $peek = $("
").appendTo($("#nav-bar")).hide().slideDown(1000);
            function lookAway() {
                if ($(window).scrollTop() > 600) {
                    setTimeout(peridot, 0);
                    $peek.hide().remove();
                    $(window).unbind('scroll', lookAway);
                }
            }
            $(window).bind('scroll', lookAway);
        });
    }
    function peridot() {
        var $peridot = $('
').mousemove(function() {
            $peridot.unbind('mousemove');
            var i = 2;
            window.requestAnimationFrame = window.requestAnimationFrame || function(c) { setTimeout(c, 1000 / 60); };
            (function animate() {
                $peridot.css('background-position', '0 -' + Math.floor(i / 2) + "em");
                if (i / 2 == 4) $('html, body').animate({ scrollTop: $(document).height() }, $(document).height() / 10, "easeInCubic");
                if (i++ / 2 < 20) return requestAnimationFrame(animate);
                $peridot.fadeOut(function() { $peridot.remove(); });
            })();
        });
        $("#nav-bar").append($peridot);
    }
    jQuery.easing['easeInCubic'] = function(x, t, b, c, d) { return c * (t /= d) * t * t + b; };
    var d = new Date();
    if (d.getMonth() == 3 && d.getDate() == 1) setTimeout(sneaky, 60000 * Math.random());
}<|endoftext|>


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

    angular.module( 'treeControl', ['contextMenu'] )
        .directive( 'treecontrol', ['$compile', function( $compile ) {
            /**
             * @param cssClass - the css class
             * @param addClassProperty - should we wrap the class name with class=""
             */
            function classIfDefined(cssClass, addClassProperty) {
                if (cssClass) {
                    if (addClassProperty)
                        return 'class="' + cssClass + '"';
                    else
                        return cssClass;
                }
                else
                    return "";
            }

            function ensureDefault(obj, prop, value) {
                if (!obj.hasOwnProperty(prop))
                    obj[prop] = value;
            }

            return {
                restrict: 'EA',
                require: "treecontrol",
                transclude: true,
                scope: {
                    treeModel: "=",
                    selectedNode: "=?",
                    selectedNodes: "=?",
                    expandedNodes: "=?",
                    onSelection: "&",
                    onNodeToggle: "&",
                    onRightClick: "&",
                    menuId: "@",
                    options: "=?",
                    orderBy: "@",
                    orderByExpression: "@",
                    reverseOrder: "@",
                    filterExpression: "=?",
                    filterComparator: "=?"
                },
                controller: ['$scope', '$attrs', function( $scope, $attrs) {

                    function defaultIsLeaf(node) {
                        return !node[$scope.options.nodeChildren] || node[$scope.options.nodeChildren].length === 0;
                    }

                    function shallowCopy(src, dst) {
                        if (angular.isArray(src)) {
                            dst = dst || [];

                            for ( var i = 0; i < src.length; i++) {
                                dst[i] = src[i];
                            }
                        } else if (angular.isObject(src)) {
                            dst = dst || {};

                            for (var key in src) {
                                if (hasOwnProperty.call(src, key) && !(key.charAt(0) === '$' && key.charAt(1) === '$')) {
                                    dst[key] = src[key];
                                }
                            }
                        }

                        return dst || src;
                    }
                    function defaultEquality(a, b) {
                        if (a === undefined || b === undefined)
                            return false;
                        a = shallowCopy(a);
                        a[$scope.options.nodeChildren] = [];
                        b = shallowCopy(b);
                        b[$scope.options.nodeChildren] = [];
                        return angular.equals(a, b);
                    }

                    $scope.options = $scope.options || {};
                    ensureDefault($scope.options, "multiSelection", false);
                    ensureDefault($scope.options, "nodeChildren", "children");
                    ensureDefault($scope.options, "dirSelectable", "true");
                    ensureDefault($scope.options, "injectClasses", {});
                    ensureDefault($scope.options.injectClasses, "ul", "");
                    ensureDefault($scope.options.injectClasses, "li", "");
                    ensureDefault($scope.options.injectClasses, "liSelected", "");
                    ensureDefault($scope.options.injectClasses, "iExpanded", "");
                    ensureDefault($scope.options.injectClasses, "iCollapsed", "");
                    ensureDefault($scope.options.injectClasses, "iLeaf", "");
                    ensureDefault($scope.options.injectClasses, "label", "");
                    ensureDefault($scope.options.injectClasses, "labelSelected", "");
                    ensureDefault($scope.options, "equality", defaultEquality);
                    ensureDefault($scope.options, "isLeaf", defaultIsLeaf);

                    $scope.selectedNodes = $scope.selectedNodes || [];
                    $scope.expandedNodes = $scope.expandedNodes || [];
                    $scope.expandedNodesMap = {};
                    for (var i=0; i < $scope.expandedNodes.length; i++) {
                        $scope.expandedNodesMap[""+i] = $scope.expandedNodes[i];
                    }
                    $scope.parentScopeOfTree = $scope.$parent;

                    function isSelectedNode(node) {
                        if (!$scope.options.multiSelection && ($scope.options.equality(node, $scope.selectedNode)))
                            return true;
                        else if ($scope.options.multiSelection && $scope.selectedNodes) {
                            for (var i = 0; (i < $scope.selectedNodes.length); i++) {
                                if ($scope.options.equality(node, $scope.selectedNodes[i])) {
                                    return true;
                                }
                            }
                            return false;
                        }
                    }

                    $scope.headClass = function(node) {
                        var liSelectionClass = classIfDefined($scope.options.injectClasses.liSelected, false);
                        var injectSelectionClass = "";
                        if (liSelectionClass && isSelectedNode(node))
                            injectSelectionClass = " " + liSelectionClass;
                        if ($scope.options.isLeaf(node))
                            return "tree-leaf" + injectSelectionClass;
                        if ($scope.expandedNodesMap[this.$id])
                            return "tree-expanded" + injectSelectionClass;
                        else
                            return "tree-collapsed" + injectSelectionClass;
                    };

                    $scope.iBranchClass = function() {
                        if ($scope.expandedNodesMap[this.$id])
                            return classIfDefined($scope.options.injectClasses.iExpanded);
                        else
                            return classIfDefined($scope.options.injectClasses.iCollapsed);
                    };

                    $scope.nodeExpanded = function() {
                        return !!$scope.expandedNodesMap[this.$id];
                    };

                    $scope.selectNodeHead = function() {
                        var expanding = $scope.expandedNodesMap[this.$id] === undefined;
                        $scope.expandedNodesMap[this.$id] = (expanding ? this.node : undefined);
                        if (expanding) {
                            $scope.expandedNodes.push(this.node);
                        }
                        else {
                            var index;
                            for (var i=0; (i < $scope.expandedNodes.length) && !index; i++) {
                                if ($scope.options.equality($scope.expandedNodes[i], this.node)) {
                                    index = i;
                                }
                            }
                            if (index !== undefined)
                                $scope.expandedNodes.splice(index, 1);
                        }
                        if ($scope.onNodeToggle)
                            $scope.onNodeToggle({node: this.node, expanded: expanding});
                    };

                    $scope.selectNodeLabel = function( selectedNode ){
                        if(!$scope.options.isLeaf(selectedNode) && !$scope.options.dirSelectable) {
                            this.selectNodeHead();
                        }
                        else {
                            var selected = false;
                            if ($scope.options.multiSelection) {
                                var pos = -1;
                                for (var i=0; i < $scope.selectedNodes.length; i++) {
                                    if($scope.options.equality(selectedNode, $scope.selectedNodes[i])) {
                                        pos = i;
                                        break;
                                    }
                                }
                                if (pos === -1) {
                                    $scope.selectedNodes.push(selectedNode);
                                    selected = true;
                                } else {
                                    $scope.selectedNodes.splice(pos, 1);
                                }
                            } else {
                                if (!$scope.options.equality(selectedNode, $scope.selectedNode)) {
                                    $scope.selectedNode = selectedNode;
                                    selected = true;
                                }
                                else {
                                    $scope.selectedNode = undefined;
                                }
                            }
                            if ($scope.onSelection)
                                $scope.onSelection({node: selectedNode, selected: selected});
                        }
                    };

                    $scope.rightClickNodeLabel = function( targetNode ) {

                        // Are are we changing the 'selected' node (as well)?
                        if ($scope.selectedNode != targetNode) {
                            this.selectNodeLabel( targetNode);
                        }

                        // ...and do the rightClick
                        if($scope.onRightClick) {
                            $scope.onRightClick({node: targetNode});
                        }
                    };

                    $scope.selectedClass = function() {
                        var isThisNodeSelected = isSelectedNode(this.node);
                        var labelSelectionClass = classIfDefined($scope.options.injectClasses.labelSelected, false);
                        var injectSelectionClass = "";
                        if (labelSelectionClass && isThisNodeSelected)
                            injectSelectionClass = " " + labelSelectionClass;

                        return isThisNodeSelected?"tree-selected" + injectSelectionClass:"";
                    };

                    //tree template
                    var orderBy = '';
                    if ($scope.orderByExpression) {
                        orderBy = ' | orderBy:' + $scope.orderByExpression + ':reverseOrder';
                    } else if ($scope.orderBy) {
                        orderBy = ' | orderBy:orderBy:reverseOrder';
                    }
                    var rcLabel = $scope.onRightClick ? ' tree-right-click="rightClickNodeLabel(node)"' : '';
                    var ctxMenuId = $scope.menuId ? ' context-menu-id="'+ $scope.menuId+'"' : '';

                    var template =
                        '
' +
                            '
' +
                            '' +
                            '' +
                            '

' +
                            '<treeitem ng-if="nodeExpanded()"></treeitem>' +
                            '
' +
                            '
';

                    this.template = $compile(template);
                }],

                compile: function(element, attrs, childTranscludeFn) {
                    return function ( scope, element, attrs, treemodelCntr ) {

                        scope.$watch("treeModel", function updateNodeOnRootScope(newValue) {
                            if (angular.isArray(newValue)) {
                                if (angular.isDefined(scope.node) && angular.equals(scope.node[scope.options.nodeChildren], newValue))
                                    return;
                                scope.node = {};
                                scope.synteticRoot = scope.node;
                                scope.node[scope.options.nodeChildren] = newValue;
                            }
                            else {
                                if (angular.equals(scope.node, newValue))
                                    return;
                                scope.node = newValue;
                            }
                        });

                        scope.$watchCollection('expandedNodes', function(newValue) {
                            var notFoundIds = 0;
                            var newExpandedNodesMap = {};
                            var $liElements = element.find('li');
                            var existingScopes = [];
                            // find all nodes visible on the tree and the scope $id of the scopes including them
                            angular.forEach($liElements, function(liElement) {
                                var $liElement = angular.element(liElement);
                                var liScope = $liElement.scope();
                                existingScopes.push(liScope);
                            });
                            // iterate over the newValue, the new expanded nodes, and for each find it in the existingNodesAndScopes
                            // if found, add the mapping $id -> node into newExpandedNodesMap
                            // if not found, add the mapping num -> node into newExpandedNodesMap
                            angular.forEach(newValue, function(newExNode) {
                                var found = false;
                                for (var i=0; (i < existingScopes.length) && !found; i++) {
                                    var existingScope = existingScopes[i];
                                    if (scope.options.equality(newExNode, existingScope.node)) {
                                        newExpandedNodesMap[existingScope.$id] = existingScope.node;
                                        found = true;
                                    }
                                }
                                if (!found)
                                    newExpandedNodesMap[notFoundIds++] = newExNode;
                            });
                            scope.expandedNodesMap = newExpandedNodesMap;
                        });

//                        scope.$watch('expandedNodesMap', function(newValue) {
//
//                        });

                        //Rendering template for a root node
                        treemodelCntr.template( scope, function(clone) {
                            element.html('').append( clone );
                        });
                        // save the transclude function from compile (which is not bound to a scope as apposed to the one from link)
                        // we can fix this to work with the link transclude function with angular 1.2.6. as for angular 1.2.0 we need
                        // to keep using the compile function
                        scope.$treeTransclude = childTranscludeFn;
                    };
                }
            };
        }])

        .directive('treeRightClick', function($parse) {
            return function(scope, element, attrs) {
                var fn = $parse(attrs.treeRightClick);
                element.bind('contextmenu', function(event) {
                    scope.$apply(function() {
                        event.preventDefault();     // Don't show the browser's normal context menu
                        fn(scope, {$event:event});  // go do our stuff
                    });
                });
            };
        })

        .directive("treeitem", function() {
            return {
                restrict: 'E',
                require: "^treecontrol",
                link: function( scope, element, attrs, treemodelCntr) {
                    // Rendering template for the current node
                    treemodelCntr.template(scope, function(clone) {
                        element.html('').append(clone);
                    });
                }
            };
        })

        .directive("treeTransclude", function() {
            return {
                link: function(scope, element, attrs, controller) {
                    if (!scope.options.isLeaf(scope.node)) {
                        angular.forEach(scope.expandedNodesMap, function (node, id) {
                            if (scope.options.equality(node, scope.node)) {
                                scope.expandedNodesMap[scope.$id] = scope.node;
                                scope.expandedNodesMap[id] = undefined;
                            }
                        });
                    }
                    if (!scope.options.multiSelection && scope.options.equality(scope.node, scope.selectedNode)) {
                        scope.selectedNode = scope.node;
                    } else if (scope.options.multiSelection) {
                        var newSelectedNodes = [];
                        for (var i = 0; (i < scope.selectedNodes.length); i++) {
                            if (scope.options.equality(scope.node, scope.selectedNodes[i])) {
                                newSelectedNodes.push(scope.node);
                            }
                        }
                        scope.selectedNodes = newSelectedNodes;
                    }

                    // create a scope for the transclusion, whos parent is the parent of the tree control
                    scope.transcludeScope = scope.parentScopeOfTree.$new();
                    scope.transcludeScope.node = scope.node;
                    scope.transcludeScope.$parentNode = (scope.$parent.node === scope.synteticRoot)?null:scope.$parent.node;
                    scope.transcludeScope.$index = scope.$index;
                    scope.transcludeScope.$first = scope.$first;
                    scope.transcludeScope.$middle = scope.$middle;
                    scope.transcludeScope.$last = scope.$last;
                    scope.transcludeScope.$odd = scope.$odd;
                    scope.transcludeScope.$even = scope.$even;
                    scope.$on('$destroy', function() {
                        scope.transcludeScope.$destroy();
                    });

                    scope.$treeTransclude(scope.transcludeScope, function(clone) {
                        element.empty();
                        element.append(clone);
                    });
                }
            };
        });
})( angular );
<commit_msg>Don't actually need  in the controller<commit_after>(function ( angular ) {
    'use strict';

    angular.module( 'treeControl', ['contextMenu'] )
        .directive( 'treecontrol', ['$compile', function( $compile ) {
            /**
             * @param cssClass - the css class
             * @param addClassProperty - should we wrap the class name with class=""
             */
            function classIfDefined(cssClass, addClassProperty) {
                if (cssClass) {
                    if (addClassProperty)
                        return 'class="' + cssClass + '"';
                    else
                        return cssClass;
                }
                else
                    return "";
            }

            function ensureDefault(obj, prop, value) {
                if (!obj.hasOwnProperty(prop))
                    obj[prop] = value;
            }

            return {
                restrict: 'EA',
                require: "treecontrol",
                transclude: true,
                scope: {
                    treeModel: "=",
                    selectedNode: "=?",
                    selectedNodes: "=?",
                    expandedNodes: "=?",
                    onSelection: "&",
                    onNodeToggle: "&",
                    onRightClick: "&",
                    menuId: "@",
                    options: "=?",
                    orderBy: "@",
                    orderByExpression: "@",
                    reverseOrder: "@",
                    filterExpression: "=?",
                    filterComparator: "=?"
                },
                controller: ['$scope', function( $scope ) {

                    function defaultIsLeaf(node) {
                        return !node[$scope.options.nodeChildren] || node[$scope.options.nodeChildren].length === 0;
                    }

                    function shallowCopy(src, dst) {
                        if (angular.isArray(src)) {
                            dst = dst || [];

                            for ( var i = 0; i < src.length; i++) {
                                dst[i] = src[i];
                            }
                        } else if (angular.isObject(src)) {
                            dst = dst || {};

                            for (var key in src) {
                                if (hasOwnProperty.call(src, key) && !(key.charAt(0) === '$' && key.charAt(1) === '$')) {
                                    dst[key] = src[key];
                                }
                            }
                        }

                        return dst || src;
                    }
                    function defaultEquality(a, b) {
                        if (a === undefined || b === undefined)
                            return false;
                        a = shallowCopy(a);
                        a[$scope.options.nodeChildren] = [];
                        b = shallowCopy(b);
                        b[$scope.options.nodeChildren] = [];
                        return angular.equals(a, b);
                    }

                    $scope.options = $scope.options || {};
                    ensureDefault($scope.options, "multiSelection", false);
                    ensureDefault($scope.options, "nodeChildren", "children");
                    ensureDefault($scope.options, "dirSelectable", "true");
                    ensureDefault($scope.options, "injectClasses", {});
                    ensureDefault($scope.options.injectClasses, "ul", "");
                    ensureDefault($scope.options.injectClasses, "li", "");
                    ensureDefault($scope.options.injectClasses, "liSelected", "");
                    ensureDefault($scope.options.injectClasses, "iExpanded", "");
                    ensureDefault($scope.options.injectClasses, "iCollapsed", "");
                    ensureDefault($scope.options.injectClasses, "iLeaf", "");
                    ensureDefault($scope.options.injectClasses, "label", "");
                    ensureDefault($scope.options.injectClasses, "labelSelected", "");
                    ensureDefault($scope.options, "equality", defaultEquality);
                    ensureDefault($scope.options, "isLeaf", defaultIsLeaf);

                    $scope.selectedNodes = $scope.selectedNodes || [];
                    $scope.expandedNodes = $scope.expandedNodes || [];
                    $scope.expandedNodesMap = {};
                    for (var i=0; i < $scope.expandedNodes.length; i++) {
                        $scope.expandedNodesMap[""+i] = $scope.expandedNodes[i];
                    }
                    $scope.parentScopeOfTree = $scope.$parent;

                    function isSelectedNode(node) {
                        if (!$scope.options.multiSelection && ($scope.options.equality(node, $scope.selectedNode)))
                            return true;
                        else if ($scope.options.multiSelection && $scope.selectedNodes) {
                            for (var i = 0; (i < $scope.selectedNodes.length); i++) {
                                if ($scope.options.equality(node, $scope.selectedNodes[i])) {
                                    return true;
                                }
                            }
                            return false;
                        }
                    }

                    $scope.headClass = function(node) {
                        var liSelectionClass = classIfDefined($scope.options.injectClasses.liSelected, false);
                        var injectSelectionClass = "";
                        if (liSelectionClass && isSelectedNode(node))
                            injectSelectionClass = " " + liSelectionClass;
                        if ($scope.options.isLeaf(node))
                            return "tree-leaf" + injectSelectionClass;
                        if ($scope.expandedNodesMap[this.$id])
                            return "tree-expanded" + injectSelectionClass;
                        else
                            return "tree-collapsed" + injectSelectionClass;
                    };

                    $scope.iBranchClass = function() {
                        if ($scope.expandedNodesMap[this.$id])
                            return classIfDefined($scope.options.injectClasses.iExpanded);
                        else
                            return classIfDefined($scope.options.injectClasses.iCollapsed);
                    };

                    $scope.nodeExpanded = function() {
                        return !!$scope.expandedNodesMap[this.$id];
                    };

                    $scope.selectNodeHead = function() {
                        var expanding = $scope.expandedNodesMap[this.$id] === undefined;
                        $scope.expandedNodesMap[this.$id] = (expanding ? this.node : undefined);
                        if (expanding) {
                            $scope.expandedNodes.push(this.node);
                        }
                        else {
                            var index;
                            for (var i=0; (i < $scope.expandedNodes.length) && !index; i++) {
                                if ($scope.options.equality($scope.expandedNodes[i], this.node)) {
                                    index = i;
                                }
                            }
                            if (index !== undefined)
                                $scope.expandedNodes.splice(index, 1);
                        }
                        if ($scope.onNodeToggle)
                            $scope.onNodeToggle({node: this.node, expanded: expanding});
                    };

                    $scope.selectNodeLabel = function( selectedNode ){
                        if(!$scope.options.isLeaf(selectedNode) && !$scope.options.dirSelectable) {
                            this.selectNodeHead();
                        }
                        else {
                            var selected = false;
                            if ($scope.options.multiSelection) {
                                var pos = -1;
                                for (var i=0; i < $scope.selectedNodes.length; i++) {
                                    if($scope.options.equality(selectedNode, $scope.selectedNodes[i])) {
                                        pos = i;
                                        break;
                                    }
                                }
                                if (pos === -1) {
                                    $scope.selectedNodes.push(selectedNode);
                                    selected = true;
                                } else {
                                    $scope.selectedNodes.splice(pos, 1);
                                }
                            } else {
                                if (!$scope.options.equality(selectedNode, $scope.selectedNode)) {
                                    $scope.selectedNode = selectedNode;
                                    selected = true;
                                }
                                else {
                                    $scope.selectedNode = undefined;
                                }
                            }
                            if ($scope.onSelection)
                                $scope.onSelection({node: selectedNode, selected: selected});
                        }
                    };

                    $scope.rightClickNodeLabel = function( targetNode ) {

                        // Are are we changing the 'selected' node (as well)?
                        if ($scope.selectedNode != targetNode) {
                            this.selectNodeLabel( targetNode);
                        }

                        // ...and do the rightClick
                        if($scope.onRightClick) {
                            $scope.onRightClick({node: targetNode});
                        }
                    };

                    $scope.selectedClass = function() {
                        var isThisNodeSelected = isSelectedNode(this.node);
                        var labelSelectionClass = classIfDefined($scope.options.injectClasses.labelSelected, false);
                        var injectSelectionClass = "";
                        if (labelSelectionClass && isThisNodeSelected)
                            injectSelectionClass = " " + labelSelectionClass;

                        return isThisNodeSelected?"tree-selected" + injectSelectionClass:"";
                    };

                    //tree template
                    var orderBy = '';
                    if ($scope.orderByExpression) {
                        orderBy = ' | orderBy:' + $scope.orderByExpression + ':reverseOrder';
                    } else if ($scope.orderBy) {
                        orderBy = ' | orderBy:orderBy:reverseOrder';
                    }
                    var rcLabel = $scope.onRightClick ? ' tree-right-click="rightClickNodeLabel(node)"' : '';
                    var ctxMenuId = $scope.menuId ? ' context-menu-id="'+ $scope.menuId+'"' : '';

                    var template =
                        '
' +
                            '
' +
                            '' +
                            '' +
                            '

' +
                            '<treeitem ng-if="nodeExpanded()"></treeitem>' +
                            '
' +
                            '
';

                    this.template = $compile(template);
                }],

                compile: function(element, attrs, childTranscludeFn) {
                    return function ( scope, element, attrs, treemodelCntr ) {

                        scope.$watch("treeModel", function updateNodeOnRootScope(newValue) {
                            if (angular.isArray(newValue)) {
                                if (angular.isDefined(scope.node) && angular.equals(scope.node[scope.options.nodeChildren], newValue))
                                    return;
                                scope.node = {};
                                scope.synteticRoot = scope.node;
                                scope.node[scope.options.nodeChildren] = newValue;
                            }
                            else {
                                if (angular.equals(scope.node, newValue))
                                    return;
                                scope.node = newValue;
                            }
                        });

                        scope.$watchCollection('expandedNodes', function(newValue) {
                            var notFoundIds = 0;
                            var newExpandedNodesMap = {};
                            var $liElements = element.find('li');
                            var existingScopes = [];
                            // find all nodes visible on the tree and the scope $id of the scopes including them
                            angular.forEach($liElements, function(liElement) {
                                var $liElement = angular.element(liElement);
                                var liScope = $liElement.scope();
                                existingScopes.push(liScope);
                            });
                            // iterate over the newValue, the new expanded nodes, and for each find it in the existingNodesAndScopes
                            // if found, add the mapping $id -> node into newExpandedNodesMap
                            // if not found, add the mapping num -> node into newExpandedNodesMap
                            angular.forEach(newValue, function(newExNode) {
                                var found = false;
                                for (var i=0; (i < existingScopes.length) && !found; i++) {
                                    var existingScope = existingScopes[i];
                                    if (scope.options.equality(newExNode, existingScope.node)) {
                                        newExpandedNodesMap[existingScope.$id] = existingScope.node;
                                        found = true;
                                    }
                                }
                                if (!found)
                                    newExpandedNodesMap[notFoundIds++] = newExNode;
                            });
                            scope.expandedNodesMap = newExpandedNodesMap;
                        });

//                        scope.$watch('expandedNodesMap', function(newValue) {
//
//                        });

                        //Rendering template for a root node
                        treemodelCntr.template( scope, function(clone) {
                            element.html('').append( clone );
                        });
                        // save the transclude function from compile (which is not bound to a scope as apposed to the one from link)
                        // we can fix this to work with the link transclude function with angular 1.2.6. as for angular 1.2.0 we need
                        // to keep using the compile function
                        scope.$treeTransclude = childTranscludeFn;
                    };
                }
            };
        }])

        .directive('treeRightClick', function($parse) {
            return function(scope, element, attrs) {
                var fn = $parse(attrs.treeRightClick);
                element.bind('contextmenu', function(event) {
                    scope.$apply(function() {
                        event.preventDefault();     // Don't show the browser's normal context menu
                        fn(scope, {$event:event});  // go do our stuff
                    });
                });
            };
        })

        .directive("treeitem", function() {
            return {
                restrict: 'E',
                require: "^treecontrol",
                link: function( scope, element, attrs, treemodelCntr) {
                    // Rendering template for the current node
                    treemodelCntr.template(scope, function(clone) {
                        element.html('').append(clone);
                    });
                }
            };
        })

        .directive("treeTransclude", function() {
            return {
                link: function(scope, element, attrs, controller) {
                    if (!scope.options.isLeaf(scope.node)) {
                        angular.forEach(scope.expandedNodesMap, function (node, id) {
                            if (scope.options.equality(node, scope.node)) {
                                scope.expandedNodesMap[scope.$id] = scope.node;
                                scope.expandedNodesMap[id] = undefined;
                            }
                        });
                    }
                    if (!scope.options.multiSelection && scope.options.equality(scope.node, scope.selectedNode)) {
                        scope.selectedNode = scope.node;
                    } else if (scope.options.multiSelection) {
                        var newSelectedNodes = [];
                        for (var i = 0; (i < scope.selectedNodes.length); i++) {
                            if (scope.options.equality(scope.node, scope.selectedNodes[i])) {
                                newSelectedNodes.push(scope.node);
                            }
                        }
                        scope.selectedNodes = newSelectedNodes;
                    }

                    // create a scope for the transclusion, whos parent is the parent of the tree control
                    scope.transcludeScope = scope.parentScopeOfTree.$new();
                    scope.transcludeScope.node = scope.node;
                    scope.transcludeScope.$parentNode = (scope.$parent.node === scope.synteticRoot)?null:scope.$parent.node;
                    scope.transcludeScope.$index = scope.$index;
                    scope.transcludeScope.$first = scope.$first;
                    scope.transcludeScope.$middle = scope.$middle;
                    scope.transcludeScope.$last = scope.$last;
                    scope.transcludeScope.$odd = scope.$odd;
                    scope.transcludeScope.$even = scope.$even;
                    scope.$on('$destroy', function() {
                        scope.transcludeScope.$destroy();
                    });

                    scope.$treeTransclude(scope.transcludeScope, function(clone) {
                        element.empty();
                        element.append(clone);
                    });
                }
            };
        });
})( angular );
<|endoftext|>


<commit_before>(function (enyo, scope) {

	/**
	* Canvas is a UI library for Enyo 2.
	*
	* The library is mainly intended as a demonstration, rather than a complete
	* tool for canvas manipulation, and there are no changes or additional
	* features planned.
	*
	* @namespace enyo.canvas
	*/

	if (enyo && enyo.version) {
		enyo.version.canvas = "2.5.3-pre.1";
	}

})(enyo, this);
<commit_msg>Update version string to 2.5.3-zzz.3<commit_after>(function (enyo, scope) {

	/**
	* Canvas is a UI library for Enyo 2.
	*
	* The library is mainly intended as a demonstration, rather than a complete
	* tool for canvas manipulation, and there are no changes or additional
	* features planned.
	*
	* @namespace enyo.canvas
	*/

	if (enyo && enyo.version) {
		enyo.version.canvas = "2.5.3-zzz.3";
	}

})(enyo, this);
<|endoftext|>