<commit_before>ChecklistItems = new Meteor.Collection('checklistItems');

Template.card.rendered = function() {
  var _this = this;
  Meteor.subscribe('checklistItems', function() {
    var data = function() {
      var clItems = ChecklistItems.find({ cardId: _this.data._id });
      var clearifyId = function (doc) {
        doc._id = doc._id._str;
        return doc;
      }
      var newClItems = clItems.map(clearifyId);
      Meteor.subscribe('card-by-id', _this.data._id._str);
      var card = Cards.findOne({ _id: _this.data._id });
      var clItemIds = card.cli_order.split(',');
      var finalClItems = new Array(clItemIds.length);
      newClItems.forEach(function(item) {
        var index = $.inArray(item._id, clItemIds);
        if (index > -1) {
          finalClItems[index] = item;
        } else {
          finalClItems[finalClItems.length - 1] = item;
        }
      });
      if (newClItems.length ===  clItemIds.length - 1) {
        for (var i=0; i< newClItems.length; i++) {
          if (!finalClItems[i]) {
            break;
          }
        }
        finalClItems.splice(i, 1);
      }

      return finalClItems;
    };
    var tmpl = function() {
      return Template.checklistItem;
    };
    Blaze.render(Blaze.Each(data, tmpl), _this.$('.checklist')[0]);
  });

  var $sortableCL = $('#card-content .checklist').sortable({
    placeholder: 'ui-state-highlight',
    stop: function(event, ui) {
      var cli_order = $sortableCL.sortable('toArray').join(',');
      Cards.update(_this.data._id, { $set: {cli_order: cli_order, moved_cli_id: ui.item[0].id }});
    }
  })
  .disableSelection();

  Tracker.autorun(function () {
    Meteor.subscribe("current-card-by-id", _this.data._id);
    var cli_order = Cards.findOne(_this.data._id).cli_order;
    var moved_cli_id = Cards.findOne(_this.data._id).moved_cli_id;
    var $moved_cli_id = $('#' + moved_cli_id);
    if ($sortableCL.children().length) {
      var index = $.inArray(moved_cli_id, cli_order.split(','));
      var length = $sortableCL.children().length;
      if (index === length) {
        if($sortableCL.children().last()[0].id !== moved_cli_id) {
          $sortableCL.append($moved_cli_id);
        }
      } else if (index === -1) {
        $moved_cli_id.remove();
      } else {
        if($sortableCL.children().eq(index)[0].id !== moved_cli_id) {
          if (index > 0) {
            if (index > $('#' + moved_cli_id).index('.checklistItem')) {
              $moved_cli_id.insertAfter($sortableCL.children().eq(index));
            } else {
              $moved_cli_id.insertBefore($sortableCL.children().eq(index));
            }
          } else {
            $sortableCL.prepend($moved_cli_id);
          }
        }
      }
    }
  });

};

Template.card.events({
  'click #card-caption .static-view span': function(event, template) {
    $cardContent = $(event.target);
    template.$('.static-view').hide();
    Blaze.renderWithData(Template.editCardName, { cardId: this._id, name: $cardContent.text() },
      template.$('#card-caption')[0]);
  },
  'click #add-cli-option': function (event, template) {
    var $cardFooter = $('#card-footer');
    var $cardContent = $('#card-content');
    if ($cardFooter.is(':hidden')) {
      if(!$('#text-addCLI').length) {
        Blaze.renderWithData(Template.addChecklistItem, {cardId: this._id}, $cardFooter[0]);
      }
      var footerHeight = 50;
      var contentHeight  =  $('#card-content').height();
      $cardContent.animate({
        height: contentHeight - footerHeight
      }, 300);
      $cardFooter.show().animate({
        height: footerHeight
      }, 300);
    }
  },
  'click .cli-delete': function (event, template) {
    ChecklistItems.remove(this._id);
  },
  'mouseenter #closeBtn': function(evnet) {
    $(evnet.target).removeClass('icon-remove-circle').addClass('icon-remove-sign');
  },
  'mouseleave #closeBtn': function(evnet) {
    $(evnet.target).removeClass('icon-remove-sign').addClass('icon-remove-circle');
  },
  'click #closeBtn': function(evnet, template) {
    var $cardFooter = $('#card-footer');
    var $cardContent = $('#card-content');
    var footerHeight = 50;
    var contentHeight  =  $('#card-content').height();
    $cardContent.animate({
      height: contentHeight + footerHeight
    }, 300);
    $cardFooter.children().not('#closeBtn').animate({
      height: 0
    }, 300, function() {  $(this).hide() }) ;
    $cardFooter.hide();
  }
});<commit_msg>append important comment<commit_after>ChecklistItems = new Meteor.Collection('checklistItems');

Template.card.rendered = function() {
  var _this = this;
  var cardId = _this.data._id;

  Meteor.subscribe('checklistItems', function() {
    var data = function() {
      var clItems = ChecklistItems.find({ cardId: cardId });
      var clearifyId = function (doc) {
        doc._id = doc._id._str;
        return doc;
      }
      // remove the string "ObjectId()" to simplify _id
      var newClItems = clItems.map(clearifyId);
      Meteor.subscribe('card-by-id', cardId._str);
      var card = Cards.findOne({ _id: cardId });
      var clItemIds = card.cli_order.split(',');
      var finalClItems = new Array(clItemIds.length);
      newClItems.forEach(function(item) {
        var index = $.inArray(item._id, clItemIds);
        if (index > -1) {
          finalClItems[index] = item;
        } else {
          // because checklistItem collection updating is priority to card collection(fields: cli_order
          // and moved_cli_id) updating, if index === -1, then we just now insert a new checklist item
          // into checklistItem collection while, at this point, card collection hasn't been updated
          // correspondingly. So we need to put it in the end.
          finalClItems[finalClItems.length - 1] = item;
        }
      });
      // if newClItems is smaller than clItemIds, is says that we just now delete one checklist item
      // from checklistItem collection. We need to remove one meaningless null value from finalClItems.
      if (newClItems.length === clItemIds.length - 1) {
        for (var i=0; i< newClItems.length; i++) {
          if (!finalClItems[i]) {
            break;
          }
        }
        finalClItems.splice(i, 1);
      }

      return finalClItems;
    };
    var tmpl = function() {
      return Template.checklistItem;
    };
    Blaze.render(Blaze.Each(data, tmpl), _this.$('.checklist')[0]);
  });

  var $sortableCL = $('#card-content .checklist').sortable({
    placeholder: 'ui-state-highlight',
    stop: function(event, ui) {
      var cli_order = $sortableCL.sortable('toArray').join(',');
      Cards.update(_this.data._id, { $set: {cli_order: cli_order, moved_cli_id: ui.item[0].id }});
    }
  })
  .disableSelection();

  // resort the checklist items of non-current client when the cli_order field of card collection
  // is updated
  Tracker.autorun(function () {
    Meteor.subscribe('current-card-by-id', cardId);
    var currentCard = Cards.findOne(cardId);
    var moved_cli_id = currentCard.moved_cli_id;
    var $moved_cli_id = $('#' + moved_cli_id);
    var $clItems = $sortableCL.children();
    var index = $.inArray(moved_cli_id, currentCard.cli_order.split(','));
    // if checklist items of current client is empty, there is no need to ajust order for other client
    if ($clItems.length) {
      // insert a new checklist item
      if (index === $clItems.length) {
        // juse need to resort checklist item in non-current client, because
        // cheklist item is in order already.
        if ($clItems.last()[0].id !== moved_cli_id) {
          $sortableCL.append($moved_cli_id);
        }
      } else if (index === -1) { // delete the checklist item with id moved_cli_id
        $moved_cli_id.remove();
      } else { // just drag and drop existing checklist items in current card
        if($clItems.eq(index)[0].id !== moved_cli_id) {
          if (index > $moved_cli_id.index('.checklistItem')) {
            $moved_cli_id.insertAfter($clItems.eq(index));
          } else {
            $moved_cli_id.insertBefore($clItems.eq(index));
          }
        }
      }
    }
  });
};

Template.card.events({
  'click #card-caption .static-view span': function(event, template) {
    $cardContent = $(event.target);
    template.$('.static-view').hide();
    Blaze.renderWithData(Template.editCardName, { cardId: this._id, name: $cardContent.text() },
      template.$('#card-caption')[0]);
  },
  'click #add-cli-option': function (event, template) {
    var $cardFooter = $('#card-footer');
    var $cardContent = $('#card-content');
    if ($cardFooter.is(':hidden')) {
      if(!$('#text-addCLI').length) {
        Blaze.renderWithData(Template.addChecklistItem, {cardId: this._id}, $cardFooter[0]);
      }
      var footerHeight = 50;
      var contentHeight  =  $('#card-content').height();
      $cardContent.animate({ height: contentHeight - footerHeight }, 300);
      $cardFooter.show().animate({ height: footerHeight }, 300);
    }
  },
  'click .cli-delete': function (event, template) {
    ChecklistItems.remove(this._id);
  },
  'mouseenter #closeBtn': function(evnet) {
    $(evnet.target).removeClass('icon-remove-circle').addClass('icon-remove-sign');
  },
  'mouseleave #closeBtn': function(evnet) {
    $(evnet.target).removeClass('icon-remove-sign').addClass('icon-remove-circle');
  },
  'click #closeBtn': function(evnet, template) {
    var $cardFooter = $('#card-footer');
    var $cardContent = $('#card-content');
    var footerHeight = 50;
    var contentHeight  =  $('#card-content').height();
    $cardContent.animate({ height: contentHeight + footerHeight }, 300);
    $cardFooter.children().not('#closeBtn').animate({ height: 0 }, 300, function() {
      $(this).hide();
    });
    $cardFooter.hide();
  }
});<|endoftext|>


<commit_before>"use strict";

const $ = require("jquery");
const templates = require("../views");
const options = require("./options");
const renderPreview = require("./renderPreview");
const utils = require("./utils");
const sorting = require("./sorting");
const constants = require("./constants");
const condensed = require("./condensed");
const JoinChannel = require("./join-channel");
const helpers_parse = require("./libs/handlebars/parse");

const chat = $("#chat");
const sidebar = $("#sidebar");

require("intersection-observer");

const historyObserver = window.IntersectionObserver ?
	new window.IntersectionObserver(loadMoreHistory, {
		root: chat.get(0),
	}) : null;

module.exports = {
	appendMessage,
	buildChannelMessages,
	renderChannel,
	renderChannelUsers,
	renderNetworks,
	trimMessageInChannel,
};

function buildChannelMessages(container, chanId, chanType, messages) {
	return messages.reduce((docFragment, message) => {
		appendMessage(docFragment, chanId, chanType, message);
		return docFragment;
	}, container);
}

function appendMessage(container, chanId, chanType, msg) {
	if (utils.lastMessageId < msg.id) {
		utils.lastMessageId = msg.id;
	}

	let lastChild = container.children(".msg, .date-marker-container").last();
	const renderedMessage = buildChatMessage(msg);

	// Check if date changed
	const msgTime = new Date(msg.time);
	const prevMsgTime = new Date(lastChild.data("time"));

	// Insert date marker if date changed compared to previous message
	if (prevMsgTime.toDateString() !== msgTime.toDateString()) {
		lastChild = $(templates.date_marker({time: msg.time}));
		container.append(lastChild);
	}

	// If current window is not a channel or this message is not condensable,
	// then just append the message to container and be done with it
	if (constants.condensedTypes.indexOf(msg.type) === -1 || chanType !== "channel") {
		container.append(renderedMessage);
		return;
	}

	const obj = {};
	obj[msg.type] = 1;

	// If the previous message is already condensed,
	// we just append to it and update text
	if (lastChild.hasClass("condensed")) {
		lastChild.append(renderedMessage);
		condensed.updateText(lastChild, obj);
		return;
	}

	// Always create a condensed container
	const newCondensed = $(templates.msg_condensed({time: msg.time}));

	condensed.updateText(newCondensed, obj);
	newCondensed.append(renderedMessage);
	container.append(newCondensed);
}

function buildChatMessage(msg) {
	const type = msg.type;
	let template = "msg";

	// See if any of the custom highlight regexes match
	if (!msg.highlight && !msg.self
		&& options.highlightsRE
		&& (type === "message" || type === "notice")
		&& options.highlightsRE.exec(msg.text)) {
		msg.highlight = true;
	}

	if (constants.actionTypes.indexOf(type) !== -1) {
		msg.template = "actions/" + type;
		template = "msg_action";
	} else if (type === "unhandled") {
		template = "msg_unhandled";
	}

	const renderedMessage = $(templates[template](msg));
	const content = renderedMessage.find(".content");

	if (template === "msg_action") {
		content.html(templates.actions[type](msg));
	}

	msg.previews.forEach((preview) => {
		renderPreview(preview, renderedMessage);
	});

	return renderedMessage;
}

function renderChannel(data) {
	renderChannelMessages(data);

	if (data.type === "channel") {
		renderChannelUsers(data);
	}

	if (historyObserver) {
		historyObserver.observe(chat.find("#chan-" + data.id + " .show-more").get(0));
	}
}

function renderChannelMessages(data) {
	const documentFragment = buildChannelMessages($(document.createDocumentFragment()), data.id, data.type, data.messages);
	const channel = chat.find("#chan-" + data.id + " .messages").append(documentFragment);

	renderUnreadMarker($(templates.unread_marker()), data.firstUnread, channel);
}

function renderUnreadMarker(template, firstUnread, channel) {
	if (firstUnread > 0) {
		let first = channel.find("#msg-" + firstUnread);

		if (!first.length) {
			template.data("unread-id", firstUnread);
			channel.prepend(template);
		} else {
			const parent = first.parent();

			if (parent.hasClass("condensed")) {
				first = parent;
			}

			first.before(template);
		}
	} else {
		channel.append(template);
	}
}

function renderChannelUsers(data) {
	const users = chat.find("#chan-" + data.id).find(".users");
	const nicks = data.users
		.concat() // Make a copy of the user list, sort is applied in-place
		.sort((a, b) => b.lastMessage - a.lastMessage)
		.map((a) => a.nick);

	const search = users
		.find(".search")
		.prop("placeholder", nicks.length + " " + (nicks.length === 1 ? "user" : "users"));

	users
		.data("nicks", nicks)
		.find(".names-original")
		.html(templates.user(data));

	// Refresh user search
	if (search.val().length) {
		search.trigger("input");
	}
}

function renderNetworks(data, singleNetwork) {
	sidebar.find(".empty").hide();
	sidebar.find(".networks").append(
		templates.network({
			networks: data.networks,
		})
	);

	// Add keyboard handlers to the "Join a channel…" form inputs/button
	JoinChannel.handleKeybinds();

	let newChannels;
	const channels = $.map(data.networks, function(n) {
		return n.channels;
	});

	if (!singleNetwork && utils.lastMessageId > -1) {
		newChannels = [];

		channels.forEach((channel) => {
			const chan = $("#chan-" + channel.id);

			if (chan.length > 0) {
				if (chan.data("type") === "channel") {
					chan
						.data("needsNamesRefresh", true)
						.find(".header .topic")
						.html(helpers_parse(channel.topic))
						.prop("title", channel.topic);
				}

				if (channel.messages.length > 0) {
					const container = chan.find(".messages");
					buildChannelMessages(container, channel.id, channel.type, channel.messages);

					const unreadMarker = container.find(".unread-marker").data("unread-id", 0);
					renderUnreadMarker(unreadMarker, channel.firstUnread, container);

					if (container.find(".msg").length >= 100) {
						container.find(".show-more").addClass("show");
					}

					container.trigger("keepToBottom");
				}
			} else {
				newChannels.push(channel);
			}
		});
	} else {
		newChannels = channels;
	}

	chat.append(
		templates.chat({
			channels: channels,
		})
	);

	newChannels.forEach((channel) => {
		renderChannel(channel);

		if (channel.type === "channel") {
			chat.find("#chan-" + channel.id).data("needsNamesRefresh", true);
		}
	});

	utils.confirmExit();
	sorting();

	if (sidebar.find(".highlight").length) {
		utils.toggleNotificationMarkers(true);
	}
}

function trimMessageInChannel(channel, messageLimit) {
	const messages = channel.find(".messages .msg").slice(0, -messageLimit);

	if (messages.length === 0) {
		return;
	}

	messages.remove();

	channel.find(".show-more").addClass("show");

	// Remove date-separators that would otherwise be "stuck" at the top of the channel
	channel.find(".date-marker-container").each(function() {
		if ($(this).next().hasClass("date-marker-container")) {
			$(this).remove();
		}
	});
}

function loadMoreHistory(entries) {
	entries.forEach((entry) => {
		if (!entry.isIntersecting) {
			return;
		}

		var target = $(entry.target).find(".show-more-button");

		if (target.prop("disabled")) {
			return;
		}

		target.trigger("click");
	});
}
<commit_msg>Fix duplicate chat containers and unread markers when reconnecting<commit_after>"use strict";

const $ = require("jquery");
const templates = require("../views");
const options = require("./options");
const renderPreview = require("./renderPreview");
const utils = require("./utils");
const sorting = require("./sorting");
const constants = require("./constants");
const condensed = require("./condensed");
const JoinChannel = require("./join-channel");
const helpers_parse = require("./libs/handlebars/parse");

const chat = $("#chat");
const sidebar = $("#sidebar");

require("intersection-observer");

const historyObserver = window.IntersectionObserver ?
	new window.IntersectionObserver(loadMoreHistory, {
		root: chat.get(0),
	}) : null;

module.exports = {
	appendMessage,
	buildChannelMessages,
	renderChannel,
	renderChannelUsers,
	renderNetworks,
	trimMessageInChannel,
};

function buildChannelMessages(container, chanId, chanType, messages) {
	return messages.reduce((docFragment, message) => {
		appendMessage(docFragment, chanId, chanType, message);
		return docFragment;
	}, container);
}

function appendMessage(container, chanId, chanType, msg) {
	if (utils.lastMessageId < msg.id) {
		utils.lastMessageId = msg.id;
	}

	let lastChild = container.children(".msg, .date-marker-container").last();
	const renderedMessage = buildChatMessage(msg);

	// Check if date changed
	const msgTime = new Date(msg.time);
	const prevMsgTime = new Date(lastChild.data("time"));

	// Insert date marker if date changed compared to previous message
	if (prevMsgTime.toDateString() !== msgTime.toDateString()) {
		lastChild = $(templates.date_marker({time: msg.time}));
		container.append(lastChild);
	}

	// If current window is not a channel or this message is not condensable,
	// then just append the message to container and be done with it
	if (constants.condensedTypes.indexOf(msg.type) === -1 || chanType !== "channel") {
		container.append(renderedMessage);
		return;
	}

	const obj = {};
	obj[msg.type] = 1;

	// If the previous message is already condensed,
	// we just append to it and update text
	if (lastChild.hasClass("condensed")) {
		lastChild.append(renderedMessage);
		condensed.updateText(lastChild, obj);
		return;
	}

	// Always create a condensed container
	const newCondensed = $(templates.msg_condensed({time: msg.time}));

	condensed.updateText(newCondensed, obj);
	newCondensed.append(renderedMessage);
	container.append(newCondensed);
}

function buildChatMessage(msg) {
	const type = msg.type;
	let template = "msg";

	// See if any of the custom highlight regexes match
	if (!msg.highlight && !msg.self
		&& options.highlightsRE
		&& (type === "message" || type === "notice")
		&& options.highlightsRE.exec(msg.text)) {
		msg.highlight = true;
	}

	if (constants.actionTypes.indexOf(type) !== -1) {
		msg.template = "actions/" + type;
		template = "msg_action";
	} else if (type === "unhandled") {
		template = "msg_unhandled";
	}

	const renderedMessage = $(templates[template](msg));
	const content = renderedMessage.find(".content");

	if (template === "msg_action") {
		content.html(templates.actions[type](msg));
	}

	msg.previews.forEach((preview) => {
		renderPreview(preview, renderedMessage);
	});

	return renderedMessage;
}

function renderChannel(data) {
	renderChannelMessages(data);

	if (data.type === "channel") {
		renderChannelUsers(data);
	}

	if (historyObserver) {
		historyObserver.observe(chat.find("#chan-" + data.id + " .show-more").get(0));
	}
}

function renderChannelMessages(data) {
	const documentFragment = buildChannelMessages($(document.createDocumentFragment()), data.id, data.type, data.messages);
	const channel = chat.find("#chan-" + data.id + " .messages").append(documentFragment);

	renderUnreadMarker($(templates.unread_marker()), data.firstUnread, channel);
}

function renderUnreadMarker(template, firstUnread, channel) {
	if (firstUnread > 0) {
		let first = channel.find("#msg-" + firstUnread);

		if (!first.length) {
			template.data("unread-id", firstUnread);
			channel.prepend(template);
		} else {
			const parent = first.parent();

			if (parent.hasClass("condensed")) {
				first = parent;
			}

			first.before(template);
		}
	} else {
		channel.append(template);
	}
}

function renderChannelUsers(data) {
	const users = chat.find("#chan-" + data.id).find(".users");
	const nicks = data.users
		.concat() // Make a copy of the user list, sort is applied in-place
		.sort((a, b) => b.lastMessage - a.lastMessage)
		.map((a) => a.nick);

	const search = users
		.find(".search")
		.prop("placeholder", nicks.length + " " + (nicks.length === 1 ? "user" : "users"));

	users
		.data("nicks", nicks)
		.find(".names-original")
		.html(templates.user(data));

	// Refresh user search
	if (search.val().length) {
		search.trigger("input");
	}
}

function renderNetworks(data, singleNetwork) {
	sidebar.find(".empty").hide();
	sidebar.find(".networks").append(
		templates.network({
			networks: data.networks,
		})
	);

	// Add keyboard handlers to the "Join a channel…" form inputs/button
	JoinChannel.handleKeybinds();

	let newChannels;
	const channels = $.map(data.networks, function(n) {
		return n.channels;
	});

	if (!singleNetwork && utils.lastMessageId > -1) {
		newChannels = [];

		channels.forEach((channel) => {
			const chan = $("#chan-" + channel.id);

			if (chan.length > 0) {
				if (chan.data("type") === "channel") {
					chan
						.data("needsNamesRefresh", true)
						.find(".header .topic")
						.html(helpers_parse(channel.topic))
						.prop("title", channel.topic);
				}

				if (channel.messages.length > 0) {
					const container = chan.find(".messages");
					buildChannelMessages(container, channel.id, channel.type, channel.messages);

					const unreadMarker = container.find(".unread-marker").data("unread-id", 0);
					renderUnreadMarker(unreadMarker, channel.firstUnread, container);

					if (container.find(".msg").length >= 100) {
						container.find(".show-more").addClass("show");
					}

					container.trigger("keepToBottom");
				}
			} else {
				newChannels.push(channel);
			}
		});
	} else {
		newChannels = channels;
	}

	if (newChannels.length > 0) {
		chat.append(
			templates.chat({
				channels: newChannels,
			})
		);

		newChannels.forEach((channel) => {
			renderChannel(channel);

			if (channel.type === "channel") {
				chat.find("#chan-" + channel.id).data("needsNamesRefresh", true);
			}
		});
	}

	utils.confirmExit();
	sorting();

	if (sidebar.find(".highlight").length) {
		utils.toggleNotificationMarkers(true);
	}
}

function trimMessageInChannel(channel, messageLimit) {
	const messages = channel.find(".messages .msg").slice(0, -messageLimit);

	if (messages.length === 0) {
		return;
	}

	messages.remove();

	channel.find(".show-more").addClass("show");

	// Remove date-separators that would otherwise be "stuck" at the top of the channel
	channel.find(".date-marker-container").each(function() {
		if ($(this).next().hasClass("date-marker-container")) {
			$(this).remove();
		}
	});
}

function loadMoreHistory(entries) {
	entries.forEach((entry) => {
		if (!entry.isIntersecting) {
			return;
		}

		var target = $(entry.target).find(".show-more-button");

		if (target.prop("disabled")) {
			return;
		}

		target.trigger("click");
	});
}
<|endoftext|>


<commit_before>/**	Provides a Class and mixin that implement simple HTML views using Handlebars as a template engine.
 *	If called directly as a function, view() is an alias of {@link module:view.View View}.
 *	@module view
 *
 *	@example
 *
Basic Usage:

 *
 *		var view = require('view');
 *
 *		// Uses Handlebars templates:
 *		var template = '
{{{title}}}
 <button id="hi">Click Me</button>';
 *
 *		// Instantiate a view:
 *		var page = view( template );
 *
 *		// Wire up some events:
 *		page.hookEvents({
 *			'click button#hi' : function() {
 *				alert( 'Title: ' + page.$$('h1').textContent );
 *			}
 *		});
 *
 *		// Insert the view into the DOM:
 *		page.insertInto( document.body );
 *
 *	@example
 *
Integrated Example:

 *
 *		define([
 *			'view',
 *			'text!templates/index.html'		// just an HTML file
 *		], function(view, template) {
 *			var page = {
 *				events : {
 *					'click #submit' : 'submit',
 *					'mouseover .tip' : 'showTip'
 *				},
 *
 *				submit : function() {
 *					this.view.$$('form').subimt();
 *				},
 *
 *				showTip : function(e) {
 *					var tip = this.view.$$('#tip');
 *					tip.textContent = e.delegationTarget.getAttribute('title');
 *					tip.style.display = '';
 *				},
 *
 *				load : function(params, router) {
 *					if (!this.view) {
 *						// initialize a view:
 *						this.view = view(template);
 *
 *						// wire up event handlers:
 *						this.view.hookEvents(this.events, this);
 *					}
 *
 *					// Template some data into the view:
 *					this.view.template({
 *						title : 'Test Title',
 *						params : params
 *					});
 *
 *					// insert view into DOM:
 *					this.view.insertInto(document.body);
 *				},
 *
 *				unload : function() {
 *					// remove view from DOM:
 *					this.view.remove();
 *				}
 *			};
 *			return page;
 *		});
 *
 *	@example
 *
Concise Example:

 *
 *		var view = require('view');
 *
 *		var list = view({
 *			template : '
 {{#items}}
{{.}}
{{/items}}
',
 *
 *			events : {
 *				'click li' : 'clickItem'
 *			},
 *
 *			clickItem : function(e) {
 *				console.log(e.delegationTarget.getAttribute('title'));
 *			}
 *		};
 *
 *		// insert into a parent node:
 *		list.insertInto(document.body);
 *
 *		// template some data:
 *		list.render({
 *			items : [ 'Peach', 'Mango', 'Orange' ]
 *		});
 */
(function(g, factory) {
	if (typeof define==='function' && define.amd) {
		define(['util', 'events', 'handlebars'], factory);
	}
	else {
		g.view = factory(g.util, g.EventEmitter, g.handlebars);
	}
}(this, function(_, events, handlebars) {
	var EventEmitter = events.EventEmitter || events,
		proto = (window.Element || document.createElement('div').constructor).prototype,
		matches;

	/** Matches selector to passed-in DOM node (see handleDelegate)
	 *	@function
	 *	@ignore
	 */
	matches = proto.matches ||
		proto.webkitMatchesSelector ||
		proto.mozMatchesSelector ||
		proto.oMatchesSelector ||
		function(sel) {
			var els = document.querySelectorAll(sel);
			for (var i=els.length; i--; )
				if (els[i]===this)
					return true;
			return false;
		};

	/** Event delegation implementation: Initial set-up for hooking event
	*	@param {Element} node - The root DOM node for delegating the event to
	*	@param {string} type - Type of event
	*	@param {string} selector - CSS Selector for DOM node
	*	@param {string} callback - Event handler (callback function)
	*	@private
	*	@returns {boolean} handlerWasAdded
	*/
	function delegateFrom(node, type, selector, callback) {
		if (!node || !type || !selector || !callback) return false;

		if (!node._eventRegistry) node._eventRegistry = [];

		if (!node._eventTypes) node._eventTypes = {};

		if (!node._eventTypes.hasOwnProperty(type)) {
			node.addEventListener(type, handleDelegate);
			node._eventTypes[type] = true;
		}

		node._eventRegistry.push({
			type: type,
			selector: selector,
			callback: callback
		});
		return true;
	}

	/** Event delegation implementation: Delegation handler
	*	@param {Object} event - Event to delegate
	*/
	function handleDelegate(event) {
		var self = this,
			reg = this._eventRegistry,
			parent = event.target || event.srcElement,
			handlers = [],
			handler, i;

		for (i=reg.length; i--; ) {
			if (reg[i].type===event.type) {
				handlers.push(reg[i]);
			}
		}

		do {
			for (i=handlers.length; i--; ) {
				handler = handlers[i];
				if (matches.call(parent, handler.selector)) {
					event.currentTarget = event.delegationTarget = parent;
					if (handler.callback.call(parent, event)===false) {
						return false;
					}
				}
			}
		} while( (parent=parent.parentNode)!==this.parentNode );
	}

	/**	The View class.
	 *	@memberOf module:view
	 *	@constructor
	 *	@param {string} tpl - Template to inject
	 *	@param {string} name - Name of template
	 */
	function View(tpl, name) {
		if (!(this instanceof View)) return new View(tpl, name);

		this.base = document.createElement('div');

		if (typeof tpl==='object') {
			_.extend(this, tpl);
			if (tpl.events) {
				this.hookEvents(tpl.events, tpl);
			}
			name = tpl.name;
			tpl = tpl.template || tpl.tpl;
		}

		this.rawView = tpl;
		this.renderTemplate = handlebars.compile(this.rawView);

		if (name) {
			this.base.setAttribute('id', name + '-base');
		}
		this.base.className = 'view-base';
		this.base.innerHTML = this.rawView;

		this.name = name;
		if (this.name) {
			this.base.setAttribute('data-view', name);
		}
	}

	_.inherits(View, EventEmitter);

	_.extend(View.prototype, /** @lends module:view.View# */ {

		/** Render the view using the given data.
		 *	@param {Object} data	Template data fields to inject.
		 */
		render : function(data) {
			if (!this.renderTemplate || !this.base) return false;
			this.templateData = data;
			this.base.innerHTML = (data && this.renderTemplate(data)) || this.rawView;
			return this;
		},

		/** Alias of {@link View#render view.render()} */
		template : function(data) {
			return View.prototype.render.call(this, data);
		},

		/** Register a hash of selector-delegated event handler functions.
		 *	@param {Object} events	Events to register. Keys are of the form `event-type some#css.selector`.
		 */
		hookEvents : function(events, ctx) {
			var sep, evt, fn, selector, c, x;
			if (!this.base) return false;
			this.events = events;
			for (x in events) {
				if (events.hasOwnProperty(x)) {
					sep = x.split(' ');
					evt = sep[0];
					selector = sep.slice(1).join(' ');
					fn = events[x];
					if (ctx && typeof fn==='string') {
						fn = ctx[fn];
					}
					if (!delegateFrom(this.base, evt, selector, fn)) {
						throw new Error('Invalid event entry "'+x+'".');
					}
				}
			}
			return this;
		},

		/**	Insert the view into a given parent node.
		 *	@param {String|Element} parent	A DOM element, or a CSS selector representing one.
		 */
		insertInto : function(parent) {
			if (typeof parent==='string') {
				parent = document.querySelector(parent);
			}
			parent.appendChild(this.base);
			return this;
		},

		/**	Insert the view immediately after a given sibling node.
		 *	@param {String|Element} selector	A DOM element, or a CSS selector representing one.
		 */
		insertAfter : function(sibling) {
			if (typeof sibling==='string') {
				sibling = document.querySelector(sibling);
			}
			sibling.parentNode.insertBefore(this.base, sibling.nextSibling);
			return this;
		},

		remove : function() {
			if (this.base && this.base.parentNode) {
				this.base.parentNode.removeChild(this.base);
			}
			return this;
		},

		/** Get an Array of nodes contained in the view that match the given selector.
		 *  @param {string} selector	A CSS selector
		 *  @returns {array} matchedNodes
		 */
		find : function(selector) {
			return Array.prototype.slice.call(this.base.querySelectorAll(selector));
		},

		/** Return the first node contained in the view that matches the given selector.
		 *  @param {string} selector	A CSS selector
		 *  @returns {Element} node
		 */
		findOne : function(selector) {
			return this.base.querySelector(selector);
		},

		/** Convenient alias of {@link View#find view.find()} */
		$ : function(selector) {
			return this.find(selector);
		},

		/** Convenient alias of {@link View#findOne view.findOne()} */
		$$ : function(selector) {
			return this.findOne(selector);
		}

	});

	/**	If the module is called as a function, returns a new {@link module:view.View View} instance.
	 *	@name module:view.view
	 *	@function
	 */
	View.View = View.view = View;

	return View;
}));
<commit_msg>Properly bind event handlers<commit_after>/**	Provides a Class and mixin that implement simple HTML views using Handlebars as a template engine.
 *	If called directly as a function, view() is an alias of {@link module:view.View View}.
 *	@module view
 *
 *	@example
 *
Basic Usage:

 *
 *		var view = require('view');
 *
 *		// Uses Handlebars templates:
 *		var template = '
{{{title}}}
 <button id="hi">Click Me</button>';
 *
 *		// Instantiate a view:
 *		var page = view( template );
 *
 *		// Wire up some events:
 *		page.hookEvents({
 *			'click button#hi' : function() {
 *				alert( 'Title: ' + page.$$('h1').textContent );
 *			}
 *		});
 *
 *		// Insert the view into the DOM:
 *		page.insertInto( document.body );
 *
 *	@example
 *
Integrated Example:

 *
 *		define([
 *			'view',
 *			'text!templates/index.html'		// just an HTML file
 *		], function(view, template) {
 *			var page = {
 *				events : {
 *					'click #submit' : 'submit',
 *					'mouseover .tip' : 'showTip'
 *				},
 *
 *				submit : function() {
 *					this.view.$$('form').subimt();
 *				},
 *
 *				showTip : function(e) {
 *					var tip = this.view.$$('#tip');
 *					tip.textContent = e.delegationTarget.getAttribute('title');
 *					tip.style.display = '';
 *				},
 *
 *				load : function(params, router) {
 *					if (!this.view) {
 *						// initialize a view:
 *						this.view = view(template);
 *
 *						// wire up event handlers:
 *						this.view.hookEvents(this.events, this);
 *					}
 *
 *					// Template some data into the view:
 *					this.view.template({
 *						title : 'Test Title',
 *						params : params
 *					});
 *
 *					// insert view into DOM:
 *					this.view.insertInto(document.body);
 *				},
 *
 *				unload : function() {
 *					// remove view from DOM:
 *					this.view.remove();
 *				}
 *			};
 *			return page;
 *		});
 *
 *	@example
 *
Concise Example:

 *
 *		var view = require('view');
 *
 *		var list = view({
 *			template : '
 {{#items}}
{{.}}
{{/items}}
',
 *
 *			events : {
 *				'click li' : 'clickItem'
 *			},
 *
 *			clickItem : function(e) {
 *				console.log(e.delegationTarget.getAttribute('title'));
 *			}
 *		};
 *
 *		// insert into a parent node:
 *		list.insertInto(document.body);
 *
 *		// template some data:
 *		list.render({
 *			items : [ 'Peach', 'Mango', 'Orange' ]
 *		});
 */
(function(g, factory) {
	if (typeof define==='function' && define.amd) {
		define(['util', 'events', 'handlebars'], factory);
	}
	else {
		g.view = factory(g.util, g.EventEmitter, g.handlebars);
	}
}(this, function(_, events, handlebars) {
	var EventEmitter = events.EventEmitter || events,
		proto = (window.Element || document.createElement('div').constructor).prototype,
		matches;

	/** Matches selector to passed-in DOM node (see handleDelegate)
	 *	@function
	 *	@ignore
	 */
	matches = proto.matches ||
		proto.webkitMatchesSelector ||
		proto.mozMatchesSelector ||
		proto.oMatchesSelector ||
		function(sel) {
			var els = document.querySelectorAll(sel);
			for (var i=els.length; i--; )
				if (els[i]===this)
					return true;
			return false;
		};

	/** Event delegation implementation: Initial set-up for hooking event
	*	@param {Element} node - The root DOM node for delegating the event to
	*	@param {string} type - Type of event
	*	@param {string} selector - CSS Selector for DOM node
	*	@param {string} callback - Event handler (callback function)
	*	@private
	*	@returns {boolean} handlerWasAdded
	*/
	function delegateFrom(node, type, selector, callback) {
		if (!node || !type || !selector || !callback) return false;

		if (!node._eventRegistry) node._eventRegistry = [];

		if (!node._eventTypes) node._eventTypes = {};

		if (!node._eventTypes.hasOwnProperty(type)) {
			node.addEventListener(type, handleDelegate);
			node._eventTypes[type] = true;
		}

		node._eventRegistry.push({
			type: type,
			selector: selector,
			callback: callback
		});
		return true;
	}

	/** Event delegation implementation: Delegation handler
	*	@param {Object} event - Event to delegate
	*/
	function handleDelegate(event) {
		var self = this,
			reg = this._eventRegistry,
			parent = event.target || event.srcElement,
			handlers = [],
			handler, i;

		for (i=reg.length; i--; ) {
			if (reg[i].type===event.type) {
				handlers.push(reg[i]);
			}
		}

		do {
			for (i=handlers.length; i--; ) {
				handler = handlers[i];
				if (matches.call(parent, handler.selector)) {
					event.currentTarget = event.delegationTarget = parent;
					if (handler.callback.call(parent, event)===false) {
						return false;
					}
				}
			}
		} while( (parent=parent.parentNode)!==this.parentNode );
	}

	/**	The View class.
	 *	@memberOf module:view
	 *	@constructor
	 *	@param {string} tpl - Template to inject
	 *	@param {string} name - Name of template
	 */
	function View(tpl, name) {
		if (!(this instanceof View)) return new View(tpl, name);

		this.base = document.createElement('div');

		if (typeof tpl==='object') {
			_.extend(this, tpl);
			if (tpl.events) {
				this.hookEvents(tpl.events, tpl);
			}
			name = tpl.name;
			tpl = tpl.template || tpl.tpl;
		}

		this.rawView = tpl;
		this.renderTemplate = handlebars.compile(this.rawView);

		if (name) {
			this.base.setAttribute('id', name + '-base');
		}
		this.base.className = 'view-base';
		this.base.innerHTML = this.rawView;

		this.name = name;
		if (this.name) {
			this.base.setAttribute('data-view', name);
		}
	}

	_.inherits(View, EventEmitter);

	_.extend(View.prototype, /** @lends module:view.View# */ {

		/** Render the view using the given data.
		 *	@param {Object} data	Template data fields to inject.
		 */
		render : function(data) {
			if (!this.renderTemplate || !this.base) return false;
			this.templateData = data;
			this.base.innerHTML = (data && this.renderTemplate(data)) || this.rawView;
			return this;
		},

		/** Alias of {@link View#render view.render()} */
		template : function(data) {
			return View.prototype.render.call(this, data);
		},

		/** Register a hash of selector-delegated event handler functions.
		 *	@param {Object} events	Events to register. Keys are of the form `event-type some#css.selector`.
		 */
		hookEvents : function(events, ctx) {
			var sep, evt, fn, selector, c, x;
			if (!this.base) return false;
			this.events = events;
			for (x in events) {
				if (events.hasOwnProperty(x)) {
					sep = x.split(' ');
					evt = sep[0];
					selector = sep.slice(1).join(' ');
					fn = events[x];
					if (ctx && typeof fn==='string') {
						fn = ctx[fn].bind(fn);
					}
					if (!delegateFrom(this.base, evt, selector, fn)) {
						throw new Error('Invalid event entry "'+x+'".');
					}
				}
			}
			return this;
		},

		/**	Insert the view into a given parent node.
		 *	@param {String|Element} parent	A DOM element, or a CSS selector representing one.
		 */
		insertInto : function(parent) {
			if (typeof parent==='string') {
				parent = document.querySelector(parent);
			}
			parent.appendChild(this.base);
			return this;
		},

		/**	Insert the view immediately after a given sibling node.
		 *	@param {String|Element} selector	A DOM element, or a CSS selector representing one.
		 */
		insertAfter : function(sibling) {
			if (typeof sibling==='string') {
				sibling = document.querySelector(sibling);
			}
			sibling.parentNode.insertBefore(this.base, sibling.nextSibling);
			return this;
		},

		remove : function() {
			if (this.base && this.base.parentNode) {
				this.base.parentNode.removeChild(this.base);
			}
			return this;
		},

		/** Get an Array of nodes contained in the view that match the given selector.
		 *  @param {string} selector	A CSS selector
		 *  @returns {array} matchedNodes
		 */
		find : function(selector) {
			return Array.prototype.slice.call(this.base.querySelectorAll(selector));
		},

		/** Return the first node contained in the view that matches the given selector.
		 *  @param {string} selector	A CSS selector
		 *  @returns {Element} node
		 */
		findOne : function(selector) {
			return this.base.querySelector(selector);
		},

		/** Convenient alias of {@link View#find view.find()} */
		$ : function(selector) {
			return this.find(selector);
		},

		/** Convenient alias of {@link View#findOne view.findOne()} */
		$$ : function(selector) {
			return this.findOne(selector);
		}

	});

	/**	If the module is called as a function, returns a new {@link module:view.View View} instance.
	 *	@name module:view.view
	 *	@function
	 */
	View.View = View.view = View;

	return View;
}));
<|endoftext|>


<commit_before>var $assign = require('spore-kit-obj/src/assign');

/**
构建一个视图模块，用命名空间的方式完成事件绑定和数据绑定
@constructor spore-kit-wapp/src/view
@param {object} [options] 选项
@example

// ------------------- list.wxml -------------------
<template name="list">
	<view class="p-listbox">
		<text>{{name}}:{{ns}}</text>
		<button type="primary" bindtap="{{ns}}:add">点击添加一项</button>
		<view class="list">
			<text wx:for="{{list}}">{{item}}</text>
		</view>
	</view>
</template>
// ------------------- /list.wxml -------------------

// ------------------- list.js -------------------
var $assign = require('spore-kit-obj/src/assign');
var $view = require('spore-kit-wapp/src/view');

function List(options){

	var conf = $assign({
		context : null,
		name : 'list'
	}, options);

	var data = {
		list : [0, 1, 2]
	};

	var self = new $view({
		context : conf.context,
		name : conf.name,
		data : data
	});

	self.add = function(){
		data.list.push(data.list.length);
		self.setData();
	};

	self.setData();

	self.attach(
		'add'
	);

	return self;

}

module.exports = List;
// ------------------- /list.js -------------------

// ------------------- index.wxml -------------------
<import src="list.wxml"/>
<view class="container">
	<template is="list" data="{{...list1}}"/>
	<template is="list" data="{{...list2}}"/>
</view>
// ------------------- /index.wxml -------------------

// ------------------- index.js -------------------
var $list = require('./list');

// 在 Page 中绑定视图对象
Page({
	data: {},
	onLoad: function() {

		// 数据被绑定在 this.data.list1
		$list({
			context : this,
			name : 'list1'
		});

		// 数据被绑定在 this.data.list2
		$list({
			context : this,
			name : 'list2'
		});

	}
});
// ------------------- /index.js -------------------

**/
var View = function(options){

	/**
	选项对象
	@type {object}
	@property {object} conf.context 绑定视图组件的根节点
	@property {string} conf.name 命名空间名称
	@property {object} conf.model 视图的数据模型
	**/
	this.conf = $assign({
		// 每个视图组件都需要传入 context 作为绑定视图的根节点
		// context 可以是顶层的 Page , 也可以是一个实例化的 view
		context : null,
		// name 参数为组件命名空间名称
		// 数据将会被绑定到 context.data[name]
		name : '',
		// 传入的视图数据模型相当于 view-model
		data : {}
	}, options);

	this.data = this.conf.data || {};
	this.context = this.conf.context;

	// 默认数据模型上提供 ns 属性来标记完整的命名空间路径，便于绑定事件
	this.name = this.conf.name;
	this.namespace = this.getNameSpace();
	this.data.name = this.name;
	this.data.ns = this.namespace;

	//绑定到视图的方法统一存放在 bound 对象
	this.bound = {};
};

View.prototype = {
	/**
	设置模型数据

	@param {object} model 要配置的数据对象
	@return {object} 混合了模型的数据
	@example
	view.setData({
		name : 'viewName',
		list : [1,2,3]
	});
	**/
	setData : function(model){
		var nsdata = {};
		if(!this.name){
			throw('Every view need argument "name" as namespace.');
		}
		$assign(this.data, model);
		var data = $assign({}, this.data);
		nsdata[this.name] = data;
		if(this.context && typeof(this.context.setData) === 'function'){
			this.context.setData(nsdata);
		}
		return data;
	},
	/**
	获取命名空间路径，基于 Page 对象

	@return {string} 路径字符串
	@example
	var parentView = new View({
		context : page,
		name : 'parent'
	});

	var view = new View({
		context : parentView,
		name : 'cihld'
	});

	console.info(view.name);	//child
	console.info(view.getNameSapce()); //parent.child
	**/
	getNameSpace : function(){
		var parent = '';
		if(this.context && typeof(this.context.getNameSpace) === 'function'){
			parent = this.context.getNameSpace();
		}

		if(!parent){
			return this.name;
		}else{
			return parent + '.' + this.name;
		}
	},
	/**
	获取视图所在的 page 对象

	@return {object} 根节点，小程序 Page 方法实例化的对象
	**/
	getPage : function(){
		var top = this.context;
		do{
			if(top.context && top.context instanceof View){
				top = top.context;
			}
		}while(top.context);
		return top;
	},
	/**
	将事件绑定到根节点，以命名空间路径作为前缀
	事件函数 self.fn 将会被绑定到 page[ns + ':' + 'fn']

	@param {string} eventName 事件名称，与对象上的方法同名
	@example

	// js
	var view = new View({
		context : page,
		name : 'demo'
	});

	view.add = function(){};
	view.remove = function(){};

	view.attach(
		'add',
		'remove'
	);

	Page({
		data: {},
		onLoad: function() {
			view({
				context : this,
				name : 'demo'
			});
		}
	});

	// wxml
	<template name="demo">
		<button bindtap="{{ns}}:add">添加</button>
		<button bindtap="{{ns}}:remove">移除</button>
	</template>

	<view class="container">
		<template is="demo" data="{{...demo}}"/>
	</view>

	**/
	attach : function(){
		var page = this.getPage();
		if(!page){return;}

		var namespace = this.namespace;

		var args = Array.prototype.slice.call(arguments);
		args.forEach(function(method){
			if(typeof this[method] === 'function'){
				this.bound[method] = function(){
					this[method].apply(this, arguments);
				}.bind(this);
				page[namespace + ':' + method] = this.bound[method];
			}
		}, this);
	},
	/**
	将事件绑定到根节点，以命名空间路径作为前缀

	@param {string} eventName 事件名称，与对象上的方法同名
	@example

	// 移除指定事件
	view.detach(
		'add'
		'remove'
	);

	// 移除所有事件
	view.detach();
	**/
	detach : function(){
		var page = this.getPage();
		if(!page){return;}

		var namespace = this.namespace;

		var args = Array.prototype.slice.call(arguments);
		var events = args.length === 0 ? Object.keys(this.bound) : args;
		events.forEach(function(method){
			delete this.bound[method];
			delete page[namespace + ':' + method];
		}, this);
	},
	// 销毁视图对象
	destroy : function(){
		this.detach();
		this.model = {};
		this.setData();
	}
};

module.exports = View;

<commit_msg>修正未能寻找到顶层page节点的bug<commit_after>var $assign = require('spore-kit-obj/src/assign');

/**
构建一个视图模块，用命名空间的方式完成事件绑定和数据绑定
@constructor spore-kit-wapp/src/view
@param {object} [options] 选项
@example

// ------------------- list.wxml -------------------
<template name="list">
	<view class="p-listbox">
		<text>{{name}}:{{ns}}</text>
		<button type="primary" bindtap="{{ns}}:add">点击添加一项</button>
		<view class="list">
			<text wx:for="{{list}}">{{item}}</text>
		</view>
	</view>
</template>
// ------------------- /list.wxml -------------------

// ------------------- list.js -------------------
var $assign = require('spore-kit-obj/src/assign');
var $view = require('spore-kit-wapp/src/view');

function List(options){

	var conf = $assign({
		context : null,
		name : 'list'
	}, options);

	var data = {
		list : [0, 1, 2]
	};

	var self = new $view({
		context : conf.context,
		name : conf.name,
		data : data
	});

	self.add = function(){
		data.list.push(data.list.length);
		self.setData();
	};

	self.setData();

	self.attach(
		'add'
	);

	return self;

}

module.exports = List;
// ------------------- /list.js -------------------

// ------------------- index.wxml -------------------
<import src="list.wxml"/>
<view class="container">
	<template is="list" data="{{...list1}}"/>
	<template is="list" data="{{...list2}}"/>
</view>
// ------------------- /index.wxml -------------------

// ------------------- index.js -------------------
var $list = require('./list');

// 在 Page 中绑定视图对象
Page({
	data: {},
	onLoad: function() {

		// 数据被绑定在 this.data.list1
		$list({
			context : this,
			name : 'list1'
		});

		// 数据被绑定在 this.data.list2
		$list({
			context : this,
			name : 'list2'
		});

	}
});
// ------------------- /index.js -------------------

**/
var View = function(options){

	/**
	选项对象
	@type {object}
	@property {object} conf.context 绑定视图组件的根节点
	@property {string} conf.name 命名空间名称
	@property {object} conf.model 视图的数据模型
	**/
	this.conf = $assign({
		// 每个视图组件都需要传入 context 作为绑定视图的根节点
		// context 可以是顶层的 Page , 也可以是一个实例化的 view
		context : null,
		// name 参数为组件命名空间名称
		// 数据将会被绑定到 context.data[name]
		name : '',
		// 传入的视图数据模型相当于 view-model
		data : {}
	}, options);

	this.data = this.conf.data || {};
	this.context = this.conf.context;

	// 默认数据模型上提供 ns 属性来标记完整的命名空间路径，便于绑定事件
	this.name = this.conf.name;
	this.namespace = this.getNameSpace();
	this.data.name = this.name;
	this.data.ns = this.namespace;

	//绑定到视图的方法统一存放在 bound 对象
	this.bound = {};
};

View.prototype = {
	/**
	设置模型数据

	@param {object} model 要配置的数据对象
	@return {object} 混合了模型的数据
	@example
	view.setData({
		name : 'viewName',
		list : [1,2,3]
	});
	**/
	setData : function(model){
		var nsdata = {};
		if(!this.name){
			throw('Every view need argument "name" as namespace.');
		}
		$assign(this.data, model);
		var data = $assign({}, this.data);
		nsdata[this.name] = data;
		if(this.context && typeof(this.context.setData) === 'function'){
			this.context.setData(nsdata);
		}
		return data;
	},
	/**
	获取命名空间路径，基于 Page 对象

	@return {string} 路径字符串
	@example
	var parentView = new View({
		context : page,
		name : 'parent'
	});

	var view = new View({
		context : parentView,
		name : 'cihld'
	});

	console.info(view.name);	//child
	console.info(view.getNameSapce()); //parent.child
	**/
	getNameSpace : function(){
		var parent = '';
		if(this.context && typeof(this.context.getNameSpace) === 'function'){
			parent = this.context.getNameSpace();
		}

		if(!parent){
			return this.name;
		}else{
			return parent + '.' + this.name;
		}
	},
	/**
	获取视图所在的 page 对象

	@return {object} 根节点，小程序 Page 方法实例化的对象
	**/
	getPage : function(){
		var top = this.context;
		while(top.context && top instanceof View){
			top = top.context;
		}
		return top;
	},
	/**
	将事件绑定到根节点，以命名空间路径作为前缀
	事件函数 self.fn 将会被绑定到 page[ns + ':' + 'fn']

	@param {string} eventName 事件名称，与对象上的方法同名
	@example

	// js
	var view = new View({
		context : page,
		name : 'demo'
	});

	view.add = function(){};
	view.remove = function(){};

	view.attach(
		'add',
		'remove'
	);

	Page({
		data: {},
		onLoad: function() {
			view({
				context : this,
				name : 'demo'
			});
		}
	});

	// wxml
	<template name="demo">
		<button bindtap="{{ns}}:add">添加</button>
		<button bindtap="{{ns}}:remove">移除</button>
	</template>

	<view class="container">
		<template is="demo" data="{{...demo}}"/>
	</view>

	**/
	attach : function(){
		var page = this.getPage();
		if(!page){return;}

		var namespace = this.namespace;

		var args = Array.prototype.slice.call(arguments);
		args.forEach(function(method){
			if(typeof this[method] === 'function'){
				this.bound[method] = function(){
					this[method].apply(this, arguments);
				}.bind(this);
				page[namespace + ':' + method] = this.bound[method];
			}
		}, this);
	},
	/**
	将事件绑定到根节点，以命名空间路径作为前缀

	@param {string} eventName 事件名称，与对象上的方法同名
	@example

	// 移除指定事件
	view.detach(
		'add'
		'remove'
	);

	// 移除所有事件
	view.detach();
	**/
	detach : function(){
		var page = this.getPage();
		if(!page){return;}

		var namespace = this.namespace;

		var args = Array.prototype.slice.call(arguments);
		var events = args.length === 0 ? Object.keys(this.bound) : args;
		events.forEach(function(method){
			delete this.bound[method];
			delete page[namespace + ':' + method];
		}, this);
	},
	// 销毁视图对象
	destroy : function(){
		this.detach();
		this.model = {};
		this.setData();
	}
};

module.exports = View;

<|endoftext|>


<commit_before>"use strict";

var util = require("substance-util");

// Substance.View
// ==========================================================================
//
// Application View abstraction, inspired by Backbone.js

var View = function(options) {
  options = options || {};
  var that = this;
  // Either use the provided element or make up a new element
  this.el = options.el || window.document.createElement(options.elementType || 'div');
  this.$el = $(this.el);

  this.dispatchDOMEvents();
};

View.Prototype = function() {

  // Shorthand for selecting elements within the view
  // ----------
  //

  this.$ = function(selector) {
    return this.$el.find(selector);
  };

  this.render = function() {
    return this;
  };

  // Dispatching DOM events (like clicks)
  // ----------
  //

  this.dispatchDOMEvents = function() {

    var that = this;

    // showReport(foo) => ["showReport(foo)", "showReport", "foo"]
    // showReport(12) => ["showReport(12)", "showReport", "12"]
    function extractFunctionCall(str) {
      var match = /(\w+)\((.*)\)/.exec(str);
      if (!match) throw new Error("Invalid click handler '"+str+"'");

      return {
        "method": match[1],
        "args": match[2].split(',')
      };
    }

    this.$el.delegate('[sbs-click]', 'click', function(e) {

      // Matches things like this
      // showReport(foo) => ["showReport(foo)", "showReport", "foo"]
      // showReport(12) => ["showReport(12)", "showReport", "12"]
      var fnCall = extractFunctionCall($(e.currentTarget).attr('sbs-click'));

      // Event bubbles up if there is no handler
      var method = that[fnCall.method];
      if (method) {
        e.stopPropagation();
        e.preventDefault();
        method.apply(that, fnCall.args);
        return false;
      }
    });
  };
};

View.Prototype.prototype = util.Events;
View.prototype = new View.Prototype();

module.exports = View;
<commit_msg>Deprecated sbs-click.<commit_after>"use strict";

var util = require("substance-util");

// Substance.View
// ==========================================================================
//
// Application View abstraction, inspired by Backbone.js

var View = function(options) {
  options = options || {};
  var that = this;
  // Either use the provided element or make up a new element
  this.el = options.el || window.document.createElement(options.elementType || 'div');
  this.$el = $(this.el);

  this.dispatchDOMEvents();
};

View.Prototype = function() {

  // Shorthand for selecting elements within the view
  // ----------
  //

  this.$ = function(selector) {
    return this.$el.find(selector);
  };

  this.render = function() {
    return this;
  };

  // Dispatching DOM events (like clicks)
  // ----------
  //

  this.dispatchDOMEvents = function() {

    var that = this;

    // showReport(foo) => ["showReport(foo)", "showReport", "foo"]
    // showReport(12) => ["showReport(12)", "showReport", "12"]
    function extractFunctionCall(str) {
      var match = /(\w+)\((.*)\)/.exec(str);
      if (!match) throw new Error("Invalid click handler '"+str+"'");

      return {
        "method": match[1],
        "args": match[2].split(',')
      };
    }

    this.$el.delegate('[sbs-click]', 'click', function(e) {
      console.error("FIXME: sbs-click is deprecated. Use jquery handlers with selectors instead.");

      // Matches things like this
      // showReport(foo) => ["showReport(foo)", "showReport", "foo"]
      // showReport(12) => ["showReport(12)", "showReport", "12"]
      var fnCall = extractFunctionCall($(e.currentTarget).attr('sbs-click'));

      // Event bubbles up if there is no handler
      var method = that[fnCall.method];
      if (method) {
        e.stopPropagation();
        e.preventDefault();
        method.apply(that, fnCall.args);
        return false;
      }
    });
  };
};

View.Prototype.prototype = util.Events;
View.prototype = new View.Prototype();

module.exports = View;
<|endoftext|>


<commit_before>/*globals require, module, Promise */

'use strict';

var EventEmitter, check, JsonStream, asyncModule, error, events, terminators, escapes;

// TODO: When testing consider gradually adding to available text

EventEmitter = require('events').EventEmitter;
check = require('check-types');
JsonStream = require('./stream');
asyncModule = require('./async');
error = require('./error');
events = require('./events');

terminators = {
    obj: '}',
    arr: ']'
};

escapes = {
    '"': '"',
    '\\': '\\',
    '/': '/',
    'b': '\b',
    'f': '\f',
    'n': '\n',
    'r': '\r',
    't': '\t'
};

module.exports = begin;

function begin (options) {
    var json, position, scopes, handlers,
        emitter, stream, async, resume,
        isWalking, isFinished, isString;

    json = '';
    position = {
        index: 0,
        current: {
            line: 1,
            column: 1
        },
        previous: {}
    };
    scopes = [];
    handlers = {
        arr: value,
        obj: property
    };

    emitter = new EventEmitter();
    stream = new JsonStream(proceed);
    async = asyncModule.initialise(options || {});

    stream.on('finish', finish);

    return {
        emitter: emitter,
        stream: stream
    };

    function proceed (chunk) {
        console.log('proceed: chunk=' + chunk + ', json=' + json + ', isWalking=' + isWalking + ', resume=' + (resume ? resume.name : 'undefined'));

        if (!chunk || chunk.length === 0) {
            return;
        }

        json += chunk;

        if (!isWalking) {
            isWalking = true;

            if (resume) {
                async.defer(resume);
                resume = undefined;
            } else {
                async.defer(value);
            }
        }
    }

    function finish () {
        isFinished = true;

        if (!isWalking) {
            end();
        }
    }

    function value () {
        console.log('value');
        ignoreWhitespace().then(function () {
            console.log('value::ignoreWhitespace');
            next().then(handleValue);
        });
    }

    function handleValue (character) {
        console.log('handleValue: character=' + character);

        switch (character) {
            case '[':
                return array();
            case '{':
                return object();
            case '"':
                return string();
            case '0':
            case '1':
            case '2':
            case '3':
            case '4':
            case '5':
            case '6':
            case '7':
            case '8':
            case '9':
            case '-':
            case '.':
                return number(character);
            case 'f':
                return literalFalse();
            case 'n':
                return literalNull();
            case 't':
                return literalTrue();
            default:
                fail(character, 'value', 'previous');
                value();
        }
    }

    function ignoreWhitespace () {
        var resolve;

        console.log('ignoreWhitespace');

        async.defer(step);

        return new Promise(function (r) {
            resolve = r;
        });

        function step () {
            console.log('ignoreWhitespace::step: isWhitespace=' + isWhitespace(character()));
            if (isWhitespace(character())) {
                return next().then(step);
            }

            resolve();
        }
    }

    function next () {
        var resolve;

        console.log('next');

        // TODO: discard old characters to save memory

        isEnd().then(after);

        return new Promise(function (r) {
            resolve = r;
        });

        function after (atEnd) {
            console.log('next::after: atEnd=' + atEnd);

            if (atEnd) {
                resume = afterResume;
                return end();
            }

            afterResume();
        }

        function afterResume () {
            var result = character();

            console.log('next::afterResume: result=' + result);

            position.index += 1;
            position.previous.line = position.current.line;
            position.previous.column = position.current.column;

            if (result === '\n') {
                position.current.line += 1;
                position.current.column = 1;
            } else {
                position.current.column += 1;
            }

            resolve(result);
        }
    }

    function isEnd () {
        var resolve;

        console.log('isEnd');

        async.defer(step);

        return new Promise(function (r) {
            resolve = r;
        });

        function step () {
            console.log('isEnd::step: isWalking=' + isWalking);

            if (isWalking) {
                return resolve(position.index === json.length);
            }

            async.delay(step);
        }
    }

    function end () {
        console.log('end: isFinished=' + isFinished + ', isWalking=' + isWalking + ', isString=' + isString + ', scopes.length=' + scopes.length);

        if (!isFinished) {
            isWalking = false;
            return;
        }

        if (isString) {
            fail('EOF', '"', 'current');
        }

        while (scopes.length > 0) {
            fail('EOF', terminators[scopes.pop()], 'current');
        }

        emitter.emit(events.end);
    }

    function fail (actual, expected, positionKey) {
        console.log('fail: actual=' + actual + ', expected=' + expected + ', positionKey=' + positionKey);

        emitter.emit(
            events.error,
            error.create(
                actual,
                expected,
                position[positionKey].line,
                position[positionKey].column
            )
        );
    }

    function character () {
        return json[position.index];
    }

    function array () {
        console.log('array');

        scope(events.array, value);
    }

    function scope (event, contentHandler) {
        console.log('scope: event=' + event + ', ' + contentHandler.name);

        emitter.emit(event);
        scopes.push(event);
        endScope(event).then(function (atScopeEnd) {
            console.log('scope::endScope: atScopeEnd=' + atScopeEnd + ', event=' + event);
            if (!atScopeEnd) {
                async.defer(contentHandler);
            }
        });
    }

    function endScope (scope) {
        console.log('endScope: scope=' + scope);

        var resolve;

        ignoreWhitespace().then(afterWhitespace);

        return new Promise(function (r) {
            resolve = r;
        });

        function afterWhitespace () {
            console.log('endScope::afterWhitespace');
            isEnd().then(afterEnd);
        }

        function afterEnd (atEnd) {
            console.log('endScope::afterEnd: atEnd=' + atEnd);
            if (atEnd) {
                return next().then(afterNext);
            }

            afterNext(character());
        }

        function afterNext (character) {
            console.log('endScope::afterNext: character=' + character + ', terminators[scope]=' + terminators[scope]);
            if (character !== terminators[scope]) {
                return resolve(false);
            }

            emitter.emit(events.endPrefix + scope);
            scopes.pop();

            next().then(function (character) {
                async.defer(endValue);
                resolve(true);
            });
        }
    }

    function object () {
        scope(events.object, property);
    }

    function property () {
        ignoreWhitespace().then(function () {
            next().then(propertyName);
        });
    }

    function propertyName (character) {
        checkCharacter(character, '"', 'previous');
        walkString(events.property).then(function () {
            ignoreWhitespace().then(function () {
                next().then(propertyValue);
            });
        });
    }

    function propertyValue (character) {
        checkCharacter(character, ':', 'previous');
        async.defer(value);
    }

    function walkString (event) {
        var isQuoting, string, resolve;

        isString = true;
        isQuoting = false;
        string = '';

        next().then(step);

        return new Promise(function (r) {
            resolve = r;
        });

        function step (character) {
            if (isQuoting) {
                isQuoting = false;

                return escape(character).then(function (escaped) {
                    string += escaped;
                    next().then(step);
                });
            }

            if (character === '\\') {
                isQuoting = true;
                return next().then(step);
            }

            if (character !== '"') {
                string += character;
                return next().then(step);
            }

            isString = false;
            emitter.emit(event, string);
            resolve();
        }
    }

    function escape (character) {
        var promise, resolve;

        promise = new Promise(function (r) {
            resolve = r;
        });

        if (escapes[character]) {
            resolve(escapes[character]);
        } else if (character === 'u') {
            escapeHex().then(resolve);
        } else {
            fail(character, 'escape character', 'previous');
            resolve('\\' + character);
        }

        return promise;
    }

    function escapeHex () {
        var hexits, resolve;

        hexits = '';

        next().then(step.bind(null, 0));

        return new Promise(function (r) {
            resolve = r;
        });

        function step (index, character) {
            if (isHexit(character)) {
                hexits += character;
            }

            if (index < 3) {
                return next().then(step.bind(null, index + 1));
            }

            if (hexits.length === 4) {
                return resolve(String.fromCharCode(parseInt(hexits, 16)));
            }

            fail(character, 'hex digit', 'previous');

            resolve('\\u' + hexits + character);
        }
    }

    function checkCharacter (character, expected, positionKey) {
        if (character !== expected) {
            fail(character, expected, positionKey);
            return false;
        }

        return true;
    }

    function endValue () {
        ignoreWhitespace().then(function () {
            if (scopes.length === 0) {
                return isEnd().then(checkEnd);
            }

            checkScope();
        });

        function checkEnd (atEnd) {
            if (!atEnd) {
                fail(character(), 'EOF', 'current');
                return async.defer(value);
            }

            checkScope();
        }

        function checkScope () {
            var scope = scopes[scopes.length - 1];

            endScope(scope).then(function (atScopeEnd) {
                var handler;

                if (!atScopeEnd) {
                    handler = handlers[scope];

                    if (checkCharacter(character(), ',', 'current')) {
                        next().then(handler);
                    } else {
                        async.defer(handler);
                    }
                }
            });
        }
    }

    function string () {
        walkString(events.string).then(endValue);
    }

    function number (firstCharacter) {
        var digits = firstCharacter;

        walkDigits().then(addDigits.bind(null, checkDecimalPlace));

        function addDigits (step, result) {
            digits += result.digits;

            if (result.atEnd) {
                return endNumber();
            }

            step();
        }

        function checkDecimalPlace () {
            if (character() === '.') {
                return next().then(function (character) {
                    digits += character;
                    walkDigits().then(addDigits.bind(null, checkExponent));
                });
            }

            checkExponent();
        }

        function checkExponent () {
            if (character() === 'e' || character() === 'E') {
                return next().then(function (character) {
                    digits += character;
                    checkSign();
                });
            }

            endNumber();
        }

        function checkSign () {
            if (character() === '+' || character() === '-') {
                return next().then(function (character) {
                    digits += character;
                    readExponent();
                });
            }

            readExponent();
        }

        function readExponent () {
            walkDigits().then(addDigits.bind(null, endNumber));
        }

        function endNumber () {
            emitter.emit(events.number, parseFloat(digits));
            async.defer(endValue);
        }
    }

    function walkDigits () {
        var digits, resolve;

        digits = '';

        isEnd().then(step);

        return new Promise(function (r) {
            resolve = r;
        });

        function step (atEnd) {
            if (atEnd || !isDigit(character())) {
                return resolve({
                    digits: digits,
                    atEnd: atEnd
                });
            }

            next().then(function (character) {
                digits += character;
                isEnd().then(step);
            });
        }
    }

    function literalFalse () {
        literal([ 'a', 'l', 's', 'e' ], false);
    }

    function literal (expectedCharacters, value) {
        var actual, expected, invalid;

        isEnd().then(step);

        function step (atEnd) {
            if (invalid || atEnd || expectedCharacters.length === 0) {
                if (invalid) {
                    fail(actual, expected, 'previous');
                } else if (expectedCharacters.length > 0) {
                    fail('EOF', expectedCharacters.shift(), 'current');
                } else {
                    emitter.emit(events.literal, value);
                }

                return async.defer(endValue);
            }

            next().then(function (character) {
                actual = character;
                expected = expectedCharacters.shift();

                if (actual !== expected) {
                    invalid = true;
                }

                isEnd().then(step);
            });
        }
    }

    function literalNull () {
        literal([ 'u', 'l', 'l' ], null);
    }

    function literalTrue () {
        literal([ 'r', 'u', 'e' ], true);
    }
}

function isWhitespace (character) {
    switch (character) {
        case ' ':
        case '\t':
        case '\r':
        case '\n':
            return true;
    }

    return false;
}

function isHexit (character) {
    return isDigit(character) ||
           isInRange(character, 'A', 'F') ||
           isInRange(character, 'a', 'f');
}

function isDigit (character) {
    return isInRange(character, '0', '9');
}

function isInRange (character, lower, upper) {
    var code = character.charCodeAt(0);

    return code >= lower.charCodeAt(0) && code <= upper.charCodeAt(0);
}

<commit_msg>Improve pause/resumption.<commit_after>/*globals require, module, Promise */

'use strict';

var EventEmitter, check, JsonStream, asyncModule, error, events, terminators, escapes;

// TODO: When testing consider gradually adding to available text

EventEmitter = require('events').EventEmitter;
check = require('check-types');
JsonStream = require('./stream');
asyncModule = require('./async');
error = require('./error');
events = require('./events');

terminators = {
    obj: '}',
    arr: ']'
};

escapes = {
    '"': '"',
    '\\': '\\',
    '/': '/',
    'b': '\b',
    'f': '\f',
    'n': '\n',
    'r': '\r',
    't': '\t'
};

module.exports = begin;

function begin (options) {
    var json, position, scopes, handlers,
        emitter, stream, async, resume,
        isWalking, isFinished, isString;

    json = '';
    position = {
        index: 0,
        current: {
            line: 1,
            column: 1
        },
        previous: {}
    };
    scopes = [];
    handlers = {
        arr: value,
        obj: property
    };

    emitter = new EventEmitter();
    stream = new JsonStream(proceed);
    async = asyncModule.initialise(options || {});

    stream.on('finish', finish);

    return {
        emitter: emitter,
        stream: stream
    };

    function proceed (chunk) {
        console.log('proceed: chunk=' + chunk + ', json=' + json + ', isWalking=' + isWalking + ', resume=' + (resume ? resume.name : 'undefined'));

        if (!chunk || chunk.length === 0) {
            return;
        }

        json += chunk;

        if (!isWalking) {
            isWalking = true;

            if (resume) {
                async.defer(resume);
                resume = undefined;
            } else {
                async.defer(value);
            }
        }
    }

    function finish () {
        isFinished = true;

        if (!isWalking) {
            end();
        }
    }

    function value () {
        console.log('value');
        ignoreWhitespace().then(function () {
            console.log('value::ignoreWhitespace');
            next().then(handleValue);
        });
    }

    function handleValue (character) {
        console.log('handleValue: character=' + character);

        switch (character) {
            case '[':
                return array();
            case '{':
                return object();
            case '"':
                return string();
            case '0':
            case '1':
            case '2':
            case '3':
            case '4':
            case '5':
            case '6':
            case '7':
            case '8':
            case '9':
            case '-':
            case '.':
                return number(character);
            case 'f':
                return literalFalse();
            case 'n':
                return literalNull();
            case 't':
                return literalTrue();
            default:
                fail(character, 'value', 'previous');
                value();
        }
    }

    function ignoreWhitespace () {
        var resolve;

        console.log('ignoreWhitespace');

        async.defer(check);

        return new Promise(function (r) {
            resolve = r;
        });

        function check () {
            console.log('ignoreWhitespace::check');
            isEnd().then(checkEnd.bind(null, step));
        }

        function step () {
            console.log('ignoreWhitespace::step: isWhitespace=' + isWhitespace(character()));
            if (isWhitespace(character())) {
                return next().then(check);
            }

            resolve();
        }
    }

    function checkEnd (after, atEnd) {
        if (atEnd) {
            resume = after;
            return end();
        }

        after();
    }

    function next () {
        var resolve;

        console.log('next');

        // TODO: discard old characters to save memory

        isEnd().then(checkEnd.bind(null, after));

        return new Promise(function (r) {
            resolve = r;
        });

        function after () {
            var result = character();

            console.log('next::after: result=' + result);

            position.index += 1;
            position.previous.line = position.current.line;
            position.previous.column = position.current.column;

            if (result === '\n') {
                position.current.line += 1;
                position.current.column = 1;
            } else {
                position.current.column += 1;
            }

            resolve(result);
        }
    }

    function isEnd () {
        var resolve;

        console.log('isEnd');

        async.defer(step);

        return new Promise(function (r) {
            resolve = r;
        });

        function step () {
            console.log('isEnd::step: isWalking=' + isWalking);

            if (isWalking) {
                return resolve(position.index === json.length);
            }

            async.delay(step);
        }
    }

    function end () {
        console.log('end: isFinished=' + isFinished + ', isWalking=' + isWalking + ', isString=' + isString + ', scopes.length=' + scopes.length);

        if (!isFinished) {
            isWalking = false;
            return;
        }

        if (isString) {
            fail('EOF', '"', 'current');
        }

        while (scopes.length > 0) {
            fail('EOF', terminators[scopes.pop()], 'current');
        }

        emitter.emit(events.end);
    }

    function fail (actual, expected, positionKey) {
        console.log('fail: actual=' + actual + ', expected=' + expected + ', positionKey=' + positionKey);

        emitter.emit(
            events.error,
            error.create(
                actual,
                expected,
                position[positionKey].line,
                position[positionKey].column
            )
        );
    }

    function character () {
        return json[position.index];
    }

    function array () {
        console.log('array');

        scope(events.array, value);
    }

    function scope (event, contentHandler) {
        console.log('scope: event=' + event + ', ' + contentHandler.name);

        emitter.emit(event);
        scopes.push(event);
        endScope(event).then(function (atScopeEnd) {
            console.log('scope::endScope: atScopeEnd=' + atScopeEnd + ', event=' + event);
            if (!atScopeEnd) {
                async.defer(contentHandler);
            }
        });
    }

    function endScope (scope) {
        console.log('endScope: scope=' + scope);

        var resolve;

        ignoreWhitespace().then(afterWhitespace);

        return new Promise(function (r) {
            resolve = r;
        });

        function afterWhitespace () {
            console.log('endScope::afterWhitespace');
            isEnd().then(afterEnd);
        }

        function afterEnd (atEnd) {
            console.log('endScope::afterEnd: atEnd=' + atEnd);
            if (atEnd) {
                return next().then(afterNext);
            }

            afterNext(character());
        }

        function afterNext (character) {
            console.log('endScope::afterNext: character=' + character + ', terminators[scope]=' + terminators[scope]);
            if (character !== terminators[scope]) {
                return resolve(false);
            }

            emitter.emit(events.endPrefix + scope);
            scopes.pop();

            next().then(function (character) {
                async.defer(endValue);
                resolve(true);
            });
        }
    }

    function object () {
        scope(events.object, property);
    }

    function property () {
        ignoreWhitespace().then(function () {
            next().then(propertyName);
        });
    }

    function propertyName (character) {
        checkCharacter(character, '"', 'previous');
        walkString(events.property).then(function () {
            ignoreWhitespace().then(function () {
                next().then(propertyValue);
            });
        });
    }

    function propertyValue (character) {
        checkCharacter(character, ':', 'previous');
        async.defer(value);
    }

    function walkString (event) {
        var isQuoting, string, resolve;

        isString = true;
        isQuoting = false;
        string = '';

        next().then(step);

        return new Promise(function (r) {
            resolve = r;
        });

        function step (character) {
            if (isQuoting) {
                isQuoting = false;

                return escape(character).then(function (escaped) {
                    string += escaped;
                    next().then(step);
                });
            }

            if (character === '\\') {
                isQuoting = true;
                return next().then(step);
            }

            if (character !== '"') {
                string += character;
                return next().then(step);
            }

            isString = false;
            emitter.emit(event, string);
            resolve();
        }
    }

    function escape (character) {
        var promise, resolve;

        promise = new Promise(function (r) {
            resolve = r;
        });

        if (escapes[character]) {
            resolve(escapes[character]);
        } else if (character === 'u') {
            escapeHex().then(resolve);
        } else {
            fail(character, 'escape character', 'previous');
            resolve('\\' + character);
        }

        return promise;
    }

    function escapeHex () {
        var hexits, resolve;

        hexits = '';

        next().then(step.bind(null, 0));

        return new Promise(function (r) {
            resolve = r;
        });

        function step (index, character) {
            if (isHexit(character)) {
                hexits += character;
            }

            if (index < 3) {
                return next().then(step.bind(null, index + 1));
            }

            if (hexits.length === 4) {
                return resolve(String.fromCharCode(parseInt(hexits, 16)));
            }

            fail(character, 'hex digit', 'previous');

            resolve('\\u' + hexits + character);
        }
    }

    function checkCharacter (character, expected, positionKey) {
        if (character !== expected) {
            fail(character, expected, positionKey);
            return false;
        }

        return true;
    }

    function endValue () {
        ignoreWhitespace().then(function () {
            if (scopes.length === 0) {
                return isEnd().then(checkEnd);
            }

            checkScope();
        });

        function checkEnd (atEnd) {
            if (!atEnd) {
                fail(character(), 'EOF', 'current');
                return async.defer(value);
            }

            checkScope();
        }

        function checkScope () {
            var scope = scopes[scopes.length - 1];

            endScope(scope).then(function (atScopeEnd) {
                var handler;

                if (!atScopeEnd) {
                    handler = handlers[scope];

                    if (checkCharacter(character(), ',', 'current')) {
                        next().then(handler);
                    } else {
                        async.defer(handler);
                    }
                }
            });
        }
    }

    function string () {
        walkString(events.string).then(endValue);
    }

    function number (firstCharacter) {
        var digits = firstCharacter;

        walkDigits().then(addDigits.bind(null, checkDecimalPlace));

        function addDigits (step, result) {
            digits += result.digits;

            if (result.atEnd) {
                return endNumber();
            }

            step();
        }

        function checkDecimalPlace () {
            if (character() === '.') {
                return next().then(function (character) {
                    digits += character;
                    walkDigits().then(addDigits.bind(null, checkExponent));
                });
            }

            checkExponent();
        }

        function checkExponent () {
            if (character() === 'e' || character() === 'E') {
                return next().then(function (character) {
                    digits += character;
                    checkSign();
                });
            }

            endNumber();
        }

        function checkSign () {
            if (character() === '+' || character() === '-') {
                return next().then(function (character) {
                    digits += character;
                    readExponent();
                });
            }

            readExponent();
        }

        function readExponent () {
            walkDigits().then(addDigits.bind(null, endNumber));
        }

        function endNumber () {
            emitter.emit(events.number, parseFloat(digits));
            async.defer(endValue);
        }
    }

    function walkDigits () {
        var digits, resolve;

        digits = '';

        isEnd().then(step);

        return new Promise(function (r) {
            resolve = r;
        });

        function step (atEnd) {
            if (atEnd || !isDigit(character())) {
                return resolve({
                    digits: digits,
                    atEnd: atEnd
                });
            }

            next().then(function (character) {
                digits += character;
                isEnd().then(step);
            });
        }
    }

    function literalFalse () {
        literal([ 'a', 'l', 's', 'e' ], false);
    }

    function literal (expectedCharacters, value) {
        var actual, expected, invalid;

        isEnd().then(step);

        function step (atEnd) {
            if (invalid || atEnd || expectedCharacters.length === 0) {
                if (invalid) {
                    fail(actual, expected, 'previous');
                } else if (expectedCharacters.length > 0) {
                    fail('EOF', expectedCharacters.shift(), 'current');
                } else {
                    emitter.emit(events.literal, value);
                }

                return async.defer(endValue);
            }

            next().then(function (character) {
                actual = character;
                expected = expectedCharacters.shift();

                if (actual !== expected) {
                    invalid = true;
                }

                isEnd().then(step);
            });
        }
    }

    function literalNull () {
        literal([ 'u', 'l', 'l' ], null);
    }

    function literalTrue () {
        literal([ 'r', 'u', 'e' ], true);
    }
}

function isWhitespace (character) {
    switch (character) {
        case ' ':
        case '\t':
        case '\r':
        case '\n':
            return true;
    }

    return false;
}

function isHexit (character) {
    return isDigit(character) ||
           isInRange(character, 'A', 'F') ||
           isInRange(character, 'a', 'f');
}

function isDigit (character) {
    return isInRange(character, '0', '9');
}

function isInRange (character, lower, upper) {
    var code = character.charCodeAt(0);

    return code >= lower.charCodeAt(0) && code <= upper.charCodeAt(0);
}

<|endoftext|>


<commit_before>/**
 * @desc when() polls or an jQuery && an element
 * You can disable the log call by adding silentWhen=true as a query parameter to the page
 * or creating a silentWhen variable on the window and setting it to true;
 *
 * @param {string} [selector] - Any valid jQuery selector OR 'stop' which will stop all polling
 * @param {function|array} [callback] - A callback function to run once the selected element has been found OR a list of jQuery things to do i.e. when('#container', 'css', 'color', 'red');
 */

import log from './log';
import getParam from './get-param';

//These two variables are used to keep track of subsequent When calls on the same page
var mainLoop = null;
var elements = [];

function when(selector, callback) {
  if(selector === 'stop') {
    clearInterval(mainLoop);
    mainLoop = null;
    return true;
  }

  //Convert the list of supplied arguments into a true array (since it's actually an object)
  var args = (arguments.length === 1?[arguments[0]]:Array.apply(null, arguments));

  //Add the new element and it's associated arguments to the list of things to look check for.
  elements.push(args);

  //If this is the first time when has been called
  if(mainLoop === null) {
    //Start a loop that checks for elements
    mainLoop = setInterval(function() {
      //Sniff for jQuery in local namespace
      var $jq = (
        typeof jQuery === 'function' ? jQuery : (
          (typeof $ === 'function' && $.fn && $.fn.jquery) ? $ : undefined
        )
      );

      for(var i=0; i<elements.length; i++) {
        //Just some reference variables to make the code easier to read
        var xSelector = elements[i][0];
        var xCallback = elements[i][1];
        var xArguments = elements[i].slice(2);

        //Search the dom for the element
        var $this = typeof $jq === 'function' ? $jq(xSelector) : [];

        //If logging isn't disabled
        if((getParam('silentWhen') === '' || getParam('silentWhen') !== 'true') && window.silentWhen !== true) {
          log('when:', xSelector, $this);
        }

        //If the desired element has been found
        if($this.length) {
          var getType = {};
          //If the supplied callback was indeed a function
          if(xCallback && getType.toString.call(xCallback) === '[object Function]') {
            //Run it
            xCallback.call($this, $this);
          }
          //If the supplied callback was actually an array that looks like a jQuery method
          else if(typeof xCallback === 'string') {
            //Run that on the element we just found
            $this[xCallback].apply($this, xArguments);
          }
          //Stop looking for this element
          elements.splice(i, 1);
        }
      }

      //If there are no more elements to check for
      if(elements.length < 1) {
        clearInterval(mainLoop);
        mainLoop = null;
      }
    }, 50);
  }
}

export default when;
<commit_msg>Hotfix to not throw an error before the element is found<commit_after>/**
 * @desc when() polls or an jQuery && an element
 * You can disable the log call by adding silentWhen=true as a query parameter to the page
 * or creating a silentWhen variable on the window and setting it to true;
 *
 * @param {string} [selector] - Any valid jQuery selector OR 'stop' which will stop all polling
 * @param {function|array} [callback] - A callback function to run once the selected element has been found OR a list of jQuery things to do i.e. when('#container', 'css', 'color', 'red');
 */

import log from './log';
import getParam from './get-param';

//These two variables are used to keep track of subsequent When calls on the same page
var mainLoop = null;
var elements = [];

function when(selector, callback) {
  if(selector === 'stop') {
    clearInterval(mainLoop);
    mainLoop = null;
    return true;
  }

  //Convert the list of supplied arguments into a true array (since it's actually an object)
  var args = (arguments.length === 1?[arguments[0]]:Array.apply(null, arguments));

  //Add the new element and it's associated arguments to the list of things to look check for.
  elements.push(args);

  //If this is the first time when has been called
  if(mainLoop === null) {
    //Start a loop that checks for elements
    mainLoop = setInterval(function() {
      //Sniff for jQuery in local namespace
      var $jq = (
        typeof jQuery === 'function' ? jQuery : (
          (typeof $ === 'function' && $.fn && $.fn.jquery) ? $ : undefined
        )
      );

      for(var i=0; i<elements.length; i++) {
        //Just some reference variables to make the code easier to read
        var xSelector = elements[i][0];
        var xCallback = elements[i][1];
        var xArguments = elements[i].slice(2);

        //Search the dom for the element
        var $this = typeof $jq === 'function' ? $jq(xSelector) : [];

        //If logging isn't disabled
        if((getParam('silentWhen') === '' || getParam('silentWhen') !== 'true') && window.silentWhen !== true) {
          log('when:', xSelector, $this);
        }

        //If the desired element has been found
        if($this !== undefined && $this.length) {
          var getType = {};
          //If the supplied callback was indeed a function
          if(xCallback && getType.toString.call(xCallback) === '[object Function]') {
            //Run it
            xCallback.call($this, $this);
          }
          //If the supplied callback was actually an array that looks like a jQuery method
          else if(typeof xCallback === 'string') {
            //Run that on the element we just found
            $this[xCallback].apply($this, xArguments);
          }
          //Stop looking for this element
          elements.splice(i, 1);
        }
      }

      //If there are no more elements to check for
      if(elements.length < 1) {
        clearInterval(mainLoop);
        mainLoop = null;
      }
    }, 50);
  }
}

export default when;
<|endoftext|>


<commit_before>/**
 * @ngdoc directive
 * @name contactIcon
 *
 * @description
 * _Please update the description and restriction._
 *
 * @restrict A
 * */
angular.module('ui.identicon')
    .directive('identicon', function () {
        return {
            restrict: 'E',
            replace: true,
            scope: {
                email: '=',
                size: '='
            },
            template: '',
            controller: function ($scope) {

                $scope.size = (typeof($scope.size) !== 'undefined' ? $scope.size : 24);

                $scope.$watch('email', function (newVal) {
                    if (newVal) {
                        $scope.data = new Identicon(window.btoa($scope.email), $scope.size).toString();
                    }
                });
            }
        };
    });
<commit_msg>should be creating module not retriving it<commit_after>/**
 * @ngdoc directive
 * @name contactIcon
 *
 * @description
 * _Please update the description and restriction._
 *
 * @restrict A
 * */
angular.module('ui.identicon', [])
    .directive('identicon', function () {
        return {
            restrict: 'E',
            replace: true,
            scope: {
                email: '=',
                size: '='
            },
            template: '',
            controller: function ($scope) {

                $scope.size = (typeof($scope.size) !== 'undefined' ? $scope.size : 24);

                $scope.$watch('email', function (newVal) {
                    if (newVal) {
                        $scope.data = new Identicon(window.btoa($scope.email), $scope.size).toString();
                    }
                });
            }
        };
    });
<|endoftext|>


<commit_before>/**
 * Flip an image horizontally. The image
 * @memberof Image
 * @instance
 * @returns {this}
 */

export default function flipX() {
    this.checkProcessable('flipX', {
        bitDepth: [8, 16]
    });

    for (let i = 0; i < this.height; i++) {

        let offsetY = i * this.width * this.channels;

        for (let j = 0; j < Math.floor(this.width / 2); j++) {

            let posCurrent = j * this.channels + offsetY;
            let posOpposite = (this.width - j - 1) * this.channels + offsetY;

            for (let k = 0; k < this.channels; k++) {
                let tmp = this.data[posCurrent + k];
                this.data[posCurrent + k] = this.data[posOpposite + k];
                this.data[posOpposite + k] = tmp;
            }

        }

    }

    return this;

}
<commit_msg>Update flipX.js<commit_after>/**
 * Flip an image horizontally.
 * @memberof Image
 * @instance
 * @returns {this}
 */

export default function flipX() {
    this.checkProcessable('flipX', {
        bitDepth: [8, 16]
    });

    for (let i = 0; i < this.height; i++) {

        let offsetY = i * this.width * this.channels;

        for (let j = 0; j < Math.floor(this.width / 2); j++) {

            let posCurrent = j * this.channels + offsetY;
            let posOpposite = (this.width - j - 1) * this.channels + offsetY;

            for (let k = 0; k < this.channels; k++) {
                let tmp = this.data[posCurrent + k];
                this.data[posCurrent + k] = this.data[posOpposite + k];
                this.data[posOpposite + k] = tmp;
            }

        }

    }

    return this;

}
<|endoftext|>