<commit_before>/**
 * @class INDraft
 * @constructor
 * @augments INMessage
 *
 * @description
 * Represents a draft message, which may or may not be synced to the server. Drafts are typically
 * created with {INMessage#reply}, {INThread#reply}, or {INNamespace#draft}.
 *
 * The draft-message may be sent without being synced to the server.
 */
function INDraft(inbox, id, namespaceId) {
  INMessage.call(this, inbox, id, namespaceId);
}

inherits(INDraft, INMessage);

/**
 * @function
 * @name INDraft#resourceName
 *
 * @description
 * Returns the name of the resource used when constructing URLs
 *
 * @returns {string} the resource path of the file.
 */
INDraft.prototype.resourceName = function() {
  return 'drafts';
};

/**
 * @function
 * @name INDraft#addRecipients
 *
 * @description
 * Adds a set of participants to {INDraft#to} --- the set of direct message recipients. These are
 * not CC'd or BCC'd.
 *
 * @param {Array<Object>} participants an array of Participant objects, containing keys 'name' and
 *   'email'. The name should be a name which identifies the recipient, and the email should be
 *   their email address.
 *
 * @returns {INDraft} the INDraft object, 'this', to enable chaining calls.
 */
INDraft.prototype.addRecipients = function(participants) {
  var to = this.to || (this.to = []);
  var i;
  var ii = arguments.length;
  var item;
  for (i = 0; i < ii; ++i) {
    item = arguments[i];
    if (isArray(item)) {
      mergeArray(to, item, 'email');
    }
  }
  return this;
};

/**
 * @function
 * @name INDraft#uploadAttachment
 *
 * @description
 * Uploads a file to the server, and adds the attachment ID from the response to the end of the
 * set of attachment IDs.
 *
 * Uploading files requires that the browser support the FormData object, or a working polyfill.
 * It also requires support for the Blob type. See http://caniuse.com/xhr2 for browser support.
 *
 * Support for legacy browsers may be possible using Flash, or clever hacks with iframes.
 *
 * @param {string|File} fileNameOrFile Either a File object. A File object is essentially a
 *   Blob with metadata (filename, mimetype). If the fileNameOrFile parameter is a File, then
 *   the second parameter may be ignored. Otherwise, if it is a string, it is treated as the
 *   filename for the associated blob object.
 * @param {Blob=} blobForFileName A Blob object containing the data to be uploaded to the
 *   server.
 *
 * @returns {Promise} a promise to be fulfilled with the response from the server, or an
 *   exception which may be thrown.
 */
INDraft.prototype.uploadAttachment = function(fileNameOrFile, blobForFileName) {
  var namespace = this.namespace();
  var self = this;
  return this.promise(function(resolve, reject) {
    uploadFile(self, fileNameOrFile, blobForFileName, function(err, response) {
      if (err) {
        if (typeof err == 'string') {
          err = new Error('Cannot invoke `uploadAttachment()` on INDraft: ' + err);
        }
        return reject(err);
      }
      self.attachmentData.push(response);
      return resolve(response);
    });
  });
};

/**
 * @function
 * @name INDraft#removeAttachment
 *
 * @description
 * Removes an attachment ID from a draft message, and prevents the attachment from being
 * associated with the message.
 *
 * @param {string|INFile} file Either a file ID as a string, or an INFile object.
 *
 * @returns {INDraft} the INDraft object, 'this', to enable chaining calls.
 */
INDraft.prototype.removeAttachment = function(file) {
  if (!file) {
    throw new TypeError(
      'Cannot invoke `removeAttachment()` on INDraft: file must be a file ID or object');
  }
  var id = typeof file === 'string' ? file : file.id;
  var i;
  var ii = this.attachmentData.length;

  for (i = 0; i < ii; ++i) {
    if (this.attachmentData[i].id === id) {
      this.attachmentData.splice(i, 1);
      break;
    }
  }
  return this;
};

/**
 * Shadow INMessage#markAsRead method with 'null', because it is meaningless for draft messages.
 */
INDraft.prototype.markAsRead = null;

/**
 * @function
 * @name INDraft#save
 *
 * @description
 * Save the draft to the server
 *
 * @returns {Promise} promise to be fulfilled with either an API response from the server, or
 *   an exception thrown by apiRequest().
 */
INDraft.prototype.save = function() {
  var pattern = this.isUnsynced() ? '%@/drafts' : '%@/drafts/%@';
  var url = formatUrl(pattern, this.namespaceUrl(), this.id);
  var inbox = this.inbox();
  var self = this;
  var rawJson = this.raw();

  // check the formatting of our participants fields. They must be either undefined or be
  // arrays, and must contain objects that have an email key
  var keys = ['from', 'to', 'cc', 'bcc'];
  for (var i = 0, ii = keys.length; i < ii; ++i) {
    var list = self[keys[i]];
    var valid = false;

    if (isArray(list)) {
      valid = true;
      for (var j = 0, jj = list.length; j < jj; ++j) {
        if ((typeof list[j] !== 'object') || (!list[j].hasOwnProperty('email'))) {
          valid = false;
          break;
        }
      }
    } else if (list === undefined) {
      valid = true;
    }

    if (!valid) {
      throw new TypeError(
      'INDraft.save(): To, From, CC, BCC must be arrays of objects with emails and optional names.');
    }
  }

  rawJson.files = map(this.attachmentData, function(data) {
    return data.id;
  });

  rawJson = toJSON(rawJson);

  return this.promise(function(resolve, reject) {
    apiRequest(inbox, 'post', url, rawJson, function(err, response) {
      if (err) return reject(err);
      // Should delete the cached version, if any
      self.update(response);
      deleteModel(self);
      persistModel(self);
      resolve(self);
    });
  });
};

/**
 * @function
 * @name INDraft#send
 *
 * @description
 * Send a draft message to recipients. The draft does not need to be saved to the server before
 * performing this task.
 *
 * @returns {Promise} promise to be fulfilled with the API response from the server, or an
 *   exception which may have been thrown.
 */
INDraft.prototype.send = function() {
  var data;
  var inbox = this.inbox();
  var url = formatUrl('%@/send', this.namespaceUrl());

  if (this.isUnsynced()) {
    // Just send a message
    data = this.raw();
    delete data.id;
    delete data.object;
    data.files = map(this.attachmentData, function(data) {
      return data.id;
    });
    data = toJSON(data);
  } else {
    // Send using the saved ID
    data = toJSON({
      'draft_id': this.id
    });
  }

  return this.promise(function(resolve, reject) {
    apiRequest(inbox, 'post', url, data, function(err, response) {
      // TODO: update a 'state' flag indicating that the value has been saved
      if (err) return reject(err);
      resolve(response);
    });
  });
};

/**
 * @function
 * @name INDraft#dispose
 *
 * @description
 * Delete the draft from both local cache and the server.
 *
 * @returns {Promise} promise fulfilled with either an error from the API, or with the draft itself.
 */
INDraft.prototype.dispose = function() {
  var self = this;
  return this.promise(function(resolve, reject) {
    deleteModel(self);
    if (self.isUnsynced()) {
      // Cached copy is already deleted --- just resolve.
      resolve(self);
    } else {
      apiRequest(self.inbox(), 'delete', this.resourceUrl(),
      function(err, response) {
        if (err) return reject(err);
        resolve(self);
      });
    }
  });
};

/**
 * @property
 * @name INDraft#state
 *
 * If present, this is the state of the draft (`draft`, `sending`, or `sent`).
 */

/**
 * @property
 * @name INDraft#object
 *
 * The resource type, always 'draft'.
 */
defineResourceMapping(INDraft, {
  'state': 'state',
  'object': 'const:draft'
}, INMessage);
<commit_msg>fix(INDraft) Compatibility with new API, which exposes `version`<commit_after>/**
 * @class INDraft
 * @constructor
 * @augments INMessage
 *
 * @description
 * Represents a draft message, which may or may not be synced to the server. Drafts are typically
 * created with {INMessage#reply}, {INThread#reply}, or {INNamespace#draft}.
 *
 * The draft-message may be sent without being synced to the server.
 */
function INDraft(inbox, id, namespaceId) {
  INMessage.call(this, inbox, id, namespaceId);
}

inherits(INDraft, INMessage);

/**
 * @function
 * @name INDraft#resourceName
 *
 * @description
 * Returns the name of the resource used when constructing URLs
 *
 * @returns {string} the resource path of the file.
 */
INDraft.prototype.resourceName = function() {
  return 'drafts';
};

/**
 * @function
 * @name INDraft#addRecipients
 *
 * @description
 * Adds a set of participants to {INDraft#to} --- the set of direct message recipients. These are
 * not CC'd or BCC'd.
 *
 * @param {Array<Object>} participants an array of Participant objects, containing keys 'name' and
 *   'email'. The name should be a name which identifies the recipient, and the email should be
 *   their email address.
 *
 * @returns {INDraft} the INDraft object, 'this', to enable chaining calls.
 */
INDraft.prototype.addRecipients = function(participants) {
  var to = this.to || (this.to = []);
  var i;
  var ii = arguments.length;
  var item;
  for (i = 0; i < ii; ++i) {
    item = arguments[i];
    if (isArray(item)) {
      mergeArray(to, item, 'email');
    }
  }
  return this;
};

/**
 * @function
 * @name INDraft#uploadAttachment
 *
 * @description
 * Uploads a file to the server, and adds the attachment ID from the response to the end of the
 * set of attachment IDs.
 *
 * Uploading files requires that the browser support the FormData object, or a working polyfill.
 * It also requires support for the Blob type. See http://caniuse.com/xhr2 for browser support.
 *
 * Support for legacy browsers may be possible using Flash, or clever hacks with iframes.
 *
 * @param {string|File} fileNameOrFile Either a File object. A File object is essentially a
 *   Blob with metadata (filename, mimetype). If the fileNameOrFile parameter is a File, then
 *   the second parameter may be ignored. Otherwise, if it is a string, it is treated as the
 *   filename for the associated blob object.
 * @param {Blob=} blobForFileName A Blob object containing the data to be uploaded to the
 *   server.
 *
 * @returns {Promise} a promise to be fulfilled with the response from the server, or an
 *   exception which may be thrown.
 */
INDraft.prototype.uploadAttachment = function(fileNameOrFile, blobForFileName) {
  var namespace = this.namespace();
  var self = this;
  return this.promise(function(resolve, reject) {
    uploadFile(self, fileNameOrFile, blobForFileName, function(err, response) {
      if (err) {
        if (typeof err == 'string') {
          err = new Error('Cannot invoke `uploadAttachment()` on INDraft: ' + err);
        }
        return reject(err);
      }
      self.attachmentData.push(response);
      return resolve(response);
    });
  });
};

/**
 * @function
 * @name INDraft#removeAttachment
 *
 * @description
 * Removes an attachment ID from a draft message, and prevents the attachment from being
 * associated with the message.
 *
 * @param {string|INFile} file Either a file ID as a string, or an INFile object.
 *
 * @returns {INDraft} the INDraft object, 'this', to enable chaining calls.
 */
INDraft.prototype.removeAttachment = function(file) {
  if (!file) {
    throw new TypeError(
      'Cannot invoke `removeAttachment()` on INDraft: file must be a file ID or object');
  }
  var id = typeof file === 'string' ? file : file.id;
  var i;
  var ii = this.attachmentData.length;

  for (i = 0; i < ii; ++i) {
    if (this.attachmentData[i].id === id) {
      this.attachmentData.splice(i, 1);
      break;
    }
  }
  return this;
};

/**
 * Shadow INMessage#markAsRead method with 'null', because it is meaningless for draft messages.
 */
INDraft.prototype.markAsRead = null;

/**
 * @function
 * @name INDraft#save
 *
 * @description
 * Save the draft to the server
 *
 * @returns {Promise} promise to be fulfilled with either an API response from the server, or
 *   an exception thrown by apiRequest().
 */
INDraft.prototype.save = function() {
  var pattern = this.isUnsynced() ? '%@/drafts' : '%@/drafts/%@';
  var url = formatUrl(pattern, this.namespaceUrl(), this.id);
  var inbox = this.inbox();
  var self = this;
  var rawJson = this.raw();

  // check the formatting of our participants fields. They must be either undefined or be
  // arrays, and must contain objects that have an email key
  var keys = ['from', 'to', 'cc', 'bcc'];
  for (var i = 0, ii = keys.length; i < ii; ++i) {
    var list = self[keys[i]];
    var valid = false;

    if (isArray(list)) {
      valid = true;
      for (var j = 0, jj = list.length; j < jj; ++j) {
        if ((typeof list[j] !== 'object') || (!list[j].hasOwnProperty('email'))) {
          valid = false;
          break;
        }
      }
    } else if (list === undefined) {
      valid = true;
    }

    if (!valid) {
      throw new TypeError(
      'INDraft.save(): To, From, CC, BCC must be arrays of objects with emails and optional names.');
    }
  }

  rawJson.files = map(this.attachmentData, function(data) {
    return data.id;
  });

  rawJson = toJSON(rawJson);

  return this.promise(function(resolve, reject) {
    apiRequest(inbox, 'post', url, rawJson, function(err, response) {
      if (err) return reject(err);
      // Should delete the cached version, if any
      self.update(response);
      deleteModel(self);
      persistModel(self);
      resolve(self);
    });
  });
};

/**
 * @function
 * @name INDraft#send
 *
 * @description
 * Send a draft message to recipients. The draft does not need to be saved to the server before
 * performing this task.
 *
 * @returns {Promise} promise to be fulfilled with the API response from the server, or an
 *   exception which may have been thrown.
 */
INDraft.prototype.send = function() {
  var data;
  var inbox = this.inbox();
  var url = formatUrl('%@/send', this.namespaceUrl());

  if (this.isUnsynced()) {
    // Just send a message
    data = this.raw();
    delete data.id;
    delete data.object;
    data.files = map(this.attachmentData, function(data) {
      return data.id;
    });
    data = toJSON(data);
  } else {
    // Send using the saved ID
    data = toJSON({
      'draft_id': this.id,
      'version': this.version
    });
  }

  return this.promise(function(resolve, reject) {
    apiRequest(inbox, 'post', url, data, function(err, response) {
      // TODO: update a 'state' flag indicating that the value has been saved
      if (err) return reject(err);
      resolve(response);
    });
  });
};

/**
 * @function
 * @name INDraft#dispose
 *
 * @description
 * Delete the draft from both local cache and the server.
 *
 * @returns {Promise} promise fulfilled with either an error from the API, or with the draft itself.
 */
INDraft.prototype.dispose = function() {
  var self = this;
  return this.promise(function(resolve, reject) {
    deleteModel(self);
    if (self.isUnsynced()) {
      // Cached copy is already deleted --- just resolve.
      resolve(self);
    } else {
      apiRequest(self.inbox(), 'delete', this.resourceUrl(),
      function(err, response) {
        if (err) return reject(err);
        resolve(self);
      });
    }
  });
};

/**
 * @property
 * @name INDraft#state
 *
 * If present, this is the state of the draft (`draft`, `sending`, or `sent`).
 */

/**
 * @property
 * @name INDraft#object
 *
 * The resource type, always 'draft'.
 */
defineResourceMapping(INDraft, {
  'state': 'state',
  'version': 'version',
  'object': 'const:draft'
}, INMessage);
<|endoftext|>


<commit_before>/*
**  ComponentJS -- Component System for JavaScript <http://componentjs.com>
**  Copyright (c) 2009-2013 Ralf S. Engelschall <http://engelschall.com>
**
**  This Source Code Form is subject to the terms of the Mozilla Public
**  License, v. 2.0. If a copy of the MPL was not distributed with this
**  file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

/*  generic pattern: event  */
$cs.pattern.event = $cs.clazz({
    mixin: [
        $cs.pattern.spec
    ],
    dynamics: {
        /*  attributes  */
        target:      $cs.attribute("target",      null),        /*  target object the event is send to  */
        propagation: $cs.attribute("propagation", true),        /*  whether event propagation should continue  */
        processing:  $cs.attribute("processing",  true),        /*  whether final default event processing should be performed  */
        dispatched:  $cs.attribute("dispatched",  false),       /*  whether event was dispatched at least once to a subscriber  */
        decline:     $cs.attribute("decline",     false),       /*  whether event was declined by subscriber  */
        state:       $cs.attribute("state",       "targeting"), /*  state of dispatching: capturing, targeting, spreading, bubbling */
        result:      $cs.attribute("result",      undefined),   /*  optional result value event subscribers can provide  */
        async:       $cs.attribute("async",       false)        /*  whether event is dispatched asynchronously  */
    }
});

/*  event factory  */
$cs.event = function () {
    /*  determine parameters  */
    var params = $cs.params("event", arguments, {
        name:        { pos: 0,     req: true        },
        spec:        {             def: {}          },
        target:      { pos: 1,     req: true        },
        propagation: { pos: 2,     def: true        },
        processing:  { pos: 3,     def: true        },
        dispatched:  { pos: 4,     def: false       },
        decline:     { pos: 5,     def: false       },
        state:       { pos: 6,     def: "targeting" },
        result:      { pos: 7,     def: undefined   },
        async:       { pos: 8,     def: false       }
    });

    /*  create new event  */
    var ev = new $cs.pattern.event();

    /*  configure event  */
    ev.name       (params.name);
    ev.target     (params.target);
    ev.propagation(params.propagation);
    ev.processing (params.processing);
    ev.dispatched (params.dispatched);
    ev.decline    (params.decline);
    ev.state      (params.state);
    ev.result     (params.result);
    ev.spec       (params.spec);
    ev.async      (params.async);

    return ev;
};

/*  generic pattern: eventing  */
$cs.pattern.eventing = $cs.trait({
    dynamics: {
        __subscription: {}
    },
    protos: {
        /*  subscribe on an event  */
        subscribe: function () {
            /*  determine parameters  */
            var params = $cs.params("subscribe", arguments, {
                name:      { pos: 0,     req: true  },
                spec:      {             def: {}    },
                ctx:       {             def: this  },
                func:      { pos: 1,     req: true  },
                args:      { pos: "...", def: []    },
                capturing: {             def: false },
                spreading: {             def: false },
                bubbling:  {             def: true  },
                noevent:   {             def: false },
                exclusive: {             def: false },
                spool:     {             def: null  }
            });

            /*  honor exclusive request  */
            var subscriptions = this._subscriptions(params.name, params.spec);
            if (subscriptions.length === 1 && subscriptions[0].exclusive)
                throw _cs.exception("subscribe", "existing exclusive subscription prevents additional one");
            if (params.exclusive && subscriptions.length > 0)
                throw _cs.exception("subscribe", "non-exclusive subscription(s) prevent exclusive one");

            /*  attach parameters to component  */
            var id = _cs.cid();
            this.__subscription[id] = params;

            /*  optionally spool reverse operation  */
            if (params.spool !== null) {
                var info = _cs.spool_spec_parse(this, params.spool);
                info.comp.spool(info.name, this, "unsubscribe", id);
            }

            return id;
        },

        /*  unsubscribe from an event  */
        unsubscribe: function () {
            /*  determine parameters  */
            var params = $cs.params("unsubscribe", arguments, {
                id: { pos: 0, req: true }
            });

            /*  detach parameters from component  */
            if (typeof this.__subscription[params.id] === "undefined")
                throw _cs.exception("unsubscribe", "subscription not found");
            delete this.__subscription[params.id];
            return;
        },

        /*  determine subscription existence  */
        subscription: function () {
            /*  determine parameters  */
            var params = $cs.params("subscription", arguments, {
                id: { pos: 0, req: true }
            });

            /*  determine whether subscription exists  */
            return (typeof this.__subscription[params.id] !== "undefined");
        },

        /*  determine subscriptions (internal)  */
        _subscriptions: function () {
            /*  determine parameters  */
            var params = $cs.params("subscriptions", arguments, {
                name:  { pos: 0, req: true },
                spec:  { pos: 1, def: {}   }
            });

            /*  make an event for matching only  */
            var ev = $cs.event({
                name:   params.name,
                spec:   params.spec,
                target: $cs.nop
            });

            /*  find and return all matching subscriptions  */
            var subscriptions = [];
            for (var id in this.__subscription) {
                if (!_cs.isown(this.__subscription, id))
                    continue;
                var s = this.__subscription[id];
                if (ev.matches(s.name, s.spec))
                    subscriptions.push(s);
            }
            return subscriptions;
        },

        /*  publish an event */
        publish: function () {
            var i;
            var self = this;

            /*  determine parameters  */
            var params = $cs.params("publish", arguments, {
                name:         { pos: 0,     req: true            },
                spec:         {             def: {}              },
                async:        {             def: false           },
                capturing:    {             def: true            },
                spreading:    {             def: false           },
                bubbling:     {             def: true            },
                completed:    {             def: $cs.nop         },
                resultinit:   {             def: undefined       },
                resultstep:   {             def: function (a, b) { return b; } },
                directresult: {             def: false           },
                noresult:     {             def: false           },
                firstonly:    {             def: false           },
                silent:       {             def: false           },
                args:         { pos: "...", def: []              }
            });

            /*  short-circuit processing (1/2) to speed up cases
                where no subscribers exist for a local event  */
            var short_circuit = false;
            if (!params.capturing && !params.spreading && !params.bubbling) {
                var subscribers = false;
                for (var id in this.__subscription) {
                    if (!_cs.isown(this.__subscription, id))
                        continue;
                    subscribers = true;
                    break;
                }
                if (!subscribers) {
                    if (params.noresult)
                        return;
                    else if (params.directresult)
                        return params.resultinit;
                    else
                        short_circuit = true;
                }
            }

            /*  create event  */
            var ev = $cs.event({
                name:        params.name,
                spec:        params.spec,
                async:       params.async,
                result:      params.resultinit,
                target:      self,
                propagation: true,
                processing:  true,
                dispatched:  false
            });

            /*  short-circuit processing (2/2)  */
            if (short_circuit)
                return ev;

            /*  tracing  */
            if (!params.silent) {
                $cs.debug(1, "event:" +
                    " " + ev.target().path("/") + ": publish:" +
                    " name=" + ev.name() +
                    " async=" + ev.async() +
                    " capturing=" + params.capturing +
                    " spreading=" + params.spreading +
                    " bubbling=" + params.bubbling +
                    " directresult=" + params.directresult +
                    " noresult=" + params.noresult +
                    " firstonly=" + params.firstonly
                );
            }

            /*  helper function for dispatching event to single component  */
            var event_dispatch_single = function (ev, comp, params, state) {
                for (var id in comp.__subscription) {
                    if (!_cs.isown(comp.__subscription, id))
                        continue;
                    var s = comp.__subscription[id];
                    if (   (   (state === "capturing" && s.capturing)
                            || (state === "targeting"               )
                            || (state === "spreading" && s.spreading)
                            || (state === "bubbling"  && s.bubbling ))
                        && ev.matches(s.name, s.spec)                 ) {

                        /*  verbosity  */
                        if (!params.silent)
                            $cs.debug(1, "event: " + comp.path("/") + ": dispatch on " + state);

                        /*  further annotate event object  */
                        ev.state(state);
                        ev.decline(false);

                        /*  call subscription method  */
                        var args = _cs.concat(
                            s.noevent ? [] : [ ev ],
                            s.args,
                            params.args
                        );
                        var result = s.func.apply(s.ctx, args);

                        /*  process return value  */
                        if (s.noevent && _cs.isdefined(result))
                            ev.result(params.resultstep(ev.result(), result));

                        /*  control the further dispatching  */
                        if (!ev.decline()) {
                            ev.dispatched(true);
                            if (params.firstonly)
                                ev.propagation(false);
                        }
                    }
                }
            };

            /*  helper function for dispatching event to all components on hierarchy path  */
            var event_dispatch_all = function (ev, comp, params) {
                /*  determine component tree path  */
                var comp_path;
                if (params.capturing || params.bubbling)
                    comp_path = comp.path();

                /*  phase 1: CAPTURING
                    optionally dispatch event downwards from root component
                    towards target component for capturing subscribers  */
                if (params.capturing) {
                    for (i = comp_path.length - 1; i >= 1; i--) {
                        event_dispatch_single(ev, comp_path[i], params, "capturing");
                        if (!ev.propagation())
                            break;
                    }
                }

                /*  phase 2: TARGETING
                    dispatch event to target component  */
                if (ev.propagation())
                    event_dispatch_single(ev, comp, params, "targeting");

                /*  phase 3: SPREADING
                    dispatch event to all descendant components  */
                if (params.spreading && ev.propagation()) {
                    var visit = function (comp, isTarget) {
                        var cont = true;
                        if (!isTarget) {
                            /*  dispatch on non-target component  */
                            event_dispatch_single(ev, comp, params, "spreading");
                            if (!ev.propagation()) {
                                /*  if propagation should stop, reset the flag again
                                    as in the spreading phase propagation stops only(!)
                                    for the particular sub-tree, not the propagation
                                    process as a whole!  */
                                ev.propagation(true);
                                cont = false;
                            }
                        }
                        if (cont) {
                            /*  dispatch onto all direct child components  */
                            var children = comp.children();
                            for (var i = 0; i < children.length; i++)
                                visit(children[i], false);
                        }
                    };
                    visit(comp, true);
                }

                /*  phase 4: BUBBLING
                    dispatch event upwards from target component towards
                    root component for bubbling (regular) subscribers  */
                if (params.bubbling && ev.propagation()) {
                    for (i = 1; i < comp_path.length; i++) {
                        event_dispatch_single(ev, comp_path[i], params, "bubbling");
                        if (!ev.propagation())
                            break;
                    }
                }

                /*  notify publisher on dispatch completion  */
                params.completed.call(comp, ev);
            };

            /*  perform event publishing,
                either asynchronous or synchronous  */
            if (ev.async())
                /* global setTimeout:false */
                setTimeout(_cs.hook("ComponentJS:settimeout:func", "pass", function () {
                    event_dispatch_all(ev, self, params);
                }), 0);
            else
                event_dispatch_all(ev, self, params);

            /*  return the event, directly the result value or no result value at all  */
            if (params.noresult)
                return;
            else if (params.directresult)
                return ev.result();
            else
                return ev;
        }
    }
});

<commit_msg>the name on subscribe() can be also a regular expression object, so stringify it<commit_after>/*
**  ComponentJS -- Component System for JavaScript <http://componentjs.com>
**  Copyright (c) 2009-2013 Ralf S. Engelschall <http://engelschall.com>
**
**  This Source Code Form is subject to the terms of the Mozilla Public
**  License, v. 2.0. If a copy of the MPL was not distributed with this
**  file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

/*  generic pattern: event  */
$cs.pattern.event = $cs.clazz({
    mixin: [
        $cs.pattern.spec
    ],
    dynamics: {
        /*  attributes  */
        target:      $cs.attribute("target",      null),        /*  target object the event is send to  */
        propagation: $cs.attribute("propagation", true),        /*  whether event propagation should continue  */
        processing:  $cs.attribute("processing",  true),        /*  whether final default event processing should be performed  */
        dispatched:  $cs.attribute("dispatched",  false),       /*  whether event was dispatched at least once to a subscriber  */
        decline:     $cs.attribute("decline",     false),       /*  whether event was declined by subscriber  */
        state:       $cs.attribute("state",       "targeting"), /*  state of dispatching: capturing, targeting, spreading, bubbling */
        result:      $cs.attribute("result",      undefined),   /*  optional result value event subscribers can provide  */
        async:       $cs.attribute("async",       false)        /*  whether event is dispatched asynchronously  */
    }
});

/*  event factory  */
$cs.event = function () {
    /*  determine parameters  */
    var params = $cs.params("event", arguments, {
        name:        { pos: 0,     req: true        },
        spec:        {             def: {}          },
        target:      { pos: 1,     req: true        },
        propagation: { pos: 2,     def: true        },
        processing:  { pos: 3,     def: true        },
        dispatched:  { pos: 4,     def: false       },
        decline:     { pos: 5,     def: false       },
        state:       { pos: 6,     def: "targeting" },
        result:      { pos: 7,     def: undefined   },
        async:       { pos: 8,     def: false       }
    });

    /*  create new event  */
    var ev = new $cs.pattern.event();

    /*  configure event  */
    ev.name       (params.name);
    ev.target     (params.target);
    ev.propagation(params.propagation);
    ev.processing (params.processing);
    ev.dispatched (params.dispatched);
    ev.decline    (params.decline);
    ev.state      (params.state);
    ev.result     (params.result);
    ev.spec       (params.spec);
    ev.async      (params.async);

    return ev;
};

/*  generic pattern: eventing  */
$cs.pattern.eventing = $cs.trait({
    dynamics: {
        __subscription: {}
    },
    protos: {
        /*  subscribe on an event  */
        subscribe: function () {
            /*  determine parameters  */
            var params = $cs.params("subscribe", arguments, {
                name:      { pos: 0,     req: true  },
                spec:      {             def: {}    },
                ctx:       {             def: this  },
                func:      { pos: 1,     req: true  },
                args:      { pos: "...", def: []    },
                capturing: {             def: false },
                spreading: {             def: false },
                bubbling:  {             def: true  },
                noevent:   {             def: false },
                exclusive: {             def: false },
                spool:     {             def: null  }
            });

            /*  honor exclusive request
                (attention: name can also be a regular expression object!)  */
            var subscriptions = this._subscriptions(params.name.toString(), params.spec);
            if (subscriptions.length === 1 && subscriptions[0].exclusive)
                throw _cs.exception("subscribe", "existing exclusive subscription prevents additional one");
            if (params.exclusive && subscriptions.length > 0)
                throw _cs.exception("subscribe", "non-exclusive subscription(s) prevent exclusive one");

            /*  attach parameters to component  */
            var id = _cs.cid();
            this.__subscription[id] = params;

            /*  optionally spool reverse operation  */
            if (params.spool !== null) {
                var info = _cs.spool_spec_parse(this, params.spool);
                info.comp.spool(info.name, this, "unsubscribe", id);
            }

            return id;
        },

        /*  unsubscribe from an event  */
        unsubscribe: function () {
            /*  determine parameters  */
            var params = $cs.params("unsubscribe", arguments, {
                id: { pos: 0, req: true }
            });

            /*  detach parameters from component  */
            if (typeof this.__subscription[params.id] === "undefined")
                throw _cs.exception("unsubscribe", "subscription not found");
            delete this.__subscription[params.id];
            return;
        },

        /*  determine subscription existence  */
        subscription: function () {
            /*  determine parameters  */
            var params = $cs.params("subscription", arguments, {
                id: { pos: 0, req: true }
            });

            /*  determine whether subscription exists  */
            return (typeof this.__subscription[params.id] !== "undefined");
        },

        /*  determine subscriptions (internal)  */
        _subscriptions: function () {
            /*  determine parameters  */
            var params = $cs.params("subscriptions", arguments, {
                name:  { pos: 0, req: true },
                spec:  { pos: 1, def: {}   }
            });

            /*  make an event for matching only  */
            var ev = $cs.event({
                name:   params.name,
                spec:   params.spec,
                target: $cs.nop
            });

            /*  find and return all matching subscriptions  */
            var subscriptions = [];
            for (var id in this.__subscription) {
                if (!_cs.isown(this.__subscription, id))
                    continue;
                var s = this.__subscription[id];
                if (ev.matches(s.name, s.spec))
                    subscriptions.push(s);
            }
            return subscriptions;
        },

        /*  publish an event */
        publish: function () {
            var i;
            var self = this;

            /*  determine parameters  */
            var params = $cs.params("publish", arguments, {
                name:         { pos: 0,     req: true            },
                spec:         {             def: {}              },
                async:        {             def: false           },
                capturing:    {             def: true            },
                spreading:    {             def: false           },
                bubbling:     {             def: true            },
                completed:    {             def: $cs.nop         },
                resultinit:   {             def: undefined       },
                resultstep:   {             def: function (a, b) { return b; } },
                directresult: {             def: false           },
                noresult:     {             def: false           },
                firstonly:    {             def: false           },
                silent:       {             def: false           },
                args:         { pos: "...", def: []              }
            });

            /*  short-circuit processing (1/2) to speed up cases
                where no subscribers exist for a local event  */
            var short_circuit = false;
            if (!params.capturing && !params.spreading && !params.bubbling) {
                var subscribers = false;
                for (var id in this.__subscription) {
                    if (!_cs.isown(this.__subscription, id))
                        continue;
                    subscribers = true;
                    break;
                }
                if (!subscribers) {
                    if (params.noresult)
                        return;
                    else if (params.directresult)
                        return params.resultinit;
                    else
                        short_circuit = true;
                }
            }

            /*  create event  */
            var ev = $cs.event({
                name:        params.name,
                spec:        params.spec,
                async:       params.async,
                result:      params.resultinit,
                target:      self,
                propagation: true,
                processing:  true,
                dispatched:  false
            });

            /*  short-circuit processing (2/2)  */
            if (short_circuit)
                return ev;

            /*  tracing  */
            if (!params.silent) {
                $cs.debug(1, "event:" +
                    " " + ev.target().path("/") + ": publish:" +
                    " name=" + ev.name() +
                    " async=" + ev.async() +
                    " capturing=" + params.capturing +
                    " spreading=" + params.spreading +
                    " bubbling=" + params.bubbling +
                    " directresult=" + params.directresult +
                    " noresult=" + params.noresult +
                    " firstonly=" + params.firstonly
                );
            }

            /*  helper function for dispatching event to single component  */
            var event_dispatch_single = function (ev, comp, params, state) {
                for (var id in comp.__subscription) {
                    if (!_cs.isown(comp.__subscription, id))
                        continue;
                    var s = comp.__subscription[id];
                    if (   (   (state === "capturing" && s.capturing)
                            || (state === "targeting"               )
                            || (state === "spreading" && s.spreading)
                            || (state === "bubbling"  && s.bubbling ))
                        && ev.matches(s.name, s.spec)                 ) {

                        /*  verbosity  */
                        if (!params.silent)
                            $cs.debug(1, "event: " + comp.path("/") + ": dispatch on " + state);

                        /*  further annotate event object  */
                        ev.state(state);
                        ev.decline(false);

                        /*  call subscription method  */
                        var args = _cs.concat(
                            s.noevent ? [] : [ ev ],
                            s.args,
                            params.args
                        );
                        var result = s.func.apply(s.ctx, args);

                        /*  process return value  */
                        if (s.noevent && _cs.isdefined(result))
                            ev.result(params.resultstep(ev.result(), result));

                        /*  control the further dispatching  */
                        if (!ev.decline()) {
                            ev.dispatched(true);
                            if (params.firstonly)
                                ev.propagation(false);
                        }
                    }
                }
            };

            /*  helper function for dispatching event to all components on hierarchy path  */
            var event_dispatch_all = function (ev, comp, params) {
                /*  determine component tree path  */
                var comp_path;
                if (params.capturing || params.bubbling)
                    comp_path = comp.path();

                /*  phase 1: CAPTURING
                    optionally dispatch event downwards from root component
                    towards target component for capturing subscribers  */
                if (params.capturing) {
                    for (i = comp_path.length - 1; i >= 1; i--) {
                        event_dispatch_single(ev, comp_path[i], params, "capturing");
                        if (!ev.propagation())
                            break;
                    }
                }

                /*  phase 2: TARGETING
                    dispatch event to target component  */
                if (ev.propagation())
                    event_dispatch_single(ev, comp, params, "targeting");

                /*  phase 3: SPREADING
                    dispatch event to all descendant components  */
                if (params.spreading && ev.propagation()) {
                    var visit = function (comp, isTarget) {
                        var cont = true;
                        if (!isTarget) {
                            /*  dispatch on non-target component  */
                            event_dispatch_single(ev, comp, params, "spreading");
                            if (!ev.propagation()) {
                                /*  if propagation should stop, reset the flag again
                                    as in the spreading phase propagation stops only(!)
                                    for the particular sub-tree, not the propagation
                                    process as a whole!  */
                                ev.propagation(true);
                                cont = false;
                            }
                        }
                        if (cont) {
                            /*  dispatch onto all direct child components  */
                            var children = comp.children();
                            for (var i = 0; i < children.length; i++)
                                visit(children[i], false);
                        }
                    };
                    visit(comp, true);
                }

                /*  phase 4: BUBBLING
                    dispatch event upwards from target component towards
                    root component for bubbling (regular) subscribers  */
                if (params.bubbling && ev.propagation()) {
                    for (i = 1; i < comp_path.length; i++) {
                        event_dispatch_single(ev, comp_path[i], params, "bubbling");
                        if (!ev.propagation())
                            break;
                    }
                }

                /*  notify publisher on dispatch completion  */
                params.completed.call(comp, ev);
            };

            /*  perform event publishing,
                either asynchronous or synchronous  */
            if (ev.async())
                /* global setTimeout:false */
                setTimeout(_cs.hook("ComponentJS:settimeout:func", "pass", function () {
                    event_dispatch_all(ev, self, params);
                }), 0);
            else
                event_dispatch_all(ev, self, params);

            /*  return the event, directly the result value or no result value at all  */
            if (params.noresult)
                return;
            else if (params.directresult)
                return ev.result();
            else
                return ev;
        }
    }
});

<|endoftext|>


<commit_before>import {Rx} from '@cycle/core'
import {createComponent,removeComponent,makeActionsFromApiFns} from './common'
import {makeModelNoHistory, mergeData} from '../../../utils/modelUtils'

////Mesh//////
export function makeMeshSystem(){
  const defaults ={
  }

  function createComponentMesh(defaults,state,input){
    console.log("createComponent", input)
    let inputValue =  {}
    if(input && input.value) inputValue = input.value

    let newAttrs = inputValue.mesh //{mesh: inputValue.mesh }// mergeData(defaults,inputValue)

    //auto increment ?
    //auto generate ?
    //let id = generateUUID()
    //if(input && input.id) id = input.id
    let id = input.id

    state = mergeData({},state)
    state[id] = newAttrs
    //FIXME big hack, use mutability
    return state
  }

  //TODO: should defaults be something like a stand in cube ?
  let updateFns = {
    createComponent: createComponentMesh.bind(null,undefined)
    , removeComponent}
  let actions   = makeActionsFromApiFns(updateFns)

  let meshes$ = makeModelNoHistory(defaults, updateFns, actions)

  return {meshes$,meshActions:actions}
}<commit_msg>refactor(mesh): minor tweaks to ensure mesh component is not immutable<commit_after>import {Rx} from '@cycle/core'
import {createComponent,removeComponent,makeActionsFromApiFns} from './common'
import {makeModelNoHistory, mergeData} from '../../../utils/modelUtils'

////Mesh//////
export function makeMeshSystem(){
  const defaults ={
  }

  function createComponentMesh(defaults,state,input){
    console.log("createComponent", input)
    let inputValue =  {}
    if(input && input.value) inputValue = input.value

    let newAttrs = inputValue.mesh //{mesh: inputValue.mesh }// mergeData(defaults,inputValue)

    //auto increment ?
    //auto generate ?
    //let id = generateUUID()
    //if(input && input.id) id = input.id
    let id = input.id

    state = mergeData({},state)
    state[id] = newAttrs
    //FIXME big hack, use mutability
    return state
  }

  //TODO: should defaults be something like a stand in cube ?
  let updateFns = {
    createComponent: createComponentMesh.bind(null,undefined)
    , removeComponent}
  let actions   = makeActionsFromApiFns(updateFns)

  let meshes$ = makeModelNoHistory(defaults, updateFns, actions, undefined, false)//last flag set to false because we
  //do not want immutable data for meshes ?

  return {meshes$,meshActions:actions}
}<|endoftext|>


<commit_before>/* =====================================================================
 * tidy-multi-select.js v0.0.1
 * http://github.com/dphaener/tidy-multi-select
 * =====================================================================
 * Copyright (c) 2014 Darin Haener
 *
 * MIT License
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 * ===================================================================== */

;(function($, window, document, undefined) {

  var Option = function(options) {
    this.options = options || {};
    this.$el = options.$el;
    this.control = options.control;
  }

  Option.prototype = {
    constructor: Option

  , updateSelected: function() {
      var $select = this.$el.parent();
      $select.trigger("change");
    }

  , value: function() {
      return this.$el.val();
    }

  , checked: function() {
      return this.$el.data("checked")
    }

  , title: function() {
      return this.$el.html();
    }

  , description: function() {
      return this.$el.data("description");
    }

  , render: function() {
      var self = this;
      var checked = typeof(this.checked()) == "undefined" ? "" : "checked";

      self.$option = $("

");
      self.$option.append("<input class='tms-input' type='checkbox' " + checked + "/>");
      self.$option.append("" + this.title() + "");

      if (this.description()) {
        self.$option.append("" + this.description() + "");
      }

      self.$checkbox = self.$option.find("input");

      self.$checkbox.on("click", function(e) {
        e.stopPropagation();
        $(this).prop("checked", $(this).prop("checked"));
        self.updateSelected();
      });

      return self.$option;
    }
  }

  var Control = function(el, options) {
    var self = this;

    self.$el = $(el);
    self.$control = $("

");

    self.options = options || {};
    self.defaultText = self.options.defaultText || "Choose an item...";
    self.width = self.options.width || self.$el.data("width") || "auto";
    self.customMultiText = self.options.customMultiText || false;
    if (self.customMultiText) self.multiText = self.options.multiText || "Multiple selected...";

    self.render();

    $("html").on("click", function() {
      self.$control.removeClass("open");
    });

    $("body").on("click", ".tms-control .tms-popover", function(e) {
      e.stopPropagation();
    });

    self.$el.on("change", function() {
      self.update();
    });

    self.$control.on("click", function(e) {
      e.preventDefault();
      self.toggle();
      return false;
    });

    self.update();
  }

  Control.prototype = {
    constructor: Control

  , close: function() {
      this.$control.removeClass("open");
    }

  , open: function() {
      $(".tms-control").removeClass("open");
      this.$control.addClass("open");
    }

  , toggle: function() {
      this.$control.hasClass("open") ? this.close() : this.open();
    }

  , update: function() {
      var $checked = this.$options.find("input:checked").parent("li");
      var selectedValues = [];
      var selectedTexts = [];

      $checked.each(function (i) {
        $el = $(this);
        value = $el.data("value");
        text = $el.find(".tms-title").html();
        selectedValues.push(value);
        selectedTexts.push(text);
      });

      this.$el.data("values", selectedValues);

      var text = selectedTexts.join(", ");

      if (!text || text == "") {
        text = this.defaultText;
      }
      else if (this.customMultiText && selectedValues.length > 1) {
        text = this.multiText;
      }

      this.$dropdown.html(text);
    }

  , reset: function(options) {
      options || (options = []);
      var selected = this.$el.val();
      this.$el.html(""); // remove existing options
      this.$el.append("<option value='' selected='selected'>" + this.defaultText + "</option>");

      for (var i = 0; i < options.length; i++) {
        var option = options[i];
        $optionEl = $("<option></option>");

        if (option.value) {
          $optionEl.val(option.value);
        }

        if (option.title) {
          $optionEl.html(option.title);
        }

        if (option.description) {
          $optionEl.data("description", option.description);
        }

        this.$el.append($optionEl);
      }

      this.render();
      this.$el.val(selected);
    }

  , render: function() {
      var self = this;
      self.$control.html("");

      // Inherit class list from the original "select"
      self.$control.attr("class", "tms-control " + self.$el.attr("class"));

      self.$dropdown = $("");
      self.$popover = $("

");
      self.$options = $("

");

      self.$el.find("option").each(function() {
        var $option = $(this);

        if ($option.val() !== "") {
          var option = new Option({
            $el: $option,
            control: self
          });

          self.$options.append(option.render());
        }
      });

      self.$popover.append(self.$options);
      self.$control.append(self.$dropdown);
      self.$control.append(self.$popover);

      self.$el.after(self.$control);

      if (self.width == "inherit") {
        self.$control.css("width", self.$el.width());
      } else if (self.width != "auto") {
        self.$control.css("width", self.width);
      }

      self.$el.css("display", "none");
      self.update();
    }
  }

  $.fn.tidyMultiSelect = function(options) {
    var args = Array.prototype.slice.call(arguments);

    return this.each(function() {
      var $this = $(this)
        , data = $this.data('tidyMultiSelect');
      if (!data) $this.data('tidyMultiSelect', (data = new Control(this, options)));
      if (typeof options == 'string') {
        return data[options].apply(data, args.slice(1));
      }
    });
  }
}(jQuery, window, document));<commit_msg>Added custom triggering Added status check for triggering<commit_after>/* =====================================================================
 * tidy-multi-select.js v0.0.1
 * http://github.com/dphaener/tidy-multi-select
 * =====================================================================
 * Copyright (c) 2014 Darin Haener
 *
 * MIT License
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 * ===================================================================== */

;(function($, window, document, undefined) {

  var Option = function(options) {
    this.options = options || {};
    this.$el = options.$el;
    this.control = options.control;
  }

  Option.prototype = {
    constructor: Option

  , updateSelected: function() {
      var $select = this.$el.parent();
      $select.trigger("change");
    }

  , value: function() {
      return this.$el.val();
    }

  , checked: function() {
      return this.$el.data("checked")
    }

  , title: function() {
      return this.$el.html();
    }

  , description: function() {
      return this.$el.data("description");
    }

  , render: function() {
      var self = this;
      var checked = typeof(this.checked()) == "undefined" ? "" : "checked";

      self.$option = $("

");
      self.$option.append("<input class='tms-input' type='checkbox' " + checked + "/>");
      self.$option.append("" + this.title() + "");

      if (this.description()) {
        self.$option.append("" + this.description() + "");
      }

      self.$checkbox = self.$option.find("input");

      self.$checkbox.on("click", function(e) {
        e.stopPropagation();
        $(this).prop("checked", $(this).prop("checked"));
        self.updateSelected();
      });

      return self.$option;
    }
  }

  var Control = function(el, options) {
    var self = this;

    self.$el = $(el);
    self.$control = $("

");

    self.options = options || {};
    self.defaultText = self.options.defaultText || "Choose an item...";
    self.width = self.options.width || self.$el.data("width") || "auto";
    self.customMultiText = self.options.customMultiText || false;
    if (self.customMultiText) self.multiText = self.options.multiText || "Multiple selected...";
    self.customEvent = self.options.customEvent || "submit";
    self.selectedValues = [];

    self.render();

    $("html").on("click", function() {
      self.$control.removeClass("open");
      if (self.previousValues != self.selectedValues) self.$el.trigger(self.customEvent);
    });

    $("body").on("click", ".tms-control .tms-popover", function(e) {
      e.stopPropagation();
    });

    self.$el.on("change", function() {
      self.update();
    });

    self.$control.on("click", function(e) {
      e.preventDefault();
      self.toggle();
      return false;
    });

    self.update();
    self.previousValues = self.selectedValues;
  }

  Control.prototype = {
    constructor: Control

  , close: function() {
      this.$control.removeClass("open");
      if (this.previousValues != this.selectedValues) this.$el.trigger(this.customEvent);
    }

  , open: function() {
      $(".tms-control").removeClass("open");
      this.$control.addClass("open");
    }

  , toggle: function() {
      this.$control.hasClass("open") ? this.close() : this.open();
    }

  , update: function() {
      var $checked = this.$options.find("input:checked").parent("li");
      var selectedValues = [];
      var selectedTexts = [];

      $checked.each(function (i) {
        $el = $(this);
        value = $el.data("value");
        text = $el.find(".tms-title").html();
        selectedValues.push(value);
        selectedTexts.push(text);
      });

      this.previousValues = this.selectedValues;
      this.selectedValues = selectedValues;
      this.$el.data("values", selectedValues);

      var text = selectedTexts.join(", ");

      if (!text || text == "") {
        text = this.defaultText;
      }
      else if (this.customMultiText && selectedValues.length > 1) {
        text = this.multiText;
      }

      this.$dropdown.html(text);
    }

  , reset: function(options) {
      options || (options = []);
      var selected = this.$el.val();
      this.$el.html(""); // remove existing options
      this.$el.append("<option value='' selected='selected'>" + this.defaultText + "</option>");

      for (var i = 0; i < options.length; i++) {
        var option = options[i];
        $optionEl = $("<option></option>");

        if (option.value) {
          $optionEl.val(option.value);
        }

        if (option.title) {
          $optionEl.html(option.title);
        }

        if (option.description) {
          $optionEl.data("description", option.description);
        }

        this.$el.append($optionEl);
      }

      this.render();
      this.$el.val(selected);
    }

  , render: function() {
      var self = this;
      self.$control.html("");

      // Inherit class list from the original "select"
      self.$control.attr("class", "tms-control " + self.$el.attr("class"));

      self.$dropdown = $("");
      self.$popover = $("

");
      self.$options = $("

");

      self.$el.find("option").each(function() {
        var $option = $(this);

        if ($option.val() !== "") {
          var option = new Option({
            $el: $option,
            control: self
          });

          self.$options.append(option.render());
        }
      });

      self.$popover.append(self.$options);
      self.$control.append(self.$dropdown);
      self.$control.append(self.$popover);

      self.$el.after(self.$control);

      if (self.width == "inherit") {
        self.$control.css("width", self.$el.width());
      } else if (self.width != "auto") {
        self.$control.css("width", self.width);
      }

      self.$el.css("display", "none");
      self.update();
    }
  }

  $.fn.tidyMultiSelect = function(options) {
    var args = Array.prototype.slice.call(arguments);

    return this.each(function() {
      var $this = $(this)
        , data = $this.data('tidyMultiSelect');
      if (!data) $this.data('tidyMultiSelect', (data = new Control(this, options)));
      if (typeof options == 'string') {
        return data[options].apply(data, args.slice(1));
      }
    });
  }
}(jQuery, window, document));<|endoftext|>


<commit_before>Navy.Class('Navy.Scene', Navy.ViewGroup.ViewGroup, {
  _pageStack: null,
  _sceneFixedFirstView: null,

  initialize: function($super, layout, callback){
    this._pageStack = [];

    $super(layout, function(){
      var viewId = this._viewsOrder[0];
      this._sceneFixedFirstView = this._views[viewId];
      this.nextPage(layout.extra.page, callback.bind(null, this));
    }.bind(this));
  },

  setLayout: function($super, layout, callback) {
    // シーン、ページの場合はsize, posは固定値でよい
    layout.visible = true;
    layout.pos = {x:0, y:0};
    layout.sizePolicy = this.SIZE_POLICY_FIXED;
    layout.size = {width: Navy.Config.app.size.width, height: Navy.Config.app.size.height};

    $super(layout, callback);
  },

  setPage: function(page) {
    // ignore
  },

  getPage: function() {
    return null;
  },

  setScene: function(scene) {
    // ignore
  },

  getScene: function() {
    return this;
  },

  getCurrentPage: function() {
    var currentStackObj = this._getCurrentStack();
    return currentStackObj.page;
  },

  // fixme: callbackを実装する.
  linkPage: function(id) {
    if (id === '$back') {
      this.backPage();
    } else {
      this.nextPage(id);
    }
  },

  nextPage: function(pageName, callback) {
    this._createPage(pageName, function(page){
      this._addPage(page);
      callback && callback(page);
    }.bind(this));
  },

  backPage: function() {
    if (this._pageStack.length >= 2) {
      var stackObj = this._pageStack[this._pageStack.length - 1];
      var transition = stackObj.transition;
      transition.back(this._onTransitionBackEnd.bind(this));
    }
  },

  onCreate: function() {
    console.log('onCreate', this.$className);
  },

  onResumeBefore: function(){
    console.log('onResumeBefore', this.$className);
  },

  onResumeAfter: function(){
    console.log('onResumeAfter', this.$className);
  },

  onPauseBefore: function(){
    console.log('onPauseBefore', this.$className);
  },

  onPauseAfter: function(){
    console.log('onPauseAfter', this.$className);
  },

  onDestroy: function(){
    console.log('onDestroy', this.$className);
  },

  // fixme: 不要？
  /*
  _getBottomPageLayout: function(layout) {
    var bottomLayout = {
      class: 'Navy.Page',
      id: '$bottom',
      pos: {x:0, y:0},
      size: {width: layout.size.width, height: layout.size.height},
      extra: {
        contentLayoutFile: layout.extra.contentBottomLayoutFile
      }
    };

    return bottomLayout;
  },
  */

  // fixme: 不要？
  /*
  _getTopPageLayout: function(layout) {
    var topLayout = {
      class: 'Navy.Page',
      id: '$top',
      pos: {x:0, y:0, z:100},
      size: {width: layout.size.width, height: layout.size.height},
      extra: {
        contentLayoutFile: layout.extra.contentTopLayoutFile
      }
    };

    return topLayout;
  },
  */

  _createPage: function(pageName, callback) {
    var layout = Navy.Config.page[pageName];
    Navy.Resource.loadScript(layout.classFile, this._onLoadScript.bind(this, layout, callback));
  },

  _createPageByLayout: function(layout, callback) {
    var _class = Navy.Resource.getClass(layout.class);
    new _class(layout, callback);
  },

  _onLoadScript: function(layout, callback) {
    var PageClass = Navy.Resource.getClass(layout.class);
    new PageClass(layout, callback);
  },

  _addPage: function(page) {
    page.onCreate();
    page.onResumeBefore();

    var currentStackObj = this._getCurrentStack();
    if (currentStackObj) {
      var beforePage = currentStackObj.page;
      beforePage.onPauseBefore();
    }

    // TODO: 組み込みだけじゃんくてカスタムのTransitionにも対応する.
    var TransitionClass = Navy.Resource.getClass(page.getLayout().extra.transition.class);
    var transition = new TransitionClass(beforePage, page);
    this._pageStack.push({
      page: page,
      transition: transition
    });
    this.addView(page, this._sceneFixedFirstView);
    transition.start(this._onTransitionStartEnd.bind(this));
  },

  _getCurrentStack: function() {
    if (this._pageStack.length >= 1) {
      return this._pageStack[this._pageStack.length - 1];
    } else {
      return null;
    }
  },

  _getPrevStack: function(){
    if (this._pageStack.length >= 2) {
      return this._pageStack[this._pageStack.length - 2];
    } else {
      return null;
    }
  },

  _onTransitionStartEnd: function(){
    var prevStackObj = this._getPrevStack();
    if (prevStackObj) {
      prevStackObj.page.onPauseAfter();
    }

    var currentStackObj = this._getCurrentStack();
    if (currentStackObj) {
      currentStackObj.page.onResumeAfter();
    }
  },

  _onTransitionBackEnd: function(){
    var prevStackObj = this._getPrevStack();
    if (prevStackObj) {
      prevStackObj.page.onResumeAfter();
    }

    var currentStackObj = this._getCurrentStack();
    if (currentStackObj) {
      currentStackObj.page.onPauseAfter();
      currentStackObj.page.onDestroy();

      var stackObj = this._pageStack.pop();
      stackObj.page.destroy();
    }
  }
});
<commit_msg>pageのライフサイクルを呼び出すときにsceneの現在の状態も考慮するようにした.<commit_after>Navy.Class('Navy.Scene', Navy.ViewGroup.ViewGroup, {
  LIFE_CYCLE_STATE_CREATE: 1,
  LIFE_CYCLE_STATE_RESUME_BEFORE: 2,
  LIFE_CYCLE_STATE_RESUME_AFTER: 3,
  LIFE_CYCLE_STATE_PAUSE_BEFORE: 4,
  LIFE_CYCLE_STATE_PAUSE_AFTER: 5,
  LIFE_CYCLE_STATE_DESTROY: 6,

  _lifeCycleState: 0,
  _pageStack: null,
  _sceneFixedFirstView: null,

  initialize: function($super, layout, callback){
    this._pageStack = [];

    $super(layout, function(){
      var viewId = this._viewsOrder[0];
      this._sceneFixedFirstView = this._views[viewId];
      this.nextPage(layout.extra.page, callback.bind(null, this));
    }.bind(this));
  },

  setLayout: function($super, layout, callback) {
    // シーン、ページの場合はsize, posは固定値でよい
    layout.visible = true;
    layout.pos = {x:0, y:0};
    layout.sizePolicy = this.SIZE_POLICY_FIXED;
    layout.size = {width: Navy.Config.app.size.width, height: Navy.Config.app.size.height};

    $super(layout, callback);
  },

  setPage: function(page) {
    // ignore
  },

  getPage: function() {
    return null;
  },

  setScene: function(scene) {
    // ignore
  },

  getScene: function() {
    return this;
  },

  getCurrentPage: function() {
    var currentStackObj = this._getCurrentStack();
    return currentStackObj.page;
  },

  // fixme: callbackを実装する.
  linkPage: function(id) {
    if (id === '$back') {
      this.backPage();
    } else {
      this.nextPage(id);
    }
  },

  nextPage: function(pageName, callback) {
    this._createPage(pageName, function(page){
      this._addPage(page);
      callback && callback(page);
    }.bind(this));
  },

  backPage: function() {
    if (this._pageStack.length >= 2) {
      var currentStackObj = this._getCurrentStack();
      var prevStackObj = this._getPrevStack();

      currentStackObj.page.onPauseBefore();
      prevStackObj.page.onResumeBefore();

      var transition = currentStackObj.transition;
      transition.back(this._onTransitionBackEnd.bind(this));
    }
  },

  onCreate: function() {
    this._lifeCycleState = this.LIFE_CYCLE_STATE_CREATE;
    console.log('onCreate', this.$className);

    var page = this.getCurrentPage();
    page.onCreate();
  },

  onResumeBefore: function(){
    this._lifeCycleState = this.LIFE_CYCLE_STATE_PAUSE_BEFORE;
    console.log('onResumeBefore', this.$className);

    var page = this.getCurrentPage();
    page.onResumeBefore();
  },

  onResumeAfter: function(){
    this._lifeCycleState = this.LIFE_CYCLE_STATE_PAUSE_AFTER;
    console.log('onResumeAfter', this.$className);

    var page = this.getCurrentPage();
    page.onResumeAfter();
  },

  onPauseBefore: function(){
    this._lifeCycleState = this.LIFE_CYCLE_STATE_PAUSE_BEFORE;
    console.log('onPauseBefore', this.$className);

    var page = this.getCurrentPage();
    page.onPauseBefore();
  },

  onPauseAfter: function(){
    this._lifeCycleState = this.LIFE_CYCLE_STATE_PAUSE_AFTER;
    console.log('onPauseAfter', this.$className);

    var page = this.getCurrentPage();
    page.onPauseAfter();
  },

  onDestroy: function(){
    this._lifeCycleState = this.LIFE_CYCLE_STATE_DESTROY;
    console.log('onDestroy', this.$className);

    // TODO: いきなりsceneが終わる場合もあるのですべてのスタックを綺麗にする必要ありそう.
    var page = this.getCurrentPage();
    page.onDestroy();
  },

  // fixme: 不要？
  /*
  _getBottomPageLayout: function(layout) {
    var bottomLayout = {
      class: 'Navy.Page',
      id: '$bottom',
      pos: {x:0, y:0},
      size: {width: layout.size.width, height: layout.size.height},
      extra: {
        contentLayoutFile: layout.extra.contentBottomLayoutFile
      }
    };

    return bottomLayout;
  },
  */

  // fixme: 不要？
  /*
  _getTopPageLayout: function(layout) {
    var topLayout = {
      class: 'Navy.Page',
      id: '$top',
      pos: {x:0, y:0, z:100},
      size: {width: layout.size.width, height: layout.size.height},
      extra: {
        contentLayoutFile: layout.extra.contentTopLayoutFile
      }
    };

    return topLayout;
  },
  */

  _createPage: function(pageName, callback) {
    var layout = Navy.Config.page[pageName];
    Navy.Resource.loadScript(layout.classFile, this._onLoadScript.bind(this, layout, callback));
  },

  _createPageByLayout: function(layout, callback) {
    var _class = Navy.Resource.getClass(layout.class);
    new _class(layout, callback);
  },

  _onLoadScript: function(layout, callback) {
    var PageClass = Navy.Resource.getClass(layout.class);
    new PageClass(layout, callback);
  },

  _addPage: function(page) {
    this._lifeCycleState >= this.LIFE_CYCLE_STATE_CREATE && page.onCreate();
    this._lifeCycleState >= this.LIFE_CYCLE_STATE_RESUME_BEFORE && page.onResumeBefore();

    var currentStackObj = this._getCurrentStack();
    if (currentStackObj) {
      var beforePage = currentStackObj.page;
      beforePage.onPauseBefore();
    }

    // TODO: 組み込みだけじゃんくてカスタムのTransitionにも対応する.
    var TransitionClass = Navy.Resource.getClass(page.getLayout().extra.transition.class);
    var transition = new TransitionClass(beforePage, page);
    this._pageStack.push({
      page: page,
      transition: transition
    });
    this.addView(page, this._sceneFixedFirstView);
    transition.start(this._onTransitionStartEnd.bind(this));
  },

  _getCurrentStack: function() {
    if (this._pageStack.length >= 1) {
      return this._pageStack[this._pageStack.length - 1];
    } else {
      return null;
    }
  },

  _getPrevStack: function(){
    if (this._pageStack.length >= 2) {
      return this._pageStack[this._pageStack.length - 2];
    } else {
      return null;
    }
  },

  _onTransitionStartEnd: function(){
    var prevStackObj = this._getPrevStack();
    if (prevStackObj) {
      prevStackObj.page.onPauseAfter();
    }

    var currentStackObj = this._getCurrentStack();
    if (currentStackObj) {
      this._lifeCycleState >= this.LIFE_CYCLE_STATE_RESUME_AFTER && currentStackObj.page.onResumeAfter();
    }
  },

  _onTransitionBackEnd: function(){
    var prevStackObj = this._getPrevStack();
    if (prevStackObj) {
      prevStackObj.page.onResumeAfter();
    }

    var currentStackObj = this._getCurrentStack();
    if (currentStackObj) {
      currentStackObj.page.onPauseAfter();
      currentStackObj.page.onDestroy();

      var stackObj = this._pageStack.pop();
      stackObj.page.destroy();
    }
  }
});
<|endoftext|>