<commit_before>
define(function(require) {

	require('less!../../css/window.less');

	var WindowView = require("tmpl!../tmpl/window.tmpl");

	var Emitter = require('core/emitter');
	var View = require('core/view');

	var Window = function (options) {
		this.signals = new Emitter();

		options = options||{
			title: "Untitle Window",
			width: 400,
			height: 200,
			x: 0,
			y: 0,
			content: ''
		};

		// View
		this.el = WindowView({
			title: options.title
		});
		this.el.listen(this.events.window, this);

		// Cache content element
		this.$content = this.el.find('.wm-content');
		if(options.content)
			this.append(options.content);

		// Cache header element
		this.$header = this.el.find('header');

		this.width = options.width || 400;
		this.height = options.height || 200;

		this.x = options.x || 0;
		this.y = options.y || 0;
		this.z = 10000;

		// Open animation
		this.el.addClass('opening');
		this.el.onAnimationEnd(function(){
			this.el.removeClass('opening');
		}, this);

		this.enabled = true;
		this.active = false;
		this.closed = false;
		this.maximized = false;
		this.minimized = false;

		this.movable = true;
		this.resizable = true;
	};

	Window.prototype = {
		_restore: null,
		_moving: null,
		_resizing: null,

		events: {
			window: {
				'click': function(e) {
					this.signals.emit('select', this, e);
				},

				'mousedown': function(e) {
					this.enabled && this.focus();
				},

				'header mousedown': function(e) {
					if(!this.enabled || !this.movable) return;

					this._moving = this.toLocal({
						x: e.clientX,
						y: e.clientY
					});

					this.el.addClass('move');
				},

				'header dblclick': function(e) {
					this.enabled && this.maximize();
				},

				'header button.wm-close click': function(e) {
					e.stopPropagation();
					e.preventDefault();

					this.enabled && this.close();
				},

				'header button.wm-maximize click': function(e) {
					e.stopPropagation();
					e.preventDefault();

					this.enabled && this.maximize();
				},

				'header button.wm-minimize click': function(e) {
					e.stopPropagation();
					e.preventDefault();

					this.enabled && this.minimize();
				},

				'header button mousedown': function(e) {
					e.stopPropagation();
					e.preventDefault();
				},

				'button.wm-resize mousedown': function(e) {
					if(!this.enabled || !this.movable) return;

					this._resizing = {
						width: this.width - e.clientX,
						height: this.height - e.clientY
					};

					this.el.addClass('resizing');
				}
			},

			space: {
				'mousemove': function(e) {
					this._moving && this.move(
						e.clientX - this._moving.x,
						e.clientY - this._moving.y
					);

					this._resizing && this.resize(
						e.clientX + this._resizing.width,
						e.clientY + this._resizing.height
					);
				},

				'mouseup': function(e) {
					if (this._moving) {
						this.el.removeClass('move');
						this._moving = null;
					}

					if (this._resizing) {
						this.el.removeClass('resizing');
						this._restore = null;
						this._resizing = null;
					}
				}
			}
		},

		set space(el) {
			if(el && !el.listen) {
				console.error("The given space element is not a valid View");
				return;
			}

			el.append(this.el);
			el.listen(this.events.space, this);
		},

		get maximized() {
			return this._maximized;
		},

		set maximized(value) {
			if(value) {
				this.stamp();
				this.signals.emit('maximize', this);
			}
			else {
				this.signals.emit('restore', this);

			}

			this._maximized = value;
		},

		get minimized() {
			return this._minimized;
		},

		set minimized(value) {
			if(value) {
				this.stamp();
				this.signals.emit('minimize', this);
			}
			else {
				this.signals.emit('restore', this);
			}

			this._minimized = value;
		},

		set active(value) {
			if(value) {
				this.signals.emit('focus', this);
				this.el.addClass('active');
			}
			else {
				this.signals.emit('blur', this);
				this.el.removeClass('active');
			}

			this._active = value;
		},

		get active() {
			return this._active;
		},

		set enabled(value) {
			if(!value) {
				this.el.addClass('disabled');
			}
			else {
				this.el.removeClass('disabled');
			}

			this._enabled = value;
		},

		get enabled() {
			return this._enabled;
		},

		set movable(value) {
			this._movable = !!value;
		},

		get movable() {
			return this._movable;
		},

		set resizable(value) {
			this._resizable = !!value;
		},

		get resizable() {
			return this._resizable;
		},

		set closed (value) {
			var self = this;
			if(value) {
				this.signals.emit('close', this);

				this.el.addClass('closing');
				this.el.onAnimationEnd(function(){
					this.el.removeClass('closing');
					this.el.addClass('closed');
				}, this);

				//this.detachContent(); @todo implement this function and attachContent();

			}

			this._closed = value;
		},

		get closed() {
			return this._closed;
		},

		set width(value) {
			this.el.width(value);
		},

		get width() {
			return parseInt(this.el.width());
		},

		set height(value) {
			// This shouldn't be done if flexible box model
			// worked properly with overflow-y: auto
			this.$content.height(value - this.$header.outerHeight());

			this.el.height(value);
		},

		get height() {
			return parseInt(this.el.height());
		},

		set x(value) {
			this.el.css('left', value);
		},

		set y(value) {
			this.el.css('top', value);
		},

		get x() {
			return parseInt(this.el.css('left'));
		},

		get y() {
			return parseInt(this.el.css('top'));
		},

		set z(value) {
			this.el.css('z-index', value);
		},

		get z() {
			return parseInt(this.el.css('z-index'));
		},

		resize: function(w, h) {
			this.width = w;
			this.height = h;
			return this;
		},

		move: function(x, y) {
			this.x = x;
			this.y = y;
			return this;
		},

		/**
		 * @return A function that restores this window
		 */
		stamp: function() {
			this.restore = (function() {
				var size = {
					width: this.width,
					height: this.height
				};

				var pos = {
					x: this.x,
					y: this.y
				};

				return function() {
					this.resize(size.width, size.height);
					this.move(pos.x, pos.y);

					return this;
				}
			}).apply(this);
		},

		restore: function(){},

		maximize: function() {
			this.el.addClass('maximazing');
			this.el.onTransitionEnd(function(){
				this.el.removeClass('maximazing');
			}, this);

			this.maximized = !this.maximized;
			return this;
		},

		minimize: function() {
			this.el.addClass('minimizing');
			this.el.onTransitionEnd(function(){
				this.el.removeClass('minimizing');
			}, this);

			this.minimized = !this.minimized;
			return this;
		},

		close: function() {
			this.closed = true;
			return this;
		},

		focus: function() {
			this.active = true;
			return this;
		},

		blur: function() {
			this.active = false;
			return this;
		},

		toLocal: function(coord) {
			return {
				x: coord.x - this.x,
				y: coord.y - this.y
			};
		},

		toGlobal: function(coord) {
			return {
				x: coord.x + this.x,
				y: coord.y + this.y
			};
		},

		append: function(el) {
			el.appendTo(this.$content);
		}
	}

	return Window;
});
<commit_msg>Fixed too general event definition<commit_after>
define(function(require) {

	require('less!../../css/window.less');

	var WindowView = require("tmpl!../tmpl/window.tmpl");

	var Emitter = require('core/emitter');
	var View = require('core/view');

	var Window = function (options) {
		this.signals = new Emitter();

		options = options||{
			title: "Untitle Window",
			width: 400,
			height: 200,
			x: 0,
			y: 0,
			content: ''
		};

		// View
		this.el = WindowView({
			title: options.title
		});
		this.el.listen(this.events.window, this);

		// Cache content element
		this.$content = this.el.find('.wm-content');
		if(options.content)
			this.append(options.content);

		// Cache header element
		this.$header = this.el.find('header');

		this.width = options.width || 400;
		this.height = options.height || 200;

		this.x = options.x || 0;
		this.y = options.y || 0;
		this.z = 10000;

		// Open animation
		this.el.addClass('opening');
		this.el.onAnimationEnd(function(){
			this.el.removeClass('opening');
		}, this);

		this.enabled = true;
		this.active = false;
		this.closed = false;
		this.maximized = false;
		this.minimized = false;

		this.movable = true;
		this.resizable = true;
	};

	Window.prototype = {
		_restore: null,
		_moving: null,
		_resizing: null,

		events: {
			window: {
				'click': function(e) {
					this.signals.emit('select', this, e);
				},

				'mousedown': function(e) {
					this.enabled && this.focus();
				},

				'.wm-window-title mousedown': function(e) {
					if(!this.enabled || !this.movable) return;

					this._moving = this.toLocal({
						x: e.clientX,
						y: e.clientY
					});

					this.el.addClass('move');
				},

				'.wm-window-title dblclick': function(e) {
					this.enabled && this.maximize();
				},

				'.wm-window-title button.wm-close click': function(e) {
					e.stopPropagation();
					e.preventDefault();

					this.enabled && this.close();
				},

				'.wm-window-title button.wm-maximize click': function(e) {
					e.stopPropagation();
					e.preventDefault();

					this.enabled && this.maximize();
				},

				'.wm-window-title button.wm-minimize click': function(e) {
					e.stopPropagation();
					e.preventDefault();

					this.enabled && this.minimize();
				},

				'.wm-window-title button mousedown': function(e) {
					e.stopPropagation();
					e.preventDefault();
				},

				'button.wm-resize mousedown': function(e) {
					if(!this.enabled || !this.movable) return;

					this._resizing = {
						width: this.width - e.clientX,
						height: this.height - e.clientY
					};

					this.el.addClass('resizing');
				}
			},

			space: {
				'mousemove': function(e) {
					this._moving && this.move(
						e.clientX - this._moving.x,
						e.clientY - this._moving.y
					);

					this._resizing && this.resize(
						e.clientX + this._resizing.width,
						e.clientY + this._resizing.height
					);
				},

				'mouseup': function(e) {
					if (this._moving) {
						this.el.removeClass('move');
						this._moving = null;
					}

					if (this._resizing) {
						this.el.removeClass('resizing');
						this._restore = null;
						this._resizing = null;
					}
				}
			}
		},

		set space(el) {
			if(el && !el.listen) {
				console.error("The given space element is not a valid View");
				return;
			}

			el.append(this.el);
			el.listen(this.events.space, this);
		},

		get maximized() {
			return this._maximized;
		},

		set maximized(value) {
			if(value) {
				this.stamp();
				this.signals.emit('maximize', this);
			}
			else {
				this.signals.emit('restore', this);

			}

			this._maximized = value;
		},

		get minimized() {
			return this._minimized;
		},

		set minimized(value) {
			if(value) {
				this.stamp();
				this.signals.emit('minimize', this);
			}
			else {
				this.signals.emit('restore', this);
			}

			this._minimized = value;
		},

		set active(value) {
			if(value) {
				this.signals.emit('focus', this);
				this.el.addClass('active');
			}
			else {
				this.signals.emit('blur', this);
				this.el.removeClass('active');
			}

			this._active = value;
		},

		get active() {
			return this._active;
		},

		set enabled(value) {
			if(!value) {
				this.el.addClass('disabled');
			}
			else {
				this.el.removeClass('disabled');
			}

			this._enabled = value;
		},

		get enabled() {
			return this._enabled;
		},

		set movable(value) {
			this._movable = !!value;
		},

		get movable() {
			return this._movable;
		},

		set resizable(value) {
			this._resizable = !!value;
		},

		get resizable() {
			return this._resizable;
		},

		set closed (value) {
			var self = this;
			if(value) {
				this.signals.emit('close', this);

				this.el.addClass('closing');
				this.el.onAnimationEnd(function(){
					this.el.removeClass('closing');
					this.el.addClass('closed');
				}, this);

				//this.detachContent(); @todo implement this function and attachContent();

			}

			this._closed = value;
		},

		get closed() {
			return this._closed;
		},

		set width(value) {
			this.el.width(value);
		},

		get width() {
			return parseInt(this.el.width());
		},

		set height(value) {
			// This shouldn't be done if flexible box model
			// worked properly with overflow-y: auto
			this.$content.height(value - this.$header.outerHeight());

			this.el.height(value);
		},

		get height() {
			return parseInt(this.el.height());
		},

		set x(value) {
			this.el.css('left', value);
		},

		set y(value) {
			this.el.css('top', value);
		},

		get x() {
			return parseInt(this.el.css('left'));
		},

		get y() {
			return parseInt(this.el.css('top'));
		},

		set z(value) {
			this.el.css('z-index', value);
		},

		get z() {
			return parseInt(this.el.css('z-index'));
		},

		resize: function(w, h) {
			this.width = w;
			this.height = h;
			return this;
		},

		move: function(x, y) {
			this.x = x;
			this.y = y;
			return this;
		},

		/**
		 * @return A function that restores this window
		 */
		stamp: function() {
			this.restore = (function() {
				var size = {
					width: this.width,
					height: this.height
				};

				var pos = {
					x: this.x,
					y: this.y
				};

				return function() {
					this.resize(size.width, size.height);
					this.move(pos.x, pos.y);

					return this;
				}
			}).apply(this);
		},

		restore: function(){},

		maximize: function() {
			this.el.addClass('maximazing');
			this.el.onTransitionEnd(function(){
				this.el.removeClass('maximazing');
			}, this);

			this.maximized = !this.maximized;
			return this;
		},

		minimize: function() {
			this.el.addClass('minimizing');
			this.el.onTransitionEnd(function(){
				this.el.removeClass('minimizing');
			}, this);

			this.minimized = !this.minimized;
			return this;
		},

		close: function() {
			this.closed = true;
			return this;
		},

		focus: function() {
			this.active = true;
			return this;
		},

		blur: function() {
			this.active = false;
			return this;
		},

		toLocal: function(coord) {
			return {
				x: coord.x - this.x,
				y: coord.y - this.y
			};
		},

		toGlobal: function(coord) {
			return {
				x: coord.x + this.x,
				y: coord.y + this.y
			};
		},

		append: function(el) {
			el.appendTo(this.$content);
		}
	}

	return Window;
});
<|endoftext|>


<commit_before>/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

'use strict'
const Immutable = require('immutable')
const siteTags = require('../constants/siteTags')
const settings = require('../constants/settings')
const getSetting = require('../settings').getSetting
const urlParse = require('url').parse

/**
 * Obtains the index of the location in sites
 *
 * @param sites The application state's Immutable sites list
 * @param siteDetail The details of the site to get the index of
 * @return index of the site or -1 if not found.
 */
module.exports.getSiteIndex = function (sites, siteDetail, tags) {
  let isBookmarkFolder = typeof tags === 'string' && tags === siteTags.BOOKMARK_FOLDER ||
    tags && typeof tags !== 'string' && tags.includes(siteTags.BOOKMARK_FOLDER)
  if (isBookmarkFolder) {
    return sites.findIndex((site) => site.get('folderId') === siteDetail.get('folderId') && site.get('tags').includes(siteTags.BOOKMARK_FOLDER))
  }
  return sites.findIndex((site) => site.get('location') === siteDetail.get('location') && (site.get('partitionNumber') || 0) === (siteDetail.get('partitionNumber') || 0))
}

/**
 * Checks if a siteDetail has the specified tag
 *
 * @param sites The application state's Immutable sites list
 * @param siteDetail The site to check if it's in the specified tag
 * @return true if the location is already bookmarked
 */
module.exports.isSiteInList = function (sites, siteDetail, tag) {
  const index = module.exports.getSiteIndex(sites, siteDetail, tag)
  if (index === -1) {
    return false
  }
  return sites.get(index).get('tags').includes(tag)
}

const getNextFolderIdItem = (sites) =>
  sites.max((siteA, siteB) => {
    const folderIdA = siteA.get('folderId')
    const folderIdB = siteB.get('folderId')
    if (folderIdA === folderIdB) {
      return 0
    }
    if (folderIdA === undefined) {
      return false
    }
    if (folderIdB === undefined) {
      return true
    }
    return folderIdA > folderIdB
  })

module.exports.getNextFolderId = (sites) => {
  const maxIdItem = getNextFolderIdItem(sites)
  return (maxIdItem ? (maxIdItem.get('folderId') || 0) : 0) + 1
}

/**
 * Adds the specified siteDetail to sites
 *
 * @param sites The application state's Immutable site list
 * @param siteDetails The site details to add a tag to
 * @param tag The tag to add for this site.
 *   See siteTags.js for supported types. No tag means just a history item.
 * @param originalSiteDetail If specified will modify the specified site detail
 * @return The new sites Immutable object
 */
module.exports.addSite = function (sites, siteDetail, tag, originalSiteDetail) {
  if (tag === undefined) {
    tag = siteDetail.getIn(['tags', 0])
  }
  const index = module.exports.getSiteIndex(sites, originalSiteDetail || siteDetail, tag)

  let folderId = siteDetail.get('folderId')
  if (!folderId && tag === siteTags.BOOKMARK_FOLDER) {
    folderId = module.exports.getNextFolderId(sites)
  }

  let tags = index !== -1 && sites.getIn([index, 'tags']) || new Immutable.List()
  if (tag) {
    tags = tags.toSet().add(tag).toList()
  }

  let oldSite
  if (index !== -1) {
    oldSite = sites.getIn([index])
  }

  let site = Immutable.fromJS({
    lastAccessedTime: siteDetail.get('lastAccessedTime') || new Date().getTime(),
    tags,
    location: siteDetail.get('location'),
    // We don't want bookmarks and other site info being renamed on users if they already exist
    // The name should remain the same while it is bookmarked forever.
    title: oldSite && tags.includes(siteTags.BOOKMARK) ? oldSite.get('title') : siteDetail.get('title')
  })
  if (folderId) {
    site = site.set('folderId', Number(folderId))
  }
  if (siteDetail.get('parentFolderId') || oldSite && oldSite.get('parentFolderId')) {
    site = site.set('parentFolderId', Number(siteDetail.get('parentFolderId') || oldSite.get('parentFolderId')))
  }
  if (siteDetail.get('customTitle') || oldSite && oldSite.get('customTitle')) {
    site = site.set('customTitle', siteDetail.get('customTitle') || oldSite.get('customTitle'))
  }
  if (siteDetail.get('partitionNumber') || oldSite && oldSite.get('partitionNumber')) {
    site = site.set('partitionNumber', Number(siteDetail.get('partitionNumber') || oldSite.get('partitionNumber')))
  }
  if (siteDetail.get('favicon') || oldSite && oldSite.get('favicon')) {
    site = site.set('favicon', siteDetail.get('favicon') || oldSite.get('favicon'))
  }

  if (index === -1) {
    return sites.push(site)
  }

  return sites.setIn([index], site)
}

/**
 * Removes the specified tag from a siteDetail
 *
 * @param sites The application state's Immutable sites list
 * @param siteDetail The siteDetail to remove a tag from
 * @return The new sites Immutable object
 */
module.exports.removeSite = function (sites, siteDetail, tag) {
  const index = module.exports.getSiteIndex(sites, siteDetail, tag)
  if (index === -1) {
    return sites
  }
  const tags = sites.getIn([index, 'tags'])
  // If there are no tags and the removeSite call was called without a specific tag
  // then remove the entry completely.
  if (tags.size === 0 && !tag) {
    sites = sites.splice(index, 1)
    return sites
  }
  return sites.setIn([index, 'tags'], tags.toSet().remove(tag).toList())
}

function fillParentFolders (parentFolderIds, bookmarkFolder, allBookmarks) {
  if (bookmarkFolder.get('parentFolderId')) {
    parentFolderIds.push(bookmarkFolder.get('parentFolderId'))
    const nextItem = allBookmarks.find((item) => item.get('folderId') === bookmarkFolder.get('parentFolderId'))
    if (nextItem) {
      fillParentFolders(parentFolderIds, nextItem, allBookmarks)
    }
  }
}

/**
 * Moves the specified site from one location to another
 *
 * @param sites The application state's Immutable sites list
 * @param siteDetail The site detail to move
 * @param destinationDetail The site detail to move to
 * @param prepend Whether the destination detail should be prepended or not, not used if destinationIsParent is true
 * @param destinationIsParent Whether the item should be moved inside of the destinationDetail.
 * @param disallowReparent If set to true, parent folder will not be set
 * @return The new sites Immutable object
 */
module.exports.moveSite = function (sites, sourceDetail, destinationDetail, prepend, destinationIsParent, disallowReparent) {
  // Disallow loops
  let parentFolderIds = []
  if (destinationDetail.get('parentFolderId') && sourceDetail.get('folderId')) {
    fillParentFolders(parentFolderIds, destinationDetail, sites)
    if (sourceDetail.get('folderId') === destinationDetail.get('folderId') ||
        parentFolderIds.includes(sourceDetail.get('folderId'))) {
      return sites
    }
  }

  const sourceSiteIndex = module.exports.getSiteIndex(sites, sourceDetail, sourceDetail.get('tags'))
  let destinationSiteIndex
  if (destinationIsParent) {
    // When the destinatiaon is the parent we want to put it at the end
    destinationSiteIndex = sites.size - 1
    prepend = false
  } else {
    destinationSiteIndex = module.exports.getSiteIndex(sites, destinationDetail, destinationDetail.get('tags'))
  }

  let newIndex = destinationSiteIndex + (prepend ? 0 : 1)
  let sourceSite = sites.get(sourceSiteIndex)
  let destinationSite = sites.get(destinationSiteIndex)
  sites = sites.splice(sourceSiteIndex, 1)
  if (newIndex > sourceSiteIndex) {
    newIndex--
  }

  if (!disallowReparent) {
    if (destinationIsParent && destinationDetail.get('folderId') !== sourceSite.get('folderId')) {
      sourceSite = sourceSite.set('parentFolderId', destinationDetail.get('folderId'))
    } else if (!destinationSite.get('parentFolderId')) {
      sourceSite = sourceSite.delete('parentFolderId')
    } else if (destinationSite.get('parentFolderId') !== sourceSite.get('parentFolderId')) {
      sourceSite = sourceSite.set('parentFolderId', destinationSite.get('parentFolderId'))
    }
  }
  return sites.splice(newIndex, 0, sourceSite)
}

module.exports.getDetailFromFrame = function (frame, tag) {
  let location = frame.get('location')
  if (frame.get('pinnedLocation') && tag === siteTags.PINNED) {
    location = frame.get('pinnedLocation')
  }

  return Immutable.fromJS({
    location,
    title: frame.get('title'),
    partitionNumber: frame.get('partitionNumber'),
    tags: tag ? [tag] : [],
    favicon: frame.get('icon')
  })
}

/**
 * Converts a siteDetail to frameOpts format
 * @param {Object} siteDetail - A site detail as per app state
 * @return {Object} A frameOpts plain JS object, not ImmutableJS
 */
module.exports.toFrameOpts = function (siteDetail) {
  return {
    location: siteDetail.get('location'),
    partitionNumber: siteDetail.get('partitionNumber')
  }
}

/**
 * Compares 2 site details
 * @param siteDetail1 The first site detail to compare.
 * @param siteDetail2 The second site detail to compare.
 * @return true if the site details should be considered the same.
 */
module.exports.isEquivalent = function (siteDetail1, siteDetail2) {
  const isFolder1 = module.exports.isFolder(siteDetail1)
  const isFolder2 = module.exports.isFolder(siteDetail2)
  if (isFolder1 !== isFolder2) {
    return false
  }

  // If they are both folders
  if (isFolder1) {
    return siteDetail1.get('folderId') === siteDetail2.get('folderId')
  }
  return siteDetail1.get('location') === siteDetail2.get('location') && siteDetail1.get('partitionNumber') === siteDetail2.get('partitionNumber')
}

/**
 * Determines if the site detail is a folder.
 * @param siteDetail The site detail to check.
 * @return true if the site detail is a folder.
 */
module.exports.isFolder = function (siteDetail) {
  return siteDetail.get('tags').includes(siteTags.BOOKMARK_FOLDER)
}

/**
 * Obtains an array of folders
 */
module.exports.getFolders = function (sites, folderId, parentId, labelPrefix) {
  parentId = parentId || 0
  let folders = []
  sites.forEach((site) => {
    if ((site.get('parentFolderId') || 0) === parentId && module.exports.isFolder(site)) {
      if (site.get('folderId') === folderId) {
        return
      }
      const label = (labelPrefix || '') + (site.get('customTitle') || site.get('title'))
      folders.push({
        folderId: site.get('folderId'),
        parentFolderId: site.get('parentFolderId'),
        label
      })
      const subsites = module.exports.getFolders(sites, folderId, site.get('folderId'), (label || '') + ' / ')
      folders = folders.concat(subsites)
    }
  })
  return folders
}

/**
 * Filters out non recent sites based on the app setting for history size.
 * @param sites The application state's Immutable sites list.
 */
module.exports.filterOutNonRecents = function (sites) {
  const sitesWithTags = sites
    .filter((site) => site.get('tags').size)
  const topHistorySites = sites
    .filter((site) => site.get('tags').size === 0)
    .sort((site1, site2) => (site2.get('lastAccessedTime') || 0) - (site1.get('lastAccessedTime') || 0))
    .take(getSetting(settings.AUTOCOMPLETE_HISTORY_SIZE))
  return sitesWithTags.concat(topHistorySites)
}

/**
 * Filters sites relative to a parent site (folder).
 * @param sites The application state's Immutable sites list.
 * @param relSite The folder to filter to.
 */
module.exports.filterSitesRelativeTo = function (sites, relSite) {
  if (!relSite.get('folderId')) {
    return sites
  }
  return sites.filter((site) => site.get('parentFolderId') === relSite.get('folderId'))
}

/**
 * Clears out all sites that have no tags.
 * @param sites The application state's Immutable sites list.
 */
module.exports.clearSitesWithoutTags = function (sites) {
  return sites.filter((site) => site.get('tags') && site.get('tags').size > 0)
}

/**
 * Determines if the sites list has any sites with no tags
 * @param sites The application state's Immutable sites list.
 */
module.exports.hasNoTagSites = function (sites) {
  return sites.findIndex((site) => !site.get('tags') || site.get('tags').size === 0) !== -1
}

/**
 * Gets a site origin (scheme + hostname + port) from a URL or null if not
 * available.
 * @param {string} location
 * @return {string?}
 */
module.exports.getOrigin = function (location) {
  // Returns scheme + hostname + port
  if (typeof location !== 'string') {
    return null
  }
  let parsed = urlParse(location)
  if (parsed.host && parsed.protocol) {
    return parsed.slashes ? [parsed.protocol, parsed.host].join('//') : [parsed.protocol, parsed.host].join('')
  } else {
    return null
  }
}
<commit_msg>Forget bookmark folder when unbookmarking<commit_after>/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

'use strict'
const Immutable = require('immutable')
const siteTags = require('../constants/siteTags')
const settings = require('../constants/settings')
const getSetting = require('../settings').getSetting
const urlParse = require('url').parse

/**
 * Obtains the index of the location in sites
 *
 * @param sites The application state's Immutable sites list
 * @param siteDetail The details of the site to get the index of
 * @return index of the site or -1 if not found.
 */
module.exports.getSiteIndex = function (sites, siteDetail, tags) {
  let isBookmarkFolder = typeof tags === 'string' && tags === siteTags.BOOKMARK_FOLDER ||
    tags && typeof tags !== 'string' && tags.includes(siteTags.BOOKMARK_FOLDER)
  if (isBookmarkFolder) {
    return sites.findIndex((site) => site.get('folderId') === siteDetail.get('folderId') && site.get('tags').includes(siteTags.BOOKMARK_FOLDER))
  }
  return sites.findIndex((site) => site.get('location') === siteDetail.get('location') && (site.get('partitionNumber') || 0) === (siteDetail.get('partitionNumber') || 0))
}

/**
 * Checks if a siteDetail has the specified tag
 *
 * @param sites The application state's Immutable sites list
 * @param siteDetail The site to check if it's in the specified tag
 * @return true if the location is already bookmarked
 */
module.exports.isSiteInList = function (sites, siteDetail, tag) {
  const index = module.exports.getSiteIndex(sites, siteDetail, tag)
  if (index === -1) {
    return false
  }
  return sites.get(index).get('tags').includes(tag)
}

const getNextFolderIdItem = (sites) =>
  sites.max((siteA, siteB) => {
    const folderIdA = siteA.get('folderId')
    const folderIdB = siteB.get('folderId')
    if (folderIdA === folderIdB) {
      return 0
    }
    if (folderIdA === undefined) {
      return false
    }
    if (folderIdB === undefined) {
      return true
    }
    return folderIdA > folderIdB
  })

module.exports.getNextFolderId = (sites) => {
  const maxIdItem = getNextFolderIdItem(sites)
  return (maxIdItem ? (maxIdItem.get('folderId') || 0) : 0) + 1
}

/**
 * Adds the specified siteDetail to sites
 *
 * @param sites The application state's Immutable site list
 * @param siteDetails The site details to add a tag to
 * @param tag The tag to add for this site.
 *   See siteTags.js for supported types. No tag means just a history item.
 * @param originalSiteDetail If specified will modify the specified site detail
 * @return The new sites Immutable object
 */
module.exports.addSite = function (sites, siteDetail, tag, originalSiteDetail) {
  if (tag === undefined) {
    tag = siteDetail.getIn(['tags', 0])
  }
  const index = module.exports.getSiteIndex(sites, originalSiteDetail || siteDetail, tag)

  let folderId = siteDetail.get('folderId')
  if (!folderId && tag === siteTags.BOOKMARK_FOLDER) {
    folderId = module.exports.getNextFolderId(sites)
  }

  let tags = index !== -1 && sites.getIn([index, 'tags']) || new Immutable.List()
  if (tag) {
    tags = tags.toSet().add(tag).toList()
  }

  let oldSite
  if (index !== -1) {
    oldSite = sites.getIn([index])
  }

  let site = Immutable.fromJS({
    lastAccessedTime: siteDetail.get('lastAccessedTime') || new Date().getTime(),
    tags,
    location: siteDetail.get('location'),
    // We don't want bookmarks and other site info being renamed on users if they already exist
    // The name should remain the same while it is bookmarked forever.
    title: oldSite && tags.includes(siteTags.BOOKMARK) ? oldSite.get('title') : siteDetail.get('title')
  })
  if (folderId) {
    site = site.set('folderId', Number(folderId))
  }
  if (siteDetail.get('parentFolderId') || oldSite && oldSite.get('parentFolderId')) {
    site = site.set('parentFolderId', Number(siteDetail.get('parentFolderId') || oldSite.get('parentFolderId')))
  }
  if (siteDetail.get('customTitle') || oldSite && oldSite.get('customTitle')) {
    site = site.set('customTitle', siteDetail.get('customTitle') || oldSite.get('customTitle'))
  }
  if (siteDetail.get('partitionNumber') || oldSite && oldSite.get('partitionNumber')) {
    site = site.set('partitionNumber', Number(siteDetail.get('partitionNumber') || oldSite.get('partitionNumber')))
  }
  if (siteDetail.get('favicon') || oldSite && oldSite.get('favicon')) {
    site = site.set('favicon', siteDetail.get('favicon') || oldSite.get('favicon'))
  }

  if (index === -1) {
    return sites.push(site)
  }

  return sites.setIn([index], site)
}

/**
 * Removes the specified tag from a siteDetail
 *
 * @param sites The application state's Immutable sites list
 * @param siteDetail The siteDetail to remove a tag from
 * @return The new sites Immutable object
 */
module.exports.removeSite = function (sites, siteDetail, tag) {
  const index = module.exports.getSiteIndex(sites, siteDetail, tag)
  if (index === -1) {
    return sites
  }
  const tags = sites.getIn([index, 'tags'])
  // If there are no tags and the removeSite call was called without a specific tag
  // then remove the entry completely.
  if (tags.size === 0 && !tag) {
    sites = sites.splice(index, 1)
    return sites
  }
  return sites
    .setIn([index, 'parentFolderId'], 0)
    .setIn([index, 'tags'], tags.toSet().remove(tag).toList())
}

function fillParentFolders (parentFolderIds, bookmarkFolder, allBookmarks) {
  if (bookmarkFolder.get('parentFolderId')) {
    parentFolderIds.push(bookmarkFolder.get('parentFolderId'))
    const nextItem = allBookmarks.find((item) => item.get('folderId') === bookmarkFolder.get('parentFolderId'))
    if (nextItem) {
      fillParentFolders(parentFolderIds, nextItem, allBookmarks)
    }
  }
}

/**
 * Moves the specified site from one location to another
 *
 * @param sites The application state's Immutable sites list
 * @param siteDetail The site detail to move
 * @param destinationDetail The site detail to move to
 * @param prepend Whether the destination detail should be prepended or not, not used if destinationIsParent is true
 * @param destinationIsParent Whether the item should be moved inside of the destinationDetail.
 * @param disallowReparent If set to true, parent folder will not be set
 * @return The new sites Immutable object
 */
module.exports.moveSite = function (sites, sourceDetail, destinationDetail, prepend, destinationIsParent, disallowReparent) {
  // Disallow loops
  let parentFolderIds = []
  if (destinationDetail.get('parentFolderId') && sourceDetail.get('folderId')) {
    fillParentFolders(parentFolderIds, destinationDetail, sites)
    if (sourceDetail.get('folderId') === destinationDetail.get('folderId') ||
        parentFolderIds.includes(sourceDetail.get('folderId'))) {
      return sites
    }
  }

  const sourceSiteIndex = module.exports.getSiteIndex(sites, sourceDetail, sourceDetail.get('tags'))
  let destinationSiteIndex
  if (destinationIsParent) {
    // When the destinatiaon is the parent we want to put it at the end
    destinationSiteIndex = sites.size - 1
    prepend = false
  } else {
    destinationSiteIndex = module.exports.getSiteIndex(sites, destinationDetail, destinationDetail.get('tags'))
  }

  let newIndex = destinationSiteIndex + (prepend ? 0 : 1)
  let sourceSite = sites.get(sourceSiteIndex)
  let destinationSite = sites.get(destinationSiteIndex)
  sites = sites.splice(sourceSiteIndex, 1)
  if (newIndex > sourceSiteIndex) {
    newIndex--
  }

  if (!disallowReparent) {
    if (destinationIsParent && destinationDetail.get('folderId') !== sourceSite.get('folderId')) {
      sourceSite = sourceSite.set('parentFolderId', destinationDetail.get('folderId'))
    } else if (!destinationSite.get('parentFolderId')) {
      sourceSite = sourceSite.delete('parentFolderId')
    } else if (destinationSite.get('parentFolderId') !== sourceSite.get('parentFolderId')) {
      sourceSite = sourceSite.set('parentFolderId', destinationSite.get('parentFolderId'))
    }
  }
  return sites.splice(newIndex, 0, sourceSite)
}

module.exports.getDetailFromFrame = function (frame, tag) {
  let location = frame.get('location')
  if (frame.get('pinnedLocation') && tag === siteTags.PINNED) {
    location = frame.get('pinnedLocation')
  }

  return Immutable.fromJS({
    location,
    title: frame.get('title'),
    partitionNumber: frame.get('partitionNumber'),
    tags: tag ? [tag] : [],
    favicon: frame.get('icon')
  })
}

/**
 * Converts a siteDetail to frameOpts format
 * @param {Object} siteDetail - A site detail as per app state
 * @return {Object} A frameOpts plain JS object, not ImmutableJS
 */
module.exports.toFrameOpts = function (siteDetail) {
  return {
    location: siteDetail.get('location'),
    partitionNumber: siteDetail.get('partitionNumber')
  }
}

/**
 * Compares 2 site details
 * @param siteDetail1 The first site detail to compare.
 * @param siteDetail2 The second site detail to compare.
 * @return true if the site details should be considered the same.
 */
module.exports.isEquivalent = function (siteDetail1, siteDetail2) {
  const isFolder1 = module.exports.isFolder(siteDetail1)
  const isFolder2 = module.exports.isFolder(siteDetail2)
  if (isFolder1 !== isFolder2) {
    return false
  }

  // If they are both folders
  if (isFolder1) {
    return siteDetail1.get('folderId') === siteDetail2.get('folderId')
  }
  return siteDetail1.get('location') === siteDetail2.get('location') && siteDetail1.get('partitionNumber') === siteDetail2.get('partitionNumber')
}

/**
 * Determines if the site detail is a folder.
 * @param siteDetail The site detail to check.
 * @return true if the site detail is a folder.
 */
module.exports.isFolder = function (siteDetail) {
  return siteDetail.get('tags').includes(siteTags.BOOKMARK_FOLDER)
}

/**
 * Obtains an array of folders
 */
module.exports.getFolders = function (sites, folderId, parentId, labelPrefix) {
  parentId = parentId || 0
  let folders = []
  sites.forEach((site) => {
    if ((site.get('parentFolderId') || 0) === parentId && module.exports.isFolder(site)) {
      if (site.get('folderId') === folderId) {
        return
      }
      const label = (labelPrefix || '') + (site.get('customTitle') || site.get('title'))
      folders.push({
        folderId: site.get('folderId'),
        parentFolderId: site.get('parentFolderId'),
        label
      })
      const subsites = module.exports.getFolders(sites, folderId, site.get('folderId'), (label || '') + ' / ')
      folders = folders.concat(subsites)
    }
  })
  return folders
}

/**
 * Filters out non recent sites based on the app setting for history size.
 * @param sites The application state's Immutable sites list.
 */
module.exports.filterOutNonRecents = function (sites) {
  const sitesWithTags = sites
    .filter((site) => site.get('tags').size)
  const topHistorySites = sites
    .filter((site) => site.get('tags').size === 0)
    .sort((site1, site2) => (site2.get('lastAccessedTime') || 0) - (site1.get('lastAccessedTime') || 0))
    .take(getSetting(settings.AUTOCOMPLETE_HISTORY_SIZE))
  return sitesWithTags.concat(topHistorySites)
}

/**
 * Filters sites relative to a parent site (folder).
 * @param sites The application state's Immutable sites list.
 * @param relSite The folder to filter to.
 */
module.exports.filterSitesRelativeTo = function (sites, relSite) {
  if (!relSite.get('folderId')) {
    return sites
  }
  return sites.filter((site) => site.get('parentFolderId') === relSite.get('folderId'))
}

/**
 * Clears out all sites that have no tags.
 * @param sites The application state's Immutable sites list.
 */
module.exports.clearSitesWithoutTags = function (sites) {
  return sites.filter((site) => site.get('tags') && site.get('tags').size > 0)
}

/**
 * Determines if the sites list has any sites with no tags
 * @param sites The application state's Immutable sites list.
 */
module.exports.hasNoTagSites = function (sites) {
  return sites.findIndex((site) => !site.get('tags') || site.get('tags').size === 0) !== -1
}

/**
 * Gets a site origin (scheme + hostname + port) from a URL or null if not
 * available.
 * @param {string} location
 * @return {string?}
 */
module.exports.getOrigin = function (location) {
  // Returns scheme + hostname + port
  if (typeof location !== 'string') {
    return null
  }
  let parsed = urlParse(location)
  if (parsed.host && parsed.protocol) {
    return parsed.slashes ? [parsed.protocol, parsed.host].join('//') : [parsed.protocol, parsed.host].join('')
  } else {
    return null
  }
}
<|endoftext|>


<commit_before>"use strict";

var Base = require( "../base/index" );
var EventEmitter = require( 'eventemitter2' ).EventEmitter2;
var async = require( "async" );
var Eventable = Base.compose( [Base, EventEmitter], {
	declareClass : "utils/Bussable",
	constructor  : function () {
		this.trigger = this.emit;
	},
	fire         : function ( event, func ) {
		var listeners = this.listeners( event ) || [];
		var handlers = [];
		if ( event === 'newListener' && !this.newListener ) {
			if ( !this._events.newListener ) { return false; }
		}

		if ( this._all ) {
			listeners = all.concat( this.listeners );
		}

		if ( this.wildcard ) {
			handlers = [];
			var ns = typeof event === 'string' ? event.split( this.delimiter ) : event.slice();
			searchListenerTree.call( this, handler, ns, this.listenerTree, 0 );
			listeners = listeners.concat( handler );
		}
		else {
			listeners = this._events[event];
		}
		var args = [];
		if ( arguments.length > 2 ) {
			args = sys.toArray( arguments );
		}

		async.eachSeries( listeners, function ( handler, done ) {

			if ( handler.async ) {
				handler.apply( null, args.concat( [done] ) );
			} else {
				handler.apply( null, args );
				done();
			}
		}, func );

	},
	destroy      : function () {
		this.removeAllListeners();
	}
} );
module.exports = Eventable;
<commit_msg>ready for release<commit_after>"use strict";
/**
 * @fileOverview A mixin that gives an object the ability to raise and respond to events within the object
 * that hosts it. This is intended to be used for communications in an object. Communications between
 * object should be handled on the system bus.
 * @module events/eventable
 */

var Base = require( "../base/index" );
var EventEmitter = require( 'eventemitter2' ).EventEmitter2;
var async = require( "async" );
var dcl = require( "dcl" );

// split the event name on the :
var eventMethodSplitter = /(^|:)(\w)/gi;
// take the event section ("section1:section2:section3")
// and turn it in to uppercase name
function getEventName( match, prefix, eventName ) {
	return eventName.toUpperCase();
}

/**
 * A mixin that gives an object the ability to raise and respond to events within the object
 * that hosts it. This is intended to be used for communications in an object. Communications between
 * object should be handled on the system bus.
 *
 * This mixin exposes tge entirety of [EventEmitter2](https://github.com/asyncly/EventEmitter2) on your object. It also
 * add a couple features:
 *
 * + The `fire` method which allows you to fire asynchronous events
 * + The ability to respond to events emitted on the same object using "on" syntax. For example an event fired as
 * `opened` will also look for a method called `onOpened`. If you format the name of the event by splitting words with `:`,
 * such as `view:ready` it will look for a method named `onViewReady`. Keep in mind that this is in addition to
 * event responders that use `.on("view:ready", myFunc)`.
 *
 * @mixin
 * @inherits module:events/eventable~Eventable
 * @inherits EventEmitter2
 */
var Eventable = Base.compose( [Base, EventEmitter], /** @lends module:events/eventable~Eventable# */{
	declareClass         : "events/Eventable",
	constructor          : function () {
		this.trigger = this.emit;
	},
	/**
	 * Fires an event just like the `emit` function,but with an added parameter: a callback that is raised when the
	 * event has fired on all listeners. But, but, but the difference is that if a listener adds a property to
	 * the function that responds called `async` and sets it to true, the `fire` method will wait for the
	 * function to return a callback before processing the next listener.
	 *
	 * *Note*: `on` syntax methods are always synchronous for now.
	 * @example
	 * var doSomething( done ){
	 *  // do something interesting asynchronously
	 *  if (sys.isFunction(done){ // you could have been called by `emit` instead of `fire`
	 *      done();
	 *  }
	 * }
	 * doSomething.async = true;
	 * obj.on( "someEvent", doSomething );
	 * // later, when raising the event
	 * obj.fire( "someEvent", function(err){
	 *  console.info("event complete");
	 * });
	 *
	 * @param {string} event The event name
	 * @param {function(callback)} func The callback raised when the event has fired all listeners
	 *
	 */
	fire                 : function ( event, func ) {
		var listeners = this.listeners( event ) || [];
		var handlers = [];
		if ( event === 'newListener' && !this.newListener ) {
			if ( !this._events.newListener ) { return false; }
		}

		if ( this._all ) {
			listeners = all.concat( this.listeners );
		}

		if ( this.wildcard ) {
			handlers = [];
			var ns = typeof event === 'string' ? event.split( this.delimiter ) : event.slice();
			searchListenerTree.call( this, handler, ns, this.listenerTree, 0 );
			listeners = listeners.concat( handler );
		}
		else {
			listeners = this._events[event];
		}
		var args = [];
		if ( arguments.length > 2 ) {
			args = sys.toArray( arguments );
		}
		this._processEventMethods( event );
		async.eachSeries( listeners, function ( handler, done ) {

			if ( handler.async ) {
				handler.apply( null, args.concat( [done] ) );
			} else {
				handler.apply( null, args );
				done();
			}
		}, func );

	},
	/**
	 * Clean up bound listeners
	 * @private
	 */
	destroy              : function () {
		this.removeAllListeners();
	},
	/**
	 * Wrap superclass `emit` so that we can handle `on` syntax
	 */
	emit                 : dcl.superCall( function ( sup ) {
		var that = this;
		return function ( event ) {
			sup.apply( that, arguments );
			that._processEventMethods( event );
		};
	} ),
	/**
	 * Handles `on` syntax methods
	 * @private
	 */
	_processEventMethods : function ( event ) {

		// get the method name from the event name
		var methodName = 'on' + event.replace( eventMethodSplitter, getEventName );
		var method = this[methodName];

		var args = sys.tail( arguments );

		// call the onMethodName if it exists
		if ( sys.isFunction( method ) ) {
			// pass all arguments, except the event name
			method.apply( this, args );
		}

	}

} );
module.exports = Eventable;
<|endoftext|>


<commit_before>'use strict';
/* Dependency Tree Handler with Diff capabilities
 *
 * This service has not much to do - the tree itself is handled
 * completely by the dependencyTree directive.
 *
 * It has however additional diff capabilities, that are triggered
 * by a global diffLoaded event.
 * Knows how to pass style information to the tree visualization in
 * case a comparison/review was done.
 * * One could experiment here that this code should go back to a diff plugin
 * itself.
 * A diff plugin would calculate style information and pass this data load
 * through the diffLoaded event. The depTree service would listen to this
 * and pass the style info to its tree.
 *
 * Let's wait on a decision for this until we have done more work on the
 * diff plugin itself and resolved issue #80.
 * (https://github.com/latin-language-toolkit/llt-annotation_environment/issues/80)
 */
angular.module('arethusa.depTree').service('depTree', [
  'state',
  'configurator',
  '$rootScope',
  function (state, configurator, $rootScope) {
    var self = this;

    function configure() {
      configurator.getConfAndDelegate('depTree', self);
      self.diffMode = false;
    }

    configure();

    this.toggleDiff = function () {
      self.diffMode = !self.diffMode;
    };

    // We have three things we can colorize as wrong in the tree
    //   Label
    //   Head
    //   and the word itself for morphological stuff
    function analyseDiffs(tokens) {
      return arethusaUtil.inject({}, tokens, function (memo, id, token) {
        var diff = token.diff;
        if (diff) {
          memo[id] = analyseDiff(diff);
        }
      });
    }

    function analyseDiff(diff) {
      return arethusaUtil.inject({}, diff, function (memo, key, val) {
        if (key === 'relation') {
          memo.label = { color: 'red' };
        } else {
          if (key === 'head') {
            memo.edge = {
              stroke: 'red',
              'stroke-width': '1px'
            };
          } else {
            memo.token = { color: 'red' };
          }
        }
      });
    }

    this.diffStyles = function () {
      if (self.diffMode) {
        return self.diffInfo;
      } else {
        return false;
      }
    };

    $rootScope.$on('diffLoaded', function () {
      self.diffPresent = true;
      self.diffInfo = analyseDiffs(state.tokens);
      self.diffMode = true;
    });

    this.tokensWithoutHeadCount = function () {
      return state.countTokens(function (token) {
        return !(token.head || {}).id;
      });
    };

    // Used inside the context menu
    this.disconnect = function(token) {
      token.head.id = "";
    };

    this.toRoot = function(token) {
      token.head.id = '0000';
    };

    this.init = function () {
      configure();
    };
  }
]);
<commit_msg>Extract method in depTree<commit_after>'use strict';
/* Dependency Tree Handler with Diff capabilities
 *
 * This service has not much to do - the tree itself is handled
 * completely by the dependencyTree directive.
 *
 * It has however additional diff capabilities, that are triggered
 * by a global diffLoaded event.
 * Knows how to pass style information to the tree visualization in
 * case a comparison/review was done.
 * * One could experiment here that this code should go back to a diff plugin
 * itself.
 * A diff plugin would calculate style information and pass this data load
 * through the diffLoaded event. The depTree service would listen to this
 * and pass the style info to its tree.
 *
 * Let's wait on a decision for this until we have done more work on the
 * diff plugin itself and resolved issue #80.
 * (https://github.com/latin-language-toolkit/llt-annotation_environment/issues/80)
 */
angular.module('arethusa.depTree').service('depTree', [
  'state',
  'configurator',
  '$rootScope',
  function (state, configurator, $rootScope) {
    var self = this;

    function configure() {
      configurator.getConfAndDelegate('depTree', self);
      self.diffMode = false;
    }

    configure();

    this.toggleDiff = function () {
      self.diffMode = !self.diffMode;
    };

    // We have three things we can colorize as wrong in the tree
    //   Label
    //   Head
    //   and the word itself for morphological stuff
    function analyseDiffs(tokens) {
      return arethusaUtil.inject({}, tokens, function (memo, id, token) {
        var diff = token.diff;
        if (diff) {
          memo[id] = analyseDiff(diff);
        }
      });
    }

    function analyseDiff(diff) {
      return arethusaUtil.inject({}, diff, function (memo, key, val) {
        if (key === 'relation') {
          memo.label = { color: 'red' };
        } else {
          if (key === 'head') {
            memo.edge = {
              stroke: 'red',
              'stroke-width': '1px'
            };
          } else {
            memo.token = { color: 'red' };
          }
        }
      });
    }

    this.diffStyles = function () {
      if (self.diffMode) {
        return self.diffInfo;
      } else {
        return false;
      }
    };

    $rootScope.$on('diffLoaded', function () {
      self.diffPresent = true;
      self.diffInfo = analyseDiffs(state.tokens);
      self.diffMode = true;
    });

    function hasNoHead(token) {
      return !(token.head || {}).id;
    }

    this.tokensWithoutHeadCount = function () {
      return state.countTokens(function (token) {
        return hasNoHead(token);
      });
    };

    // Used inside the context menu
    this.disconnect = function(token) {
      token.head.id = "";
    };

    this.toRoot = function(token) {
      token.head.id = '0000';
    };

    this.init = function () {
      configure();
    };
  }
]);
<|endoftext|>


<commit_before>var loopback = require('loopback');
var boot = require('loopback-boot');

var app = module.exports = loopback();

app.start = function() {
  // start the web server
  return app.listen(function() {
    app.emit('started');
    console.log('Web server listening at: %s', app.get('url'));
  });
};

// Bootstrap the application, configure models, datasources and middleware.
// Sub-apps like REST API are mounted via boot scripts.
boot(app, __dirname, function(err) {
  if (err) throw err;

  // start the server if `$ node server.js`
  if (require.main === module)
    app.start();
});
<commit_msg>server.js can accept callback<commit_after>var loopback = require('loopback');
var boot = require('loopback-boot');

var app = module.exports = loopback();

app.start = function(callback) {
  boot(app, __dirname, function(err) {
    if (err) throw err;

      // start the web server
      app.listen(function() {
        app.emit('started');
        console.log('Web server listening at: %s', app.get('url'));
        if (callback) callback();
      });
  });
};

// Bootstrap the application, configure models, datasources and middleware.
// Sub-apps like REST API are mounted via boot scripts.

// start the server if `$ node server.js`
if (require.main === module) {
  app.start();
}
<|endoftext|>


<commit_before>/**
 *
 */
$(function() {
	$('.showImport')
	.button({icons:{primary:'ui-icon-arrowthick-1-n'}})
	.click(function(){
		$('.importDialog').dialog('open');
	});

	$('.importDialog').dialog({
		autoOpen: false,
		modal:true,
	});

	$('.importDialog').on('click', '.textImport', function () {
		$('.importDialog .importFile').click();
	});

	$('.importDialog').on('change', '.importFile', function () {
		var selectedFile = $(this).get(0).files[0];
		console.log('Importing: ' + selectedFile.name);
		var reader = new FileReader();
		reader.onload = function(e) {
			console.log(e.target.result);
			$('.importDialog').dialog('close');
//			alert('Arquivo Importado com sucesso');
		};
		reader.readAsText(selectedFile);
	});
});<commit_msg>Importing working, for now, but is very insecure method for loading objects.<commit_after>/**
 *
 */
$(function() {
	$('.showImport')
	.button({icons:{primary:'ui-icon-arrowthick-1-n'}})
	.click(function(){
		$('.importDialog').dialog('open');
	});

	$('.importDialog').dialog({
		autoOpen: false,
		modal:true,
	});

	$('.importDialog').on('click', '.textImport', function () {
		$('.importDialog .importFile').click();
	});

	$('.importDialog').on('change', '.importFile', function () {
		var selectedFile = $(this).get(0).files[0];
		console.log('Importing: ' + selectedFile.name);
		var reader = new FileReader();
		reader.onload = function(e) {
			//console.log(e.target.result);
			tasks = JSON.parse(e.target.result, function(k, v) {
				if(v.id !== undefined){
					return $.extend(new Task(), v);
				}
				return v;
			});
			$viewGroup = $('.viewGroup');
			$viewGroup.children('.task').detach();
			tasks.forEach(function(value) {
				addTaskChrono($viewGroup, value);
			});
			$('.importDialog').dialog('close');
		};
		reader.readAsText(selectedFile);
	});
});<|endoftext|>


<commit_before>'use strict';

/* Controllers */

angular.module('KMCModule').controller('PlayerEditCtrl',
    ['$scope', 'PlayerData', '$routeParams', '$filter', 'menuSvc', 'PlayerService', 'apiService', 'localStorageService', 'userEntries', '$timeout',
        function($scope, PlayerData, $routeParams, $filter, menuSvc, PlayerService, apiService, localStorageService, userEntries, $timeout) {
            $scope.ks = localStorageService.get('ks');
            $scope.playerId = PlayerData.id;
            $scope.title = ($routeParams.id) ? $filter('i18n')('Edit player') : $filter('i18n')('New  player');
            $scope.data = PlayerData;
            $scope.data.config = angular.fromJson($scope.data.config); // convert string to json object
            $scope.masterData = angular.copy($scope.data);
            $scope.userEntriesList = [];
            $scope.userEntries = userEntries;
            $scope.settings = {};
            window.scope = $scope;
            // set tags
            $scope.tags = [];
            // all of the next block is just to show how to push into the tags autocomplete/dropdown the list of available tags should be loaded this way instead,
            // the model tags of the player are actually set properly from the ng-model of the tags directive and are not needed here
            if (typeof $scope.data.tags != "undefined" && $scope.data.tags) { //can also be null
                var tags = typeof $scope.data.tags == "string" ? $scope.data.tags.split(",") : $scope.data.tags;
                for (var i = 0; i < tags.length; i++)
                    $scope.tags.push({id: tags[i], text: tags[i]});
            }
            //registers the tags to be available to the directive
            menuSvc.registerAction('getTags', function() {
                return $scope.tags;
            });
            angular.forEach($scope.userEntries.objects, function(value) {
                $scope.userEntriesList.push({'id': value.id, 'text': value.name});
            });
            menuSvc.registerAction('listEntries', function() { // those should be the first 20...
                return $scope.userEntriesList;
            });
            menuSvc.registerAction('queryEntries', function(query) {
                var data = {results: []};
                console.log(query.term);
                if (query.term) {
                    // here you should do some AJAX API call with the query term and then()...
                    angular.forEach($scope.userEntriesList, function(item, key) {
                        if (query.term.toUpperCase() === item.text.substring(0, query.term.length).toUpperCase()) {
                            data.results.push(item);
                        }
                    });
                    return query.callback(data);
                }
                else
                    return query.callback({results: $scope.userEntriesList});

            });

            if (parseFloat($scope.data.version) < PlayerService.getRequiredVersion()) {
                menuSvc.registerAction('update', function() {
                    PlayerService.playerUpdate($scope.data);
                });
            }
            $scope.settings.previewEntry = ( PlayerService.getPreviewEntry()) ? PlayerService.getPreviewEntry() : $scope.userEntriesList[0]; //default entry
            $scope.$watch('settings.previewEntry', function(newVal, oldVal) {
                if (newVal != oldVal) {
                    PlayerService.setPreviewEntry($scope.settings.previewEntry);
                    PlayerService.renderPlayer();
                }
            });
            $(document).ready(function() {
                $scope.masterData = angular.copy($scope.data);
                // get the preview entry
                PlayerService.setPreviewEntry($scope.settings.previewEntry);
                PlayerService.renderPlayer();
            });
            $scope.save = function() {
                menuSvc.menuScope.playerEdit.$dirty = false;
                $scope.masterData = angular.copy($scope.data);
                //we should render from $scope.data but save $scope.masterData this way we can also offer a revert edit button
                //cl(menuSvc.menuScope.playerEdit);
            };
            $scope.$watch(function() {
                if (typeof menuSvc.menuScope.playerEdit != 'undefined') {
                    if (menuSvc.menuScope.playerEdit.$error) {
                        return menuSvc.menuScope.playerEdit.$error;

                    }
                }
            }, function(obj, oldVal) {
                if (obj != oldVal) {
                    $scope.validationObject = obj;
                }

            });
            $scope.saveEnabled = function() {
                //instead of using the form dirty state we compare to the master copy.
                if (typeof menuSvc.menuScope.playerEdit != 'undefined') {
                    if (menuSvc.menuScope.playerEdit.$valid)
                        return !angular.equals($scope.data, $scope.masterData);
                    else
                        return false;
                }
            };
        }
    ]);

angular.module('KMCModule').controller('editPageDataCntrl', ['$scope', function($scope) {

}]);<commit_msg>added Save functionality<commit_after>'use strict';

/* Controllers */

angular.module('KMCModule').controller('PlayerEditCtrl',
    ['$scope', 'PlayerData', '$routeParams', '$filter', 'menuSvc', 'PlayerService', 'apiService', 'localStorageService', 'userEntries', '$timeout','$modal',
        function($scope, PlayerData, $routeParams, $filter, menuSvc, PlayerService, apiService, localStorageService, userEntries, $timeout, $modal) {
            $scope.ks = localStorageService.get('ks');
            $scope.playerId = PlayerData.id;
            $scope.title = ($routeParams.id) ? $filter('i18n')('Edit player') : $filter('i18n')('New  player');
            $scope.data = PlayerData;
            $scope.data.config = angular.fromJson($scope.data.config); // convert string to json object
            $scope.masterData = angular.copy($scope.data);
            $scope.userEntriesList = [];
            $scope.userEntries = userEntries;
            $scope.settings = {};
            window.scope = $scope;
            // set tags
            $scope.tags = [];
            // all of the next block is just to show how to push into the tags autocomplete/dropdown the list of available tags should be loaded this way instead,
            // the model tags of the player are actually set properly from the ng-model of the tags directive and are not needed here
            if (typeof $scope.data.tags != "undefined" && $scope.data.tags) { //can also be null
                var tags = typeof $scope.data.tags == "string" ? $scope.data.tags.split(",") : $scope.data.tags;
                for (var i = 0; i < tags.length; i++)
                    $scope.tags.push({id: tags[i], text: tags[i]});
            }
            //registers the tags to be available to the directive
            menuSvc.registerAction('getTags', function() {
                return $scope.tags;
            });
            angular.forEach($scope.userEntries.objects, function(value) {
                $scope.userEntriesList.push({'id': value.id, 'text': value.name});
            });
            menuSvc.registerAction('listEntries', function() { // those should be the first 20...
                return $scope.userEntriesList;
            });
            menuSvc.registerAction('queryEntries', function(query) {
                var data = {results: []};
                console.log(query.term);
                if (query.term) {
                    // here you should do some AJAX API call with the query term and then()...
                    angular.forEach($scope.userEntriesList, function(item, key) {
                        if (query.term.toUpperCase() === item.text.substring(0, query.term.length).toUpperCase()) {
                            data.results.push(item);
                        }
                    });
                    return query.callback(data);
                }
                else
                    return query.callback({results: $scope.userEntriesList});

            });

            if (parseFloat($scope.data.version) < PlayerService.getRequiredVersion()) {
                menuSvc.registerAction('update', function() {
                    PlayerService.playerUpdate($scope.data);
                });
            }
            $scope.settings.previewEntry = ( PlayerService.getPreviewEntry()) ? PlayerService.getPreviewEntry() : $scope.userEntriesList[0]; //default entry
            $scope.$watch('settings.previewEntry', function(newVal, oldVal) {
                if (newVal != oldVal) {
                    PlayerService.setPreviewEntry($scope.settings.previewEntry);
                    PlayerService.renderPlayer();
                }
            });
            $(document).ready(function() {
                $scope.masterData = angular.copy($scope.data);
                // get the preview entry
                PlayerService.setPreviewEntry($scope.settings.previewEntry);
                PlayerService.renderPlayer();
            });
            $scope.save = function() {
                var request = {
                    'service': 'uiConf',
                    'action': 'update',
                    'id': $scope.playerId,
                    'uiConf:name': $scope.data.name,
                    'uiConf:tags': $scope.data.tags,
                    'uiConf:description': $scope.data.description ? $scope.data.description : '',
                    'uiConf:config': angular.toJson($scope.data.config)
                };
                apiService.doRequest(request).then(function(result) {
                        // cleanup
                        menuSvc.menuScope.playerEdit.$dirty = false;
                        $scope.masterData = angular.copy($scope.data);
                        // user message TODO: replace with floating success message that will disappear after few seconds
                        $modal.open({ templateUrl: 'template/dialog/message.html',
                            controller: 'ModalInstanceCtrl',
                            resolve: {
                                settings: function() {
                                    return {
                                        'title': 'Save Player Settings',
                                        'message': 'Player Saved Successfully'
                                    };
                                }
                            }
                        });
                    }, function(msg) {
                        $modal.open({ templateUrl: 'template/dialog/message.html',
                            controller: 'ModalInstanceCtrl',
                            resolve: {
                                settings: function() {
                                    return {
                                        'title': 'Player save failure',
                                        'message': msg
                                    };
                                }
                            }
                        });
                    }
                );

            };
            $scope.$watch(function() {
                if (typeof menuSvc.menuScope.playerEdit != 'undefined') {
                    if (menuSvc.menuScope.playerEdit.$error) {
                        return menuSvc.menuScope.playerEdit.$error;

                    }
                }
            }, function(obj, oldVal) {
                if (obj != oldVal) {
                    $scope.validationObject = obj;
                }

            });
            $scope.saveEnabled = function() {
                //instead of using the form dirty state we compare to the master copy.
                if (typeof menuSvc.menuScope.playerEdit != 'undefined') {
                    if (menuSvc.menuScope.playerEdit.$valid)
                        return !angular.equals($scope.data, $scope.masterData);
                    else
                        return false;
                }
            };
        }
    ]);

angular.module('KMCModule').controller('editPageDataCntrl', ['$scope', function($scope) {

}]);<|endoftext|>


<commit_before>/* @flow */

"use strict";

/*
// Perseus module uses React directly and uses $._ directly for
// localization, so we do this as a hack to get it to work
function perseusPrep(katex, KAS, MathJax) {
    $._ = function(x) { return x; };
    window.$_ = function(x) { return x; };
    window.katex = katex;
    window.KAS = KAS;
    window.MathJax = MathJax;
}
*/

var React = require("react/addons"),
    Util = require("../util"),
    APIClient = require("../apiclient");

/**
 * Represents a single exercise, it will load the exercise dynamically and
 * display it to the user.
 */
var ExerciseViewer = React.createClass({
    propTypes: {
        exercise: React.PropTypes.object.isRequired
    },
    mixins: [Util.BackboneMixin],
    getBackboneModels: function(): Array<any> {
        return [this.props.exercise];
    },
    getInitialState: function() {
        return {
            hintsUsed: 0,
            currentHint: -1
        };
    },
    refreshRandomAssessment: function() {

        // Pick a random problem type:
        var problemTypes = this.exercise.problem_types;
        var randomProblemTypeGroupIndex = Math.floor(Math.random() * problemTypes.length);
        var randomProblemTypeGroup = this.exercise.problem_types[randomProblemTypeGroupIndex];
        this.problemTypeName = randomProblemTypeGroup.name;

        var randomProblemTypeIndex = Math.floor(Math.random() * randomProblemTypeGroup.items.length);

        this.randomAssessmentSHA1 = randomProblemTypeGroup.items[randomProblemTypeIndex].sha1;
        this.randomAssessmentId = randomProblemTypeGroup.items[randomProblemTypeIndex].id;

        APIClient.getAssessmentItem(this.randomAssessmentId).done((result) => {
            var assessment = JSON.parse(result.item_data);
            Util.log("Got assessment item: %o: item data: %o", result, assessment);
            this.setState({
                perseusItemData: assessment
            });
        });
    },
    onClickRequestHint: function() {
        this.refs.itemRenderer.showHint();
        this.setState({
            hintsUsed: this.state.hintsUsed + 1,
            currentHint: this.state.currentHint + 1
        });
    },
    onClickSubmitAnswer: function() {
        var score = this.refs.itemRenderer.scoreInput();
        Util.log("score: %o", score);
        var attemptNumber = 1; // TODO
        var isCorrect = score.correct;
        var secondsTaken = 10; //TODO
        var data = {};
        APIClient.getTaskIfnoByExerciseName(this.props.exercise.getName()).then((info) => {
            data.taskId = info.id;
            return APIClient.getUserExercise(this.props.exercise.getName());
        }).then((info) => {
            var problemNumber = info.total_done + 1;
            Util.log("submitting exercise progress for problemNumber: %i", problemNumber);
            console.log("taskId: " + data.taskId);
            console.log("problemNumber: " + problemNumber);
            console.log("problem type name: " + this.problemTypeName);
            return APIClient.reportExerciseProgress(this.props.exercise.getName(), problemNumber,
                                                    this.randomAssessmentSHA1, this.randomAssessmentId,
                                                    secondsTaken, this.state.hintsUsed, isCorrect,
                                                    attemptNumber, this.problemTypeName, data.taskId);
        }).done(() => {
            this.refreshRandomAssessment();
        });
    },
    componentWillMount: function() {
        if (this.props.exercise.isPerseusExercise()) {
            APIClient.getExerciseByName(this.props.exercise.getName()).done((result) => {
                this.exercise = result;
                Util.log("got exercise: %o", result);
                this.refreshRandomAssessment();
            });
        }

        /*
         * TODO
        requirejs(["../../khan-exercises/khan-exercise"], function(Khan) {
            window.Khan = Khan;
            window.$ = $;
            window._ = _;
            window.React = React;
            window.KhanUtil = Khan.Util;

            katex = require("../../bower_components/katex/build/katex.min"),
            KAS = require("../../bower_components/KAS/kas"),
            MathJax = require("../../bower_components/MathJax/MathJax");

            perseusPrep(katex, KAS, MathJax, Khan.Util);
            var Perseus = require("../../webapp/javascript/perseus-package/perseus-2");
            Perseus.init({}).then(() => {
                Util.log("Perseus init done %o, %o", Perseus);
                this.ItemRenderer = Perseus.ItemRenderer;
                this.forceUpdate();
            });
        });
        */
    },
    componentDidMount: function() {
    },
    componentWillUnmount: function() {
    },
    render: function(): any {
        var content;
        if (this.state.error) {
            content =
Could not load exercise
;
        } else if (this.props.exercise.isKhanExercisesExercise()) {
            var path = `/khan-exercises/exercises/${this.props.exercise.getFilename()}`;
            content = ;
        } else if (this.ItemRenderer && this.state.perseusItemData) {
            var showHintsButton = this.state.perseusItemData.hints.length > this.state.hintsUsed;
            var hint;
            if (this.state.currentHint !== -1 &&
                    this.state.currentHint < this.state.perseusItemData.hints.length) {
            }
            content =

                          <this.ItemRenderer ref="itemRenderer"
                                             item={this.state.perseusItemData}
                                             problemNum={Math.floor(Math.random() * 50) + 1}
                                             initialHintsVisible={0}
                                             enabledFeatures={{
                                                 highlight: true,
                                                 toolTipFormats: true
                                             }} />

                          <button className="submit-answer-button"
                                  data-l10n-id="submit-answer"
                                  onClick={this.onClickSubmitAnswer}>Submit Answer</button>
                          { !showHintsButton ? null :
                          <button className="submit-answer-button"
                                  data-l10n-id="hint"
                                  onClick={this.onClickRequestHint}>Hint</button>
                          }
                          {hint}

;
        }

        Util.log("render exercise: :%o", this.props.exercise);
        return

            {content}

;
    }
});

module.exports = {
    ExerciseViewer,
};
<commit_msg>Get closer to getting exercises working with CommonJS. khan-exercises module is a pain<commit_after>/* @flow */

"use strict";

// Perseus module uses React directly and uses $._ directly for
// localization, so we do this as a hack to get it to work
function perseusPrep(katex, KAS, MathJax) {
    $._ = function(x) { return x; };
    window.$_ = function(x) { return x; };
    window.katex = katex;
    window.KAS = KAS;
    window.MathJax = MathJax;
}

var React = require("react/addons"),
    Util = require("../util"),
    loadPackage = require("../load-package"),
    APIClient = require("../apiclient"),
    _ = require("underscore"),
    $ = require("jquery");

/**
 * Represents a single exercise, it will load the exercise dynamically and
 * display it to the user.
 */
var ExerciseViewer = React.createClass({
    propTypes: {
        exercise: React.PropTypes.object.isRequired
    },
    mixins: [Util.BackboneMixin],
    getBackboneModels: function(): Array<any> {
        return [this.props.exercise];
    },
    getInitialState: function() {
        return {
            hintsUsed: 0,
            currentHint: -1
        };
    },
    refreshRandomAssessment: function() {

        // Pick a random problem type:
        var problemTypes = this.exercise.problem_types;
        var randomProblemTypeGroupIndex = Math.floor(Math.random() * problemTypes.length);
        var randomProblemTypeGroup = this.exercise.problem_types[randomProblemTypeGroupIndex];
        this.problemTypeName = randomProblemTypeGroup.name;

        var randomProblemTypeIndex = Math.floor(Math.random() * randomProblemTypeGroup.items.length);

        this.randomAssessmentSHA1 = randomProblemTypeGroup.items[randomProblemTypeIndex].sha1;
        this.randomAssessmentId = randomProblemTypeGroup.items[randomProblemTypeIndex].id;

        APIClient.getAssessmentItem(this.randomAssessmentId).done((result) => {
            var assessment = JSON.parse(result.item_data);
            Util.log("Got assessment item: %o: item data: %o", result, assessment);
            this.setState({
                perseusItemData: assessment
            });
        });
    },
    onClickRequestHint: function() {
        this.refs.itemRenderer.showHint();
        this.setState({
            hintsUsed: this.state.hintsUsed + 1,
            currentHint: this.state.currentHint + 1
        });
    },
    onClickSubmitAnswer: function() {
        var score = this.refs.itemRenderer.scoreInput();
        Util.log("score: %o", score);
        var attemptNumber = 1; // TODO
        var isCorrect = score.correct;
        var secondsTaken = 10; //TODO
        var data = {};
        APIClient.getTaskIfnoByExerciseName(this.props.exercise.getName()).then((info) => {
            data.taskId = info.id;
            return APIClient.getUserExercise(this.props.exercise.getName());
        }).then((info) => {
            var problemNumber = info.total_done + 1;
            Util.log("submitting exercise progress for problemNumber: %i", problemNumber);
            console.log("taskId: " + data.taskId);
            console.log("problemNumber: " + problemNumber);
            console.log("problem type name: " + this.problemTypeName);
            return APIClient.reportExerciseProgress(this.props.exercise.getName(), problemNumber,
                                                    this.randomAssessmentSHA1, this.randomAssessmentId,
                                                    secondsTaken, this.state.hintsUsed, isCorrect,
                                                    attemptNumber, this.problemTypeName, data.taskId);
        }).done(() => {
            this.refreshRandomAssessment();
        });
    },
    componentWillMount: function() {
        if (this.props.exercise.isPerseusExercise()) {
            APIClient.getExerciseByName(this.props.exercise.getName()).done((result) => {
                this.exercise = result;
                Util.log("got exercise: %o", result);
                this.refreshRandomAssessment();
            });
        }

        loadPackage("exercise-package.js");

        // TODO: Make this load async
        window._ = _;
        window.React = React;
        window.$ = $;
        var Khan = require("Khan");
        window.Khan = Khan;
        window.KhanUtil = Khan.Util;

        var katex = require("katex"),
            KAS = require("KAS"),
            MathJax = require("MathJax"),
            Perseus = require("Perseus");
        perseusPrep(katex, KAS, MathJax, Khan.Util);
        Perseus.init({}).then(() => {
            Util.log("Perseus init done %o, %o", Perseus);
            this.ItemRenderer = Perseus.ItemRenderer;
            this.forceUpdate();
        });
    },
    componentDidMount: function() {
    },
    componentWillUnmount: function() {
    },
    render: function(): any {
        var content;
        if (this.state.error) {
            content =
Could not load exercise
;
        } else if (this.props.exercise.isKhanExercisesExercise()) {
            var path = `/khan-exercises/exercises/${this.props.exercise.getFilename()}`;
            content = ;
        } else if (this.ItemRenderer && this.state.perseusItemData) {
            var showHintsButton = this.state.perseusItemData.hints.length > this.state.hintsUsed;
            var hint;
            if (this.state.currentHint !== -1 &&
                    this.state.currentHint < this.state.perseusItemData.hints.length) {
            }
            content =

                          <this.ItemRenderer ref="itemRenderer"
                                             item={this.state.perseusItemData}
                                             problemNum={Math.floor(Math.random() * 50) + 1}
                                             initialHintsVisible={0}
                                             enabledFeatures={{
                                                 highlight: true,
                                                 toolTipFormats: true
                                             }} />

                          <button className="submit-answer-button"
                                  data-l10n-id="submit-answer"
                                  onClick={this.onClickSubmitAnswer}>Submit Answer</button>
                          { !showHintsButton ? null :
                          <button className="submit-answer-button"
                                  data-l10n-id="hint"
                                  onClick={this.onClickRequestHint}>Hint</button>
                          }
                          {hint}

;
        }

        Util.log("render exercise: :%o", this.props.exercise);
        return

            {content}

;
    }
});

module.exports = {
    ExerciseViewer,
};
<|endoftext|>


<commit_before>var Backbone = require('./backbone');
var _ = require('underscore');
var HTTPServer = require('express').HTTPServer;
var middleware = require('..').middleware;

module.exports = Server;
function Server(plugin) {
    HTTPServer.call(this, []);
    this.init([]);
    this.plugin = plugin;
    this.initialize(plugin);
    this.conclude(plugin);
};

Server.prototype.__proto__ = HTTPServer.prototype;

_.extend(Server.prototype, Backbone.Events, {
    initialize : function(plugin) {},

    conclude: function(plugin) {
        if (this.port) {
            this.use(middleware.notFound());
            this.error(middleware.showError());
        }
    },

    port: null,

    start: function(callback) {
        this.port && this.listen(this.port, callback);
        return this;
    },

    toString: function() {
        if (this.port) {
            return '[Server ' + this.constructor.title + ':' + this.address().port + ']';
        } else {
            return '[Server ' + this.constructor.title + ']';
        }
    }
});

Server.augment = Backbone.Controller.augment;
Server.extend = Backbone.Controller.extend;
Server.toString = function() {
    return '<Server ' + this.title + '>';
};
<commit_msg>Remove extra init call.<commit_after>var Backbone = require('./backbone');
var _ = require('underscore');
var HTTPServer = require('express').HTTPServer;
var middleware = require('..').middleware;

module.exports = Server;
function Server(plugin) {
    HTTPServer.call(this, []);
    this.plugin = plugin;
    this.initialize(plugin);
    this.conclude(plugin);
};

Server.prototype.__proto__ = HTTPServer.prototype;

_.extend(Server.prototype, Backbone.Events, {
    initialize : function(plugin) {},

    conclude: function(plugin) {
        if (this.port) {
            this.use(middleware.notFound());
            this.error(middleware.showError());
        }
    },

    port: null,

    start: function(callback) {
        this.port && this.listen(this.port, callback);
        return this;
    },

    toString: function() {
        if (this.port) {
            return '[Server ' + this.constructor.title + ':' + this.address().port + ']';
        } else {
            return '[Server ' + this.constructor.title + ']';
        }
    }
});

Server.augment = Backbone.Controller.augment;
Server.extend = Backbone.Controller.extend;
Server.toString = function() {
    return '<Server ' + this.title + '>';
};
<|endoftext|>