<commit_before>import { isString } from 'lodash';
import minimist from 'minimist';

import { appendSettings, getSettings } from '../configuration/manageSettings';
import { setConfig } from '../configuration/manageConfig';
import { setContext } from '../context/helpers/manageContext';
import addRaw from '../configuration/addRaw';
import buildDocumentationObject from '../documentation/buildDocumentationObject';
import execute from '../execute';
import getAbsolutePath from '../helpers/getAbsolutePath';
import getSuggestions from '../helpers/getSuggestions';
import initContext from '../context/initContext';
import log from '../log/default';
import merge from '../helpers/merge';
import runHook from '../hooks/runHook';
import validateSettingsWrapper from '../validation/validateSettingsWrapper';

import extractCommand from './commands/helpers/extractCommand';
import generateAliases from './commands/helpers/generateAliases';
import generateCommandDocumentation from './commands/documentation/generateCommandDocumentation';
import generateCommandsDocumentation from './commands/documentation/generateCommandsDocumentation';
import getMappings from './commands/getMappings';
import parseArguments from './commands/parseArguments';
import parseOptions from './commands/parseOptions';

/**
 * Invokes the Roc cli.
 * Returns what the command is returning. If the command is a string command a promise will be
 * returned that is resolved when the command has been completed.
 */
export default function runCli({
    info = { version: 'Unknown', name: 'Unknown' },
    commands: initialCommands,
    argv = process.argv,
    invoke = true,
}) {
    const input = parseCliInput(minimist(argv.slice(2), { '--': true }));

    // If version is selected output that and stop
    if (input.coreOptions.version || input.coreOptions.v) {
        return console.log(info.version);
    }

    if (input.coreOptions.betterFeedback || input.coreOptions.b) {
        require('source-map-support').install(); // eslint-disable-line
        require('loud-rejection')(); // eslint-disable-line
    }

    // Possible to set a command in verbose mode
    const verboseMode = !!(input.coreOptions.verbose || input.coreOptions.V);

    // Get the project configuration path
    const projectConfigPath = input.coreOptions.c || input.coreOptions.config;

    // Get the directory path
    const dirPath = getAbsolutePath(input.coreOptions.directory || input.coreOptions.d);

    // Set temporary context
    setContext({
        verbose: verboseMode,
        directory: dirPath,
    });

    // Initialize the complete context
    const context = initContext({
        verbose: verboseMode,
        commands: initialCommands,
        directory: dirPath,
        projectConfigPath,
        name: info.name,
    });

    // If we have no command we will display some help information about all possible commands
    if (!input.groupOrCommand) {
        return console.log(
            generateCommandsDocumentation(context.commands, info.name)
        );
    }

    // Given context commands and user input, extract command data from potentially
    // nested group/command structure
    const commandData = extractCommand(
        context.commands,
        input.groupOrCommand,
        input.argsWithoutOptions,
        info.name
    );

    // command data as string indicates no match, so print the provided feedback
    if (isString(commandData)) {
        return console.log(commandData);
    }

    let commandSuggestions = Object.keys(commandData.commands);

    // If there is no direct match we will search through the tree for a match
    if (!commandData.commands[commandData.commandName]) {
        const commandAliases = generateAliases(
            commandData.commands,
            commandData.commandName,
            commandData.parents
        );

        if (!commandAliases) {
            return undefined;
        } else if (commandAliases.commands) {
            commandData.commands = commandAliases.commands;
            commandData.parents = commandAliases.parents;
        }

        commandSuggestions = commandSuggestions.concat(commandAliases.mappings);
    }

    const command = commandData.commands[commandData.commandName];

    if (!command) {
        log.large.error(
            getSuggestions([commandData.commandName], commandSuggestions),
            'Invalid command'
        );
    }

    // Show command help information if requested
    // Will ignore application configuration
    if (input.coreOptions.help || input.coreOptions.h) {
        return console.log(
            generateCommandDocumentation(
                context.extensionConfig.settings,
                context.meta.settings,
                commandData.commands,
                commandData.commandName,
                info.name,
                commandData.parents
            )
        );
    }

    let documentationObject;
    // Only parse arguments if the command accepts it
    if (command && command.settings) {
        // Get config from application and only parse options that this command cares about.
        const filter = command.settings === true ? [] : command.settings;
        documentationObject = buildDocumentationObject(context.config.settings, context.meta.settings, filter);
    }

    const { settings, parsedOptions } =
        parseOptions(input.extOptions, getMappings(documentationObject), command);

    const configToValidate = merge(context.config, {
        settings,
    });

    // Validate configuration
    if (command && command.settings) {
        validateSettingsWrapper(configToValidate.settings, context.meta.settings, command.settings);
    }

    // Does this after the validation so that things set by the CLI always will have the highest priority
    // We do not want to do validation on RAW configuration
    context.config = merge(addRaw(context.config), {
        settings,
    });

    // Set the configuration object
    setConfig(context.config);

    // Run hook to make it possible for extensions to update the settings before anything other uses them
    // This means that they can inspect what has been defined by other extensions, the user through config
    // and through command line options
    runHook('roc')('update-settings', () => getSettings())(
        (newSettings) => appendSettings(newSettings)
    );

    if (invoke) {
        // If string run as shell command
        if (isString(command.command)) {
            return execute(command.command, {
                context: command.__context,
                args: input.extraArguments,
                cwd: dirPath,
            }).catch((error) => {
                process.exitCode = error.getCode ? error.getCode() : 1;
                log.small.error('An error happened when running the Roc command', error);
            });
        }

        const parsedArguments = parseArguments(commandData.commandName, commandData.commands, input.argsWithoutOptions);

        // Run the command
        return command.command({
            info,
            arguments: parsedArguments,
            options: parsedOptions,
            extraArguments: input.extraArguments,
            // Roc Context
            context,
        });
    }

    return undefined;
}

/**
 * Wrap all relevant data from minimist with descriptive names
 */
function parseCliInput(minimistData) {
    const {
        _,
        h,
        help,
        V,
        verbose,
        v,
        version,
        c,
        config,
        d,
        directory,
        b,
        'better-feedback': betterFeedback,
        '--': extraArguments,
        ...extOptions,
    } = minimistData;

    // The first should be our command or commandgroup, if there is one
    const [groupOrCommand, ...argsWithoutOptions] = _;

    return {
        groupOrCommand, // commandgroup or command
        coreOptions: { // options managed and parsed by core
            h,
            help,
            V,
            verbose,
            v,
            version,
            c,
            config,
            d,
            directory,
            b,
            betterFeedback,
        },
        extOptions, // options that will be forwarded to commands from context
        argsWithoutOptions, // remaining arguments with no associated options
        extraArguments, // arguments after the ended argument list (--)
    };
}
<commit_msg>Improved feedback when a command fails<commit_after>import { isString } from 'lodash';
import minimist from 'minimist';
import isPromise from 'is-promise';

import { appendSettings, getSettings } from '../configuration/manageSettings';
import { setConfig } from '../configuration/manageConfig';
import { setContext } from '../context/helpers/manageContext';
import addRaw from '../configuration/addRaw';
import buildDocumentationObject from '../documentation/buildDocumentationObject';
import execute from '../execute';
import getAbsolutePath from '../helpers/getAbsolutePath';
import getSuggestions from '../helpers/getSuggestions';
import initContext from '../context/initContext';
import log from '../log/default';
import merge from '../helpers/merge';
import runHook from '../hooks/runHook';
import validateSettingsWrapper from '../validation/validateSettingsWrapper';

import extractCommand from './commands/helpers/extractCommand';
import generateAliases from './commands/helpers/generateAliases';
import generateCommandDocumentation from './commands/documentation/generateCommandDocumentation';
import generateCommandsDocumentation from './commands/documentation/generateCommandsDocumentation';
import getMappings from './commands/getMappings';
import parseArguments from './commands/parseArguments';
import parseOptions from './commands/parseOptions';

/**
 * Invokes the Roc cli.
 * Returns what the command is returning. If the command is a string command a promise will be
 * returned that is resolved when the command has been completed.
 */
export default function runCli({
    info = { version: 'Unknown', name: 'Unknown' },
    commands: initialCommands,
    argv = process.argv,
    invoke = true,
}) {
    const input = parseCliInput(minimist(argv.slice(2), { '--': true }));

    // If version is selected output that and stop
    if (input.coreOptions.version || input.coreOptions.v) {
        return console.log(info.version);
    }

    if (input.coreOptions.betterFeedback || input.coreOptions.b) {
        require('source-map-support').install(); // eslint-disable-line
        require('loud-rejection')(); // eslint-disable-line
    }

    // Possible to set a command in verbose mode
    const verboseMode = !!(input.coreOptions.verbose || input.coreOptions.V);

    // Get the project configuration path
    const projectConfigPath = input.coreOptions.c || input.coreOptions.config;

    // Get the directory path
    const dirPath = getAbsolutePath(input.coreOptions.directory || input.coreOptions.d);

    // Set temporary context
    setContext({
        verbose: verboseMode,
        directory: dirPath,
    });

    // Initialize the complete context
    const context = initContext({
        verbose: verboseMode,
        commands: initialCommands,
        directory: dirPath,
        projectConfigPath,
        name: info.name,
    });

    // If we have no command we will display some help information about all possible commands
    if (!input.groupOrCommand) {
        return console.log(
            generateCommandsDocumentation(context.commands, info.name)
        );
    }

    // Given context commands and user input, extract command data from potentially
    // nested group/command structure
    const commandData = extractCommand(
        context.commands,
        input.groupOrCommand,
        input.argsWithoutOptions,
        info.name
    );

    // command data as string indicates no match, so print the provided feedback
    if (isString(commandData)) {
        return console.log(commandData);
    }

    let commandSuggestions = Object.keys(commandData.commands);

    // If there is no direct match we will search through the tree for a match
    if (!commandData.commands[commandData.commandName]) {
        const commandAliases = generateAliases(
            commandData.commands,
            commandData.commandName,
            commandData.parents
        );

        if (!commandAliases) {
            return undefined;
        } else if (commandAliases.commands) {
            commandData.commands = commandAliases.commands;
            commandData.parents = commandAliases.parents;
        }

        commandSuggestions = commandSuggestions.concat(commandAliases.mappings);
    }

    const command = commandData.commands[commandData.commandName];

    if (!command) {
        log.large.error(
            getSuggestions([commandData.commandName], commandSuggestions),
            'Invalid command'
        );
    }

    // Show command help information if requested
    // Will ignore application configuration
    if (input.coreOptions.help || input.coreOptions.h) {
        return console.log(
            generateCommandDocumentation(
                context.extensionConfig.settings,
                context.meta.settings,
                commandData.commands,
                commandData.commandName,
                info.name,
                commandData.parents
            )
        );
    }

    let documentationObject;
    // Only parse arguments if the command accepts it
    if (command && command.settings) {
        // Get config from application and only parse options that this command cares about.
        const filter = command.settings === true ? [] : command.settings;
        documentationObject = buildDocumentationObject(context.config.settings, context.meta.settings, filter);
    }

    const { settings, parsedOptions } =
        parseOptions(input.extOptions, getMappings(documentationObject), command);

    const configToValidate = merge(context.config, {
        settings,
    });

    // Validate configuration
    if (command && command.settings) {
        validateSettingsWrapper(configToValidate.settings, context.meta.settings, command.settings);
    }

    // Does this after the validation so that things set by the CLI always will have the highest priority
    // We do not want to do validation on RAW configuration
    context.config = merge(addRaw(context.config), {
        settings,
    });

    // Set the configuration object
    setConfig(context.config);

    // Run hook to make it possible for extensions to update the settings before anything other uses them
    // This means that they can inspect what has been defined by other extensions, the user through config
    // and through command line options
    runHook('roc')('update-settings', () => getSettings())(
        (newSettings) => appendSettings(newSettings)
    );

    if (invoke) {
        // If string run as shell command
        if (isString(command.command)) {
            return execute(command.command, {
                context: command.__context,
                args: input.extraArguments,
                cwd: dirPath,
            }).catch((error) => {
                process.exitCode = error.getCode ? error.getCode() : 1;
                log.small.error('A problem happened when running the Roc command');
            });
        }

        const parsedArguments = parseArguments(commandData.commandName, commandData.commands, input.argsWithoutOptions);

        // Run the command
        const commandResult = command.command({
            info,
            arguments: parsedArguments,
            options: parsedOptions,
            extraArguments: input.extraArguments,
            // Roc Context
            context,
        });

        if (isPromise(commandResult)) {
            return commandResult
                .catch((error) => {
                    log.small.warn('A problem happened when running the Roc command', error);
                });
        }

        return commandResult;
    }

    return undefined;
}

/**
 * Wrap all relevant data from minimist with descriptive names
 */
function parseCliInput(minimistData) {
    /* eslint-disable object-property-newline */
    const {
        _,
        h, help,
        V, verbose,
        v, version,
        c, config,
        d, directory,
        b, 'better-feedback': betterFeedback,
        '--': extraArguments,
        ...extOptions,
    } = minimistData;

    // The first should be our command or commandgroup, if there is one
    const [groupOrCommand, ...argsWithoutOptions] = _;

    return {
        groupOrCommand, // commandgroup or command
        coreOptions: { // options managed and parsed by core
            h, help,
            V, verbose,
            v, version,
            c, config,
            d, directory,
            b, betterFeedback,
        },
        extOptions, // options that will be forwarded to commands from context
        argsWithoutOptions, // remaining arguments with no associated options
        extraArguments, // arguments after the ended argument list (--)
    };
    /* eslint-enable object-property-newline */
}
<|endoftext|>


<commit_before>/*global
    $serverSide,
    assignView, assignTemplate, createRegistryWrapper, dataObject, getValue,
    modelCidAttributeName, modelIdAttributeName, viewCidAttributeName
*/
var _fetch = Backbone.Collection.prototype.fetch,
    _set = Backbone.Collection.prototype.set,
    _replaceHTML = Thorax.View.prototype._replaceHTML,
    collectionCidAttributeName = 'data-collection-cid',
    collectionEmptyAttributeName = 'data-collection-empty',
    collectionElementAttributeName = 'data-collection-element',
    ELEMENT_NODE_TYPE = 1;

Thorax.Collection = Backbone.Collection.extend({
  model: Thorax.Model || Backbone.Model,
  initialize: function() {
    this.cid = _.uniqueId('collection');
    return Backbone.Collection.prototype.initialize.apply(this, arguments);
  },
  isEmpty: function() {
    if (this.length > 0) {
      return false;
    } else {
      return this.length === 0 && this.isPopulated();
    }
  },
  isPopulated: function() {
    return this._fetched || this.length > 0 || (!this.length && !getValue(this, 'url'));
  },
  shouldFetch: function(options) {
    return options.fetch && !!getValue(this, 'url') && !this.isPopulated();
  },
  fetch: function(options) {
    options = options || {};
    var success = options.success;
    options.success = function(collection, response) {
      collection._fetched = true;
      success && success(collection, response);
    };
    return _fetch.apply(this, arguments);
  },
  set: function(models, options) {
    this._fetched = !!models;
    return _set.call(this, models, options);
  }
});

_.extend(Thorax.View.prototype, {
  getCollectionViews: function(collection) {
    return _.filter(this.children, function(child) {
      if (!(child instanceof Thorax.CollectionView)) {
        return false;
      }

      return !collection || (child.collection === collection);
    });
  },
  updateFilter: function(collection) {
    _.invoke(this.getCollectionViews(collection), 'updateFilter');
  }
});

Thorax.Collections = {};
createRegistryWrapper(Thorax.Collection, Thorax.Collections);

dataObject('collection', {
  set: 'setCollection',
  bindCallback: onSetCollection,
  defaultOptions: {
    render: undefined,    // Default to deferred rendering
    fetch: true,
    success: false,
    invalid: true,
    change: true          // Wether or not to re-render on model:change
  },
  change: onCollectionReset,
  $el: 'getCollectionElement',
  cidAttrName: collectionCidAttributeName
});

Thorax.CollectionView = Thorax.View.extend({
  _defaultTemplate: Handlebars.VM.noop,
  _collectionSelector: '[' + collectionElementAttributeName + ']',

  // preserve collection element if it was not created with {{collection}} helper
  _replaceHTML: function(html) {
    if (this.collection && this.getObjectOptions(this.collection) && this._renderCount) {
      var element;
      var oldCollectionElement = this._collectionElement;
      element = _replaceHTML.call(this, html);

      this._lookupCollectionElement();

      if (!oldCollectionElement.attr('data-view-cid')) {
        this._collectionElement.replaceWith(oldCollectionElement);
      }
    } else {
      var ret = _replaceHTML.call(this, html);
      this._lookupCollectionElement();

      return ret;
    }
  },

  render: function() {
    var shouldRender = this.shouldRender();

    Thorax.View.prototype.render.apply(this, arguments);
    if (!shouldRender) {
      this.renderCollection();
    }
  },

  restoreCollection: function(el) {
    // This is called as an event so we don't force render our content when there are nested
    // child views.
    var self = this,
        children = this.$el.children(),
        toRemove = [];

    this._lookupCollectionElement();

    // Find any items annotated with server info and restore. Else rerender
    this.$('[' + modelIdAttributeName + ']').each(function() {
      var id = this.getAttribute(modelIdAttributeName),
          model = self.collection.get(id);
      if (!model) {
        toRemove.push(this);
      } else {
        self.restoreItem(model, children.index(this), this);
      }
    });
    $('[data-view-empty]', el).each(function() {
      self.restoreEmpty(this);
    });

    if (toRemove.length) {
      // TODO : Is it better to just rerender the whole thing since we don't necessarily
      // know what state the list is in at this point?
      // Kill off any now invalid nodes
      _.each(toRemove, function(el) {
        el.parentNode.removeChild(el);
      });

      // Render anything that we might have locally but was missed
      var $el = this._collectionElement;
      this.collection.each(function(model) {
        if (!$el.find('[' + modelCidAttributeName + '="' + model.cid + '"]').length) {
          self.appendItem(model);
        }
      });
    }
  },

  //appendItem(model [,index])
  //appendItem(html_string, index)
  //appendItem(view, index)
  appendItem: function(model, index, options) {
    //empty item
    if (!model) {
      return;
    }
    var itemView,
        $el = this._collectionElement,
        collection = this.collection,

        filter = !options || options.filter == null || options.filter;

    //if index argument is a view
    index && index.el && (index = $el.children().indexOf(index.el) + 1);

    //if argument is a view, or html string
    if (model.el || _.isString(model)) {
      itemView = model;
      model = false;
    } else {
      index = index || collection.indexOf(model) || 0;
      // Using call here to avoid v8 prototype inline optimization bug that helper views
      // expose under Android 4.3 (at minimum)
      // https://twitter.com/kpdecker/status/422149634929082370
      itemView = this.renderItem.call(this, model, index);
    }

    if (itemView) {
      if (itemView.cid) {
        itemView.ensureRendered();
        this._addChild(itemView);
      }

      //if the renderer's output wasn't contained in a tag, wrap it in a div
      //plain text, or a mixture of top level text nodes and element nodes
      //will get wrapped
      if (_.isString(itemView) && !itemView.match(/^\s*</m)) {
        itemView = '<div>' + itemView + '</div>';
      }
      var itemElement = itemView.$el || $($.trim(itemView)).filter(function() {
        // Only output nodes. DOM || Fruit Loops
        return this.nodeType === ELEMENT_NODE_TYPE || this.type === 'tag';
      });

      if (model) {
        itemElement.attr({
          'data-model-id': model.id,
          'data-model-cid': model.cid
        });
      }
      var previousModel = index > 0 ? collection.at(index - 1) : false;
      if (!previousModel) {
        $el.prepend(itemElement);
      } else {
        //use last() as appendItem can accept multiple nodes from a template
        var last = $el.children('[' + modelCidAttributeName + '="' + previousModel.cid + '"]').last();
        last.after(itemElement);
      }

      this.trigger('append', null, function($el) {
        $el.attr({
          'data-model-cid': model.cid,
          'data-model-id': model.id,
        });
      });

      if (!options || !options.silent) {
        this.trigger('rendered:item', this, collection, model, itemElement, index);
      }
      if (filter) {
        applyItemVisiblityFilter.call(this, model);
      }
    }
    return itemView;
  },

  // updateItem only useful if there is no item view, otherwise
  // itemView.render() provides the same functionality
  updateItem: function(model) {
    var $el = this._collectionElement,
        viewEl = $el.find('[' + modelCidAttributeName + '="' + model.cid + '"]');

    // NOP For views
    if (viewEl.attr(viewCidAttributeName)) {
      return;
    }

    this.removeItem(viewEl);
    this.appendItem(model);
  },

  removeItem: function(model) {
    var self = this,
        $viewEl = model;

    if (model.cid) {
      var $el = this._collectionElement;
      $viewEl = $el.find('[' + modelCidAttributeName + '="' + model.cid + '"]');
    }
    if (!$viewEl.length) {
      return false;
    }

    function cleanCid($viewEl) {
      var cid = $viewEl.attr(viewCidAttributeName),
          child = self.children[cid];
      if (child) {
        self._removeChild(child);
      }
    }

    $viewEl.find('[' + viewCidAttributeName + ']').each(function(i, el) {
      cleanCid($(el));
    });
    cleanCid($viewEl);

    $viewEl.detach();

    return true;
  },

  renderCollection: function() {
    if (this.collection) {
      if (this.collection.isEmpty()) {
        handleChangeFromNotEmptyToEmpty.call(this);
      } else {
        handleChangeFromEmptyToNotEmpty.call(this);
        this.collection.forEach(function(item, i) {
          this.appendItem(item, i);
        }, this);
      }
      this.trigger('rendered:collection', this, this.collection);
    } else {
      handleChangeFromNotEmptyToEmpty.call(this);
    }
  },
  emptyClass: 'empty',
  renderEmpty: function() {
    if (!this.emptyView) {
      assignView.call(this, 'emptyView', {
        extension: '-empty'
      });
    }
    if (!this.emptyTemplate && !this.emptyView) {
      assignTemplate.call(this, 'emptyTemplate', {
        extension: '-empty',
        required: false
      });
    }
    if (this.emptyView) {
      var viewOptions = {};
      if (this.emptyTemplate) {
        viewOptions.template = this.emptyTemplate;
      }
      var view = Thorax.Util.getViewInstance(this.emptyView, viewOptions);
      view.$el.attr('data-view-empty', 'true');
      return view;
    } else {
      return this.emptyTemplate && this.renderTemplate(this.emptyTemplate);
    }
  },
  restoreEmpty: function(el) {
    var child = this.renderEmpty();

    child.restore(el);
    this._addChild(child);
    return child;
  },

  renderItem: function(model, i) {
    if (!this.itemView) {
      assignView.call(this, 'itemView', {
        extension: '-item',
        required: false
      });
    }
    if (!this.itemTemplate && !this.itemView) {
      assignTemplate.call(this, 'itemTemplate', {
        extension: '-item',
        // only require an itemTemplate if an itemView
        // is not present
        required: !this.itemView
      });
    }
    if (this.itemView) {
      var viewOptions = {
        model: model
      };
      if (this.itemTemplate) {
        viewOptions.template = this.itemTemplate;
      }
      return Thorax.Util.getViewInstance(this.itemView, viewOptions);
    } else {
      // Using call here to avoid v8 prototype inline optimization bug that helper views
      // expose under Android 4.3 (at minimum)
      // https://twitter.com/kpdecker/status/422149634929082370
      return this.renderTemplate(this.itemTemplate, this.itemContext.call(this, model, i));
    }
  },
  restoreItem: function(model, i, el) {
    // Associate the element with the proper model.
    el.setAttribute(modelCidAttributeName, model.cid);

    // If we are dealing with something other than a template then reinstantiate the view.
    if (this.itemView || this.renderItem !== Thorax.CollectionView.prototype.renderItem) {
      var child = this.renderItem(model, i);

      if (!_.isString(child)) {
        child.restore(el);
        this._addChild(child);
      }
    }
  },
  itemContext: function(model /*, i */) {
    return model.attributes;
  },
  appendEmpty: function() {
    var $el = this._collectionElement;
    $el.empty();

    // Using call here to avoid v8 prototype inline optimization bug that helper views
    // expose under Android 4.3 (at minimum)
    // https://twitter.com/kpdecker/status/422149634929082370
    var emptyContent = this.renderEmpty.call(this);
    emptyContent && this.appendItem(emptyContent, 0, {
      silent: true,
      filter: false
    });
    this.trigger('rendered:empty', this, this.collection);
  },
  getCollectionElement: function() {
    return this._collectionElement;
  },
  _lookupCollectionElement: function() {
    var $collectionElement = this.$(this._collectionSelector);
    this._collectionElement = $collectionElement.length ? $collectionElement : this.$el;
  },

  updateFilter: function() {
    applyVisibilityFilter.call(this);
  }
});

Thorax.CollectionView.on({
  restore: 'restoreCollection',

  collection: {
    reset: onCollectionReset,
    sort: onCollectionReset,
    change: function(model) {
      var options = this.getObjectOptions(this.collection);
      if (options && options.change) {
        this.updateItem(model);
      }
      applyItemVisiblityFilter.call(this, model);
    },
    add: function(model) {
      var $el = this._collectionElement;
      if ($el.length) {
        if (this.collection.length === 1) {
          handleChangeFromEmptyToNotEmpty.call(this);
        }

        var index = this.collection.indexOf(model);
        this.appendItem(model, index);
      }
    },
    remove: function(model) {
      var $el = this._collectionElement;
      this.removeItem(model);
      this.collection.length === 0 && $el.length && handleChangeFromNotEmptyToEmpty.call(this);
    }
  }
});

Thorax.View.on({
  collection: {
    invalid: function(collection, message) {
      if (this.getObjectOptions(collection).invalid) {
        this.trigger('invalid', message, collection);
      }
    },
    error: function(collection, resp /*, options */) {
      this.trigger('error', resp, collection);
    }
  }
});

function onCollectionReset(collection) {
  // Undefined to force conditional render
  var options = this.getObjectOptions(collection) || undefined;
  if (this.shouldRender(options && options.render)) {
    this.renderCollection && this.renderCollection();
  }
}

// Even if the view is not a CollectionView
// ensureRendered() to provide similar behavior
// to a model
function onSetCollection(collection) {
  // Undefined to force conditional render
  var options = this.getObjectOptions(collection) || undefined;
  if (this.shouldRender(options && options.render)) {
    // Ensure that something is there if we are going to render the collection.
    this.ensureRendered();
  }
}

function applyVisibilityFilter() {
  if (this.itemFilter) {
    this.collection.forEach(applyItemVisiblityFilter, this);
  }
}

function applyItemVisiblityFilter(model) {
  var $el = this._collectionElement;
  this.itemFilter && $el.find('[' + modelCidAttributeName + '="' + model.cid + '"]')[itemShouldBeVisible.call(this, model) ? 'show' : 'hide']();
}

function itemShouldBeVisible(model) {
  // Using call here to avoid v8 prototype inline optimization bug that helper views
  // expose under Android 4.3 (at minimum)
  // https://twitter.com/kpdecker/status/422149634929082370
  return this.itemFilter.call(this, model, this.collection.indexOf(model));
}

function handleChangeFromEmptyToNotEmpty() {
  var $el = this._collectionElement;
  this.emptyClass && $el.removeClass(this.emptyClass);
  $el.removeAttr(collectionEmptyAttributeName);
  $el.empty();
}

function handleChangeFromNotEmptyToEmpty() {
  var $el = this._collectionElement;
  this.emptyClass && $el.addClass(this.emptyClass);
  $el.attr(collectionEmptyAttributeName, true);
  this.appendEmpty();
}

//$(selector).collection() helper
$.fn.collection = function(view) {
  if (view && view.collection) {
    return view.collection;
  }
  var $this = $(this),
      collectionElement = $this.closest('[' + collectionCidAttributeName + ']'),
      collectionCid = collectionElement && collectionElement.attr(collectionCidAttributeName);
  if (collectionCid) {
    view = $this.view();
    if (view) {
      return view.collection;
    }
  }
  return false;
};
<commit_msg>Add note about conditional<commit_after>/*global
    $serverSide,
    assignView, assignTemplate, createRegistryWrapper, dataObject, getValue,
    modelCidAttributeName, modelIdAttributeName, viewCidAttributeName
*/
var _fetch = Backbone.Collection.prototype.fetch,
    _set = Backbone.Collection.prototype.set,
    _replaceHTML = Thorax.View.prototype._replaceHTML,
    collectionCidAttributeName = 'data-collection-cid',
    collectionEmptyAttributeName = 'data-collection-empty',
    collectionElementAttributeName = 'data-collection-element',
    ELEMENT_NODE_TYPE = 1;

Thorax.Collection = Backbone.Collection.extend({
  model: Thorax.Model || Backbone.Model,
  initialize: function() {
    this.cid = _.uniqueId('collection');
    return Backbone.Collection.prototype.initialize.apply(this, arguments);
  },
  isEmpty: function() {
    if (this.length > 0) {
      return false;
    } else {
      return this.length === 0 && this.isPopulated();
    }
  },
  isPopulated: function() {
    return this._fetched || this.length > 0 || (!this.length && !getValue(this, 'url'));
  },
  shouldFetch: function(options) {
    return options.fetch && !!getValue(this, 'url') && !this.isPopulated();
  },
  fetch: function(options) {
    options = options || {};
    var success = options.success;
    options.success = function(collection, response) {
      collection._fetched = true;
      success && success(collection, response);
    };
    return _fetch.apply(this, arguments);
  },
  set: function(models, options) {
    this._fetched = !!models;
    return _set.call(this, models, options);
  }
});

_.extend(Thorax.View.prototype, {
  getCollectionViews: function(collection) {
    return _.filter(this.children, function(child) {
      if (!(child instanceof Thorax.CollectionView)) {
        return false;
      }

      return !collection || (child.collection === collection);
    });
  },
  updateFilter: function(collection) {
    _.invoke(this.getCollectionViews(collection), 'updateFilter');
  }
});

Thorax.Collections = {};
createRegistryWrapper(Thorax.Collection, Thorax.Collections);

dataObject('collection', {
  set: 'setCollection',
  bindCallback: onSetCollection,
  defaultOptions: {
    render: undefined,    // Default to deferred rendering
    fetch: true,
    success: false,
    invalid: true,
    change: true          // Wether or not to re-render on model:change
  },
  change: onCollectionReset,
  $el: 'getCollectionElement',
  cidAttrName: collectionCidAttributeName
});

Thorax.CollectionView = Thorax.View.extend({
  _defaultTemplate: Handlebars.VM.noop,
  _collectionSelector: '[' + collectionElementAttributeName + ']',

  // preserve collection element if it was not created with {{collection}} helper
  _replaceHTML: function(html) {
    if (this.collection && this.getObjectOptions(this.collection) && this._renderCount) {
      var element;
      var oldCollectionElement = this._collectionElement;
      element = _replaceHTML.call(this, html);

      this._lookupCollectionElement();

      if (!oldCollectionElement.attr('data-view-cid')) {
        this._collectionElement.replaceWith(oldCollectionElement);
      }
    } else {
      var ret = _replaceHTML.call(this, html);
      this._lookupCollectionElement();

      return ret;
    }
  },

  render: function() {
    var shouldRender = this.shouldRender();

    Thorax.View.prototype.render.apply(this, arguments);
    if (!shouldRender) {
      this.renderCollection();
    }
  },

  restoreCollection: function(el) {
    // This is called as an event so we don't force render our content when there are nested
    // child views.
    var self = this,
        children = this.$el.children(),
        toRemove = [];

    this._lookupCollectionElement();

    // Find any items annotated with server info and restore. Else rerender
    this.$('[' + modelIdAttributeName + ']').each(function() {
      var id = this.getAttribute(modelIdAttributeName),
          model = self.collection.get(id);
      if (!model) {
        toRemove.push(this);
      } else {
        self.restoreItem(model, children.index(this), this);
      }
    });
    $('[data-view-empty]', el).each(function() {
      self.restoreEmpty(this);
    });

    if (toRemove.length) {
      // TODO : Is it better to just rerender the whole thing since we don't necessarily
      // know what state the list is in at this point?
      // Kill off any now invalid nodes
      _.each(toRemove, function(el) {
        el.parentNode.removeChild(el);
      });

      // Render anything that we might have locally but was missed
      var $el = this._collectionElement;
      this.collection.each(function(model) {
        if (!$el.find('[' + modelCidAttributeName + '="' + model.cid + '"]').length) {
          self.appendItem(model);
        }
      });
    }
  },

  //appendItem(model [,index])
  //appendItem(html_string, index)
  //appendItem(view, index)
  appendItem: function(model, index, options) {
    //empty item
    if (!model) {
      return;
    }
    var itemView,
        $el = this._collectionElement,
        collection = this.collection,

        filter = !options || options.filter == null || options.filter;

    //if index argument is a view
    index && index.el && (index = $el.children().indexOf(index.el) + 1);

    //if argument is a view, or html string
    if (model.el || _.isString(model)) {
      itemView = model;
      model = false;
    } else {
      index = index || collection.indexOf(model) || 0;
      // Using call here to avoid v8 prototype inline optimization bug that helper views
      // expose under Android 4.3 (at minimum)
      // https://twitter.com/kpdecker/status/422149634929082370
      itemView = this.renderItem.call(this, model, index);
    }

    if (itemView) {
      if (itemView.cid) {
        itemView.ensureRendered();
        this._addChild(itemView);
      }

      //if the renderer's output wasn't contained in a tag, wrap it in a div
      //plain text, or a mixture of top level text nodes and element nodes
      //will get wrapped
      if (_.isString(itemView) && !itemView.match(/^\s*</m)) {
        itemView = '<div>' + itemView + '</div>';
      }
      var itemElement = itemView.$el || $($.trim(itemView)).filter(function() {
        // Only output nodes. DOM || Fruit Loops
        return this.nodeType === ELEMENT_NODE_TYPE || this.type === 'tag';
      });

      if (model) {
        itemElement.attr({
          'data-model-id': model.id,
          'data-model-cid': model.cid
        });
      }
      var previousModel = index > 0 ? collection.at(index - 1) : false;
      if (!previousModel) {
        $el.prepend(itemElement);
      } else {
        //use last() as appendItem can accept multiple nodes from a template
        var last = $el.children('[' + modelCidAttributeName + '="' + previousModel.cid + '"]').last();
        last.after(itemElement);
      }

      this.trigger('append', null, function($el) {
        $el.attr({
          'data-model-cid': model.cid,
          'data-model-id': model.id,
        });
      });

      if (!options || !options.silent) {
        this.trigger('rendered:item', this, collection, model, itemElement, index);
      }
      if (filter) {
        applyItemVisiblityFilter.call(this, model);
      }
    }
    return itemView;
  },

  // updateItem only useful if there is no item view, otherwise
  // itemView.render() provides the same functionality
  updateItem: function(model) {
    var $el = this._collectionElement,
        viewEl = $el.find('[' + modelCidAttributeName + '="' + model.cid + '"]');

    // NOP For views
    if (viewEl.attr(viewCidAttributeName)) {
      return;
    }

    this.removeItem(viewEl);
    this.appendItem(model);
  },

  removeItem: function(model) {
    var self = this,
        $viewEl = model;

    if (model.cid) {
      var $el = this._collectionElement;
      $viewEl = $el.find('[' + modelCidAttributeName + '="' + model.cid + '"]');
    }
    if (!$viewEl.length) {
      return false;
    }

    function cleanCid($viewEl) {
      var cid = $viewEl.attr(viewCidAttributeName),
          child = self.children[cid];
      if (child) {
        self._removeChild(child);
      }
    }

    $viewEl.find('[' + viewCidAttributeName + ']').each(function(i, el) {
      cleanCid($(el));
    });
    cleanCid($viewEl);

    $viewEl.detach();

    return true;
  },

  renderCollection: function() {
    if (this.collection) {
      if (this.collection.isEmpty()) {
        handleChangeFromNotEmptyToEmpty.call(this);
      } else {
        handleChangeFromEmptyToNotEmpty.call(this);
        this.collection.forEach(function(item, i) {
          this.appendItem(item, i);
        }, this);
      }
      this.trigger('rendered:collection', this, this.collection);
    } else {
      handleChangeFromNotEmptyToEmpty.call(this);
    }
  },
  emptyClass: 'empty',
  renderEmpty: function() {
    if (!this.emptyView) {
      assignView.call(this, 'emptyView', {
        extension: '-empty'
      });
    }
    if (!this.emptyTemplate && !this.emptyView) {
      assignTemplate.call(this, 'emptyTemplate', {
        extension: '-empty',
        required: false
      });
    }
    if (this.emptyView) {
      var viewOptions = {};
      if (this.emptyTemplate) {
        viewOptions.template = this.emptyTemplate;
      }
      var view = Thorax.Util.getViewInstance(this.emptyView, viewOptions);
      view.$el.attr('data-view-empty', 'true');
      return view;
    } else {
      return this.emptyTemplate && this.renderTemplate(this.emptyTemplate);
    }
  },
  restoreEmpty: function(el) {
    var child = this.renderEmpty();

    child.restore(el);
    this._addChild(child);
    return child;
  },

  renderItem: function(model, i) {
    if (!this.itemView) {
      assignView.call(this, 'itemView', {
        extension: '-item',
        required: false
      });
    }
    if (!this.itemTemplate && !this.itemView) {
      assignTemplate.call(this, 'itemTemplate', {
        extension: '-item',
        // only require an itemTemplate if an itemView
        // is not present
        required: !this.itemView
      });
    }
    if (this.itemView) {
      var viewOptions = {
        model: model
      };
      if (this.itemTemplate) {
        viewOptions.template = this.itemTemplate;
      }
      return Thorax.Util.getViewInstance(this.itemView, viewOptions);
    } else {
      // Using call here to avoid v8 prototype inline optimization bug that helper views
      // expose under Android 4.3 (at minimum)
      // https://twitter.com/kpdecker/status/422149634929082370
      return this.renderTemplate(this.itemTemplate, this.itemContext.call(this, model, i));
    }
  },
  restoreItem: function(model, i, el) {
    // Associate the element with the proper model.
    el.setAttribute(modelCidAttributeName, model.cid);

    // If we are dealing with something other than a template then reinstantiate the view.
    if (this.itemView || this.renderItem !== Thorax.CollectionView.prototype.renderItem) {
      var child = this.renderItem(model, i);

      // If we are passed a string assume that the upstream implementation has a consistent
      // rendering.
      if (!_.isString(child)) {
        child.restore(el);
        this._addChild(child);
      }
    }
  },
  itemContext: function(model /*, i */) {
    return model.attributes;
  },
  appendEmpty: function() {
    var $el = this._collectionElement;
    $el.empty();

    // Using call here to avoid v8 prototype inline optimization bug that helper views
    // expose under Android 4.3 (at minimum)
    // https://twitter.com/kpdecker/status/422149634929082370
    var emptyContent = this.renderEmpty.call(this);
    emptyContent && this.appendItem(emptyContent, 0, {
      silent: true,
      filter: false
    });
    this.trigger('rendered:empty', this, this.collection);
  },
  getCollectionElement: function() {
    return this._collectionElement;
  },
  _lookupCollectionElement: function() {
    var $collectionElement = this.$(this._collectionSelector);
    this._collectionElement = $collectionElement.length ? $collectionElement : this.$el;
  },

  updateFilter: function() {
    applyVisibilityFilter.call(this);
  }
});

Thorax.CollectionView.on({
  restore: 'restoreCollection',

  collection: {
    reset: onCollectionReset,
    sort: onCollectionReset,
    change: function(model) {
      var options = this.getObjectOptions(this.collection);
      if (options && options.change) {
        this.updateItem(model);
      }
      applyItemVisiblityFilter.call(this, model);
    },
    add: function(model) {
      var $el = this._collectionElement;
      if ($el.length) {
        if (this.collection.length === 1) {
          handleChangeFromEmptyToNotEmpty.call(this);
        }

        var index = this.collection.indexOf(model);
        this.appendItem(model, index);
      }
    },
    remove: function(model) {
      var $el = this._collectionElement;
      this.removeItem(model);
      this.collection.length === 0 && $el.length && handleChangeFromNotEmptyToEmpty.call(this);
    }
  }
});

Thorax.View.on({
  collection: {
    invalid: function(collection, message) {
      if (this.getObjectOptions(collection).invalid) {
        this.trigger('invalid', message, collection);
      }
    },
    error: function(collection, resp /*, options */) {
      this.trigger('error', resp, collection);
    }
  }
});

function onCollectionReset(collection) {
  // Undefined to force conditional render
  var options = this.getObjectOptions(collection) || undefined;
  if (this.shouldRender(options && options.render)) {
    this.renderCollection && this.renderCollection();
  }
}

// Even if the view is not a CollectionView
// ensureRendered() to provide similar behavior
// to a model
function onSetCollection(collection) {
  // Undefined to force conditional render
  var options = this.getObjectOptions(collection) || undefined;
  if (this.shouldRender(options && options.render)) {
    // Ensure that something is there if we are going to render the collection.
    this.ensureRendered();
  }
}

function applyVisibilityFilter() {
  if (this.itemFilter) {
    this.collection.forEach(applyItemVisiblityFilter, this);
  }
}

function applyItemVisiblityFilter(model) {
  var $el = this._collectionElement;
  this.itemFilter && $el.find('[' + modelCidAttributeName + '="' + model.cid + '"]')[itemShouldBeVisible.call(this, model) ? 'show' : 'hide']();
}

function itemShouldBeVisible(model) {
  // Using call here to avoid v8 prototype inline optimization bug that helper views
  // expose under Android 4.3 (at minimum)
  // https://twitter.com/kpdecker/status/422149634929082370
  return this.itemFilter.call(this, model, this.collection.indexOf(model));
}

function handleChangeFromEmptyToNotEmpty() {
  var $el = this._collectionElement;
  this.emptyClass && $el.removeClass(this.emptyClass);
  $el.removeAttr(collectionEmptyAttributeName);
  $el.empty();
}

function handleChangeFromNotEmptyToEmpty() {
  var $el = this._collectionElement;
  this.emptyClass && $el.addClass(this.emptyClass);
  $el.attr(collectionEmptyAttributeName, true);
  this.appendEmpty();
}

//$(selector).collection() helper
$.fn.collection = function(view) {
  if (view && view.collection) {
    return view.collection;
  }
  var $this = $(this),
      collectionElement = $this.closest('[' + collectionCidAttributeName + ']'),
      collectionCid = collectionElement && collectionElement.attr(collectionCidAttributeName);
  if (collectionCid) {
    view = $this.view();
    if (view) {
      return view.collection;
    }
  }
  return false;
};
<|endoftext|>


<commit_before>///////////////////////////
///   THE COLLECTION    ///
///////////////////////////

Collection.prototype = new Super();
Collection.constructor = Collection;
Collection.id = 0;

function Collection (manager, options) {
    var self = this;
    self.nipples = [];
    self.idles = [];
    self.actives = [];
    self.ids = [];
    self.pressureIntervals = {};
    self.manager = manager;
    self.id = Collection.id;
    Collection.id += 1;

    // Defaults
    self.defaults = {
        zone: document.body,
        multitouch: false,
        maxNumberOfNipples: 10,
        mode: 'dynamic',
        position: {top: 0, left: 0},
        catchDistance: 200,
        size: 100,
        threshold: 0.1,
        color: 'white',
        fadeTime: 250,
        dataOnly: false,
        restOpacity: 0.5
    };

    self.config(options);

    // Overwrites
    if (self.options.mode === 'static' || self.options.mode === 'semi') {
        self.options.multitouch = false;
    }

    if (!self.options.multitouch) {
        self.options.maxNumberOfNipples = 1;
    }

    self.updateBox();
    self.prepareNipples();
    self.bindings();
    self.begin();

    return self.nipples;
}

Collection.prototype.prepareNipples = function () {
    var self = this;
    var nips = self.nipples;

    // Public API Preparation.
    nips.on = self.on.bind(self);
    nips.off = self.off.bind(self);
    nips.options = self.options;
    nips.destroy = self.destroy.bind(self);
    nips.ids = self.ids;
    nips.id = self.id;
    nips.processOnMove = self.processOnMove.bind(self);
    nips.processOnEnd = self.processOnEnd.bind(self);
    nips.get = function (id) {
        if (id === undefined) {
            return nips[0];
        }
        for (var i = 0, max = nips.length; i < max; i += 1) {
            if (nips[i].identifier === id) {
                return nips[i];
            }
        }
        return false;
    };
};

Collection.prototype.bindings = function () {
    var self = this;
    // Touch start event.
    self.bindEvt(self.options.zone, 'start');
};

Collection.prototype.begin = function () {
    var self = this;
    var opts = self.options;

    // We place our static nipple
    // if needed.
    if (opts.mode === 'static') {
        var nipple = self.createNipple(
            opts.position
        );
        // Add it to the dom.
        nipple.add();
        // Store it in idles.
        self.idles.push(nipple);
    }
};

// Nipple Factory
Collection.prototype.createNipple = function (position, identifier) {
    var self = this;
    var scroll = u.getScroll();
    var toPutOn = {};
    var opts = self.options;

    if (position.x && position.y) {
        toPutOn = {
            x: position.x -
                (scroll.x + self.box.left),
            y: position.y -
                (scroll.y + self.box.top)
        };
    } else if (
            position.top ||
            position.right ||
            position.bottom ||
            position.left
        ) {

        // We need to compute the position X / Y of the joystick.
        var dumb = document.createElement('DIV');
        dumb.style.display = 'hidden';
        dumb.style.top = position.top;
        dumb.style.right = position.right;
        dumb.style.bottom = position.bottom;
        dumb.style.left = position.left;
        dumb.style.position = 'absolute';

        opts.zone.appendChild(dumb);
        var dumbBox = dumb.getBoundingClientRect();
        opts.zone.removeChild(dumb);

        toPutOn = position;
        position = {
            x: dumbBox.left + scroll.x,
            y: dumbBox.top + scroll.y
        };
    }

    var nipple = new Nipple(self, {
        color: opts.color,
        size: opts.size,
        threshold: opts.threshold,
        fadeTime: opts.fadeTime,
        dataOnly: opts.dataOnly,
        restOpacity: opts.restOpacity,
        mode: opts.mode,
        identifier: identifier,
        position: position,
        zone: opts.zone,
        frontPosition: {
            x: 0,
            y: 0
        }
    });

    if (!opts.dataOnly) {
        u.applyPosition(nipple.ui.el, toPutOn);
        u.applyPosition(nipple.ui.front, nipple.frontPosition);
    }
    self.nipples.push(nipple);
    self.trigger('added ' + nipple.identifier + ':added', nipple);
    self.manager.trigger('added ' + nipple.identifier + ':added', nipple);

    self.bindNipple(nipple);

    return nipple;
};

Collection.prototype.updateBox = function () {
    var self = this;
    self.box = self.options.zone.getBoundingClientRect();
};

Collection.prototype.bindNipple = function (nipple) {
    var self = this;
    var type;
    // Bubble up identified events.
    var handler = function (evt, data) {
        // Identify the event type with the nipple's identifier.
        type = evt.type + ' ' + evt.target.identifier + ':' + evt.type;
        self.trigger(type, data);
    };

    // When it gets destroyed.
    nipple.on('destroyed', self.onDestroyed.bind(self));

    // Other events that will get bubbled up.
    nipple.on('shown hidden rested dir plain', handler);
    nipple.on('dir:up dir:right dir:down dir:left', handler);
    nipple.on('plain:up plain:right plain:down plain:left', handler);
};

Collection.prototype.pressureFn = function (touch, nipple, identifier) {
    var self = this;
    var previousPressure = 0;
    clearInterval(self.pressureIntervals[identifier]);
    // Create an interval that will read the pressure every 100ms
    self.pressureIntervals[identifier] = setInterval(function () {
        var pressure = touch.force || touch.pressure ||
            touch.webkitForce || 0;
        if (pressure !== previousPressure) {
            nipple.trigger('pressure', pressure);
            self.trigger('pressure ' +
                nipple.identifier + ':pressure', pressure);
            previousPressure = pressure;
        }
    }.bind(self), 100);
};

Collection.prototype.onstart = function (evt) {
    var self = this;
    var opts = self.options;
    evt = u.prepareEvent(evt);

    // Update the box position
    self.updateBox();

    var process = function (touch) {
        // If we can create new nipples
        // meaning we don't have more active nipples than we should.
        if (self.actives.length < opts.maxNumberOfNipples) {
            self.processOnStart(touch);
        }
    };

    u.map(evt, process);

    // We ask upstream to bind the document
    // on 'move' and 'end'
    self.manager.bindDocument();
    return false;
};

Collection.prototype.processOnStart = function (evt) {
    var self = this;
    var opts = self.options;
    var indexInIdles;
    var identifier = self.manager.getIdentifier(evt);
    var pressure = evt.force || evt.pressure || evt.webkitForce || 0;
    var position = {
        x: evt.pageX,
        y: evt.pageY
    };
    var nipple = self.getOrCreate(identifier, position);
    var process = function (nip) {
        nip.show();
        if (pressure > 0) {
            self.pressureFn(evt, nip, nip.identifier);
        }
        // Trigger the first move event.
        self.processOnMove(evt);
    };

    // Transfer it from idles to actives.
    if ((indexInIdles = self.idles.indexOf(nipple)) >= 0) {
        self.idles.splice(indexInIdles, 1);
    }

    // Store the nipple in the actives array
    self.actives.push(nipple);
    self.ids.push(identifier);

    if (opts.mode !== 'semi') {
        process(nipple);
    } else {
        // In semi we check the distance of the touch
        // to decide if we have to reset the nipple
        var distance = u.distance(position, nipple.position);
        if (distance <= opts.catchDistance) {
            process(nipple);
        } else {
            nipple.destroy();
            self.processOnStart(evt);
            return;
        }
    }

    nipple.trigger('start', nipple);
    self.trigger('start ' + nipple.identifier + ':start', nipple);
    return nipple;
};

Collection.prototype.getOrCreate = function (identifier, position) {
    var self = this;
    var opts = self.options;
    // If we're in static or semi, we might already have an active.
    if (/(semi|static)/.test(opts.mode)) {
        // Get the active one
        // and update its identifier.
        nipple = self.idles[0];
        if (nipple) {
            nipple.identifier = identifier;
            self.idles.splice(0, 1);
            return nipple;
        }

        if (opts.mode === 'semi') {
            // If we're in semi mode, we need to create one.
            return self.createNipple(position, identifier);
        }

        console.warn('Coudln\'t find the needed nipple.');
        return false;
    }
    // In dynamic, we create a new one.
    nipple = self.createNipple(position, identifier);
    return nipple;
};

Collection.prototype.processOnMove = function (evt) {
    var self = this;
    var opts = self.options;
    var identifier = self.manager.getIdentifier(evt);
    var nipple = self.nipples.get(identifier);

    if (!nipple) {
        // This is here just for safety.
        // It shouldn't happen.
        self.manager.removeIdentifier(identifier);
        return;
    }

    var size = nipple.options.size / 2;
    var pos = {
        x: evt.pageX,
        y: evt.pageY
    };

    var dist = u.distance(pos, nipple.position);
    var angle = u.angle(pos, nipple.position);
    var rAngle = u.radians(angle);
    var force = dist / size;

    // If distance is bigger than nipple's size
    // we clamp the position.
    if (dist > size) {
        dist = size;
        pos = u.findCoord(nipple.position, dist, angle);
    }

    nipple.frontPosition = {
        x: pos.x - nipple.position.x,
        y: pos.y - nipple.position.y
    };

    if (!opts.dataOnly) {
        u.applyPosition(nipple.ui.front, nipple.frontPosition);
    }

    // Prepare event's datas.
    var toSend = {
        identifier: nipple.identifier,
        position: pos,
        force: force,
        pressure: evt.force || evt.pressure || evt.webkitForce || 0,
        distance: dist,
        angle: {
            radian: rAngle,
            degree: angle
        },
        instance: nipple
    };

    // Compute the direction's datas.
    toSend = nipple.computeDirection(toSend);

    // Offset angles to follow units circle.
    toSend.angle = {
        radian: u.radians(180 - angle),
        degree: 180 - angle
    };

    // Send everything to everyone.
    nipple.trigger('move', toSend);
    self.trigger('move ' + identifier + ':move', toSend);
};

Collection.prototype.processOnEnd = function (evt) {
    var self = this;
    var opts = self.options;
    var identifier = self.manager.getIdentifier(evt);
    var nipple = self.nipples.get(identifier);
    self.manager.removeIdentifier(identifier);

    if (!nipple) {
        return;
    }

    if (!opts.dataOnly) {
        nipple.hide(function () {
                if (opts.mode === 'dynamic') {
                    nipple.trigger('removed', nipple);
                    self.trigger('removed ' + identifier + ':removed', nipple);
                    self.manager.trigger('removed ' + identifier + ':removed',
                        nipple);
                    nipple.destroy();
                }
            });
    }

    // Clear the pressure interval reader
    clearInterval(self.pressureIntervals[identifier]);

    nipple.trigger('end', nipple);
    self.trigger('end ' + identifier + ':end', nipple);

    // Remove identifier from our bank.
    if (self.ids.indexOf(identifier) >= 0) {
        self.ids.splice(self.ids.indexOf(identifier), 1);
    }

    // Clean our actives array.
    if (self.actives.indexOf(nipple) >= 0) {
        self.actives.splice(self.actives.indexOf(nipple), 1);
    }

    if (/(semi|static)/.test(opts.mode)) {
        // Transfer nipple from actives to idles
        // if we're in semi or static mode.
        self.idles.push(nipple);
    } else if (self.nipples.indexOf(nipple) >= 0) {
        // Only if we're not in semi or static mode
        // we can remove the instance.
        self.nipples.splice(self.nipples.indexOf(nipple), 1);
    }

    // We unbind move and end.
    self.manager.unbindDocument();
};

// Remove destroyed nipple from the lists
Collection.prototype.onDestroyed = function(evt, nipple) {
    var self = this;
    if (self.nipples.indexOf(nipple) >= 0) {
        self.nipples.splice(self.nipples.indexOf(nipple), 1);
    }
    if (self.actives.indexOf(nipple) >= 0) {
        self.actives.splice(self.actives.indexOf(nipple), 1);
    }
    if (self.idles.indexOf(nipple) >= 0) {
        self.idles.splice(self.idles.indexOf(nipple), 1);
    }
};

// Cleanly destroy the manager
Collection.prototype.destroy = function () {
    var self = this;
    self.unbindEvt(self.options.zone, 'start');

    // Destroy nipples.
    self.nipples.forEach(function(nipple) {
        nipple.destroy();
    });

    // Clean 3DTouch intervals.
    for (var i in self.pressureIntervals) {
        if (self.pressureIntervals.hasOwnProperty(i)) {
            clearInterval(self.pressureIntervals[i]);
        }
    }

    // Notify the manager passing the instance
    self.trigger('destroyed', self.nipples);
    // Unbind everything.
    self.off();
};
<commit_msg>fix: unbind document when collection is manually destroyed<commit_after>///////////////////////////
///   THE COLLECTION    ///
///////////////////////////

Collection.prototype = new Super();
Collection.constructor = Collection;
Collection.id = 0;

function Collection (manager, options) {
    var self = this;
    self.nipples = [];
    self.idles = [];
    self.actives = [];
    self.ids = [];
    self.pressureIntervals = {};
    self.manager = manager;
    self.id = Collection.id;
    Collection.id += 1;

    // Defaults
    self.defaults = {
        zone: document.body,
        multitouch: false,
        maxNumberOfNipples: 10,
        mode: 'dynamic',
        position: {top: 0, left: 0},
        catchDistance: 200,
        size: 100,
        threshold: 0.1,
        color: 'white',
        fadeTime: 250,
        dataOnly: false,
        restOpacity: 0.5
    };

    self.config(options);

    // Overwrites
    if (self.options.mode === 'static' || self.options.mode === 'semi') {
        self.options.multitouch = false;
    }

    if (!self.options.multitouch) {
        self.options.maxNumberOfNipples = 1;
    }

    self.updateBox();
    self.prepareNipples();
    self.bindings();
    self.begin();

    return self.nipples;
}

Collection.prototype.prepareNipples = function () {
    var self = this;
    var nips = self.nipples;

    // Public API Preparation.
    nips.on = self.on.bind(self);
    nips.off = self.off.bind(self);
    nips.options = self.options;
    nips.destroy = self.destroy.bind(self);
    nips.ids = self.ids;
    nips.id = self.id;
    nips.processOnMove = self.processOnMove.bind(self);
    nips.processOnEnd = self.processOnEnd.bind(self);
    nips.get = function (id) {
        if (id === undefined) {
            return nips[0];
        }
        for (var i = 0, max = nips.length; i < max; i += 1) {
            if (nips[i].identifier === id) {
                return nips[i];
            }
        }
        return false;
    };
};

Collection.prototype.bindings = function () {
    var self = this;
    // Touch start event.
    self.bindEvt(self.options.zone, 'start');
};

Collection.prototype.begin = function () {
    var self = this;
    var opts = self.options;

    // We place our static nipple
    // if needed.
    if (opts.mode === 'static') {
        var nipple = self.createNipple(
            opts.position
        );
        // Add it to the dom.
        nipple.add();
        // Store it in idles.
        self.idles.push(nipple);
    }
};

// Nipple Factory
Collection.prototype.createNipple = function (position, identifier) {
    var self = this;
    var scroll = u.getScroll();
    var toPutOn = {};
    var opts = self.options;

    if (position.x && position.y) {
        toPutOn = {
            x: position.x -
                (scroll.x + self.box.left),
            y: position.y -
                (scroll.y + self.box.top)
        };
    } else if (
            position.top ||
            position.right ||
            position.bottom ||
            position.left
        ) {

        // We need to compute the position X / Y of the joystick.
        var dumb = document.createElement('DIV');
        dumb.style.display = 'hidden';
        dumb.style.top = position.top;
        dumb.style.right = position.right;
        dumb.style.bottom = position.bottom;
        dumb.style.left = position.left;
        dumb.style.position = 'absolute';

        opts.zone.appendChild(dumb);
        var dumbBox = dumb.getBoundingClientRect();
        opts.zone.removeChild(dumb);

        toPutOn = position;
        position = {
            x: dumbBox.left + scroll.x,
            y: dumbBox.top + scroll.y
        };
    }

    var nipple = new Nipple(self, {
        color: opts.color,
        size: opts.size,
        threshold: opts.threshold,
        fadeTime: opts.fadeTime,
        dataOnly: opts.dataOnly,
        restOpacity: opts.restOpacity,
        mode: opts.mode,
        identifier: identifier,
        position: position,
        zone: opts.zone,
        frontPosition: {
            x: 0,
            y: 0
        }
    });

    if (!opts.dataOnly) {
        u.applyPosition(nipple.ui.el, toPutOn);
        u.applyPosition(nipple.ui.front, nipple.frontPosition);
    }
    self.nipples.push(nipple);
    self.trigger('added ' + nipple.identifier + ':added', nipple);
    self.manager.trigger('added ' + nipple.identifier + ':added', nipple);

    self.bindNipple(nipple);

    return nipple;
};

Collection.prototype.updateBox = function () {
    var self = this;
    self.box = self.options.zone.getBoundingClientRect();
};

Collection.prototype.bindNipple = function (nipple) {
    var self = this;
    var type;
    // Bubble up identified events.
    var handler = function (evt, data) {
        // Identify the event type with the nipple's identifier.
        type = evt.type + ' ' + evt.target.identifier + ':' + evt.type;
        self.trigger(type, data);
    };

    // When it gets destroyed.
    nipple.on('destroyed', self.onDestroyed.bind(self));

    // Other events that will get bubbled up.
    nipple.on('shown hidden rested dir plain', handler);
    nipple.on('dir:up dir:right dir:down dir:left', handler);
    nipple.on('plain:up plain:right plain:down plain:left', handler);
};

Collection.prototype.pressureFn = function (touch, nipple, identifier) {
    var self = this;
    var previousPressure = 0;
    clearInterval(self.pressureIntervals[identifier]);
    // Create an interval that will read the pressure every 100ms
    self.pressureIntervals[identifier] = setInterval(function () {
        var pressure = touch.force || touch.pressure ||
            touch.webkitForce || 0;
        if (pressure !== previousPressure) {
            nipple.trigger('pressure', pressure);
            self.trigger('pressure ' +
                nipple.identifier + ':pressure', pressure);
            previousPressure = pressure;
        }
    }.bind(self), 100);
};

Collection.prototype.onstart = function (evt) {
    var self = this;
    var opts = self.options;
    evt = u.prepareEvent(evt);

    // Update the box position
    self.updateBox();

    var process = function (touch) {
        // If we can create new nipples
        // meaning we don't have more active nipples than we should.
        if (self.actives.length < opts.maxNumberOfNipples) {
            self.processOnStart(touch);
        }
    };

    u.map(evt, process);

    // We ask upstream to bind the document
    // on 'move' and 'end'
    self.manager.bindDocument();
    return false;
};

Collection.prototype.processOnStart = function (evt) {
    var self = this;
    var opts = self.options;
    var indexInIdles;
    var identifier = self.manager.getIdentifier(evt);
    var pressure = evt.force || evt.pressure || evt.webkitForce || 0;
    var position = {
        x: evt.pageX,
        y: evt.pageY
    };
    var nipple = self.getOrCreate(identifier, position);
    var process = function (nip) {
        nip.show();
        if (pressure > 0) {
            self.pressureFn(evt, nip, nip.identifier);
        }
        // Trigger the first move event.
        self.processOnMove(evt);
    };

    // Transfer it from idles to actives.
    if ((indexInIdles = self.idles.indexOf(nipple)) >= 0) {
        self.idles.splice(indexInIdles, 1);
    }

    // Store the nipple in the actives array
    self.actives.push(nipple);
    self.ids.push(identifier);

    if (opts.mode !== 'semi') {
        process(nipple);
    } else {
        // In semi we check the distance of the touch
        // to decide if we have to reset the nipple
        var distance = u.distance(position, nipple.position);
        if (distance <= opts.catchDistance) {
            process(nipple);
        } else {
            nipple.destroy();
            self.processOnStart(evt);
            return;
        }
    }

    nipple.trigger('start', nipple);
    self.trigger('start ' + nipple.identifier + ':start', nipple);
    return nipple;
};

Collection.prototype.getOrCreate = function (identifier, position) {
    var self = this;
    var opts = self.options;
    // If we're in static or semi, we might already have an active.
    if (/(semi|static)/.test(opts.mode)) {
        // Get the active one
        // and update its identifier.
        nipple = self.idles[0];
        if (nipple) {
            nipple.identifier = identifier;
            self.idles.splice(0, 1);
            return nipple;
        }

        if (opts.mode === 'semi') {
            // If we're in semi mode, we need to create one.
            return self.createNipple(position, identifier);
        }

        console.warn('Coudln\'t find the needed nipple.');
        return false;
    }
    // In dynamic, we create a new one.
    nipple = self.createNipple(position, identifier);
    return nipple;
};

Collection.prototype.processOnMove = function (evt) {
    var self = this;
    var opts = self.options;
    var identifier = self.manager.getIdentifier(evt);
    var nipple = self.nipples.get(identifier);

    if (!nipple) {
        // This is here just for safety.
        // It shouldn't happen.
        self.manager.removeIdentifier(identifier);
        return;
    }

    var size = nipple.options.size / 2;
    var pos = {
        x: evt.pageX,
        y: evt.pageY
    };

    var dist = u.distance(pos, nipple.position);
    var angle = u.angle(pos, nipple.position);
    var rAngle = u.radians(angle);
    var force = dist / size;

    // If distance is bigger than nipple's size
    // we clamp the position.
    if (dist > size) {
        dist = size;
        pos = u.findCoord(nipple.position, dist, angle);
    }

    nipple.frontPosition = {
        x: pos.x - nipple.position.x,
        y: pos.y - nipple.position.y
    };

    if (!opts.dataOnly) {
        u.applyPosition(nipple.ui.front, nipple.frontPosition);
    }

    // Prepare event's datas.
    var toSend = {
        identifier: nipple.identifier,
        position: pos,
        force: force,
        pressure: evt.force || evt.pressure || evt.webkitForce || 0,
        distance: dist,
        angle: {
            radian: rAngle,
            degree: angle
        },
        instance: nipple
    };

    // Compute the direction's datas.
    toSend = nipple.computeDirection(toSend);

    // Offset angles to follow units circle.
    toSend.angle = {
        radian: u.radians(180 - angle),
        degree: 180 - angle
    };

    // Send everything to everyone.
    nipple.trigger('move', toSend);
    self.trigger('move ' + identifier + ':move', toSend);
};

Collection.prototype.processOnEnd = function (evt) {
    var self = this;
    var opts = self.options;
    var identifier = self.manager.getIdentifier(evt);
    var nipple = self.nipples.get(identifier);
    self.manager.removeIdentifier(identifier);

    if (!nipple) {
        return;
    }

    if (!opts.dataOnly) {
        nipple.hide(function () {
                if (opts.mode === 'dynamic') {
                    nipple.trigger('removed', nipple);
                    self.trigger('removed ' + identifier + ':removed', nipple);
                    self.manager.trigger('removed ' + identifier + ':removed',
                        nipple);
                    nipple.destroy();
                }
            });
    }

    // Clear the pressure interval reader
    clearInterval(self.pressureIntervals[identifier]);

    nipple.trigger('end', nipple);
    self.trigger('end ' + identifier + ':end', nipple);

    // Remove identifier from our bank.
    if (self.ids.indexOf(identifier) >= 0) {
        self.ids.splice(self.ids.indexOf(identifier), 1);
    }

    // Clean our actives array.
    if (self.actives.indexOf(nipple) >= 0) {
        self.actives.splice(self.actives.indexOf(nipple), 1);
    }

    if (/(semi|static)/.test(opts.mode)) {
        // Transfer nipple from actives to idles
        // if we're in semi or static mode.
        self.idles.push(nipple);
    } else if (self.nipples.indexOf(nipple) >= 0) {
        // Only if we're not in semi or static mode
        // we can remove the instance.
        self.nipples.splice(self.nipples.indexOf(nipple), 1);
    }

    // We unbind move and end.
    self.manager.unbindDocument();
};

// Remove destroyed nipple from the lists
Collection.prototype.onDestroyed = function(evt, nipple) {
    var self = this;
    if (self.nipples.indexOf(nipple) >= 0) {
        self.nipples.splice(self.nipples.indexOf(nipple), 1);
    }
    if (self.actives.indexOf(nipple) >= 0) {
        self.actives.splice(self.actives.indexOf(nipple), 1);
    }
    if (self.idles.indexOf(nipple) >= 0) {
        self.idles.splice(self.idles.indexOf(nipple), 1);
    }
    // We unbind move and end.
    self.manager.unbindDocument();
};

// Cleanly destroy the manager
Collection.prototype.destroy = function () {
    var self = this;
    self.unbindEvt(self.options.zone, 'start');

    // Destroy nipples.
    self.nipples.forEach(function(nipple) {
        nipple.destroy();
    });

    // Clean 3DTouch intervals.
    for (var i in self.pressureIntervals) {
        if (self.pressureIntervals.hasOwnProperty(i)) {
            clearInterval(self.pressureIntervals[i]);
        }
    }

    // Notify the manager passing the instance
    self.trigger('destroyed', self.nipples);
    // We unbind move and end.
    self.manager.unbindDocument();
    // Unbind everything.
    self.off();
};
<|endoftext|>


<commit_before>import config from './config';

module.exports = {
  /**
   * 包装系统的console，指定force=true可以强制在非Debug模式下打印信息
   *
   * @param {any} obj 要打印的数据
   * @param {string} [mark='未定义标签'] 提示标签
   * @param {boolean} [force=false] 强制输出此信息
   */
  print: ( obj, mark = '未定义标签', force = false ) => {
    if ( config.debug || force ) {
      console.log( '>>> ' + mark + ': ', obj );
    }
  },
  /**
   * 将model和action组合成路径形式
   *
   * @param {any} model
   * @param {any} action
   * @returns model/action
   */
  fuse: ( model, action ) => {
    return model + '/' + action;
  }
}<commit_msg>update fun<commit_after>import config from './config';

module.exports = {
  /**
   * 包装系统的console，指定force=true可以强制在非Debug模式下打印信息
   *
   * @param {any} obj 要打印的数据
   * @param {string} [mark='未定义标签'] 提示标签
   * @param {boolean} [force=false] 强制输出此信息
   */
  print: ( obj, mark = '未定义标签', moudle = 'none', force = false ) => {
    if ( config.debug || force ) {
      console.log( moudle + ' >>> ' + mark + ': ', obj );
    }
  },
  /**
   * 将model和action组合成路径形式
   *
   * @param {any} model
   * @param {any} action
   * @returns model/action
   */
  fuse: ( model, action ) => {
    return model + '/' + action;
  }
}<|endoftext|>


<commit_before>angular.module('drg.ngIncludeScope', [])
.directive( 'ngIncludeScope', function() {
    'use strict';

    return {
        restrict: 'A',
        link : function( scope, elem, attrs ) {
            var keys = [];
            scope.$watch( attrs.ngIncludeScope, function( newScope ) {
                keys = newScope ? Object.keys( newScope ) : [];

                // set getters
                angular.forEach( keys, function(key) {
                    if( newScope.hasOwnProperty( key ) && angular.isUndefined( scope[ key ] ) ) {
                        Object.defineProperty( scope, key, {
                            get: function() {
                                return !!~keys.indexOf( key ) ? scope.$eval( attrs.ngIncludeScope + "['" + key.replace("'", "\\'") + "']" ) : undefined;
                            }
                        } );
                    }
                } );
            } );
        }
    };
});
<commit_msg>use watchCollection instead of watch<commit_after>angular.module('drg.ngIncludeScope', [])
.directive( 'ngIncludeScope', function() {
    'use strict';

    return {
        restrict: 'A',
        link : function( scope, elem, attrs ) {
            var keys = [];
            scope.$watchCollection( attrs.ngIncludeScope, function( newScope ) {
                keys = newScope ? Object.keys( newScope ) : [];
                // set getters
                angular.forEach( keys, function(key) {
                    if( angular.isUndefined( scope[ key ] ) ) {
                        Object.defineProperty( scope, key, {
                            get: function() {
                                return !!~keys.indexOf( key ) ? scope.$eval( attrs.ngIncludeScope + "['" + key.replace("'", "\\'") + "']" ) : undefined;
                            }
                        } );
                    }
                } );
            } );
        }
    };
});
<|endoftext|>