<commit_before>import Renderer from 'client/test-helpers/apollo_renderer';

import { expect } from 'chai';
import { step } from 'mocha-steps';
import _ from 'lodash';

import routes from 'client/app/routes';
import POSTS_SUBSCRIPTION from '../graphql/posts_subscription.graphql';
import POST_SUBSCRIPTION from '../graphql/post_subscription.graphql';
import COMMENT_SUBSCRIPTION from '../graphql/post_comment_subscription.graphql';

const createNode = (id) => ({
  id: `${id}`,
  title: `Post title ${id}`,
  content: `Post content ${id}`,
  comments: [
    {id: id * 1000 + 1, content: "Post comment 1", __typename: "Comment"},
    {id: id * 1000 + 2, content: "Post comment 2", __typename: "Comment"},
  ],
  __typename: "Post"
});

const mutations = {
  editPost: true,
  addComment: true,
  editComment: true
};

const mocks = {
  Query: () => ({
    postsQuery(ignored, { after }) {
      const totalCount = 4;
      const edges = [];
      for (let i = +after + 1; i <= +after + 2; i++) {
        edges.push({
          cursor: `${i}`,
          node: createNode(i),
          __typename: "Edges"
        });
      }
      return {
        totalCount,
        edges,
        pageInfo: {
          endCursor: edges[edges.length - 1].cursor,
          hasNextPage: true,
          __typename: "PageInfo"
        },
        __typename: "PostsQuery"
      };
    },
    post(obj, { id }) {
      return createNode(id);
    },
  }),
  Mutation: () => ({
    deletePost: (obj, { id }) => createNode(id),
    deleteComment: (obj, {input}) => input,
    ...mutations
  })
};

describe('Posts and comments example UI works', () => {
  const renderer = new Renderer(mocks, {});
  let app;
  let content;

  beforeEach(() => {
    // Reset spy mutations on each step
    Object.keys(mutations).forEach(key => delete mutations[key]);
  });

  step('Posts page renders without data', () => {
    app = renderer.mount(routes);
    renderer.history.push('/posts');
    content = app.find('#content');

    expect(content.children().html()).to.equal('

');
  });

  step('Posts page renders with data', () => {
    expect(content.text()).to.include('Post title 1');
    expect(content.text()).to.include('Post title 2');
    expect(content.text()).to.include('2 / 4');
  });

  step('Clicking load more works', () => {
    const loadMoreButton = content.find('#load-more');
    loadMoreButton.simulate("click");
  });

  step('Clicking load more loads more posts', () => {
    expect(content.text()).to.include('Post title 3');
    expect(content.text()).to.include('Post title 4');
    expect(content.text()).to.include('4 / 4');
  });

  step('Check subscribed to post list updates', () => {
    expect(renderer.getSubscriptions(POSTS_SUBSCRIPTION)).has.lengthOf(1);
  });

  step('Updates post list on post delete from subscription', () => {
    const subscription = renderer.getSubscriptions(POSTS_SUBSCRIPTION)[0];
    subscription(null, {
      postsUpdated: {
        mutation: 'DELETED', node: createNode(2), __typename: 'UpdatePostPayload'
      }
    });

    expect(content.text()).to.not.include('Post title 2');
    expect(content.text()).to.include('3 / 3');
  });

  step('Updates post list on post create from subscription', () => {
    const subscription = renderer.getSubscriptions(POSTS_SUBSCRIPTION)[0];
    subscription(null, _.cloneDeep({
      postsUpdated: {
        mutation: 'CREATED', node: createNode(2),  __typename: 'UpdatePostPayload'
      }
    }));

    expect(content.text()).to.include('Post title 2');
    expect(content.text()).to.include('4 / 4');
  });

  step('Clicking delete optimistically removes post', () => {
    mutations.deletePost = (obj, { id }) => {
      return createNode(id);
    };

    const deleteButtons = content.find('.delete-button');
    expect(deleteButtons).has.lengthOf(4);
    deleteButtons.last().simulate("click");

    expect(content.text()).to.not.include('Post title 4');
    expect(content.text()).to.include('3 / 3');
  });

  step('Clicking delete removes the post', () => {
    expect(content.text()).to.include('Post title 3');
    expect(content.text()).to.not.include('Post title 4');
    expect(content.text()).to.include('3 / 3');
  });

  step('Clicking on post works', () => {
    const postLinks = content.find('.post-link');
    postLinks.last().simulate("click", { button: 0 });
  });

  step('Clicking on post opens post form', () => {
    const postForm = content.find('form[name="post"]');

    expect(content.text()).to.include('Edit Post');
    expect(postForm.find('[name="title"]').node.value).to.equal('Post title 3');
    expect(postForm.find('[name="content"]').node.value).to.equal('Post content 3');

    console.log(content.text());
  });

  step('Check subscribed to post updates', () => {
    expect(renderer.getSubscriptions(POST_SUBSCRIPTION)).has.lengthOf(1);
  });

  step('Updates post form on post updated from subscription', () => {
    const subscription = renderer.getSubscriptions(POST_SUBSCRIPTION)[0];
    subscription(null, {
      postUpdated: {
        id: "3",
        title: "Post title 203",
        content: "Post content 204",
        __typename: "Post"
      }
    });
    const postForm = content.find('form[name="post"]');

    expect(postForm.find('[name="title"]').node.value).to.equal('Post title 203');
    expect(postForm.find('[name="content"]').node.value).to.equal('Post content 204');
  });

  step('Post editing form works', done => {
    mutations.editPost = (obj, { input })  => {
      expect(input.id).to.equal('3');
      expect(input.title).to.equal('Post title 33');
      expect(input.content).to.equal('Post content 33');
      done();
      return input;
    };

    const postForm = content.find('form[name="post"]');
    postForm.find('[name="title"]').simulate('change', { target: { value: 'Post title 33' } });
    postForm.find('[name="content"]').simulate('change', { target: { value: 'Post content 33' } });
    postForm.simulate('submit');
  });

  step('Check opening post by URL', () => {
    renderer.history.push('/post/3');
  });

  step('Opening post by URL works', () => {
    const postForm = content.find('form[name="post"]');

    expect(content.text()).to.include('Edit Post');
    expect(postForm.find('[name="title"]').node.value).to.equal('Post title 33');
    expect(postForm.find('[name="content"]').node.value).to.equal('Post content 33');
    expect(content.text()).to.include('Edit Post');
  });

  step('Comment adding works', done => {
    mutations.addComment = (obj, { input })  => {
      expect(input.postId).to.equal('3');
      expect(input.content).to.equal('Post comment 24');
      done();
      return input;
    };

    const commentForm = content.find('form[name="comment"]');
    commentForm.find('[name="content"]').simulate('change', { target: { value: 'Post comment 24' } });
    commentForm.simulate('submit');
    expect(content.text()).to.include('Post comment 24');
  });

  step('Updates comment form on comment added got from subscription', () => {
    const subscription = renderer.getSubscriptions(COMMENT_SUBSCRIPTION)[0];
    subscription(null, {
      commentUpdated: {
        mutation: "CREATED",
        id: "3003",
        postId: "3",
        node: {
          id: "3003",
          content: "Post comment 3",
          __typename: "Comment"
        },
        __typename: "UpdateCommentPayload"
      }
    });

    expect(content.text()).to.include("Post comment 3");
  });

  step('Updates comment form on comment deleted got from subscription', () => {
    const subscription = renderer.getSubscriptions(COMMENT_SUBSCRIPTION)[0];
    subscription(null, {
      commentUpdated: {
        mutation: "DELETED",
        id: "3003",
        postId: "3",
        node: {
          id: "3003",
          content: "Post comment 3",
          __typename: "Comment"
        },
        __typename: "UpdateCommentPayload"
      }
    });
    expect(content.text()).to.not.include("Post comment 3");
  });

  step('Comment deleting optimistically removes comment', () => {
    const deleteButtons = content.find('.delete-comment');
    expect(deleteButtons).has.lengthOf(3);
    deleteButtons.last().simulate("click");

    expect(content.text()).to.not.include('Post comment 24');
    expect(content.find('.delete-comment')).has.lengthOf(2);
  });

  step('Clicking comment delete removes the comment', () => {
    expect(content.text()).to.not.include('Post comment 24');
    expect(content.find('.delete-comment')).has.lengthOf(2);
  });

  step('Comment editing works', done => {
    mutations.editComment = (obj, { input })  => {
      expect(input.postId).to.equal('3');
      expect(input.content).to.equal('Edited comment 2');
      done();
      return input;
    };

    const editButtons = content.find('.edit-comment');
    expect(editButtons).has.lengthOf(2);
    editButtons.last().simulate("click");

    const commentForm = content.find('form[name="comment"]');
    expect(commentForm.find('[name="content"]').node.value).to.equal('Post comment 2');
    commentForm.find('[name="content"]').simulate('change', { target: { value: 'Edited comment 2' } });
    commentForm.simulate('submit');

    expect(content.text()).to.include('Edited comment 2');
  });

  step('Clicking back button takes to post list', () => {
    const backButton = content.find('#back-button');
    backButton.simulate('click', { button: 0 });
    expect(content.text()).to.include('Post title 33');
  });
});<commit_msg>Remove debugging console.log statement<commit_after>import Renderer from 'client/test-helpers/apollo_renderer';

import { expect } from 'chai';
import { step } from 'mocha-steps';
import _ from 'lodash';

import routes from 'client/app/routes';
import POSTS_SUBSCRIPTION from '../graphql/posts_subscription.graphql';
import POST_SUBSCRIPTION from '../graphql/post_subscription.graphql';
import COMMENT_SUBSCRIPTION from '../graphql/post_comment_subscription.graphql';

const createNode = (id) => ({
  id: `${id}`,
  title: `Post title ${id}`,
  content: `Post content ${id}`,
  comments: [
    {id: id * 1000 + 1, content: "Post comment 1", __typename: "Comment"},
    {id: id * 1000 + 2, content: "Post comment 2", __typename: "Comment"},
  ],
  __typename: "Post"
});

const mutations = {
  editPost: true,
  addComment: true,
  editComment: true
};

const mocks = {
  Query: () => ({
    postsQuery(ignored, { after }) {
      const totalCount = 4;
      const edges = [];
      for (let i = +after + 1; i <= +after + 2; i++) {
        edges.push({
          cursor: `${i}`,
          node: createNode(i),
          __typename: "Edges"
        });
      }
      return {
        totalCount,
        edges,
        pageInfo: {
          endCursor: edges[edges.length - 1].cursor,
          hasNextPage: true,
          __typename: "PageInfo"
        },
        __typename: "PostsQuery"
      };
    },
    post(obj, { id }) {
      return createNode(id);
    },
  }),
  Mutation: () => ({
    deletePost: (obj, { id }) => createNode(id),
    deleteComment: (obj, {input}) => input,
    ...mutations
  })
};

describe('Posts and comments example UI works', () => {
  const renderer = new Renderer(mocks, {});
  let app;
  let content;

  beforeEach(() => {
    // Reset spy mutations on each step
    Object.keys(mutations).forEach(key => delete mutations[key]);
  });

  step('Posts page renders without data', () => {
    app = renderer.mount(routes);
    renderer.history.push('/posts');
    content = app.find('#content');

    expect(content.children().html()).to.equal('

');
  });

  step('Posts page renders with data', () => {
    expect(content.text()).to.include('Post title 1');
    expect(content.text()).to.include('Post title 2');
    expect(content.text()).to.include('2 / 4');
  });

  step('Clicking load more works', () => {
    const loadMoreButton = content.find('#load-more');
    loadMoreButton.simulate("click");
  });

  step('Clicking load more loads more posts', () => {
    expect(content.text()).to.include('Post title 3');
    expect(content.text()).to.include('Post title 4');
    expect(content.text()).to.include('4 / 4');
  });

  step('Check subscribed to post list updates', () => {
    expect(renderer.getSubscriptions(POSTS_SUBSCRIPTION)).has.lengthOf(1);
  });

  step('Updates post list on post delete from subscription', () => {
    const subscription = renderer.getSubscriptions(POSTS_SUBSCRIPTION)[0];
    subscription(null, {
      postsUpdated: {
        mutation: 'DELETED', node: createNode(2), __typename: 'UpdatePostPayload'
      }
    });

    expect(content.text()).to.not.include('Post title 2');
    expect(content.text()).to.include('3 / 3');
  });

  step('Updates post list on post create from subscription', () => {
    const subscription = renderer.getSubscriptions(POSTS_SUBSCRIPTION)[0];
    subscription(null, _.cloneDeep({
      postsUpdated: {
        mutation: 'CREATED', node: createNode(2),  __typename: 'UpdatePostPayload'
      }
    }));

    expect(content.text()).to.include('Post title 2');
    expect(content.text()).to.include('4 / 4');
  });

  step('Clicking delete optimistically removes post', () => {
    mutations.deletePost = (obj, { id }) => {
      return createNode(id);
    };

    const deleteButtons = content.find('.delete-button');
    expect(deleteButtons).has.lengthOf(4);
    deleteButtons.last().simulate("click");

    expect(content.text()).to.not.include('Post title 4');
    expect(content.text()).to.include('3 / 3');
  });

  step('Clicking delete removes the post', () => {
    expect(content.text()).to.include('Post title 3');
    expect(content.text()).to.not.include('Post title 4');
    expect(content.text()).to.include('3 / 3');
  });

  step('Clicking on post works', () => {
    const postLinks = content.find('.post-link');
    postLinks.last().simulate("click", { button: 0 });
  });

  step('Clicking on post opens post form', () => {
    const postForm = content.find('form[name="post"]');

    expect(content.text()).to.include('Edit Post');
    expect(postForm.find('[name="title"]').node.value).to.equal('Post title 3');
    expect(postForm.find('[name="content"]').node.value).to.equal('Post content 3');
  });

  step('Check subscribed to post updates', () => {
    expect(renderer.getSubscriptions(POST_SUBSCRIPTION)).has.lengthOf(1);
  });

  step('Updates post form on post updated from subscription', () => {
    const subscription = renderer.getSubscriptions(POST_SUBSCRIPTION)[0];
    subscription(null, {
      postUpdated: {
        id: "3",
        title: "Post title 203",
        content: "Post content 204",
        __typename: "Post"
      }
    });
    const postForm = content.find('form[name="post"]');

    expect(postForm.find('[name="title"]').node.value).to.equal('Post title 203');
    expect(postForm.find('[name="content"]').node.value).to.equal('Post content 204');
  });

  step('Post editing form works', done => {
    mutations.editPost = (obj, { input })  => {
      expect(input.id).to.equal('3');
      expect(input.title).to.equal('Post title 33');
      expect(input.content).to.equal('Post content 33');
      done();
      return input;
    };

    const postForm = content.find('form[name="post"]');
    postForm.find('[name="title"]').simulate('change', { target: { value: 'Post title 33' } });
    postForm.find('[name="content"]').simulate('change', { target: { value: 'Post content 33' } });
    postForm.simulate('submit');
  });

  step('Check opening post by URL', () => {
    renderer.history.push('/post/3');
  });

  step('Opening post by URL works', () => {
    const postForm = content.find('form[name="post"]');

    expect(content.text()).to.include('Edit Post');
    expect(postForm.find('[name="title"]').node.value).to.equal('Post title 33');
    expect(postForm.find('[name="content"]').node.value).to.equal('Post content 33');
    expect(content.text()).to.include('Edit Post');
  });

  step('Comment adding works', done => {
    mutations.addComment = (obj, { input })  => {
      expect(input.postId).to.equal('3');
      expect(input.content).to.equal('Post comment 24');
      done();
      return input;
    };

    const commentForm = content.find('form[name="comment"]');
    commentForm.find('[name="content"]').simulate('change', { target: { value: 'Post comment 24' } });
    commentForm.simulate('submit');
    expect(content.text()).to.include('Post comment 24');
  });

  step('Updates comment form on comment added got from subscription', () => {
    const subscription = renderer.getSubscriptions(COMMENT_SUBSCRIPTION)[0];
    subscription(null, {
      commentUpdated: {
        mutation: "CREATED",
        id: "3003",
        postId: "3",
        node: {
          id: "3003",
          content: "Post comment 3",
          __typename: "Comment"
        },
        __typename: "UpdateCommentPayload"
      }
    });

    expect(content.text()).to.include("Post comment 3");
  });

  step('Updates comment form on comment deleted got from subscription', () => {
    const subscription = renderer.getSubscriptions(COMMENT_SUBSCRIPTION)[0];
    subscription(null, {
      commentUpdated: {
        mutation: "DELETED",
        id: "3003",
        postId: "3",
        node: {
          id: "3003",
          content: "Post comment 3",
          __typename: "Comment"
        },
        __typename: "UpdateCommentPayload"
      }
    });
    expect(content.text()).to.not.include("Post comment 3");
  });

  step('Comment deleting optimistically removes comment', () => {
    const deleteButtons = content.find('.delete-comment');
    expect(deleteButtons).has.lengthOf(3);
    deleteButtons.last().simulate("click");

    expect(content.text()).to.not.include('Post comment 24');
    expect(content.find('.delete-comment')).has.lengthOf(2);
  });

  step('Clicking comment delete removes the comment', () => {
    expect(content.text()).to.not.include('Post comment 24');
    expect(content.find('.delete-comment')).has.lengthOf(2);
  });

  step('Comment editing works', done => {
    mutations.editComment = (obj, { input })  => {
      expect(input.postId).to.equal('3');
      expect(input.content).to.equal('Edited comment 2');
      done();
      return input;
    };

    const editButtons = content.find('.edit-comment');
    expect(editButtons).has.lengthOf(2);
    editButtons.last().simulate("click");

    const commentForm = content.find('form[name="comment"]');
    expect(commentForm.find('[name="content"]').node.value).to.equal('Post comment 2');
    commentForm.find('[name="content"]').simulate('change', { target: { value: 'Edited comment 2' } });
    commentForm.simulate('submit');

    expect(content.text()).to.include('Edited comment 2');
  });

  step('Clicking back button takes to post list', () => {
    const backButton = content.find('#back-button');
    backButton.simulate('click', { button: 0 });
    expect(content.text()).to.include('Post title 33');
  });
});<|endoftext|>


<commit_before>function Client(url, debug, debugMessageBox) {

	var channelIDIndex = 0;
	var channelArray = [];
	var websocket = null;
	var webSocketOnOpenCallbacks = [];
	var webSocketOnCloseCallbacks = [];
	var webSocketOnErrorCallbacks = [];
	var onServerMessageCallbacks = [];
	var clientSelf = this;
	var debug = debug;
	var debugMessageBox = debugMessageBox;
	var defaultTypeVersion = 1;
    this.isLive = false;

	openWebSocket(url);

    /**
	 * Add a callback to WebSocket onOpen event.
	 * @param {Client~WebSocketEventCallback} callback the callback function on WebSocket open event.
	 */
	this.addWebSocketOnOpenCallback = function(callback) {
		webSocketOnOpenCallbacks.push(callback);
	};

	/**
	 * Remove a WebSocket onOpen callback.
	 * @param {Client~WebSocketEventCallback} callback the callback function on WebSocket open event.
	 */
	this.removeWebSocketOnOpenCallback = function(callback){
		webSocketOnOpenCallbacks.splice(webSocketOnOpenCallbacks.indexOf(callback), 1);
	};

	/**
	 * Add a callback to WebSocket onClose event.
	 * @param {Client~WebSocketEventCallback} callback the callback function on WebSocket close event.
	 *
	 */
	this.addWebSocketOnCloseCallback = function(callback) {
		webSocketOnCloseCallbacks.push(callback);
	};

	/**
	 * Add a callback to WebSocket onError event.
	 * @param {Client~WebSocketEventCallback} callback the callback function on WebSocket error event.
	 *
	 */
	this.addWebSocketOnErrorCallback = function(callback) {
		webSocketOnErrorCallbacks.push(callback);
	};

	/**A callback function that will be notified when there is a message from server.
	 * @callback Client~OnServerMessageCallback
	 *
	 */

	/**
	 * Add a callback that will be notified when there is a notification message from server.
	 * @param {Client~OnServerMessageCallback} callback the callback
	 *
	 */
	this.addOnServerMessageCallback = function(callback) {
		onServerMessageCallbacks.push(callback);
	};

    this.subscribeChannel = function(name, readOnly, type, version, maxRate) {
        var typeJson;
        if (type != null) {
            if (version == null)
                version = defaultVersion;
            typeJson = JSON.stringify({
                "name" : type,
                "version" : version
            });

        }
        var json = JSON.stringify({
            "message" : "subscribe",
            "id" : channelIDIndex,
            "channel" : name,
            "readOnly" : readOnly,
            "maxRate" : maxRate,
            "type" : typeJson
        });
        var channel = new Channel(name);
        channelArray[channelIDIndex] = channel;
        channel.id = channelIDIndex;
        channel.name = name;
        channel.readOnly = readOnly;
        channel.type = type;
        channel.connected = true;
        if(this.isLive)
            this.sendText(json);
        else{
            var webpdaSelf = this;
            var listener = null;
            listener = function(evt){
                clientSelf.sendText(json);
                setTimeout(function(){
                    clientSelf.removeWebSocketOnOpenCallback(listener);
                }, 0);
            };
            this.addWebSocketOnOpenCallback(listener);
        }
        channelIDIndex++;
        return channel;
    };

    function openWebSocket(url) {
        if ('WebSocket' in window) {
            websocket = new WebSocket(url, "org.client");
        } else if ('MozWebSocket' in window) {
            websocket = new MozWebSocket(url, "org.client");
        } else {
            throw new Error('WebSocket is not supported by this browser.');
        }
        websocket.binaryType = "arraybuffer";
        websocket.onopen = function(evt) {
            fireOnOpen(evt);
        };

        websocket.onmessage = function(evt) {
            var json;
            json = JSON.parse(evt.data);
            dispatchMessage(json);

        };

        websocket.onerror = function(evt) {
            fireOnError(evt);
        };

    }

    function writeToScreen(message) {
        if (debugMessageBox != null)
            document.getElementById(debugMessageBox).value = message;
    }

    /**
     * Close Websocket.
     */
    this.close = function() {
        if (websocket != null)
            websocket.close();
        websocket = null;
    };

    this.sendText =function(text) {
        websocket.send(text);
        if(debug)
            writeToScreen(text);
    };

    /**
     * Get the Channel from its id.
     * @param {number} id id of the Channel.
     * @returns {Client~Channel} the Channel.
     */
    this.getChannel = function(id){
        return channelArray[id];
    };

    /**Get all Channels on this client.
     * @returns {Array.<Client~Channel>} All Channels in an array.
     */
    this.getAllChannels = function() {
        return channelArray;
    };

    function dispatchMessage(json) {
        if (json.message != null)
            handleServerMessage(json);
        if (json.id != null) {
            if (channelArray[json.id] != null)
                channelArray[json.id].fireChannelEventFunc(json);
        }
    }

    function handleServerMessage(json) {
		if(json.type == "error")
			console.log("Error: " + json.error);
		fireOnServerMessage(json);
	}

	function processJsonForChannel(json, Channel) {
        switch (json.type) {
        case "connection":
            Channel.connected = json.connected;
            Channel.readOnly = !json.writeConnected;
            break;
        case "value":
            Channel.value =  json.value;
            break;
        default:
            break;
        }
    };
    function fireOnOpen(evt) {
        clientSelf.isLive = true;
        for ( var i in webSocketOnOpenCallbacks) {
            webSocketOnOpenCallbacks[i](evt);
        }
    }

    function fireOnClose(evt) {
        for(var channel in channelArray){
            channel.unsubscribe();
        }
        for ( var i in webSocketOnCloseCallbacks) {
            webSocketOnCloseCallbacks[i](evt);
        }
    }

    function fireOnError(evt) {
        for ( var i in webSocketOnErrorCallbacks) {
            webSocketOnErrorCallbacks[i](evt);
        }
    }

    function fireOnServerMessage(json) {
        for ( var i in onServerMessageCallbacks) {
            onServerMessageCallbacks[i](json);
        }
    }

    function unsubscribe(channelIDIndex) {
		var json = JSON.stringify({
			"message" : "unsubscribe",
			"id" : channelIDIndex
		});
		clientSelf.sendText(json);
		delete channelArray[channelIDIndex];
	}

	function pauseChannel(channelIDIndex){
		var json = JSON.stringify({
			"message" : "pause",
			"id" : channelIDIndex,
		});
		clientSelf.sendText(json);
	}

	function resumeChannel(channelIDIndex){
		var json = JSON.stringify({
			"message" : "resume",
			"id" : channelIDIndex,
		});
		clientSelf.sendText(json);
	}

	function setChannelValue(channelIDIndex, value){
		var json = JSON.stringify({
            "message" : "write",
			"id" : channelIDIndex,
			"value" : value
		});
		clientSelf.sendText(json);
	}

	function Channel(name) {
        /**Name of the Channel.
         * @type {string}
         */
        this.name = name;
        this.id = -1;
        this.value = null;
        this.channelCallbacks = [];
        this.paused = false;
        this.connected = false;
        this.readOnly = true;

    }
    /**If the channel is connected to the device.
     * @returns {boolean} true if the channel is connected.
     */
    Channel.prototype.isConnected = function(){
        return this.connected;
    };
    /**
     * Get id of the Channel.
     * return {object} the value which is a data structure depending on the Channel.
     */
    Channel.prototype.getId = function(){
        return this.id;
    };

    /**
     * If write operation is allowed on the channel
     * @return {boolean}
     */
    Channel.prototype.isWriteAllowed = function(){
        return !this.readOnly;
    };

    /**
     * If the channel is paused
     * @return {boolean}
     */
    Channel.prototype.isPaused = function(){
        return this.paused;
    };

    /**
     * Get value of the Channel.
     * return {object} the value which is a data structure depending on the Channel.
     */
    Channel.prototype.getValue = function(){
        return this.value;
    };
    /**
     * Add a callback to the Channel that will be notified on Channel's event.
     * @param {Client~Channel~ChannelCallback} callback the callback function.
     */
    Channel.prototype.addCallback = function(callback) {
        this.channelCallbacks.push(callback);
    };

    /**
     * Remove a callback.
     * @param {Client~Channel~ChannelCallback} callback the callback function.
     */
    Channel.prototype.removeCallback = function(callback) {
        for ( var i in this.channelCallbacks) {
            if (this.channelCallbacks[i] == callback)
                delete this.channelCallbacks[i];
        }
    };
    /**
     * Set channel value.
     * @param {object} value
     *        the value to be set. It must be a value type that the Channel can accept,
     *        for example, a number for numeric Channel.
     */
    Channel.prototype.setValue = function(value) {
        setChannelValue(this.id, value);
    };

    Channel.prototype.updateValue = function() {
            setChannelValue(this.id, this.value.value);
    };

    /**
     * unsubscribe channel .
     *
     */
    Channel.prototype.unsubscribe = function() {
        unsubscribe(this.id);
    };

    Channel.prototype.pause = function() {
        this.paused = true;
        pauseChannel(this.id);
    };

    Channel.prototype.resume = function() {
        this.paused = false;
        resumeChannel(this.id);
    };

    Channel.prototype.fireChannelEventFunc = function(json) {
        // update the  properties of the channel
        // processJson should be implemented in specific protocol library
        processJsonForChannel(json, this);
        for ( var i in this.channelCallbacks) {
            this.channelCallbacks[i](json, this);
        }
    };
}<commit_msg>make readOnly = true as default<commit_after>function Client(url, debug, debugMessageBox) {

	var channelIDIndex = 0;
	var channelArray = [];
	var websocket = null;
	var webSocketOnOpenCallbacks = [];
	var webSocketOnCloseCallbacks = [];
	var webSocketOnErrorCallbacks = [];
	var onServerMessageCallbacks = [];
	var clientSelf = this;
	var debug = debug;
	var debugMessageBox = debugMessageBox;
	var defaultTypeVersion = 1;
    this.isLive = false;

	openWebSocket(url);

    /**
	 * Add a callback to WebSocket onOpen event.
	 * @param {Client~WebSocketEventCallback} callback the callback function on WebSocket open event.
	 */
	this.addWebSocketOnOpenCallback = function(callback) {
		webSocketOnOpenCallbacks.push(callback);
	};

	/**
	 * Remove a WebSocket onOpen callback.
	 * @param {Client~WebSocketEventCallback} callback the callback function on WebSocket open event.
	 */
	this.removeWebSocketOnOpenCallback = function(callback){
		webSocketOnOpenCallbacks.splice(webSocketOnOpenCallbacks.indexOf(callback), 1);
	};

	/**
	 * Add a callback to WebSocket onClose event.
	 * @param {Client~WebSocketEventCallback} callback the callback function on WebSocket close event.
	 *
	 */
	this.addWebSocketOnCloseCallback = function(callback) {
		webSocketOnCloseCallbacks.push(callback);
	};

	/**
	 * Add a callback to WebSocket onError event.
	 * @param {Client~WebSocketEventCallback} callback the callback function on WebSocket error event.
	 *
	 */
	this.addWebSocketOnErrorCallback = function(callback) {
		webSocketOnErrorCallbacks.push(callback);
	};

	/**A callback function that will be notified when there is a message from server.
	 * @callback Client~OnServerMessageCallback
	 *
	 */

	/**
	 * Add a callback that will be notified when there is a notification message from server.
	 * @param {Client~OnServerMessageCallback} callback the callback
	 *
	 */
	this.addOnServerMessageCallback = function(callback) {
		onServerMessageCallbacks.push(callback);
	};

    this.subscribeChannel = function(name, readOnly, type, version, maxRate) {
        var typeJson;
        if(readOnly == null) {
            readOnly = true;
        }
        if (type != null) {
            if (version == null)
                version = defaultVersion;
            typeJson = JSON.stringify({
                "name" : type,
                "version" : version
            });

        }
        var json = JSON.stringify({
            "message" : "subscribe",
            "id" : channelIDIndex,
            "channel" : name,
            "readOnly" : readOnly,
            "maxRate" : maxRate,
            "type" : typeJson
        });
        var channel = new Channel(name);
        channelArray[channelIDIndex] = channel;
        channel.id = channelIDIndex;
        channel.name = name;
        channel.readOnly = readOnly;
        channel.type = type;
        channel.connected = true;
        if(this.isLive)
            this.sendText(json);
        else{
            var webpdaSelf = this;
            var listener = null;
            listener = function(evt){
                clientSelf.sendText(json);
                setTimeout(function(){
                    clientSelf.removeWebSocketOnOpenCallback(listener);
                }, 0);
            };
            this.addWebSocketOnOpenCallback(listener);
        }
        channelIDIndex++;
        return channel;
    };

    function openWebSocket(url) {
        if ('WebSocket' in window) {
            websocket = new WebSocket(url, "org.client");
        } else if ('MozWebSocket' in window) {
            websocket = new MozWebSocket(url, "org.client");
        } else {
            throw new Error('WebSocket is not supported by this browser.');
        }
        websocket.binaryType = "arraybuffer";
        websocket.onopen = function(evt) {
            fireOnOpen(evt);
        };

        websocket.onmessage = function(evt) {
            var json;
            json = JSON.parse(evt.data);
            dispatchMessage(json);

        };

        websocket.onerror = function(evt) {
            fireOnError(evt);
        };

    }

    function writeToScreen(message) {
        if (debugMessageBox != null)
            document.getElementById(debugMessageBox).value = message;
    }

    /**
     * Close Websocket.
     */
    this.close = function() {
        if (websocket != null)
            websocket.close();
        websocket = null;
    };

    this.sendText =function(text) {
        websocket.send(text);
        if(debug)
            writeToScreen(text);
    };

    /**
     * Get the Channel from its id.
     * @param {number} id id of the Channel.
     * @returns {Client~Channel} the Channel.
     */
    this.getChannel = function(id){
        return channelArray[id];
    };

    /**Get all Channels on this client.
     * @returns {Array.<Client~Channel>} All Channels in an array.
     */
    this.getAllChannels = function() {
        return channelArray;
    };

    function dispatchMessage(json) {
        if (json.message != null)
            handleServerMessage(json);
        if (json.id != null) {
            if (channelArray[json.id] != null)
                channelArray[json.id].fireChannelEventFunc(json);
        }
    }

    function handleServerMessage(json) {
		if(json.type == "error")
			console.log("Error: " + json.error);
		fireOnServerMessage(json);
	}

	function processJsonForChannel(json, Channel) {
        switch (json.type) {
        case "connection":
            Channel.connected = json.connected;
            Channel.readOnly = !json.writeConnected;
            break;
        case "value":
            Channel.value =  json.value;
            break;
        default:
            break;
        }
    };
    function fireOnOpen(evt) {
        clientSelf.isLive = true;
        for ( var i in webSocketOnOpenCallbacks) {
            webSocketOnOpenCallbacks[i](evt);
        }
    }

    function fireOnClose(evt) {
        for(var channel in channelArray){
            channel.unsubscribe();
        }
        for ( var i in webSocketOnCloseCallbacks) {
            webSocketOnCloseCallbacks[i](evt);
        }
    }

    function fireOnError(evt) {
        for ( var i in webSocketOnErrorCallbacks) {
            webSocketOnErrorCallbacks[i](evt);
        }
    }

    function fireOnServerMessage(json) {
        for ( var i in onServerMessageCallbacks) {
            onServerMessageCallbacks[i](json);
        }
    }

    function unsubscribe(channelIDIndex) {
		var json = JSON.stringify({
			"message" : "unsubscribe",
			"id" : channelIDIndex
		});
		clientSelf.sendText(json);
		delete channelArray[channelIDIndex];
	}

	function pauseChannel(channelIDIndex){
		var json = JSON.stringify({
			"message" : "pause",
			"id" : channelIDIndex,
		});
		clientSelf.sendText(json);
	}

	function resumeChannel(channelIDIndex){
		var json = JSON.stringify({
			"message" : "resume",
			"id" : channelIDIndex,
		});
		clientSelf.sendText(json);
	}

	function setChannelValue(channelIDIndex, value){
		var json = JSON.stringify({
            "message" : "write",
			"id" : channelIDIndex,
			"value" : value
		});
		clientSelf.sendText(json);
	}

	function Channel(name) {
        /**Name of the Channel.
         * @type {string}
         */
        this.name = name;
        this.id = -1;
        this.value = null;
        this.channelCallbacks = [];
        this.paused = false;
        this.connected = false;
        this.readOnly = true;

    }
    /**If the channel is connected to the device.
     * @returns {boolean} true if the channel is connected.
     */
    Channel.prototype.isConnected = function(){
        return this.connected;
    };
    /**
     * Get id of the Channel.
     * return {object} the value which is a data structure depending on the Channel.
     */
    Channel.prototype.getId = function(){
        return this.id;
    };

    /**
     * If write operation is allowed on the channel
     * @return {boolean}
     */
    Channel.prototype.isWriteAllowed = function(){
        return !this.readOnly;
    };

    /**
     * If the channel is paused
     * @return {boolean}
     */
    Channel.prototype.isPaused = function(){
        return this.paused;
    };

    /**
     * Get value of the Channel.
     * return {object} the value which is a data structure depending on the Channel.
     */
    Channel.prototype.getValue = function(){
        return this.value;
    };
    /**
     * Add a callback to the Channel that will be notified on Channel's event.
     * @param {Client~Channel~ChannelCallback} callback the callback function.
     */
    Channel.prototype.addCallback = function(callback) {
        this.channelCallbacks.push(callback);
    };

    /**
     * Remove a callback.
     * @param {Client~Channel~ChannelCallback} callback the callback function.
     */
    Channel.prototype.removeCallback = function(callback) {
        for ( var i in this.channelCallbacks) {
            if (this.channelCallbacks[i] == callback)
                delete this.channelCallbacks[i];
        }
    };
    /**
     * Set channel value.
     * @param {object} value
     *        the value to be set. It must be a value type that the Channel can accept,
     *        for example, a number for numeric Channel.
     */
    Channel.prototype.setValue = function(value) {
        setChannelValue(this.id, value);
    };

    Channel.prototype.updateValue = function() {
            setChannelValue(this.id, this.value.value);
    };

    /**
     * unsubscribe channel .
     *
     */
    Channel.prototype.unsubscribe = function() {
        unsubscribe(this.id);
    };

    Channel.prototype.pause = function() {
        this.paused = true;
        pauseChannel(this.id);
    };

    Channel.prototype.resume = function() {
        this.paused = false;
        resumeChannel(this.id);
    };

    Channel.prototype.fireChannelEventFunc = function(json) {
        // update the  properties of the channel
        // processJson should be implemented in specific protocol library
        processJsonForChannel(json, this);
        for ( var i in this.channelCallbacks) {
            this.channelCallbacks[i](json, this);
        }
    };
}<|endoftext|>


<commit_before>
var template = [];

// template object 123
if(window.Ravel.isLoaded) {

	var _TEMPLATE = function( url ) {

			this.url = url;

			this.load = function( params ) {

				if (window.XMLHttpRequest) { ajax = new XMLHttpRequest() }

				if(typeof(params) == "object" ) {

					p = JSON.stringify(params).replace(/[\"\}\{]/g, '');
					param = p.replace(/[\:]/g,"=").replace(/[\,]/g, "&");

					ajax.open("GET", "document/web/template/" + this.url + "?" + param, false);
					ajax.setRequestHeader("Content-Type", "text/html; charset=UTF-8");
					ajax.send(null);
					ajax_response = ajax.responseText;

					return ajax_response;

				} else {

					ajax.open("GET", "document/web/template/" + this.url, false);
					ajax.setRequestHeader("Content-Type", "text/html; charset=UTF-8");
					ajax.send();
					ajax_response = ajax.responseText;

					return ajax_response;

				}

			}

			return this;

	}

}
<commit_msg>Update template.js<commit_after>
var template = [];

// template object
if(window.Ravel.isLoaded) {

	var _TEMPLATE = function( url ) {

			this.url = url;

			this.load = function( params ) {

				if (window.XMLHttpRequest) { ajax = new XMLHttpRequest() }

				if(typeof(params) == "object" ) {

					p = JSON.stringify(params).replace(/[\"\}\{]/g, '');
					param = p.replace(/[\:]/g,"=").replace(/[\,]/g, "&");

					ajax.open("GET", "document/web/template/" + this.url + "?" + param, false);
					ajax.setRequestHeader("Content-Type", "text/html; charset=UTF-8");
					ajax.send(null);
					ajax_response = ajax.responseText;

					return ajax_response;

				} else {

					ajax.open("GET", "document/web/template/" + this.url, false);
					ajax.setRequestHeader("Content-Type", "text/html; charset=UTF-8");
					ajax.send();
					ajax_response = ajax.responseText;

					return ajax_response;

				}

			}

			return this;

	}

}
<|endoftext|>


<commit_before>module.exports = function(grunt) {
    'use strict';

    grunt.initConfig({
        bboxcache: {
            default: {
                files: {
                    'static/hack/bboxcache.json': ['static/map/*.svg'],
                },
            },
        },
        bower_concat: {
            all: {
                dest: 'static/dist/js/bower-libs.js',
                cssDest: 'static/dist/css/bower-libs.css',
                dependencies: {
                    'kartograph.js': ['jquery']
                },
                mainFiles: {
                    'angular-i18n': 'angular-locale_cs-cz.js'
                }
            }
        },
        concat: {
            geography: {
                src: ['static/js/*.js'],
                dest: 'static/dist/js/geography.js'
            }
        },
        copy: {
            'above-fold': {
                src: 'static/dist/css/above-fold.css',
                dest: 'templates/dist/above-fold.css'
            }
        },
        html2js: {
            options: {
                base: '.',
                module: 'proso.geography.templates',
                singleModule: true,
                useStric: true
            },
            geography: {
                src: ['static/tpl/*.html'],
                dest: 'static/dist/js/geography.html.js',
            }
        },
        sass: {
            options: {
                sourcemap: "inline",
                style: "compressed"
            },
            geography: {
                files: [{
                    expand: true,
                    cwd: 'static/sass',
                    src: ['*.sass'],
                    dest: 'static/dist/css',
                    ext: '.css'
                }]
            }
        },
        shell: {
            bower_install: {
                command: 'bower install -f'
            }
        },
        'string-replace': {
            bboxcache: {
                options: {
                    replacements: [{
                        pattern: '{{bboxes}}',
                        replacement: "<%= grunt.file.read('static/hack/bboxcache.json') %>"
                    }]
                },
                src: ['static/jstpl/bbox.js'],
                dest: 'static/dist/js/bbox.js',
            }
        },
        uglify: {
            libs: {
                options: {
                    mangle: {
                        except: ['Kartograph']
                    },
                    sourceMap: true,
                    sourceMapIncludeSources: true,
                    sourceMapName: 'static/dist/js/bower-libs.min.js.map'
                },
                src: 'static/dist/js/bower-libs.js',
                dest: 'static/dist/js/bower-libs.min.js'
            },
            geography: {
                options: {
                    sourceMap: true,
                    sourceMapIncludeSources: true,
                    sourceMapName: 'static/dist/geography.min.js.map'
                },
                src: 'static/dist/js/geography.js',
                dest: 'static/dist/js/geography.min.js'
            },
            'geography-tpls': {
                options: {
                    sourceMap: true,
                    sourceMapIncludeSources: true,
                    sourceMapName: 'static/dist/geography-tpls.min.js.map'
                },
                src: 'static/dist/js/geography-tpls.js',
                dest: 'static/dist/js/geography-tpls.min.js'
            },

        }
    });

    grunt.loadNpmTasks('grunt-bower-concat');
    grunt.loadNpmTasks('grunt-contrib-concat');
    grunt.loadNpmTasks('grunt-contrib-copy');
    grunt.loadNpmTasks('grunt-contrib-uglify');
    grunt.loadNpmTasks('grunt-contrib-sass');
    grunt.loadNpmTasks('grunt-shell');
    grunt.loadNpmTasks('grunt-string-replace');
    grunt.loadNpmTasks('grunt-html2js');

    grunt.registerTask('bboxcache-all', ['bboxcache', 'string-replace:bboxcache']);
    grunt.registerTask('collect-libs', ['bower_concat:all', 'uglify:libs']);
    grunt.registerTask('prepare-libs', ['shell:bower_install', 'collect-libs']);
    grunt.registerTask('prepare', ['html2js:geography', 'concat:geography', 'uglify:geography', 'sass:geography', 'copy:above-fold']);
    grunt.registerTask('default', ['bboxcache-all', 'prepare-libs', 'prepare']);

    /* CUSTOM TASKS */

    grunt.registerMultiTask('bboxcache', 'Precompute bbox of svg paths.', function() {

        var raphael = require('node-raphael');
        var DomJS = require("dom-js").DomJS;
        var RANDOM_CONST = 42;

        function mapNameFromFilepath (filepath) {
            var splittedPath = filepath.split('/');
            var filename = splittedPath[splittedPath.length -1];
            return filename.split('.')[0];
        }

        // Iterate over all specified file groups.
        this.files.forEach(function(f) {
            var cache = {
                bboxes : {},
                maps : {},
            };

            f.src.filter(function(filepath) {
                // Warn on and remove invalid source files (if nonull was set).
                if (!grunt.file.exists(filepath)) {
                    grunt.log.warn('Source file "' + filepath + '" not found.');
                    return false;
                } else {
                    return true;
                }
            }).map(function(filepath) {

                var domjs = new DomJS();

                domjs.parse(grunt.file.read(filepath), function(err, dom) {
                    var mapName = mapNameFromFilepath(filepath);
                    var map = {
                        width :  parseInt(dom.attributes.width),
                        height :  parseInt(dom.attributes.height),
                    };
                    cache.maps[mapName] = map;

                    raphael.generate(RANDOM_CONST, RANDOM_CONST, function draw(paper) {
                        dom.children.filter(function(e) {
                            return e.name == 'g';
                        }).map(function (e) {
                            return e.children.filter(function(ch) {
                                return ch.name == 'path' && ch.attributes['data-code'];
                            }).map(function(ch) {
                                var d = ch.attributes.d;
                                var code = ch.attributes['data-code'];
                                if (code) {
                                    var path = paper.path(d);
                                    var bbox =  path.getBBox();
                                    var keys = ['x', 'y', 'cx', 'cy', 'x2', 'y2', 'width', 'height'];
                                    for (var j = 0; j < keys.length; j++) {
                                        bbox[keys[j]] = Math.round(bbox[keys[j]]);
                                    }
                                    bbox.map = mapName;
                                    if (!cache.bboxes[code]) {
                                        cache.bboxes[code] = bbox;
                                    }
                                }
                            });
                        });
                    });
                });
                return;
            });

            // Write the destination file.
            grunt.file.write(f.dest, JSON.stringify(cache));

            // Print a success message.
            grunt.log.writeln('File "' + f.dest + '" created.');
        });
    });
}
<commit_msg>remove needless option<commit_after>module.exports = function(grunt) {
    'use strict';

    grunt.initConfig({
        bboxcache: {
            default: {
                files: {
                    'static/hack/bboxcache.json': ['static/map/*.svg'],
                },
            },
        },
        bower_concat: {
            all: {
                dest: 'static/dist/js/bower-libs.js',
                cssDest: 'static/dist/css/bower-libs.css',
                dependencies: {
                    'kartograph.js': ['jquery']
                },
                mainFiles: {
                    'angular-i18n': 'angular-locale_cs-cz.js'
                }
            }
        },
        concat: {
            geography: {
                src: ['static/js/*.js'],
                dest: 'static/dist/js/geography.js'
            }
        },
        copy: {
            'above-fold': {
                src: 'static/dist/css/above-fold.css',
                dest: 'templates/dist/above-fold.css'
            }
        },
        html2js: {
            options: {
                base: '.',
                module: 'proso.geography.templates',
                singleModule: true,
                useStric: true
            },
            geography: {
                src: ['static/tpl/*.html'],
                dest: 'static/dist/js/geography.html.js',
            }
        },
        sass: {
            options: {
                style: "compressed"
            },
            geography: {
                files: [{
                    expand: true,
                    cwd: 'static/sass',
                    src: ['*.sass'],
                    dest: 'static/dist/css',
                    ext: '.css'
                }]
            }
        },
        shell: {
            bower_install: {
                command: 'bower install -f'
            }
        },
        'string-replace': {
            bboxcache: {
                options: {
                    replacements: [{
                        pattern: '{{bboxes}}',
                        replacement: "<%= grunt.file.read('static/hack/bboxcache.json') %>"
                    }]
                },
                src: ['static/jstpl/bbox.js'],
                dest: 'static/dist/js/bbox.js',
            }
        },
        uglify: {
            libs: {
                options: {
                    mangle: {
                        except: ['Kartograph']
                    },
                    sourceMap: true,
                    sourceMapIncludeSources: true,
                    sourceMapName: 'static/dist/js/bower-libs.min.js.map'
                },
                src: 'static/dist/js/bower-libs.js',
                dest: 'static/dist/js/bower-libs.min.js'
            },
            geography: {
                options: {
                    sourceMap: true,
                    sourceMapIncludeSources: true,
                    sourceMapName: 'static/dist/geography.min.js.map'
                },
                src: 'static/dist/js/geography.js',
                dest: 'static/dist/js/geography.min.js'
            },
            'geography-tpls': {
                options: {
                    sourceMap: true,
                    sourceMapIncludeSources: true,
                    sourceMapName: 'static/dist/geography-tpls.min.js.map'
                },
                src: 'static/dist/js/geography-tpls.js',
                dest: 'static/dist/js/geography-tpls.min.js'
            },

        }
    });

    grunt.loadNpmTasks('grunt-bower-concat');
    grunt.loadNpmTasks('grunt-contrib-concat');
    grunt.loadNpmTasks('grunt-contrib-copy');
    grunt.loadNpmTasks('grunt-contrib-uglify');
    grunt.loadNpmTasks('grunt-contrib-sass');
    grunt.loadNpmTasks('grunt-shell');
    grunt.loadNpmTasks('grunt-string-replace');
    grunt.loadNpmTasks('grunt-html2js');

    grunt.registerTask('bboxcache-all', ['bboxcache', 'string-replace:bboxcache']);
    grunt.registerTask('collect-libs', ['bower_concat:all', 'uglify:libs']);
    grunt.registerTask('prepare-libs', ['shell:bower_install', 'collect-libs']);
    grunt.registerTask('prepare', ['html2js:geography', 'concat:geography', 'uglify:geography', 'sass:geography', 'copy:above-fold']);
    grunt.registerTask('default', ['bboxcache-all', 'prepare-libs', 'prepare']);

    /* CUSTOM TASKS */

    grunt.registerMultiTask('bboxcache', 'Precompute bbox of svg paths.', function() {

        var raphael = require('node-raphael');
        var DomJS = require("dom-js").DomJS;
        var RANDOM_CONST = 42;

        function mapNameFromFilepath (filepath) {
            var splittedPath = filepath.split('/');
            var filename = splittedPath[splittedPath.length -1];
            return filename.split('.')[0];
        }

        // Iterate over all specified file groups.
        this.files.forEach(function(f) {
            var cache = {
                bboxes : {},
                maps : {},
            };

            f.src.filter(function(filepath) {
                // Warn on and remove invalid source files (if nonull was set).
                if (!grunt.file.exists(filepath)) {
                    grunt.log.warn('Source file "' + filepath + '" not found.');
                    return false;
                } else {
                    return true;
                }
            }).map(function(filepath) {

                var domjs = new DomJS();

                domjs.parse(grunt.file.read(filepath), function(err, dom) {
                    var mapName = mapNameFromFilepath(filepath);
                    var map = {
                        width :  parseInt(dom.attributes.width),
                        height :  parseInt(dom.attributes.height),
                    };
                    cache.maps[mapName] = map;

                    raphael.generate(RANDOM_CONST, RANDOM_CONST, function draw(paper) {
                        dom.children.filter(function(e) {
                            return e.name == 'g';
                        }).map(function (e) {
                            return e.children.filter(function(ch) {
                                return ch.name == 'path' && ch.attributes['data-code'];
                            }).map(function(ch) {
                                var d = ch.attributes.d;
                                var code = ch.attributes['data-code'];
                                if (code) {
                                    var path = paper.path(d);
                                    var bbox =  path.getBBox();
                                    var keys = ['x', 'y', 'cx', 'cy', 'x2', 'y2', 'width', 'height'];
                                    for (var j = 0; j < keys.length; j++) {
                                        bbox[keys[j]] = Math.round(bbox[keys[j]]);
                                    }
                                    bbox.map = mapName;
                                    if (!cache.bboxes[code]) {
                                        cache.bboxes[code] = bbox;
                                    }
                                }
                            });
                        });
                    });
                });
                return;
            });

            // Write the destination file.
            grunt.file.write(f.dest, JSON.stringify(cache));

            // Print a success message.
            grunt.log.writeln('File "' + f.dest + '" created.');
        });
    });
}
<|endoftext|>


<commit_before>// Application class
// Requries a datamodel in the "Model" module
// Manages switching ui components which are views and associated viewmodels
// Views are HTML partial pages, ViewModels are the underlying databindings and behavior
// of the View. The ViewModel should take and return data to the Model object.
define(['knockout', 'underscore', 'jquery', 'guid'],
function (ko, _, $, Guid)
{
    // Page Composition Algorithm:
    // -----------------------------------
    // 1. Find all data-component nodes and insert into queue (processing the DOM breadth-first)
    // 2. Take a node off the queue, parse the nodes parameters if any
    // 3. Add events and fields to the node and loaded component
    // 4. Find the components parent, use Application if none, and add variable containers for that component
    // 5. Add visibility and context databindings to the root node
    // 6. Insert the expanded template into the components root as html
    // 7. Parse the components html children for data-component nodes and add them to the queue
    // 8. When the queue is empty, the page is finished loading

    // Application public interface
    var Application = {

        Model: ko.observable({}),

        Name: ko.observable(''),

        Components: ko.observableArray([]),

        Visible: ko.observable(true),

        Children: ko.observableArray(),

        // Composes the page and applies data bindings
        Compose: function (components)
        {
            this.Components(components);
            LoadComponents();

            // Apply initial component types
            TypeComponents($('body'));

            var topLevelComponents = $('[data-component]').toArray();
            var expandedModels = ExpandComponents(topLevelComponents);

            // trigger loaded event from bottom up
            expandedModels.reverse();
            _(expandedModels).each(function (comp) { $(comp).triggerHandler('Loaded'); });

            // Finally trigger that the application as a whole is loaded
            $(this).triggerHandler('Loaded');

            ko.applyBindings(this);
        },

        // Activate a component, attached to all child components ViewModels
        Activate: function ()
        {
            this.Visible(true);

            if (this.View().data('componentType') == 'collection')
            {
                var data = ko.dataFor(this.View().get(0));

                var params = [data].concat(arguments);
                $(this).triggerHandler('Activate', params);
            }
            else
            {
                $(this).triggerHandler('Activate', arguments);
            }
        },

        // Finishes a component, attached to all child components ViewModels
        Finish: function ()
        {
            this.Visible(false);
            $(this).triggerHandler('Finish', arguments);
        },

        // Find an component by id using a depth first search (recursive)
        Find: function (componentId)
        {
            var target = null;

            _(this.Children()).each(function (child)
            {
                var foundChild = null;
                if (child.Uid == componentId)
                {
                    foundChild = child;
                }
                else
                {
                    foundChild = child.Find(componentId);
                }

                if (foundChild != null)
                {
                    target = foundChild;
                }
            });

            return target;
        },

        // Inject a component dynamically
        InjectComponent: function (component)
        {
            // Expand the component into the DOM
            var viewModels = ExpandComponent(component);
            viewModels.reverse();

            // Trigger loaded handlers from the bottom up
            _(viewModels).each(function (vm) { $(vm).triggerHandler('Loaded'); });

            // Apply bindings to the root level injected node using its parent
            var parent = _(viewModels).last().Parent();
            ko.applyBindingsToNode(component, null, parent);

            // Return the root of the injected componentsQueue
            return _(viewModels).last();
        }
    }

    // Application private interface

    // Parses a javascript object from a string, NOT json
    function ParseJsObject(string)
    {
        // Use 'eval' to create a javascript object from the malformed JSON
        // NOTE: there are some security concerns with this method
        var parenString = '(' + string + ')'; // object literals must be enclosed in parenthesis for eval to work properly
        var obj = eval(parenString);

        return obj;
    }

    // Loads view templates and styles for each component
    function _LoadComponents()
    {
        // Put JQuery AJAX into synchronous mode for this algorithm to work, we will clear this flag once page loading is complete
        $.ajaxSetup({ async: false });

        // Preload styles and templates for each component
        _(this.Components()).each(function (comp)
        {
            // Append the style node to the pages head
            if ('Style' in comp)
            {
                var styleLink = $('<link rel="stylesheet" type="text/css" href="' + comp.Style + '"/>');
                $('head').append(styleLink);
            }

            // Load the view template and store it for later
            $.get(comp.View, {}, function (viewData)
            {
                comp.Template = viewData;
            });
        });

        // Put JQuery back into asynchronous mode for future ajax requests
        $.ajaxSetup({ async: true });
    }
    var LoadComponents = _.bind(_LoadComponents, Application);

    // Shorthand to expand a single component
    // returns a tree structure in an array of the expanded components
    function _ExpandComponent(component)
    {
        return ExpandComponents([component]);
    }
    var ExpandComponent = _.bind(_ExpandComponent, Application);

    // Expands a data-component element into its component, uses a breadth-first traversal
    // Returns a list of the constructed view models that were expanded
    function _ExpandComponents(components)
    {
        var componentsQueue = components;
        var viewModels = [];

        while (componentsQueue.length > 0)
        {
            var componentRoot = $(componentsQueue.shift()); // dequeue operation
            var componentName = componentRoot.data('component');

            // Check the component type to make sure it isnt a collection component
            var componentType = componentRoot.data('componentType');
            if (componentType != 'collection')
            {
                // Find the component description
                var component = _(this.Components()).findWhere({ Name: componentName });
                if (component)
                {
                    var viewModel = BuildComponent(componentRoot, component);

                    // Add the view model to the list of view models that were processed
                    viewModels.push(viewModel);

                    // Find any child data-component nodes and push them onto the queue
                    var childComponents = componentRoot.find('[data-component]').toArray();
                    _(childComponents).each(function (child)
                    {
                        componentsQueue.push(child);
                    });
                }
            }
        }

        return viewModels;
    }
    var ExpandComponents = _.bind(_ExpandComponents, Application);

    function _BuildComponent(componentRoot, component, type)
    {
        // Get the components view parameters
        var params = componentRoot.data('parameters');
        if (params)
        {
            params = ParseJsObject(params);
        }

        // Get the name to use as a field name
        var fieldName = componentRoot.data('name');

        // Create the view model and add standard fields
        var viewModel = new component.ViewModel();
        viewModel.Visible = ko.observable(false); // Hidden by default
        viewModel.Name = fieldName;
        viewModel.ViewParameters = params;
        viewModel.Children = ko.observableArray();
        viewModel.Find = this.Find;
        viewModel.Activate = this.Activate;
        viewModel.Finish = this.Finish;
        viewModel.Uid = Guid.NewGuid();
        viewModel.View = function () { return $('#' + this.Uid); };

        // Find the parent of the view, using app when there is no parent
        var parentRoot = componentRoot.parent().closest('[data-component]');
        var parent = this;
        if (parentRoot.length > 0)
        {
            var parent = this.Find(parentRoot.attr('id'));
        }

        // Add the viewmodel to its parent and add a parent property to the viewmodel
        if (type != 'collection')
        {
            parent[fieldName] = ko.observable(viewModel);

            // Add databinding for visibility and context to the component root node
            componentRoot.attr('data-bind', 'visible: ' + fieldName + '().Visible, with: ' + fieldName);
        }
        else
        {
            if (!(fieldName in parent))
            {
                parent[fieldName] = {};
            }

            parent[fieldName][viewModel.Uid] = ko.observable(viewModel);

            // Add databinding for visibility and context to the component root node
            componentRoot.attr('data-bind', 'visible: $parent.' + fieldName + '["' + viewModel.Uid + '"]().Visible, with: $parent.' + fieldName + '["' + viewModel.Uid + '"]');
        }

        parent.Children.push(viewModel);
        viewModel.Parent = ko.observable(parent);

        componentRoot.attr('id', viewModel.Uid);
        componentRoot.hide();   // Hide by default so that the views don't flash on the screen before knockout kicks in

        // Compile the view using its parameters
        var compiledView = _.template(component.Template, params);

        // Insert the compiled view into the DOM
        componentRoot.html(compiledView);

        // Type any child components
        TypeComponents(componentRoot);

        return viewModel;
    }
    var BuildComponent = _.bind(_BuildComponent, Application);

    function TypeComponents(root)
    {
        // Select foreach data-binds and mark components inside as having type "collection"
        var collectionComponents = root.find('[data-bind*="foreach:"] [data-component]');
        collectionComponents.attr('data-component-type', 'collection');

        // Select if and ifnot data-binds and mark components inside as having type "conditional"
        var conditionalComponents = root.find('[data-bind*="if:"],[data-bind*="ifnot:"] [data-component]');
        conditionalComponents.attr('data-component-type', 'conditional');
    }

    // Updates the title element when the application name is set
    Application.Name.subscribe(function (value)
    {
        var titleElem = $('title');
        if (titleElem.length > 0)
        {
            titleElem.text(value);
        }
        else
        {
            titleElem = $('<title>' + value + '</title>');
            $('head').append(titleElem);
        }
    });

    // Add node processor for foreach bindings
    ko.bindingProvider.instance.preprocessNode = function (node)
    {
        var componentType = $(node).data('componentType');
        var componentName = $(node).data('component');

        if (componentType && componentType == 'collection')
        {
            // Find the component description
            var component = _(Application.Components()).findWhere({ Name: componentName });
            if (component)
            {
                var viewModel = BuildComponent($(node), component, componentType);

                var childComponents = viewModel.View().find('[data-component]').toArray();
                var expandedModels = ExpandComponents(childComponents);

                // trigger loaded event from bottom up
                expandedModels.reverse();
                _(expandedModels).each(function (comp) { $(comp).triggerHandler('Loaded'); });

                $(viewModel).triggerHandler('Loaded');
            }
        }
        else if (componentType && componentType == 'conditional')
        {
            Application.InjectComponent(node);
        }
    };

    return Application;
});
<commit_msg>Added automatic cleanup of viewmodels when their view is removed from the DOM for any reason<commit_after>// Application class
// Requries a datamodel in the "Model" module
// Manages switching ui components which are views and associated viewmodels
// Views are HTML partial pages, ViewModels are the underlying databindings and behavior
// of the View. The ViewModel should take and return data to the Model object.
define(['knockout', 'underscore', 'jquery', 'guid'],
function (ko, _, $, Guid)
{
    // Page Composition Algorithm:
    // -----------------------------------
    // 1. Find all data-component nodes and insert into queue (processing the DOM breadth-first)
    // 2. Take a node off the queue, parse the nodes parameters if any
    // 3. Add events and fields to the node and loaded component
    // 4. Find the components parent, use Application if none, and add variable containers for that component
    // 5. Add visibility and context databindings to the root node
    // 6. Insert the expanded template into the components root as html
    // 7. Parse the components html children for data-component nodes and add them to the queue
    // 8. When the queue is empty, the page is finished loading

    // Application public interface
    var Application = {

        Model: ko.observable({}),

        Name: ko.observable(''),

        Components: ko.observableArray([]),

        Visible: ko.observable(true),

        Children: ko.observableArray(),

        // Composes the page and applies data bindings
        Compose: function (components)
        {
            this.Components(components);
            LoadComponents();

            // Apply initial component types
            TypeComponents($('body'));

            var topLevelComponents = $('[data-component]').toArray();
            var expandedModels = ExpandComponents(topLevelComponents);

            // trigger loaded event from bottom up
            expandedModels.reverse();
            _(expandedModels).each(function (comp) { $(comp).triggerHandler('Loaded'); });

            // Finally trigger that the application as a whole is loaded
            $(this).triggerHandler('Loaded');

            ko.applyBindings(this);
        },

        // Activate a component, attached to all child components ViewModels
        Activate: function ()
        {
            this.Visible(true);

            if (this.View().data('componentType') == 'collection')
            {
                var data = ko.dataFor(this.View().get(0));

                var params = [data].concat(arguments);
                $(this).triggerHandler('Activate', params);
            }
            else
            {
                $(this).triggerHandler('Activate', arguments);
            }
        },

        // Finishes a component, attached to all child components ViewModels
        Finish: function ()
        {
            this.Visible(false);
            $(this).triggerHandler('Finish', arguments);
        },

        // Find an component by id using a depth first search (recursive)
        Find: function (componentId)
        {
            var target = null;

            _(this.Children()).each(function (child)
            {
                var foundChild = null;
                if (child.Uid == componentId)
                {
                    foundChild = child;
                }
                else
                {
                    foundChild = child.Find(componentId);
                }

                if (foundChild != null)
                {
                    target = foundChild;
                }
            });

            return target;
        },

        // Inject a component dynamically
        InjectComponent: function (component)
        {
            // Expand the component into the DOM
            var viewModels = ExpandComponent(component);
            viewModels.reverse();

            // Trigger loaded handlers from the bottom up
            _(viewModels).each(function (vm) { $(vm).triggerHandler('Loaded'); });

            // Apply bindings to the root level injected node using its parent
            var parent = _(viewModels).last().Parent();
            ko.applyBindingsToNode(component, null, parent);

            // Return the root of the injected componentsQueue
            return _(viewModels).last();
        }
    }

    // Application private interface

    // Parses a javascript object from a string, NOT json
    function ParseJsObject(string)
    {
        // Use 'eval' to create a javascript object from the malformed JSON
        // NOTE: there are some security concerns with this method
        var parenString = '(' + string + ')'; // object literals must be enclosed in parenthesis for eval to work properly
        var obj = eval(parenString);

        return obj;
    }

    // Loads view templates and styles for each component
    function _LoadComponents()
    {
        // Put JQuery AJAX into synchronous mode for this algorithm to work, we will clear this flag once page loading is complete
        $.ajaxSetup({ async: false });

        // Preload styles and templates for each component
        _(this.Components()).each(function (comp)
        {
            // Append the style node to the pages head
            if ('Style' in comp)
            {
                var styleLink = $('<link rel="stylesheet" type="text/css" href="' + comp.Style + '"/>');
                $('head').append(styleLink);
            }

            // Load the view template and store it for later
            $.get(comp.View, {}, function (viewData)
            {
                comp.Template = viewData;
            });
        });

        // Put JQuery back into asynchronous mode for future ajax requests
        $.ajaxSetup({ async: true });
    }
    var LoadComponents = _.bind(_LoadComponents, Application);

    // Shorthand to expand a single component
    // returns a tree structure in an array of the expanded components
    function _ExpandComponent(component)
    {
        return ExpandComponents([component]);
    }
    var ExpandComponent = _.bind(_ExpandComponent, Application);

    // Expands a data-component element into its component, uses a breadth-first traversal
    // Returns a list of the constructed view models that were expanded
    function _ExpandComponents(components)
    {
        var componentsQueue = components;
        var viewModels = [];

        while (componentsQueue.length > 0)
        {
            var componentRoot = $(componentsQueue.shift()); // dequeue operation
            var componentName = componentRoot.data('component');

            // Check the component type to make sure it isnt a collection component
            var componentType = componentRoot.data('componentType');
            if (componentType != 'collection')
            {
                // Find the component description
                var component = _(this.Components()).findWhere({ Name: componentName });
                if (component)
                {
                    var viewModel = BuildComponent(componentRoot, component);

                    // Add the view model to the list of view models that were processed
                    viewModels.push(viewModel);

                    // Find any child data-component nodes and push them onto the queue
                    var childComponents = componentRoot.find('[data-component]').toArray();
                    _(childComponents).each(function (child)
                    {
                        componentsQueue.push(child);
                    });
                }
            }
        }

        return viewModels;
    }
    var ExpandComponents = _.bind(_ExpandComponents, Application);

    function _BuildComponent(componentRoot, component, type)
    {
        // Get the components view parameters
        var params = componentRoot.data('parameters');
        if (params)
        {
            params = ParseJsObject(params);
        }

        // Get the name to use as a field name
        var fieldName = componentRoot.data('name');

        // Create the view model and add standard fields
        var viewModel = new component.ViewModel();
        viewModel.Visible = ko.observable(false); // Hidden by default
        viewModel.Name = fieldName;
        viewModel.ViewParameters = params;
        viewModel.Children = ko.observableArray();
        viewModel.Find = this.Find;
        viewModel.Activate = this.Activate;
        viewModel.Finish = this.Finish;
        viewModel.Uid = Guid.NewGuid();
        viewModel.View = function () { return $('#' + this.Uid); };

        // Find the parent of the view, using app when there is no parent
        var parentRoot = componentRoot.parent().closest('[data-component]');
        var parent = this;
        if (parentRoot.length > 0)
        {
            var parent = this.Find(parentRoot.attr('id'));
        }

        // Add the viewmodel to its parent and add a parent property to the viewmodel
        if (type != 'collection')
        {
            parent[fieldName] = ko.observable(viewModel);

            // Add remove handler to clean up viewmodel when removed from parent
            viewModel.View.on('DOMNodeRemoved', function ()
            {
                delete parent[fieldName];
            });

            // Add databinding for visibility and context to the component root node
            componentRoot.attr('data-bind', 'visible: ' + fieldName + '().Visible, with: ' + fieldName);
        }
        else
        {
            if (!(fieldName in parent))
            {
                parent[fieldName] = {};
            }

            parent[fieldName][viewModel.Uid] = ko.observable(viewModel);

            // Add remove handler to clean up viewmodel when removed from parent
            viewModel.View.on('DOMNodeRemoved', function ()
            {
                delete parent[fieldName][viewModel.Uid];

                if($.isEmptyObject(parent[fieldName]))
                {
                    delete parent[fieldName];
                }
            });

            // Add databinding for visibility and context to the component root node
            componentRoot.attr('data-bind', 'visible: $parent.' + fieldName + '["' + viewModel.Uid + '"]().Visible, with: $parent.' + fieldName + '["' + viewModel.Uid + '"]');
        }

        parent.Children.push(viewModel);
        viewModel.Parent = ko.observable(parent);

        // Add handler to clean up child array when view is removed
        viewModel.View.on('DOMNodeRemoved', function ()
        {
            var childIndex = parent.Children.indexOf(viewModel);
            if(childIndex > -1)
            {
                parent.Children.splice(childIndex, 1);
            }
        });

        componentRoot.attr('id', viewModel.Uid);
        componentRoot.hide();   // Hide by default so that the views don't flash on the screen before knockout kicks in

        // Compile the view using its parameters
        var compiledView = _.template(component.Template, params);

        // Insert the compiled view into the DOM
        componentRoot.html(compiledView);

        // Type any child components
        TypeComponents(componentRoot);

        return viewModel;
    }
    var BuildComponent = _.bind(_BuildComponent, Application);

    function TypeComponents(root)
    {
        // Select foreach data-binds and mark components inside as having type "collection"
        var collectionComponents = root.find('[data-bind*="foreach:"] [data-component]');
        collectionComponents.attr('data-component-type', 'collection');

        // Select if and ifnot data-binds and mark components inside as having type "conditional"
        var conditionalComponents = root.find('[data-bind*="if:"],[data-bind*="ifnot:"] [data-component]');
        conditionalComponents.attr('data-component-type', 'conditional');
    }

    // Updates the title element when the application name is set
    Application.Name.subscribe(function (value)
    {
        var titleElem = $('title');
        if (titleElem.length > 0)
        {
            titleElem.text(value);
        }
        else
        {
            titleElem = $('<title>' + value + '</title>');
            $('head').append(titleElem);
        }
    });

    // Add node processor for foreach bindings
    ko.bindingProvider.instance.preprocessNode = function (node)
    {
        var componentType = $(node).data('componentType');
        var componentName = $(node).data('component');

        if (componentType && componentType == 'collection')
        {
            // Find the component description
            var component = _(Application.Components()).findWhere({ Name: componentName });
            if (component)
            {
                var viewModel = BuildComponent($(node), component, componentType);

                var childComponents = viewModel.View().find('[data-component]').toArray();
                var expandedModels = ExpandComponents(childComponents);

                // trigger loaded event from bottom up
                expandedModels.reverse();
                _(expandedModels).each(function (comp) { $(comp).triggerHandler('Loaded'); });

                $(viewModel).triggerHandler('Loaded');
            }
        }
        else if (componentType && componentType == 'conditional')
        {
            Application.InjectComponent(node);
        }
    };

    return Application;
});
<|endoftext|>