<commit_before>declare class flyd$Stream<+T> {
  static val: T;

  end(boolean): flyd$Stream<boolean>;
  map<T, V>((T) => V): flyd$Stream<V>;
  ap<V>(flyd$Stream<V>): flyd$Stream<V>;
  of<V>(...values: V[]): flyd$Stream<V>;

  (...rest: Array<void>): T;
  (*): flyd$Stream<*>;
}
type Stream = flyd$Stream<*>;
type CombineFn = (...args: Array<Stream>) => *;
type MapFn = (*) => *;
type ScanFn = (*, *) => *;
type LiftFn = (...args: Array<*>) => *;

type RegularObj = { [key: string]: * | RegularObj };
type StreamObj = { [key: string]: Stream | StreamObj };

declare module 'flyd' {
  declare type Stream = Stream;
  declare type Flyd$Stream<T> = flyd$Stream<T>;

  declare module.exports: {
    stream<T>(v: T): flyd$Stream<T>,
    immediate<T>(s: flyd$Stream<?T>): flyd$Stream<?T>,
    isStream(s: *): boolean,
    combine: CurriedFunction2<CombineFn, Array<flyd$Stream<*>>, Stream>,
    endsOn: CurriedFunction2<Stream, Stream, Stream>,
    map: CurriedFunction2<MapFn, Stream, Stream>,
    on: CurriedFunction2<MapFn, Stream, Stream>,
    scan: CurriedFunction3<ScanFn, *, Stream, Stream>,
    merge: CurriedFunction2<Stream, Stream, Stream>,
    transduce: CurriedFunction2<MapFn | Function, Stream, Stream>,
    curryN(length: number, fn: (...args: Array<*>) => *): Function,
  };
}

declare module 'flyd/module/aftersilence' {
  declare module.exports: CurriedFunction2<number, Stream, Stream>;
}

declare module 'flyd/module/diff' {
  declare module.exports: CurriedFunction2<ScanFn, Stream, Stream>;
}

declare module 'flyd/module/droprepeats' {
  declare module.exports: {
    dropRepeats: (s: Stream) => Stream,
    dropRepeatsWith: CurriedFunction2<(*) => boolean, Stream, Stream>,
  };
}

declare module 'flyd/module/every' {
  declare module.exports: (number) => Stream;
}

declare module 'flyd/module/filter' {
  declare module.exports: CurriedFunction2<(*) => boolean, Stream, Stream>;
}

declare module 'flyd/module/flatmap' {
  declare module.exports: CurriedFunction2<MapFn, Stream, Stream>;
}

declare module 'flyd/module/forwardto' {
  declare module.exports: CurriedFunction2<Stream, (*) => *, Stream>;
}

declare module 'flyd/module/inlast' {
  declare module.exports: CurriedFunction2<number, Stream, Stream>;
}

declare module 'flyd/module/keepwhen' {
  declare module.exports: CurriedFunction2<Stream, Stream, Stream>;
}

declare module 'flyd/module/lift' {
  declare module.exports: (LiftFn, ...args: Array<Stream>) => Stream;
}

declare module 'flyd/module/mergeall' {
  declare module.exports: (Array<Stream>) => Stream;
}

declare module 'flyd/module/obj' {
  declare module.exports: {
    streamProps(obj: RegularObj): StreamObj,
    extractProps(obj: StreamObj): RegularObj,
    stream(obj: StreamObj): Stream,
  };
}

declare module 'flyd/module/previous' {
  declare module.exports: (Stream) => Stream;
}

declare module 'flyd/module/sampleon' {
  declare module.exports: CurriedFunction2<Stream, Stream, Stream>;
}

declare module 'flyd/module/scanmerge' {
  declare module.exports: CurriedFunction2<[Stream, ScanFn], *, Stream>;
}

declare module 'flyd/module/switchlatest' {
  declare module.exports: (Stream) => Stream;
}

declare module 'flyd/module/takeuntil' {
  declare module.exports: CurriedFunction2<Stream, Stream, Stream>;
}

// Filename aliases
declare module 'flyd/flyd' {
  declare module.exports: $Exports<'flyd'>;
}
declare module 'flyd/flyd.js' {
  declare module.exports: $Exports<'flyd'>;
}
declare module 'flyd/lib' {
  declare module.exports: $Exports<'flyd'>;
}
declare module 'flyd/lib/index' {
  declare module.exports: $Exports<'flyd/lib'>;
}
declare module 'flyd/lib/index.js' {
  declare module.exports: $Exports<'flyd/lib'>;
}

// copy from ramda because importing won't work...
type CurriedFunction2<T1, T2, R> =
  & ((t1: T1, t2: T2) => R)
  & ((t1: T1, ...rest: Array<void>) => (t2: T2) => R);

declare type CurriedFunction3<T1, T2, T3, R> =
  & ((t1: T1, t2: T2, t3: T3) => R)
  & ((t1: T1, t2: T2, ...rest: Array<void>) => (t3: T3) => R)
  & ((t1: T1, ...rest: Array<void>) => CurriedFunction2<T2, T3, R>)
<commit_msg>Fix Flyd `scanMerge` module def (#1024)<commit_after>declare class flyd$Stream<+T> {
  static val: T;

  end(boolean): flyd$Stream<boolean>;
  map<T, V>((T) => V): flyd$Stream<V>;
  ap<V>(flyd$Stream<V>): flyd$Stream<V>;
  of<V>(...values: V[]): flyd$Stream<V>;

  (...rest: Array<void>): T;
  (*): flyd$Stream<*>;
}
type Stream = flyd$Stream<*>;
type CombineFn = (...args: Array<Stream>) => *;
type MapFn = (*) => *;
type ScanFn = (*, *) => *;
type LiftFn = (...args: Array<*>) => *;

type RegularObj = { [key: string]: * | RegularObj };
type StreamObj = { [key: string]: Stream | StreamObj };

declare module 'flyd' {
  declare type Stream = Stream;
  declare type Flyd$Stream<T> = flyd$Stream<T>;

  declare module.exports: {
    stream<T>(v: T): flyd$Stream<T>,
    immediate<T>(s: flyd$Stream<?T>): flyd$Stream<?T>,
    isStream(s: *): boolean,
    combine: CurriedFunction2<CombineFn, Array<flyd$Stream<*>>, Stream>,
    endsOn: CurriedFunction2<Stream, Stream, Stream>,
    map: CurriedFunction2<MapFn, Stream, Stream>,
    on: CurriedFunction2<MapFn, Stream, Stream>,
    scan: CurriedFunction3<ScanFn, *, Stream, Stream>,
    merge: CurriedFunction2<Stream, Stream, Stream>,
    transduce: CurriedFunction2<MapFn | Function, Stream, Stream>,
    curryN(length: number, fn: (...args: Array<*>) => *): Function,
  };
}

declare module 'flyd/module/aftersilence' {
  declare module.exports: CurriedFunction2<number, Stream, Stream>;
}

declare module 'flyd/module/diff' {
  declare module.exports: CurriedFunction2<ScanFn, Stream, Stream>;
}

declare module 'flyd/module/droprepeats' {
  declare module.exports: {
    dropRepeats: (s: Stream) => Stream,
    dropRepeatsWith: CurriedFunction2<(*) => boolean, Stream, Stream>,
  };
}

declare module 'flyd/module/every' {
  declare module.exports: (number) => Stream;
}

declare module 'flyd/module/filter' {
  declare module.exports: CurriedFunction2<(*) => boolean, Stream, Stream>;
}

declare module 'flyd/module/flatmap' {
  declare module.exports: CurriedFunction2<MapFn, Stream, Stream>;
}

declare module 'flyd/module/forwardto' {
  declare module.exports: CurriedFunction2<Stream, (*) => *, Stream>;
}

declare module 'flyd/module/inlast' {
  declare module.exports: CurriedFunction2<number, Stream, Stream>;
}

declare module 'flyd/module/keepwhen' {
  declare module.exports: CurriedFunction2<Stream, Stream, Stream>;
}

declare module 'flyd/module/lift' {
  declare module.exports: (LiftFn, ...args: Array<Stream>) => Stream;
}

declare module 'flyd/module/mergeall' {
  declare module.exports: (Array<Stream>) => Stream;
}

declare module 'flyd/module/obj' {
  declare module.exports: {
    streamProps(obj: RegularObj): StreamObj,
    extractProps(obj: StreamObj): RegularObj,
    stream(obj: StreamObj): Stream,
  };
}

declare module 'flyd/module/previous' {
  declare module.exports: (Stream) => Stream;
}

declare module 'flyd/module/sampleon' {
  declare module.exports: CurriedFunction2<Stream, Stream, Stream>;
}

declare module 'flyd/module/scanmerge' {
  declare module.exports: CurriedFunction2<Array<[Stream, ScanFn]>, *, Stream>;
}

declare module 'flyd/module/switchlatest' {
  declare module.exports: (Stream) => Stream;
}

declare module 'flyd/module/takeuntil' {
  declare module.exports: CurriedFunction2<Stream, Stream, Stream>;
}

// Filename aliases
declare module 'flyd/flyd' {
  declare module.exports: $Exports<'flyd'>;
}
declare module 'flyd/flyd.js' {
  declare module.exports: $Exports<'flyd'>;
}
declare module 'flyd/lib' {
  declare module.exports: $Exports<'flyd'>;
}
declare module 'flyd/lib/index' {
  declare module.exports: $Exports<'flyd/lib'>;
}
declare module 'flyd/lib/index.js' {
  declare module.exports: $Exports<'flyd/lib'>;
}

// copy from ramda because importing won't work...
type CurriedFunction2<T1, T2, R> =
  & ((t1: T1, t2: T2) => R)
  & ((t1: T1, ...rest: Array<void>) => (t2: T2) => R);

declare type CurriedFunction3<T1, T2, T3, R> =
  & ((t1: T1, t2: T2, t3: T3) => R)
  & ((t1: T1, t2: T2, ...rest: Array<void>) => (t3: T3) => R)
  & ((t1: T1, ...rest: Array<void>) => CurriedFunction2<T2, T3, R>)
<|endoftext|>


<commit_before>Vue.component('settings-realname', {

    data(){
        return {
            forms: {
                realName: $.extend(true, new Form({
                    requireRealname: false,
                    realnameSetting: 'OFF',
                }), {})
            }
        }
    },

    mounted(){
        if (ServerData != null) {
            this.forms.realName.requireRealname = ServerData.require_realname;
            this.forms.realName.realnameSetting = ServerData.realname;
        }
    },

    methods: {
        sendForm(){
            if (this.forms.realName.realnameSetting === "OFF") {
                this.forms.realName.requireRealname = false;
            }
            Panel.post('/dashboard/' + Server.id + '/realname', this.forms.realName);
        }
    }
});

Vue.component('settings-commands', {
    data(){
        return {
            forms: {
                cmdDiscriminator: $.extend(true, new Form({
                    discriminator: '!'
                }), {}),
                editCommand: $.extend(true, new Form({
                    name: '',
                    description: '',
                    clearance: '',
                    id: ''
                }), {})
            },
            commands: [],
            addingCommand: false,
            toDelete: ''
        }
    },

    mounted(){
        if (ServerData != null) {
            this.forms.cmdDiscriminator.discriminator = ServerData.command_discriminator
        }
        if (Commands != null) {
            this.commands = Commands;
        }
    },

    methods: {
        localizeClearance(clearance){
            switch (clearance) {
                case "BOT_OWNER":
                    return "Bot Owner";
                case "SERVER_OWNER":
                    return "Server Owner";
                case "SERVER_ADMINISTRATOR":
                    return "Server Administrator";
                case "BOT_MANAGER":
                    return "Bot Manager";
                case "USER":
                    return "User";
                case "BOT":
                    return "Bot";
            }
        },

        editCommand(id, newCommand){
            this.forms.editCommand.successful = false;
            this.forms.editCommand.busy = false;
            this.forms.editCommand.errors.forget();
            this.addingCommand = newCommand;
            if (!newCommand) {
                this.commands.forEach(c => {
                    if (c.id === id) {
                        this.forms.editCommand.name = c.name;
                        this.forms.editCommand.clearance = c.clearance;
                        this.forms.editCommand.description = c.data;
                        this.forms.editCommand.id = c.id;
                    }
                });
            }
            let vm = this;
            $("#edit-command-modal").modal({
                closable: false,
                transition: 'scale',
                onApprove(){
                    if (newCommand) {
                        Panel.put('/dashboard/' + Server.id + '/commands', vm.forms.editCommand).then(() => {
                            // fufilled
                            vm.refreshCommands();
                            setTimeout(() => {
                                $("#edit-command-modal").modal('hide');
                            }, 1000)
                        }, () => {
                            // rejected
                        });
                        return false;
                    } else {
                        Panel.sendForm('patch', '/dashboard/' + Server.id + '/commands', vm.forms.editCommand).then(() => {
                            // fufilled
                            vm.refreshCommands();
                            setTimeout(() => {
                                $("#edit-command-modal").modal('hide');
                            }, 1000)
                        }, () => {
                            // rejected
                        });
                        return false
                    }
                },
                onDeny(){
                    vm.forms.editCommand.name = "";
                    vm.forms.editCommand.clearance = "BOT";
                    vm.forms.editCommand.description = "";
                },

            }).modal('show');
        },

        confirmDelete(id){
            this.toDelete = id;
            let vm = this;
            $("#delete-command-modal").modal({
                closable: false,
                transition: 'scale',
                onApprove(){
                    axios.delete('/dashboard/' + Server.id + '/command/' + id).then(resp=>{
                        vm.refreshCommands();
                    })
                }
            }).modal('show');
        },

        saveDiscrim(){
            Panel.sendForm('patch', '/dashboard/' + Server.id + '/discriminator', this.forms.cmdDiscriminator);
        },

        refreshCommands(){
            axios.get('/dashboard/' + Server.id + '/commands').then(response => {
                this.commands = response.data;
            })
        }
    }
});<commit_msg>Clean up the active editing command when the modal is closed<commit_after>Vue.component('settings-realname', {

    data(){
        return {
            forms: {
                realName: $.extend(true, new Form({
                    requireRealname: false,
                    realnameSetting: 'OFF',
                }), {})
            }
        }
    },

    mounted(){
        if (ServerData != null) {
            this.forms.realName.requireRealname = ServerData.require_realname;
            this.forms.realName.realnameSetting = ServerData.realname;
        }
    },

    methods: {
        sendForm(){
            if (this.forms.realName.realnameSetting === "OFF") {
                this.forms.realName.requireRealname = false;
            }
            Panel.post('/dashboard/' + Server.id + '/realname', this.forms.realName);
        }
    }
});

Vue.component('settings-commands', {
    data(){
        return {
            forms: {
                cmdDiscriminator: $.extend(true, new Form({
                    discriminator: '!'
                }), {}),
                editCommand: $.extend(true, new Form({
                    name: '',
                    description: '',
                    clearance: '',
                    id: ''
                }), {})
            },
            commands: [],
            addingCommand: false,
            toDelete: ''
        }
    },

    mounted(){
        if (ServerData != null) {
            this.forms.cmdDiscriminator.discriminator = ServerData.command_discriminator
        }
        if (Commands != null) {
            this.commands = Commands;
        }
    },

    methods: {
        localizeClearance(clearance){
            switch (clearance) {
                case "BOT_OWNER":
                    return "Bot Owner";
                case "SERVER_OWNER":
                    return "Server Owner";
                case "SERVER_ADMINISTRATOR":
                    return "Server Administrator";
                case "BOT_MANAGER":
                    return "Bot Manager";
                case "USER":
                    return "User";
                case "BOT":
                    return "Bot";
            }
        },

        editCommand(id, newCommand){
            this.forms.editCommand.successful = false;
            this.forms.editCommand.busy = false;
            this.forms.editCommand.errors.forget();
            this.addingCommand = newCommand;
            if (!newCommand) {
                this.commands.forEach(c => {
                    if (c.id === id) {
                        this.forms.editCommand.name = c.name;
                        this.forms.editCommand.clearance = c.clearance;
                        this.forms.editCommand.description = c.data;
                        this.forms.editCommand.id = c.id;
                    }
                });
            }
            let vm = this;
            $("#edit-command-modal").modal({
                closable: false,
                transition: 'scale',
                onApprove(){
                    if (newCommand) {
                        Panel.put('/dashboard/' + Server.id + '/commands', vm.forms.editCommand).then(() => {
                            // fufilled
                            vm.refreshCommands();
                            setTimeout(() => {
                                $("#edit-command-modal").modal('hide');
                            }, 1000)
                        }, () => {
                            // rejected
                        });
                        return false;
                    } else {
                        Panel.sendForm('patch', '/dashboard/' + Server.id + '/commands', vm.forms.editCommand).then(() => {
                            // fufilled
                            vm.refreshCommands();
                            setTimeout(() => {
                                $("#edit-command-modal").modal('hide');
                            }, 1000)
                        }, () => {
                            // rejected
                        });
                        return false
                    }
                },
                onHide(){
                    vm.forms.editCommand.name = "";
                    vm.forms.editCommand.clearance = "";
                    vm.forms.editCommand.description = "";
                    vm.forms.editCommand.id = "";
                }

            }).modal('show');
        },

        confirmDelete(id){
            this.toDelete = id;
            let vm = this;
            $("#delete-command-modal").modal({
                closable: false,
                transition: 'scale',
                onApprove(){
                    axios.delete('/dashboard/' + Server.id + '/command/' + id).then(resp=>{
                        vm.refreshCommands();
                    })
                }
            }).modal('show');
        },

        saveDiscrim(){
            Panel.sendForm('patch', '/dashboard/' + Server.id + '/discriminator', this.forms.cmdDiscriminator);
        },

        refreshCommands(){
            axios.get('/dashboard/' + Server.id + '/commands').then(response => {
                this.commands = response.data;
            })
        }
    }
});<|endoftext|>


<commit_before>import React from 'react'
import {connect} from 'react-redux'

export default class EventCard extends React.Component {
  render() {
    const e = this.props.event;
    return

{this.props.event.title}

        <button
          onClick={
            ()=>{
              window.location=`/post_question/${e.lookup_id}`;
            }
          }
          className="event-card-button">
          Submit a question
        </button>
        <button
          onClick={
            ()=>{
              window.location=`/e/${encodeURIComponent(e.title.replace('-','_'))}-${e.lookup_id}`;
            }
          }
          className="event-card-button">
          View, vote, and comment on questions
        </button>

  }
}
<commit_msg>readable links<commit_after>import React from 'react'
import {connect} from 'react-redux'

export default class EventCard extends React.Component {
  render() {
    const e = this.props.event;
    const clean_title = encodeURIComponent(e.title.replace(/[- ]/g,'_'));
    return

{this.props.event.title}

        <button
          onClick={
            ()=>{
              window.location=`/post_question/${e.lookup_id}`;
            }
          }
          className="event-card-button">
          Submit a question
        </button>
        <button
          onClick={
            ()=>{
              window.location=`/e/${clean_title}-${e.lookup_id}`;
            }
          }
          className="event-card-button">
          View, vote, and comment on questions
        </button>

  }
}
<|endoftext|>


<commit_before>'use strict';

const
  request = require('request'),
  webhook = require('./webhook.js'),
  standardUpdates = require('./updates.js'),
  standardMethods = require('./methods.js');

/* Telegram Bot */

class TeleBot {

  constructor(cfg) {

    if (typeof cfg != 'object') cfg = { token: cfg };

    if (!cfg.token || cfg.token.split(':').length != 2) {
      throw Error('[bot.error] invalid bot token');
    }

    this.cfg = cfg;
    this.token = cfg.token;
    this.id = this.token.split(':')[0];
    this.api = `https://api.telegram.org/bot${ this.token }`;
    this.fileLink = `https://api.telegram.org/file/bot${ this.token }/`;

    let poll = cfg.polling;

    // Migration
    if (!poll) {
      if (cfg.pooling) {
        poll = cfg.pooling;
        console.warn('[bot.warning] use "polling" option instead of "pooling"!');
      } else {
        poll = {};
        // Set cfg.polling
        for (let name of ['limit', 'timeout', 'retryTimeout', 'allowed_updates']) {
          poll[name] = cfg[name];
        }
        // cfg.sleep renamed to cfg.polling.interval
        poll.interval = cfg.sleep;
      }
    }

    this.limit = poll.limit > 0 && poll.limit <= 100 ? poll.limit : 100;
    this.interval = poll.interval >= 0 ? poll.interval : 1000;
    this.timeout = poll.timeout >= 0 ? poll.timeout : 0;
    this.retryTimeout = poll.retryTimeout >= 0 ? poll.retryTimeout : 5000;
    this.allowed_updates = typeof poll.allowed_updates == 'string' || Array.isArray(poll.allowed_updates) ? poll.allowed_updates : undefined;

    this.webhook = cfg.webhook;

    this.updateId = 0;
    this.loopFn = null;

    this.flags = {
      poll: false,
      retry: false,
      looping: false
    };

    this.modList = {};
    this.eventList = {};

    this.updateTypes = standardUpdates;

    this.processUpdate = (update, props) => {
      for (let name in this.updateTypes) {
        if (name in update) {
          update = update[name];
          return this.updateTypes[name].call(this, update, props);
        }
      }
    };

  }

  /* Modules */

  use(fn) {
    return fn.call(this, this, this.cfg.modules);
  }

  /* Connection */

  connect() {

    const f = this.flags;

    // Set webhook
    if (this.webhook) {
      let { url, cert } = this.webhook;
      if (url) url = `${ url }/${ this.token }`;
      return this.setWebhook(url, cert).then(x => {
        console.log(`[bot.webhook] set to "${url}"`);
        return webhook.call(this, this, this.webhook);
      }).catch(error => {
        console.error('[bot.error.webhook]', error);
        this.event('error', { error });
        return;
      });

    }

    // Delete webhook
    this.setWebhook().then(data => {
      f.poll = true;
      if (data.description == 'Webhook was deleted')
        console.log('[bot.webhook] webhook was deleted');
      console.log('[bot.info] bot started');
    }).catch(error => {
      console.error('[bot.error.webhook]', error);
      this.event('error', { error });
      return;
    });

    f.looping = true;

    this.event('connect');

    // Global loop function
    this.loopFn = setInterval(x => {

      // Stop on false looping flag
      if (!f.looping) clearInterval(this.loopFn);

      // Skip processing on false poll flag
      if (!f.poll) return;

      f.poll = false;

      // Get updates
      this.getUpdates().then(x => {

        // Retry connecting
        if (f.retry) {

          const now = Date.now();
          const diff = (now - f.retry) / 1000;

          console.log(`[bot.info.update] reconnected after ${ diff } seconds`);
          this.event('reconnected', {
            startTime: f.retry, endTime: now, diffTime: diff
          });

          f.retry = false;

        }

        // Tick
        return this.event('tick');

      }).then(x => {

        // Seems okay for the next poll
        f.poll = true;

      }).catch(error => {

        // Set retry flag as current date (for timeout calculations)
        if (f.retry === false) f.retry = Date.now();

        console.error(`[bot.error.update]`, error.stack || error);
        this.event(['error', 'error.update'], { error });

        return Promise.reject();

      }).catch(x => {

        const seconds = this.retryTimeout / 1000;
        console.log(`[bot.info.update] reconnecting in ${ seconds } seconds...`);
        this.event('reconnecting');

        // Set reconnecting timeout
        setTimeout(x => (f.poll = true), this.retryTimeout);

      });

    }, this.interval);

  }

  /* Stop looping */

  disconnect(message) {
    this.flags.looping = false;
    console.log(`[bot.info] bot disconnected ${ message ? ': ' + message : '' }`);
    this.event('disconnect', message);
  }

  /* Fetch updates */

  getUpdates(offset=this.updateId, limit=this.limit, timeout=this.timeout, allowed_updates=this.allowed_updates) {

    // Request updates from Telegram server
    return this.request('/getUpdates', {
      offset, limit, timeout, allowed_updates
    }).then(body =>
      this.receiveUpdates(body.result)
    );

  }

  /* Recive updates */

  receiveUpdates(updateList) {

    // Globals
    var
      mod,
      props = {},
      promise = Promise.resolve();

    // No updates
    if (!updateList.length) return promise;

    // We have updates
    return this.event('update', updateList).then(eventProps => {

      // Run update list modifiers
      mod = this.modRun('updateList', {
        list: updateList,
        props: extendProps(props, eventProps)
      });

      updateList = mod.list;
      props = mod.props;

      // Every Telegram update
      for (let update of updateList) {

        // Update ID
        const nextId = ++update.update_id;
        if (this.updateId < nextId) this.updateId = nextId;

        // Run update modifiers
        mod = this.modRun('update', { update, props });

        update = mod.update;
        props = mod.props;

        // Process update
        promise = promise.then(x => this.processUpdate(update, props));

      }

      return promise;

    }).catch(error => {

      console.log('[bot.error]', error.stack || error);
      this.event('error', { error });

      // Don't trigger server reconnect
      return Promise.resolve();

    });

  }

  /* Send request to server */

  request(url, form, data) {
    const options = { url: this.api + url, json: true };
    if (form) {
      options.form = form;
    } else {
      for (let item in data) {
        const type = typeof data[item];
        if (type == 'string' || type == 'object') continue;
        data[item] = JSON.stringify(data[item]);
      }
      options.formData = data
    };
    return new Promise((resolve, reject) => {
      request.post(options, (error, response, body) => {
        if (error || !body.ok || response.statusCode == 404) {
          return reject(error || body || 404);
        }
        return resolve(body);
      });
    });
  }

  /* Modifications */

  mod(names, fn) {
    if (typeof names == 'string') names = [names];
    const mods = this.modList;
    for (let name of names) {
      if (!mods[name]) mods[name] = [];
      if (mods[name].includes(fn)) return;
      mods[name].push(fn);
    }
    return fn;
  }

  modRun(name, data) {
    const list = this.modList[name];
    if (!list || !list.length) return data;
    for (let fn of list) data = fn.call(this, data);
    return data;
  }

  removeMod(name, fn) {
    let list = this.modList[name];
    if (!list) return false;
    let index = list.indexOf(fn);
    if (index == -1) return false;
    list.splice(index, 1);
    return true;
  }

  /* Events */

  on(types, fn, opt) {
    if (!opt) opt = {};
    if (typeof types == 'string') types = [types];
    for (let type of types) {
      let event = this.eventList[type];
      if (!event) {
        this.eventList[type] = { fired: null, list: [fn] };
      } else {
        if (event.list.includes(fn)) continue;
        event.list.push(fn);
        if (opt.fired && event.fired) {
          let fired = event.fired;
          new Promise((resolve, reject) => {
            let output = fn.call(fired.self, fired.data, fired.self, fired.details);
            if (output instanceof Promise) output.then(resolve).catch(reject);
            else resolve(output);
          }).catch(error => {
            eventPromiseError.call(this, type, fired, error);
          });
          if (opt.cleanFired) this.eventList[type].fired = null;
        }
      }
    }
  }

  event(types, data, self) {
    let promises = [];
    if (typeof types == 'string') types = [types];
    for (let type of types) {
      let event = this.eventList[type];
      let details = { type, time: Date.now() };
      let fired = { self, data, details };
      if (!event) {
        this.eventList[type] = { fired, list: [] };
        continue;
      }
      event.fired = fired;
      event = event.list;
      for (let fn of event) {
        promises.push((new Promise((resolve, reject) => {
          let that = this;
          details.remove = (function(fn) {
            return x => that.removeEvent(type, fn);
          }(fn));
          fn = fn.call(self, data, self, details);
          if (fn instanceof Promise) {
            fn.then(resolve).catch(reject);
          } else {
            resolve(fn);
          }
        })).catch(error => {
          eventPromiseError.call(this, type, fired, error);
        }));
      }
    }
    return Promise.all(promises);
  }

  cleanEvent(type) {
    let events = this.eventList;
    if (!events.hasOwnProperty(type)) return false;
    events[type].fired = null;
    return true;
  }

  removeEvent(type, fn) {
    let events = this.eventList;
    if (!events.hasOwnProperty(type)) return false;
    let event = events[type].list;
    let index = event.indexOf(fn);
    if (index == -1) return false;
    event.splice(index, 1);
    return true;
  }

  destroyEvent(type) {
    let events = this.eventList;
    if (!events.hasOwnProperty(type)) return false;
    delete events[type];
    return true;
  }

  /* Process global properties */

  properties(form={}, opt={}) {

    // Reply to message
    if (opt.reply) form.reply_to_message_id = opt.reply;

    // Markdown/HTML support for message
    if (opt.parse) form.parse_mode = opt.parse;

    // User notification
    if (opt.notify === false) form.disable_notification = true;

    // Web preview
    if (opt.preview === false) form.disable_web_page_preview = true;

    // Markup object
    if (opt.markup !== undefined) {
      if (opt.markup == 'hide' || opt.markup === false) {
        // Hide keyboard
        form.reply_markup = JSON.stringify({ hide_keyboard: true });
      } else if (opt.markup == 'reply') {
        // Fore reply
        form.reply_markup = JSON.stringify({ force_reply: true });
      } else {
        // JSON keyboard
        form.reply_markup = opt.markup;
      }
    }

    return (this.modRun('property', { form, options: opt })).form;

  }

  /* Method adder */

  static addMethods(methods) {

    for (let id in methods) {

      const method = methods[id];

      // If method is a function
      if (typeof method == 'function') {
        this.prototype[id] = method;
        continue;
      }

      // Set method name
      const name = method.short || id;

      // Argument function
      let argFn = method.arguments;
      if (argFn && typeof argFn != 'function') {
        if (typeof argFn == 'string') argFn = [argFn];
        let args = argFn;
        argFn = function() {
          const form = {};
          args.forEach((v, i) => form[v] = arguments[i]);
          return form;
        };
      }

      // Options function
      let optFn = method.options;

      // Create method
      this.prototype[name] = function() {
        this.event(name, arguments);
        let form = {}, args = [].slice.call(arguments);
        let options = args[args.length - 1];
        if (typeof options != 'object') options = {};
        if (argFn) form = argFn.apply(this, args);
        if (optFn) options = optFn.apply(this, [].concat(form, options));
        form = this.properties(form, options);
        let request = this.request(`/${id}`, form);
        if (method.then) request = request.then(method.then);
        return request;
      };

    }
  }
};

/* Add standard methods */

TeleBot.addMethods(standardMethods);

/* Functions */

function eventPromiseError(type, fired, error) {
  return new Promise((resolve, reject) => {
    console.error('[bot.error.event]', error.stack || error);
    if (type != 'error' && type != 'error.event') {
      this.event(['error', 'error.event'], { error, data: fired.data })
        .then(resolve).catch(reject);
    } else {
      resolve();
    }
  });
}

function extendProps(props, input) {
  for (let obj of input) {
    for (let naprops in obj) {
      const key = props[naprops], value = obj[naprops];
      if (key !== undefined) {
        if (!Array.isArray(key)) props[naprops] = [key];
        props[naprops].push(value);
        continue;
      }
      props[naprops] = value;
    }
  }
  return props;
}

/* Exports */

module.exports = TeleBot;
<commit_msg>Change undefined to an empty array<commit_after>'use strict';

const
  request = require('request'),
  webhook = require('./webhook.js'),
  standardUpdates = require('./updates.js'),
  standardMethods = require('./methods.js');

/* Telegram Bot */

class TeleBot {

  constructor(cfg) {

    if (typeof cfg != 'object') cfg = { token: cfg };

    if (!cfg.token || cfg.token.split(':').length != 2) {
      throw Error('[bot.error] invalid bot token');
    }

    this.cfg = cfg;
    this.token = cfg.token;
    this.id = this.token.split(':')[0];
    this.api = `https://api.telegram.org/bot${ this.token }`;
    this.fileLink = `https://api.telegram.org/file/bot${ this.token }/`;

    let poll = cfg.polling;

    // Migration
    if (!poll) {
      if (cfg.pooling) {
        poll = cfg.pooling;
        console.warn('[bot.warning] use "polling" option instead of "pooling"!');
      } else {
        poll = {};
        // Set cfg.polling
        for (let name of ['limit', 'timeout', 'retryTimeout', 'allowed_updates']) {
          poll[name] = cfg[name];
        }
        // cfg.sleep renamed to cfg.polling.interval
        poll.interval = cfg.sleep;
      }
    }

    this.limit = poll.limit > 0 && poll.limit <= 100 ? poll.limit : 100;
    this.interval = poll.interval >= 0 ? poll.interval : 1000;
    this.timeout = poll.timeout >= 0 ? poll.timeout : 0;
    this.retryTimeout = poll.retryTimeout >= 0 ? poll.retryTimeout : 5000;
    this.allowed_updates = typeof poll.allowed_updates == 'string' || Array.isArray(poll.allowed_updates) ? poll.allowed_updates : [];

    this.webhook = cfg.webhook;

    this.updateId = 0;
    this.loopFn = null;

    this.flags = {
      poll: false,
      retry: false,
      looping: false
    };

    this.modList = {};
    this.eventList = {};

    this.updateTypes = standardUpdates;

    this.processUpdate = (update, props) => {
      for (let name in this.updateTypes) {
        if (name in update) {
          update = update[name];
          return this.updateTypes[name].call(this, update, props);
        }
      }
    };

  }

  /* Modules */

  use(fn) {
    return fn.call(this, this, this.cfg.modules);
  }

  /* Connection */

  connect() {

    const f = this.flags;

    // Set webhook
    if (this.webhook) {
      let { url, cert } = this.webhook;
      if (url) url = `${ url }/${ this.token }`;
      return this.setWebhook(url, cert).then(x => {
        console.log(`[bot.webhook] set to "${url}"`);
        return webhook.call(this, this, this.webhook);
      }).catch(error => {
        console.error('[bot.error.webhook]', error);
        this.event('error', { error });
        return;
      });

    }

    // Delete webhook
    this.setWebhook().then(data => {
      f.poll = true;
      if (data.description == 'Webhook was deleted')
        console.log('[bot.webhook] webhook was deleted');
      console.log('[bot.info] bot started');
    }).catch(error => {
      console.error('[bot.error.webhook]', error);
      this.event('error', { error });
      return;
    });

    f.looping = true;

    this.event('connect');

    // Global loop function
    this.loopFn = setInterval(x => {

      // Stop on false looping flag
      if (!f.looping) clearInterval(this.loopFn);

      // Skip processing on false poll flag
      if (!f.poll) return;

      f.poll = false;

      // Get updates
      this.getUpdates().then(x => {

        // Retry connecting
        if (f.retry) {

          const now = Date.now();
          const diff = (now - f.retry) / 1000;

          console.log(`[bot.info.update] reconnected after ${ diff } seconds`);
          this.event('reconnected', {
            startTime: f.retry, endTime: now, diffTime: diff
          });

          f.retry = false;

        }

        // Tick
        return this.event('tick');

      }).then(x => {

        // Seems okay for the next poll
        f.poll = true;

      }).catch(error => {

        // Set retry flag as current date (for timeout calculations)
        if (f.retry === false) f.retry = Date.now();

        console.error(`[bot.error.update]`, error.stack || error);
        this.event(['error', 'error.update'], { error });

        return Promise.reject();

      }).catch(x => {

        const seconds = this.retryTimeout / 1000;
        console.log(`[bot.info.update] reconnecting in ${ seconds } seconds...`);
        this.event('reconnecting');

        // Set reconnecting timeout
        setTimeout(x => (f.poll = true), this.retryTimeout);

      });

    }, this.interval);

  }

  /* Stop looping */

  disconnect(message) {
    this.flags.looping = false;
    console.log(`[bot.info] bot disconnected ${ message ? ': ' + message : '' }`);
    this.event('disconnect', message);
  }

  /* Fetch updates */

  getUpdates(offset=this.updateId, limit=this.limit, timeout=this.timeout, allowed_updates=this.allowed_updates) {

    // Request updates from Telegram server
    return this.request('/getUpdates', {
      offset, limit, timeout, allowed_updates
    }).then(body =>
      this.receiveUpdates(body.result)
    );

  }

  /* Recive updates */

  receiveUpdates(updateList) {

    // Globals
    var
      mod,
      props = {},
      promise = Promise.resolve();

    // No updates
    if (!updateList.length) return promise;

    // We have updates
    return this.event('update', updateList).then(eventProps => {

      // Run update list modifiers
      mod = this.modRun('updateList', {
        list: updateList,
        props: extendProps(props, eventProps)
      });

      updateList = mod.list;
      props = mod.props;

      // Every Telegram update
      for (let update of updateList) {

        // Update ID
        const nextId = ++update.update_id;
        if (this.updateId < nextId) this.updateId = nextId;

        // Run update modifiers
        mod = this.modRun('update', { update, props });

        update = mod.update;
        props = mod.props;

        // Process update
        promise = promise.then(x => this.processUpdate(update, props));

      }

      return promise;

    }).catch(error => {

      console.log('[bot.error]', error.stack || error);
      this.event('error', { error });

      // Don't trigger server reconnect
      return Promise.resolve();

    });

  }

  /* Send request to server */

  request(url, form, data) {
    const options = { url: this.api + url, json: true };
    if (form) {
      options.form = form;
    } else {
      for (let item in data) {
        const type = typeof data[item];
        if (type == 'string' || type == 'object') continue;
        data[item] = JSON.stringify(data[item]);
      }
      options.formData = data
    };
    return new Promise((resolve, reject) => {
      request.post(options, (error, response, body) => {
        if (error || !body.ok || response.statusCode == 404) {
          return reject(error || body || 404);
        }
        return resolve(body);
      });
    });
  }

  /* Modifications */

  mod(names, fn) {
    if (typeof names == 'string') names = [names];
    const mods = this.modList;
    for (let name of names) {
      if (!mods[name]) mods[name] = [];
      if (mods[name].includes(fn)) return;
      mods[name].push(fn);
    }
    return fn;
  }

  modRun(name, data) {
    const list = this.modList[name];
    if (!list || !list.length) return data;
    for (let fn of list) data = fn.call(this, data);
    return data;
  }

  removeMod(name, fn) {
    let list = this.modList[name];
    if (!list) return false;
    let index = list.indexOf(fn);
    if (index == -1) return false;
    list.splice(index, 1);
    return true;
  }

  /* Events */

  on(types, fn, opt) {
    if (!opt) opt = {};
    if (typeof types == 'string') types = [types];
    for (let type of types) {
      let event = this.eventList[type];
      if (!event) {
        this.eventList[type] = { fired: null, list: [fn] };
      } else {
        if (event.list.includes(fn)) continue;
        event.list.push(fn);
        if (opt.fired && event.fired) {
          let fired = event.fired;
          new Promise((resolve, reject) => {
            let output = fn.call(fired.self, fired.data, fired.self, fired.details);
            if (output instanceof Promise) output.then(resolve).catch(reject);
            else resolve(output);
          }).catch(error => {
            eventPromiseError.call(this, type, fired, error);
          });
          if (opt.cleanFired) this.eventList[type].fired = null;
        }
      }
    }
  }

  event(types, data, self) {
    let promises = [];
    if (typeof types == 'string') types = [types];
    for (let type of types) {
      let event = this.eventList[type];
      let details = { type, time: Date.now() };
      let fired = { self, data, details };
      if (!event) {
        this.eventList[type] = { fired, list: [] };
        continue;
      }
      event.fired = fired;
      event = event.list;
      for (let fn of event) {
        promises.push((new Promise((resolve, reject) => {
          let that = this;
          details.remove = (function(fn) {
            return x => that.removeEvent(type, fn);
          }(fn));
          fn = fn.call(self, data, self, details);
          if (fn instanceof Promise) {
            fn.then(resolve).catch(reject);
          } else {
            resolve(fn);
          }
        })).catch(error => {
          eventPromiseError.call(this, type, fired, error);
        }));
      }
    }
    return Promise.all(promises);
  }

  cleanEvent(type) {
    let events = this.eventList;
    if (!events.hasOwnProperty(type)) return false;
    events[type].fired = null;
    return true;
  }

  removeEvent(type, fn) {
    let events = this.eventList;
    if (!events.hasOwnProperty(type)) return false;
    let event = events[type].list;
    let index = event.indexOf(fn);
    if (index == -1) return false;
    event.splice(index, 1);
    return true;
  }

  destroyEvent(type) {
    let events = this.eventList;
    if (!events.hasOwnProperty(type)) return false;
    delete events[type];
    return true;
  }

  /* Process global properties */

  properties(form={}, opt={}) {

    // Reply to message
    if (opt.reply) form.reply_to_message_id = opt.reply;

    // Markdown/HTML support for message
    if (opt.parse) form.parse_mode = opt.parse;

    // User notification
    if (opt.notify === false) form.disable_notification = true;

    // Web preview
    if (opt.preview === false) form.disable_web_page_preview = true;

    // Markup object
    if (opt.markup !== undefined) {
      if (opt.markup == 'hide' || opt.markup === false) {
        // Hide keyboard
        form.reply_markup = JSON.stringify({ hide_keyboard: true });
      } else if (opt.markup == 'reply') {
        // Fore reply
        form.reply_markup = JSON.stringify({ force_reply: true });
      } else {
        // JSON keyboard
        form.reply_markup = opt.markup;
      }
    }

    return (this.modRun('property', { form, options: opt })).form;

  }

  /* Method adder */

  static addMethods(methods) {

    for (let id in methods) {

      const method = methods[id];

      // If method is a function
      if (typeof method == 'function') {
        this.prototype[id] = method;
        continue;
      }

      // Set method name
      const name = method.short || id;

      // Argument function
      let argFn = method.arguments;
      if (argFn && typeof argFn != 'function') {
        if (typeof argFn == 'string') argFn = [argFn];
        let args = argFn;
        argFn = function() {
          const form = {};
          args.forEach((v, i) => form[v] = arguments[i]);
          return form;
        };
      }

      // Options function
      let optFn = method.options;

      // Create method
      this.prototype[name] = function() {
        this.event(name, arguments);
        let form = {}, args = [].slice.call(arguments);
        let options = args[args.length - 1];
        if (typeof options != 'object') options = {};
        if (argFn) form = argFn.apply(this, args);
        if (optFn) options = optFn.apply(this, [].concat(form, options));
        form = this.properties(form, options);
        let request = this.request(`/${id}`, form);
        if (method.then) request = request.then(method.then);
        return request;
      };

    }
  }
};

/* Add standard methods */

TeleBot.addMethods(standardMethods);

/* Functions */

function eventPromiseError(type, fired, error) {
  return new Promise((resolve, reject) => {
    console.error('[bot.error.event]', error.stack || error);
    if (type != 'error' && type != 'error.event') {
      this.event(['error', 'error.event'], { error, data: fired.data })
        .then(resolve).catch(reject);
    } else {
      resolve();
    }
  });
}

function extendProps(props, input) {
  for (let obj of input) {
    for (let naprops in obj) {
      const key = props[naprops], value = obj[naprops];
      if (key !== undefined) {
        if (!Array.isArray(key)) props[naprops] = [key];
        props[naprops].push(value);
        continue;
      }
      props[naprops] = value;
    }
  }
  return props;
}

/* Exports */

module.exports = TeleBot;
<|endoftext|>


<commit_before>Readium.Views.TocViewBase = Backbone.View.extend({

	el: "#readium-toc",

	initialize: function() {
		this.model.on("change", this.render, this);
		this.model.on("change:visible", this.setVisibility, this);
	},

	events: {
		"click a": "handleClick",
		"click #close-toc-button": "closeToc"
	},

	setVisibility: function() {
		this.$el.toggle(this.model.get("visible"));
	},

	handleClick: function(e) {
		e.preventDefault();
		href = $(e.currentTarget).attr("href");
		this.model.handleLink(href);
	},

	handleSelect : function (e) {

		var href = e.val;
		this.model.handleLink(href);
	},

	closeToc: function(e) {
		e.preventDefault();
		this.model.hide();
	}
});

Readium.Views.NcxTocView = Readium.Views.TocViewBase.extend({

	initialize: function() {
		Readium.Views.TocViewBase.prototype.initialize.call(this);
		this.nav_template = Handlebars.templates.ncx_nav_template;
	},

	render: function() {
		this.setVisibility();
		var ol = $("

");
		var navs = this.model.get("navs");
		for(var i = 0; i < navs.length; i++) {
			ol.append( this.nav_template(navs[i]) );
		}
		this.$('#toc-body').html("
" + (this.model.get("title") || "Contents") + "
")
		this.$('#toc-body').append(ol);
		this.$('#toc-body').append("
");
		return this;
	}

});

Readium.Views.XhtmlTocView = Readium.Views.TocViewBase.extend({

	// ------------------------------------------------------------------------------------ //
	//  "PUBLIC" METHODS (THE API)                                                          //
	// ------------------------------------------------------------------------------------ //

	events : {

		"click a": "handleClick",
		"click #close-toc-button": "closeToc",
		"change #toc-body" : "handleSelect"
	},

	render: function() {

		this.$('#toc-body').html( this.model.get("body").html() );
		this.formatPageListNavigation();
		this.$('#toc-body').append("
");
		return this;
	},

	// ------------------------------------------------------------------------------------ //
	//  "PRIVATE" HELPERS                                                                   //
	// ------------------------------------------------------------------------------------ //

	formatPageListNavigation : function () {

		var $navElements;
		var $pageListNavElement;
		var pageListData = [];

		// Search for a nav element with epub:type="page-list". A nav element of this type must not occur more than once.
		$navElements = this.$("nav");
		$pageListNavElement = $navElements.filter(function () {

			if ($(this).attr("epub:type") === 'page-list') {

				// Hide the standard XHTML page-list nav element, as we'll be displaying a select2 drop-down control for this.
				$(this).attr("id", "page-list-select");
				$(this).hide();
				return true;
			}
		});

		// Each nav element has a single ordered list of page numbers. Extract this data into an array so it can be
		//   loaded in the page-list control
		// TODO: span elements can be used to create sub-headings. Implement functionality to account for this at some point.
		$.each($('a', $pageListNavElement), function () {

			var $navTarget = $(this);
			pageListData.push({

				id : $navTarget.attr("href"),
				text : "Page-" + $navTarget.text()
			});
		});

		// Create the select2 control
		$("#page-list-select").select2({

			placeholder : "Select a page",
			data : pageListData
		});

		$("#s2id_page-list-select").css("padding-left", "1.5em");
		$("#s2id_page-list-select").css("width", "20em");
	}
});<commit_msg>removed the content security policy errors thrown by select2<commit_after>Readium.Views.TocViewBase = Backbone.View.extend({

	el: "#readium-toc",

	initialize: function() {
		this.model.on("change", this.render, this);
		this.model.on("change:visible", this.setVisibility, this);
	},

	events: {
		"click a": "handleClick",
		"click #close-toc-button": "closeToc"
	},

	setVisibility: function() {
		this.$el.toggle(this.model.get("visible"));
	},

	handleClick: function(e) {
		e.preventDefault();
		href = $(e.currentTarget).attr("href");
		this.model.handleLink(href);
	},

	handleSelect : function (e) {

		var href = e.val;
		this.model.handleLink(href);
	},

	closeToc: function(e) {
		e.preventDefault();
		this.model.hide();
	}
});

Readium.Views.NcxTocView = Readium.Views.TocViewBase.extend({

	initialize: function() {
		Readium.Views.TocViewBase.prototype.initialize.call(this);
		this.nav_template = Handlebars.templates.ncx_nav_template;
	},

	render: function() {
		this.setVisibility();
		var ol = $("

");
		var navs = this.model.get("navs");
		for(var i = 0; i < navs.length; i++) {
			ol.append( this.nav_template(navs[i]) );
		}
		this.$('#toc-body').html("
" + (this.model.get("title") || "Contents") + "
")
		this.$('#toc-body').append(ol);
		this.$('#toc-body').append("
");
		return this;
	}

});

Readium.Views.XhtmlTocView = Readium.Views.TocViewBase.extend({

	// ------------------------------------------------------------------------------------ //
	//  "PUBLIC" METHODS (THE API)                                                          //
	// ------------------------------------------------------------------------------------ //

	events : {

		"click a": "handleClick",
		"click #close-toc-button": "closeToc",
		"change #toc-body" : "handleSelect"
	},

	render: function() {

		this.$('#toc-body').html( this.model.get("body").html() );
		this.formatPageListNavigation();
		this.$('#toc-body').append("
");
		return this;
	},

	// ------------------------------------------------------------------------------------ //
	//  "PRIVATE" HELPERS                                                                   //
	// ------------------------------------------------------------------------------------ //

	formatPageListNavigation : function () {

		var $navElements;
		var $pageListNavElement;
		var pageListData = [];

		// Search for a nav element with epub:type="page-list". A nav element of this type must not occur more than once.
		$navElements = this.$("nav");
		$pageListNavElement = $navElements.filter(function () {

			if ($(this).attr("epub:type") === 'page-list') {

				// Hide the standard XHTML page-list nav element, as we'll be displaying a select2 drop-down control for this.
				$(this).attr("id", "page-list-select");
				$(this).hide();
				return true;
			}
		});

		// Each nav element has a single ordered list of page numbers. Extract this data into an array so it can be
		//   loaded in the page-list control
		// TODO: span elements can be used to create sub-headings. Implement functionality to account for this at some point.
		$.each($('a', $pageListNavElement), function () {

			var $navTarget = $(this);
			pageListData.push({

				id : $navTarget.attr("href"),
				text : "Page-" + $navTarget.text()
			});
		});

		// Create the select2 control
		$("#page-list-select").select2({

			placeholder : "Select a page",
			data : pageListData
		});

		// the select2 adds no-op inline click handlers, but these are not allowed
		// by chromes content securty policy so just remove them
		this.$('[onclick]').removeAttr('onclick');

		$("#s2id_page-list-select").css("padding-left", "1.5em");
		$("#s2id_page-list-select").css("width", "20em");
	}
});<|endoftext|>


<commit_before>/*
 * AngularAgility Form Extensions
 *
 * http://www.johnculviner.com
 *
 * Copyright (c) 2014 - John Culviner
 *
 * Licensed under the MIT license:
 *   http://www.opensource.org/licenses/mit-license.php
 */

;(function () {
    var formExtensions = angular.module('aa.formExtensions', [])
        .directive('aaSaveForm', function() {
            return {
                link: function() {
                    throw "aaSaveForm has been deprecated in favor of a more sensically named aaSubmitForm";
                }
            };
        })
        .directive('aaSubmitForm', ['aaFormExtensions', function (aaFormExtensions) {
            return {
                scope: {
                    onInvalidAttempt: '&',
                    aaSubmitForm: '&'
                },
                restrict: 'A',
                require: '^form',
                link: function (scope, element, attrs, ngForm) {

                    ensureaaFormExtensions(ngForm);

                    element.on('click', function () {
                        scope.$apply(function () {

                            ngForm.$aaFormExtensions.$submitAttempt();

                            if (ngForm.$valid) {
                                scope.aaSubmitForm();
                            } else {
                                var hasScopeFunction = typeof scope.onInvalidAttempt() === 'function';
                                var hasGlobalFunction = typeof aaFormExtensions.defaultOnInvalidAttempt === 'function';

                                if(hasScopeFunction || hasGlobalFunction) {
                                    //calc error messages

                                    var errorMessages = [];

                                    angular.forEach(ngForm.$aaFormExtensions, function(fieldObj, fieldName) {

                                        if(fieldName.indexOf('$') === 0){ return; }

                                        errorMessages = errorMessages.concat(fieldObj.$errorMessages);
                                    });

                                    if(hasScopeFunction) {
                                        scope.onInvalidAttempt(errorMessages, ngForm);
                                        return;
                                    }

                                    aaFormExtensions.defaultOnInvalidAttempt(errorMessages, ngForm);
                                }
                            }
                        });
                    });

                }
            };
        }])

        //constructs myForm.$aaFormExtensions.myFieldName object
        //including validation messages for all ngModels at form.$aaFormExtensions.
        //messages can be used there manually or emitted automatically with aaValMsg
        .directive('ngModel', ['aaFormExtensions', '$document', '$timeout', function (aaFormExtensions, $document, $timeout) {
            return {
                require: ['ngModel', '?^form'],
                priority: 1,
                link: function (scope, element, attrs, controllers) {
                    var ngModel = controllers[0],
                        ngForm = controllers[1],
                        fieldName = "This field";

                    if(!ngForm)
                        return; //only for validation with forms

                    if(attrs.aaLabel) {
                        //use default label
                        fieldName = attrs.aaLabel;

                    } else if(element[0].id){
                        //is there a label for this field?
                        angular.forEach($document.find('label'), function(label) {
                            if(label.getAttribute("for") === element[0].id) {
                                fieldName = (label.innerHTML || "").replace('*', '').trim();
                            }
                        });
                    }

                    ensureaaFormExtensionsFieldExists(ngForm, ngModel.$name);
                    ngForm.$aaFormExtensions[ngModel.$name].$element = element;
                    element.on('blur', function() {

                        scope.$apply(function() {
                            ngForm.$aaFormExtensions[ngModel.$name].$hadFocus = true;
                            element.addClass('aa-had-focus');
                        });
                    });

                    scope.$watch(function() {
                        return ngForm.$aaFormExtensions.$invalidAttempt;
                    }, function(val) {
                        if(val) {
                            element.addClass('aa-invalid-attempt');
                        }
                    });

                    //need this to run AFTER Angular's 'ngModel' runs... another way?
                    $timeout(calcErrorMessages, 0);

                    //subsequent runs after value changes in GUI...
                    ngModel.$parsers.push(calcErrorMessages);

                    function calcErrorMessages(val) {
                        var errorMessages = ngForm.$aaFormExtensions[ngModel.$name].$errorMessages;
                        errorMessages.length = 0;

                        for (var key in ngModel.$error) {
                            if(ngModel.$error[key]) {

                                if(key === 'minlength') {
                                    errorMessages.push(
                                        stringFormat(aaFormExtensions.validationMessages[key], fieldName, attrs.ngMinlength)
                                    );
                                } else if (key === 'maxlength') {
                                    errorMessages.push(
                                        stringFormat(aaFormExtensions.validationMessages[key], fieldName, attrs.ngMaxlength)
                                    );
                                } else if (key === 'min') {
                                    errorMessages.push(
                                        stringFormat(aaFormExtensions.validationMessages[key], fieldName, attrs.min)
                                    );
                                } else if (key === 'max') {
                                    errorMessages.push(
                                        stringFormat(aaFormExtensions.validationMessages[key], fieldName, attrs.max)
                                    );
                                } else if (key === 'required' && element[0].type === 'number') {
                                    //angular doesn't correctly flag numbers as invalid rather as required when something wrong is filled in
                                    //hack around it
                                    errorMessages.push(
                                        /*jshint -W069 */
                                        stringFormat(aaFormExtensions.validationMessages['number'], fieldName)
                                    );
                                } else if (aaFormExtensions.validationMessages[key]) {
                                    errorMessages.push(
                                        stringFormat(aaFormExtensions.validationMessages[key], fieldName)
                                    );
                                }
                            }
                        }
                        return val;
                    }
                }
            };
        }])

        //place on an element with ngModel to generate validation messages for it
        //will use the default configured validation message placement strategy unless a custom strategy is passed in
        .directive('aaValMsg', ['$compile', 'aaFormExtensions', function($compile, aaFormExtensions) {
            return {
                require: ['ngModel', '^form'],
                link: function(scope, element, attrs, ctrls) {

                    var ngModel = ctrls[0];
                    var form = ctrls[1];

                    var msgElement = aaFormExtensions.valMsgPlacementStrategies[attrs.aaValMsg ||  aaFormExtensions.defaultValMsgPlacementStrategy](
                        element, form.$name, attrs.name
                    );

                    $compile(msgElement)(scope);
                }
            };
        }])

        //if used directly rather than passively with aaValMsg allows for direct placement of validation messages
        //for a given form field. ex. pass "myForm.myFieldName"
        .directive('aaValMsgFor', ['aaFormExtensions', function(aaFormExtensions) {
            //generate the validation message for a particular form field here
            return {
                require: ['^form'],
                priority: 1,
                scope: true,
                link: function($scope, element, attrs) {

                    var fullFieldPath = attrs.aaValMsgFor,
                        fieldInForm = $scope.$eval(fullFieldPath),
                        formObj = $scope.$eval(fullFieldPath.substring(0, fullFieldPath.indexOf('.')));

                    //could nest multiple forms so can't trust directive require and have to eval to handle edge cases...
                    ensureaaFormExtensionsFieldExists(formObj, fieldInForm.$name);
                    var fieldInFormExtensions = $scope.$eval(fullFieldPath.replace('.', '.$aaFormExtensions.'));

                    $scope.$watchCollection(
                        function() {
                            return fieldInFormExtensions.$errorMessages;
                        },
                        function(val) {
                            $scope.errorMessages = val;
                        }
                    );

                    $scope.$watchCollection(
                        function() {
                            return [
                                formObj.$aaFormExtensions.$invalidAttempt,
                                fieldInFormExtensions.$hadFocus
                            ];
                        },
                        function(watches) {
                            var invalidAttempt = watches[0],
                                hadFocus = watches[1];

                            $scope.showMessages = invalidAttempt || hadFocus;
                        }
                    );
                },
                template: aaFormExtensions.valMsgForTemplate,
                replace: true
            };
        }])

        //generate a label for an input generating an ID for it if it doesn't already exist
        .directive('aaLabel', ['aaFormExtensions', function (aaFormExtensions) {
            return {
                link: function (scope, element, attrs) {

                    var strategyName = attrs.aaLabelStrategy || aaFormExtensions.defaultLabelStrategy;
                    var isRequiredField = (attrs.required !== undefined);

                    //auto generate an ID for compliant label names
                    if (!element[0].id) {
                        element[0].id = guid();
                    }

                    aaFormExtensions.labelStrategies[strategyName](element, attrs.aaLabel, isRequiredField);
                }
            };
        }])

        .directive('aaAutoField', ['$compile', function ($compile) {
            return {
                restrict: 'A',
                scope: false,
                replace: true,
                priority: 1000,
                terminal: true,
                compile: function(element, attrs) {

                    //use the passed value for ng-model
                    element.attr("ng-model", attrs.aaAutoField);

                    var lastPartOfName = attrs.aaAutoField.substring(attrs.aaAutoField.lastIndexOf('.') + 1);

                    //if no name set calc one
                    if (!attrs.name) {
                        element.attr("name", lastPartOfName);
                    }

                    //if no label and "no-label" don't calc one
                    if (!attrs.aaLabel && attrs.noLabel === undefined) {
                        element.attr('aa-label', toTitleCase(splitCamelCase(lastPartOfName)));
                    }

                    element.attr("aa-val-msg", "");

                    element.removeAttr('aa-auto-field');

                    element.replaceWith(outerHTML(element[0]));

                    return function(scope, element, attrs) {
                        $compile(element)(scope);
                    };
                }
            };
        }])

        .directive('aaValidIcon', ['aaFormExtensions', function(aaFormExtensions) {
            return {
                require: 'ngModel',
                scope: false,
                compile: function(element) {

                    var container = aaFormExtensions.validIconStrategy.getContainer(element);

                    var validIcon = angular.element(aaFormExtensions.validIconStrategy.validIcon);
                    container.append(validIcon);
                    validIcon[0].style.display = 'none';

                    var invalidIcon = angular.element(aaFormExtensions.validIconStrategy.invalidIcon);
                    container.append(invalidIcon);
                    invalidIcon[0].style.display = 'none';

                    return function(scope, element, attrs, ngModel) {
                        ngModel.$parsers.push(function(val) {

                            if(ngModel.$valid) {
                                validIcon[0].style.display = '';
                                invalidIcon[0].style.display = 'none';
                            } else {
                                validIcon[0].style.display = 'none';
                                invalidIcon[0].style.display = '';
                            }

                            return val;
                        });
                    };
                }
            };
        }])

        .provider('aaFormExtensions', function () {

            var self = this;

            this.defaultLabelStrategy = "default";
            this.setDefaultLabelStrategy = function(strategyName) {
                this.defaultLabelStrategy = strategyName;
            };
            this.labelStrategies = {

                //create a bootstrap3 style label
                bootstrap3InlineForm: function (ele, labelText, isRequired) {

                    var label = angular.element('')
                        .attr('for', ele[0].id)
                        .addClass('col-sm-2 control-label')
                        .html(labelText + (isRequired ? ' *' : ''));

                    var unsupported = [
                        'button',
                        'submit'
                    ];

                    if(unsupported.indexOf(ele[0].type) !== -1) {
                        throw "Generating a label for and input type " + ele[0].type + " is unsupported.";
                    }

                    ele.parent().parent().prepend(label);
                },

                //create a no-frills label directly before the element
                default: function (ele, labelText, isRequired) {
                    ele[0].parentNode.insertBefore(
                        angular.element('')
                            .attr('for', ele[0].id)
                            .html(labelText + (isRequired ? ' *' : ''))[0],
                        ele[0]);
                }

                //add you own here using registerLabelStrategy
            };
            this.registerLabelStrategy = function (name, strategy) {
                this.labelStrategies[name] = strategy;
            };

            this.defaultValMsgPlacementStrategy = "default";
            this.setDefaultValMsgPlacementStrategy = function(strategyName) {
                this.defaultValMsgPlacementStrategy = strategyName;
            };
            this.valMsgPlacementStrategies = {

                default: function(formFieldElement, formName, formFieldName) {

                    var msgElement = angular.element(stringFormat('

', formName, formFieldName));
                    var fieldType = formFieldElement[0].type.toLowerCase();

                    if(fieldType === 'radio') {
                        //radios tend to be wrapped, go up a few levels (of course you can customize this with your own strategy)
                        formFieldElement.parent().parent().append(msgElement);

                    } else {
                        formFieldElement.after(msgElement);
                    }

                    return msgElement;
                }
            };
            this.registerValMsgPlacementStrategy = function(name, strategy) {
                this.valMsgPlacementStrategies[name] = strategy;
            };

            //bootstrap 3 + font awesome
            this.validIconStrategy = {
                validIcon: '',
                invalidIcon: '',
                getContainer: function(element) {
                    var ele = angular.element('
');
                    element.parent().after(ele);
                    return ele;
                }
            };
            this.setValidIconStrategy = function(strategy) {
                self.validIconStrategy = strategy;
            };

            this.validationMessages = {
                required: "{0} is required.",
                email: "The field {0} must be an email.",
                minlength: "{0} must be at least {1} character(s).",
                maxlength: "{0} must be less than {1} characters.",
                min: "{0} must be at least {1}.",
                max: "{0} must be at most {1}.",
                pattern: "{0} is invalid.",
                url: "{0} must be a valid URL.",
                number: "{0} must be number."
            };
            this.setValidationMessage = function(directiveName, message) {
                self.validationMessages[directiveName] = message;
            };
            this.setValidationMessages = function(messages) {
                self.validationMessages = messages;
            };

            this.valMsgForTemplate ='
{{msg}}
';
            this.setValMsgForTemplate = function(valMsgForTemplate) {
                this.valMsgForTemplate = valMsgForTemplate;
            };

            this.defaultOnInvalidAttempt = function() {
                //todo integrate with 'growl like' notifications
            };
            this.setDefaultOnInvalidAttempt = function(func) {
                this.defaultOnInvalidAttempt = func;
            };

            this.$get = function () {
                return {
                    defaultLabelStrategy: self.defaultLabelStrategy,
                    labelStrategies: self.labelStrategies,
                    validIconStrategy: self.validIconStrategy,
                    validationMessages: self.validationMessages,
                    valMsgForTemplate: self.valMsgForTemplate,
                    valMsgPlacementStrategies: self.valMsgPlacementStrategies,
                    defaultValMsgPlacementStrategy: self.defaultValMsgPlacementStrategy,
                    defaultOnInvalidAttempt: self.defaultOnInvalidAttempt
                };
            };
        });

    //utility
    function guid(a) {
        return a ? (a ^ Math.random() * 16 >> a / 4).toString(16) : ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, guid);
    }

    function toTitleCase(str) {
        return str.replace(/\w\S*/g, function (txt) {
            return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
        });
    }

    function splitCamelCase(str) {
        return str.replace(/([a-z](?=[A-Z]))/g, '$1 ');
    }

    function outerHTML(node){
        // if IE, Chrome take the internal method otherwise build one
        return node.outerHTML || (
            function(n){
                var div = document.createElement('div'), h;
                div.appendChild( n.cloneNode(true) );
                h = div.innerHTML;
                div = null;
                return h;
            })(node);
    }

    function stringFormat(format) {
        var args = Array.prototype.slice.call(arguments, 1);
        return format.replace(/{(\d+)}/g, function(match, number) {
            return typeof args[number] !== 'undefined' ? args[number] : match;
        });
    }

    function ensureaaFormExtensions(form) {
        if(!form.$aaFormExtensions) {
            form.$aaFormExtensions = {
                '$submitAttempt': function() {
                    setAttemptRecursively(form, form.$invalid);
                }
            };
        }

        function setAttemptRecursively(form, isInvalid) {
            form.$aaFormExtensions.$invalidAttempt = isInvalid;
            angular.forEach(form, function(fieldVal, fieldName) {
                if(fieldName.indexOf('$') !== 0 && form.constructor === fieldVal.constructor){
                    setAttemptRecursively(fieldVal, isInvalid);
                }
            });
        }
    }

    function ensureaaFormExtensionsFieldExists(form, fieldName) {
        ensureaaFormExtensions(form);

        if(!form.$aaFormExtensions[fieldName]) {
            form.$aaFormExtensions[fieldName] = {
                $hadFocus: false,
                $errorMessages: [],
                $element: null
            };
        }
    }
})();<commit_msg>added ability to pass in label strategy func<commit_after>/*
 * AngularAgility Form Extensions
 *
 * http://www.johnculviner.com
 *
 * Copyright (c) 2014 - John Culviner
 *
 * Licensed under the MIT license:
 *   http://www.opensource.org/licenses/mit-license.php
 */

;(function () {
    var formExtensions = angular.module('aa.formExtensions', [])
        .directive('aaSaveForm', function() {
            return {
                link: function() {
                    throw "aaSaveForm has been deprecated in favor of a more sensically named aaSubmitForm";
                }
            };
        })
        .directive('aaSubmitForm', ['aaFormExtensions', function (aaFormExtensions) {
            return {
                scope: {
                    onInvalidAttempt: '&',
                    aaSubmitForm: '&'
                },
                restrict: 'A',
                require: '^form',
                link: function (scope, element, attrs, ngForm) {

                    ensureaaFormExtensions(ngForm);

                    element.on('click', function () {
                        scope.$apply(function () {

                            ngForm.$aaFormExtensions.$submitAttempt();

                            if (ngForm.$valid) {
                                scope.aaSubmitForm();
                            } else {
                                var hasScopeFunction = typeof scope.onInvalidAttempt() === 'function';
                                var hasGlobalFunction = typeof aaFormExtensions.defaultOnInvalidAttempt === 'function';

                                if(hasScopeFunction || hasGlobalFunction) {
                                    //calc error messages

                                    var errorMessages = [];

                                    angular.forEach(ngForm.$aaFormExtensions, function(fieldObj, fieldName) {

                                        if(fieldName.indexOf('$') === 0){ return; }

                                        errorMessages = errorMessages.concat(fieldObj.$errorMessages);
                                    });

                                    if(hasScopeFunction) {
                                        scope.onInvalidAttempt(errorMessages, ngForm);
                                        return;
                                    }

                                    aaFormExtensions.defaultOnInvalidAttempt(errorMessages, ngForm);
                                }
                            }
                        });
                    });

                }
            };
        }])

        //constructs myForm.$aaFormExtensions.myFieldName object
        //including validation messages for all ngModels at form.$aaFormExtensions.
        //messages can be used there manually or emitted automatically with aaValMsg
        .directive('ngModel', ['aaFormExtensions', '$document', '$timeout', function (aaFormExtensions, $document, $timeout) {
            return {
                require: ['ngModel', '?^form'],
                priority: 1,
                link: function (scope, element, attrs, controllers) {
                    var ngModel = controllers[0],
                        ngForm = controllers[1],
                        fieldName = "This field";

                    if(!ngForm)
                        return; //only for validation with forms

                    if(attrs.aaLabel) {
                        //use default label
                        fieldName = attrs.aaLabel;

                    } else if(element[0].id){
                        //is there a label for this field?
                        angular.forEach($document.find('label'), function(label) {
                            if(label.getAttribute("for") === element[0].id) {
                                fieldName = (label.innerHTML || "").replace('*', '').trim();
                            }
                        });
                    }

                    ensureaaFormExtensionsFieldExists(ngForm, ngModel.$name);
                    ngForm.$aaFormExtensions[ngModel.$name].$element = element;
                    element.on('blur', function() {

                        scope.$apply(function() {
                            ngForm.$aaFormExtensions[ngModel.$name].$hadFocus = true;
                            element.addClass('aa-had-focus');
                        });
                    });

                    scope.$watch(function() {
                        return ngForm.$aaFormExtensions.$invalidAttempt;
                    }, function(val) {
                        if(val) {
                            element.addClass('aa-invalid-attempt');
                        }
                    });

                    //need this to run AFTER Angular's 'ngModel' runs... another way?
                    $timeout(calcErrorMessages, 0);

                    //subsequent runs after value changes in GUI...
                    ngModel.$parsers.push(calcErrorMessages);

                    function calcErrorMessages(val) {
                        var errorMessages = ngForm.$aaFormExtensions[ngModel.$name].$errorMessages;
                        errorMessages.length = 0;

                        for (var key in ngModel.$error) {
                            if(ngModel.$error[key]) {

                                if(key === 'minlength') {
                                    errorMessages.push(
                                        stringFormat(aaFormExtensions.validationMessages[key], fieldName, attrs.ngMinlength)
                                    );
                                } else if (key === 'maxlength') {
                                    errorMessages.push(
                                        stringFormat(aaFormExtensions.validationMessages[key], fieldName, attrs.ngMaxlength)
                                    );
                                } else if (key === 'min') {
                                    errorMessages.push(
                                        stringFormat(aaFormExtensions.validationMessages[key], fieldName, attrs.min)
                                    );
                                } else if (key === 'max') {
                                    errorMessages.push(
                                        stringFormat(aaFormExtensions.validationMessages[key], fieldName, attrs.max)
                                    );
                                } else if (key === 'required' && element[0].type === 'number') {
                                    //angular doesn't correctly flag numbers as invalid rather as required when something wrong is filled in
                                    //hack around it
                                    errorMessages.push(
                                        /*jshint -W069 */
                                        stringFormat(aaFormExtensions.validationMessages['number'], fieldName)
                                    );
                                } else if (aaFormExtensions.validationMessages[key]) {
                                    errorMessages.push(
                                        stringFormat(aaFormExtensions.validationMessages[key], fieldName)
                                    );
                                }
                            }
                        }
                        return val;
                    }
                }
            };
        }])

        //place on an element with ngModel to generate validation messages for it
        //will use the default configured validation message placement strategy unless a custom strategy is passed in
        .directive('aaValMsg', ['$compile', 'aaFormExtensions', function($compile, aaFormExtensions) {
            return {
                require: ['ngModel', '^form'],
                link: function(scope, element, attrs, ctrls) {

                    var ngModel = ctrls[0];
                    var form = ctrls[1];

                    var msgElement = aaFormExtensions.valMsgPlacementStrategies[attrs.aaValMsg ||  aaFormExtensions.defaultValMsgPlacementStrategy](
                        element, form.$name, attrs.name
                    );

                    $compile(msgElement)(scope);
                }
            };
        }])

        //if used directly rather than passively with aaValMsg allows for direct placement of validation messages
        //for a given form field. ex. pass "myForm.myFieldName"
        .directive('aaValMsgFor', ['aaFormExtensions', function(aaFormExtensions) {
            //generate the validation message for a particular form field here
            return {
                require: ['^form'],
                priority: 1,
                scope: true,
                link: function($scope, element, attrs) {

                    var fullFieldPath = attrs.aaValMsgFor,
                        fieldInForm = $scope.$eval(fullFieldPath),
                        formObj = $scope.$eval(fullFieldPath.substring(0, fullFieldPath.indexOf('.')));

                    //could nest multiple forms so can't trust directive require and have to eval to handle edge cases...
                    ensureaaFormExtensionsFieldExists(formObj, fieldInForm.$name);
                    var fieldInFormExtensions = $scope.$eval(fullFieldPath.replace('.', '.$aaFormExtensions.'));

                    $scope.$watchCollection(
                        function() {
                            return fieldInFormExtensions.$errorMessages;
                        },
                        function(val) {
                            $scope.errorMessages = val;
                        }
                    );

                    $scope.$watchCollection(
                        function() {
                            return [
                                formObj.$aaFormExtensions.$invalidAttempt,
                                fieldInFormExtensions.$hadFocus
                            ];
                        },
                        function(watches) {
                            var invalidAttempt = watches[0],
                                hadFocus = watches[1];

                            $scope.showMessages = invalidAttempt || hadFocus;
                        }
                    );
                },
                template: aaFormExtensions.valMsgForTemplate,
                replace: true
            };
        }])

        //generate a label for an input generating an ID for it if it doesn't already exist
        .directive('aaLabel', ['aaFormExtensions', function (aaFormExtensions) {
            return {
                link: function (scope, element, attrs) {
                    var strategy = aaFormExtensions.labelStrategies[attrs.aaLabelStrategy];

                    //this could be a one off strategy on scope. lets try...
                    if(!strategy) {
                        var maybe = scope.$eval(attrs.aaLabelStrategy);
                        if(angular.isFunction(maybe)){
                            strategy = maybe;
                        }
                    }

                    //use default
                    if(!strategy) {
                        strategy = aaFormExtensions.labelStrategies[aaFormExtensions.defaultLabelStrategy];
                    }

                    var isRequiredField = (attrs.required !== undefined);

                    //auto generate an ID for compliant label names
                    if (!element[0].id) {
                        element[0].id = guid();
                    }

                    strategy(element, attrs.aaLabel, isRequiredField);
                }
            };
        }])

        .directive('aaAutoField', ['$compile', function ($compile) {
            return {
                restrict: 'A',
                scope: false,
                replace: true,
                priority: 1000,
                terminal: true,
                compile: function(element, attrs) {

                    //use the passed value for ng-model
                    element.attr("ng-model", attrs.aaAutoField);

                    var lastPartOfName = attrs.aaAutoField.substring(attrs.aaAutoField.lastIndexOf('.') + 1);

                    //if no name set calc one
                    if (!attrs.name) {
                        element.attr("name", lastPartOfName);
                    }

                    //if no label and "no-label" don't calc one
                    if (!attrs.aaLabel && attrs.noLabel === undefined) {
                        element.attr('aa-label', toTitleCase(splitCamelCase(lastPartOfName)));
                    }

                    element.attr("aa-val-msg", "");

                    element.removeAttr('aa-auto-field');

                    element.replaceWith(outerHTML(element[0]));

                    return function(scope, element, attrs) {
                        $compile(element)(scope);
                    };
                }
            };
        }])

        .directive('aaValidIcon', ['aaFormExtensions', function(aaFormExtensions) {
            return {
                require: 'ngModel',
                scope: false,
                compile: function(element) {

                    var container = aaFormExtensions.validIconStrategy.getContainer(element);

                    var validIcon = angular.element(aaFormExtensions.validIconStrategy.validIcon);
                    container.append(validIcon);
                    validIcon[0].style.display = 'none';

                    var invalidIcon = angular.element(aaFormExtensions.validIconStrategy.invalidIcon);
                    container.append(invalidIcon);
                    invalidIcon[0].style.display = 'none';

                    return function(scope, element, attrs, ngModel) {
                        ngModel.$parsers.push(function(val) {

                            if(ngModel.$valid) {
                                validIcon[0].style.display = '';
                                invalidIcon[0].style.display = 'none';
                            } else {
                                validIcon[0].style.display = 'none';
                                invalidIcon[0].style.display = '';
                            }

                            return val;
                        });
                    };
                }
            };
        }])

        .provider('aaFormExtensions', function () {

            var self = this;

            this.defaultLabelStrategy = "default";
            this.setDefaultLabelStrategy = function(strategyName) {
                this.defaultLabelStrategy = strategyName;
            };
            this.labelStrategies = {

                //create a bootstrap3 style label
                bootstrap3InlineForm: function (ele, labelText, isRequired) {

                    var label = angular.element('')
                        .attr('for', ele[0].id)
                        .addClass('col-sm-2 control-label')
                        .html(labelText + (isRequired ? ' *' : ''));

                    var unsupported = [
                        'button',
                        'submit'
                    ];

                    if(unsupported.indexOf(ele[0].type) !== -1) {
                        throw "Generating a label for and input type " + ele[0].type + " is unsupported.";
                    }

                    ele.parent().parent().prepend(label);
                },

                //create a no-frills label directly before the element
                default: function (ele, labelText, isRequired) {
                    ele[0].parentNode.insertBefore(
                        angular.element('')
                            .attr('for', ele[0].id)
                            .html(labelText + (isRequired ? ' *' : ''))[0],
                        ele[0]);
                }

                //add you own here using registerLabelStrategy
            };
            this.registerLabelStrategy = function (name, strategy) {
                this.labelStrategies[name] = strategy;
            };

            this.defaultValMsgPlacementStrategy = "default";
            this.setDefaultValMsgPlacementStrategy = function(strategyName) {
                this.defaultValMsgPlacementStrategy = strategyName;
            };
            this.valMsgPlacementStrategies = {

                default: function(formFieldElement, formName, formFieldName) {

                    var msgElement = angular.element(stringFormat('

', formName, formFieldName));
                    var fieldType = formFieldElement[0].type.toLowerCase();

                    if(fieldType === 'radio') {
                        //radios tend to be wrapped, go up a few levels (of course you can customize this with your own strategy)
                        formFieldElement.parent().parent().append(msgElement);

                    } else {
                        formFieldElement.after(msgElement);
                    }

                    return msgElement;
                }
            };
            this.registerValMsgPlacementStrategy = function(name, strategy) {
                this.valMsgPlacementStrategies[name] = strategy;
            };

            //bootstrap 3 + font awesome
            this.validIconStrategy = {
                validIcon: '',
                invalidIcon: '',
                getContainer: function(element) {
                    var ele = angular.element('
');
                    element.parent().after(ele);
                    return ele;
                }
            };
            this.setValidIconStrategy = function(strategy) {
                self.validIconStrategy = strategy;
            };

            this.validationMessages = {
                required: "{0} is required.",
                email: "The field {0} must be an email.",
                minlength: "{0} must be at least {1} character(s).",
                maxlength: "{0} must be less than {1} characters.",
                min: "{0} must be at least {1}.",
                max: "{0} must be at most {1}.",
                pattern: "{0} is invalid.",
                url: "{0} must be a valid URL.",
                number: "{0} must be number."
            };
            this.setValidationMessage = function(directiveName, message) {
                self.validationMessages[directiveName] = message;
            };
            this.setValidationMessages = function(messages) {
                self.validationMessages = messages;
            };

            this.valMsgForTemplate ='
{{msg}}
';
            this.setValMsgForTemplate = function(valMsgForTemplate) {
                this.valMsgForTemplate = valMsgForTemplate;
            };

            this.defaultOnInvalidAttempt = function() {
                //todo integrate with 'growl like' notifications
            };
            this.setDefaultOnInvalidAttempt = function(func) {
                this.defaultOnInvalidAttempt = func;
            };

            this.$get = function () {
                return {
                    defaultLabelStrategy: self.defaultLabelStrategy,
                    labelStrategies: self.labelStrategies,
                    validIconStrategy: self.validIconStrategy,
                    validationMessages: self.validationMessages,
                    valMsgForTemplate: self.valMsgForTemplate,
                    valMsgPlacementStrategies: self.valMsgPlacementStrategies,
                    defaultValMsgPlacementStrategy: self.defaultValMsgPlacementStrategy,
                    defaultOnInvalidAttempt: self.defaultOnInvalidAttempt
                };
            };
        });

    //utility
    function guid(a) {
        return a ? (a ^ Math.random() * 16 >> a / 4).toString(16) : ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, guid);
    }

    function toTitleCase(str) {
        return str.replace(/\w\S*/g, function (txt) {
            return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
        });
    }

    function splitCamelCase(str) {
        return str.replace(/([a-z](?=[A-Z]))/g, '$1 ');
    }

    function outerHTML(node){
        // if IE, Chrome take the internal method otherwise build one
        return node.outerHTML || (
            function(n){
                var div = document.createElement('div'), h;
                div.appendChild( n.cloneNode(true) );
                h = div.innerHTML;
                div = null;
                return h;
            })(node);
    }

    function stringFormat(format) {
        var args = Array.prototype.slice.call(arguments, 1);
        return format.replace(/{(\d+)}/g, function(match, number) {
            return typeof args[number] !== 'undefined' ? args[number] : match;
        });
    }

    function ensureaaFormExtensions(form) {
        if(!form.$aaFormExtensions) {
            form.$aaFormExtensions = {
                '$submitAttempt': function() {
                    setAttemptRecursively(form, form.$invalid);
                }
            };
        }

        function setAttemptRecursively(form, isInvalid) {
            form.$aaFormExtensions.$invalidAttempt = isInvalid;
            angular.forEach(form, function(fieldVal, fieldName) {
                if(fieldName.indexOf('$') !== 0 && form.constructor === fieldVal.constructor){
                    setAttemptRecursively(fieldVal, isInvalid);
                }
            });
        }
    }

    function ensureaaFormExtensionsFieldExists(form, fieldName) {
        ensureaaFormExtensions(form);

        if(!form.$aaFormExtensions[fieldName]) {
            form.$aaFormExtensions[fieldName] = {
                $hadFocus: false,
                $errorMessages: [],
                $element: null
            };
        }
    }
})();<|endoftext|>