define([
'jquery',
'underscore',
'view',
'slickgrid',
'chosen'
], function($, _, DecompositionView, SlickGrid, Chosen) {
/**
*
* @class EmperorViewControllerABC
*
* Initializes an abstract tab. This has to be contained in a DOM object and
* will use the full size of that container. The title represents the title
* of the jQuery tab. The description will be used as help text to describe
* the functionality of each subclass tab.
*
* @param {Node} container Container node to create the controller in.
* @param {String} title title of the tab.
* @param {String} description helper description.
*
* @return {EmperorViewControllerABC} Returns an instance of the
* EmperorViewControllerABC.
* @constructs EmperorViewControllerABC
*
*/
function EmperorViewControllerABC(container, title, description) {
/**
* @type {Node}
* jQuery element for the parent container.
*/
this.$container = $(container);
/**
* @type {String}
* Human-readable title of the tab.
*/
this.title = title;
/**
* @type {String}
* Human-readable description of the tab.
*/
this.description = description;
/**
* @type {Node}
* jQuery element for the canvas, which contains the header and the body.
*/
this.$canvas = null;
/**
* @type {Node}
* jQuery element for the body, which contains the lowermost elements
* displayed in tab. This goes below the header.
*/
this.$body = null;
/**
* @type {Node}
* jQuery element for the header which contains the uppermost elements
* displayed in a tab.
*/
this.$header = null;
/**
* @type {Boolean}
* Indicates whether the tab is front most
* @default false
*/
this.active = false;
/**
* @type {String}
* Unique hash identifier for the tab instance.
* @default "EMPtab-xxxxxxx"
*/
this.identifier = 'EMPtab-' + Math.round(1000000 * Math.random());
/**
* @type {Boolean}
* Indicates if tab can be accessed.
* @default true
*/
this.enabled = true;
if (this.$container.length < 1) {
throw new Error('Emperor requires a valid container, ' +
this.$container + ' does not exist in the DOM.');
}
// the canvas contains both the header and the body, note that for all
// these divs the width should be 100% (whatever we have available), but
// the height is much trickier, see the resize method for more information
this.$canvas = $('<div name="emperor-view-controller-canvas"></div>');
this.$canvas.width('100%');
this.$container.append(this.$canvas);
this.$canvas.width(this.$container.width());
this.$canvas.height(this.$container.height());
// the margin and width properties are set this way to center all the
// contents of the divs themselves, see this SO answer:
// http://stackoverflow.com/a/114549
this.$header = $('<div name="emperor-view-controller-header"></div>');
this.$header.css('margin', '0 auto');
this.$header.css('width', '100%');
this.$body = $('<div name="emperor-view-controller-body"></div>');
this.$body.css('margin', '0 auto');
this.$body.css('width', '100%');
// inherit the size of the container minus the space being used for the
// header
this.$body.height(this.$canvas.height() - this.$header.height());
this.$body.width(this.$canvas.width());
this.$canvas.append(this.$header);
this.$canvas.append(this.$body);
return this;
}
/**
* Sets whether or not the tab can be modified or accessed.
*
* @param {Boolean} trulse option to enable tab.
*/
EmperorViewControllerABC.prototype.setEnabled = function(trulse) {
if (typeof(trulse) === 'boolean') {
this.enabled = trulse;
}
else {
throw new Error('`trulse` can only be of boolean type');
}
};
/**
* Sets whether or not the tab is visible.
*
* @param {Boolean} trulse option to activate tab
* (i.e. move tab to foreground).
*/
EmperorViewControllerABC.prototype.setActive = function(trulse) {
if (this.enabled === true) {
if (typeof(trulse) === 'boolean') {
this.active = trulse;
}
else {
throw new Error('`trulse` can only be of boolean type');
}
}
};
/**
* Resizes the container, note that the body will take whatever space is
* available after considering the size of the header. The header shouldn't
* have height variable objects, once added their height shouldn't really
* change.
*
* @param {Float} width the container width.
* @param {Float} height the container height.
*/
EmperorViewControllerABC.prototype.resize = function(width, height) {
// This padding is required in order to make space
// for the horizontal menus
var padding = 10;
this.$canvas.height(height);
this.$canvas.width(width - padding);
this.$header.width(width - padding);
// the body has to account for the size used by the header
this.$body.width(width - padding);
this.$body.height(height - this.$header.height());
};
/**
*
* Converts the current instance into a JSON string.
*
* @return {Object} ready to serialize representation of self.
*/
EmperorViewControllerABC.prototype.toJSON = function() {
throw Error('Not implemented');
};
/**
* Decodes JSON string and modifies its own instance variables accordingly.
*
* @param {Object} parsed JSON string representation of an instance.
*/
EmperorViewControllerABC.prototype.fromJSON = function(jsonString) {
throw Error('Not implemented');
};
/**
*
* @class EmperorViewControllerABC
*
* Initializes an abstract tab for attributes i.e. shape, color, size, etc.
* This has to be contained in a DOM object and will use the full size of
* that container.
*
* @param {Node} container Container node to create the controller in.
* @param {String} title title of the tab.
* @param {String} description helper description.
* @param {Object} decompViewDict This is object is keyed by unique
* identifiers and the values are DecompositionView objects referring to a
* set of objects presented on screen. This dictionary will usually be shared
* by all the tabs in the application. This argument is passed by reference.
* @param {Object} options This is a dictionary of options used to build
* the view controller. Used to set attributes of the slick grid and the
* metadata category drop down. At the moment the constructor only expects
* the following attributes:
* - categorySelectionCallback: a function object that's called when a new
* metadata category is selected in the dropdown living in the header.
* See [change]{@link https://api.jquery.com/change/}.
* - valueUpdatedCallback: a function object that's called when a metadata
* visualization attribute is modified (i.e. a change of color).
* See [onCellChange]{@link
* https://github.com/mleibman/SlickGrid/wiki/Grid-Events}.
* - slickGridColumn: a dictionary specifying options to be passed into the
* slickGrid. For instance, the ColorFormatter and the ColorEditor would be
* passed here. For more information, refer to the Slick Grid
* documentation.
*
* @return {EmperorAttributeABC} Returns an instance of the
* EmperorAttributeABC class.
* @constructs EmperorAttributeABC
* @extends EmperorViewControllerABC
*
*/
function EmperorAttributeABC(container, title, description,
decompViewDict, options) {
EmperorViewControllerABC.call(this, container, title, description);
if (decompViewDict === undefined) {
throw Error('The decomposition view dictionary cannot be undefined');
}
for (var dv in decompViewDict) {
if (!dv instanceof DecompositionView) {
throw Error('The decomposition view dictionary ' +
'can only have decomposition views');
}
}
if (_.size(decompViewDict) <= 0) {
throw Error('The decomposition view dictionary cannot be empty');
}
// Picks the first key in the dictionary as the active key
/**
* @type {String}
* This is the key of the active decomposition view.
*/
this.activeViewKey = Object.keys(decompViewDict)[0];
/**
* @type {Object}
* This is object is keyed by unique identifiers and the values are
* DecompositionView objects referring to a set of objects presented on
* screen. This dictionary will usually be shared by all the tabs in the
* application. This argument is passed by reference.
*/
this.decompViewDict = decompViewDict;
/**
* @type {Node}
* jQuery element for the div containing the slickgrid of sample information
*/
this.$gridDiv = $('<div name="emperor-grid-div"></div>');
this.$gridDiv.css('margin', '0 auto');
this.$gridDiv.css('width', '100%');
this.$gridDiv.css('height', '100%');
this.$body.append(this.$gridDiv);
/**
* @type {String}
* Metadata column name.
*/
this.metadataField = null;
var dm = decompViewDict[this.activeViewKey].decomp;
var scope = this;
// http://stackoverflow.com/a/6602002
this.$select = $('<select>');
_.each(dm.md_headers, function(header) {
scope.$select.append($('<option>').attr('value', header).text(header));
});
this.$header.append(this.$select);
// there's a few attributes we can only set on "ready" so list them up here
$(function() {
// setup the slick grid
scope._buildGrid(options);
// setup chosen
scope.$select.chosen({width: '100%', search_contains: true});
// only subclasses will provide this callback
if (options.categorySelectionCallback !== undefined) {
scope.$select.chosen().change(options.categorySelectionCallback);
// now that we have the chosen selector and the table fire a callback
// to initialize the data grid
options.categorySelectionCallback(
null, {selected: scope.$select.val()});
}
});
return this;
}
EmperorAttributeABC.prototype = Object.create(
EmperorViewControllerABC.prototype);
EmperorAttributeABC.prototype.constructor = EmperorViewControllerABC;
/**
* Changes the metadata column name to control.
*
* @param {String} m Metadata column name to control.
*/
EmperorAttributeABC.prototype.setMetadataField = function(m) {
// FIXME: this should be validated against decompViewDict i.e. we should be
// verifying that the metadata field indeed exists in the decomposition
// model
this.metadataField = m;
};
/**
* Retrieves the metadata field currently being controlled
*
* @return {String} A key corresponding to the active decomposition view.
*/
EmperorAttributeABC.prototype.getActiveDecompViewKey = function() {
return this.activeViewKey;
};
/**
* Changes the metadata column name to control.
*
* @param {String} k Key corresponding to active decomposition view.
*/
EmperorAttributeABC.prototype.setActiveDecompViewKey = function(k) {
// FIXME: this should be validated against decompViewDict i.e. we should be
// verifying that the key indeed exists
this.activeViewKey = k;
};
/**
* Retrieves the underlying data in the slick grid
* @return {Array} Returns an array of objects
* displayed by the body grid.
*/
EmperorAttributeABC.prototype.getSlickGridDataset = function() {
return this.bodyGrid.getData();
};
/**
* Changes the underlying data in the slick grid
*
* @param {Array} data data.
*/
EmperorAttributeABC.prototype.setSlickGridDataset = function(data) {
// Re-render
this.bodyGrid.setData(data);
this.bodyGrid.invalidate();
this.bodyGrid.render();
};
/**
* Method in charge of initializing the SlickGrid object
*
* @param {Object} [options] additional options to initialize the slick grid
* of this object.
* @private
*
*/
EmperorAttributeABC.prototype._buildGrid = function(options) {
var columns = [{id: 'field1', name: '', field: 'category'}];
var gridOptions = {editable: true, enableAddRow: false,
enableCellNavigation: true, forceFitColumns: true,
enableColumnReorder: false, autoEdit: true};
// If there's a custom slickgrid column then add it to the object
if (options.slickGridColumn !== undefined) {
columns.unshift(options.slickGridColumn);
}
/**
* @type {Slick.Grid}
* Container that lists the metadata categories described under the
* `metadataField` column and the attribute that can be modified.
*/
this.bodyGrid = new Slick.Grid(this.$gridDiv, [], columns, gridOptions);
// hide the header row of the grid
// http://stackoverflow.com/a/29827664/379593
$(this.$body).find('.slick-header').css('display', 'none');
// subscribe to events when a cell is changed
this.bodyGrid.onCellChange.subscribe(options.valueUpdatedCallback);
};
/**
* Resizes the container and the individual elements.
*
* Note, the consumer of this class, likely the main controller should call
* the resize function any time a resizing event happens.
*
* @param {Float} width the container width.
* @param {Float} height the container height.
*/
EmperorAttributeABC.prototype.resize = function(width, height) {
// call super, most of the header and body resizing logic is done there
EmperorViewControllerABC.prototype.resize.call(this, width, height);
// the whole code is asynchronous, so there may be situations where
// bodyGrid doesn't exist yet, so check before trying to modify the object
if (this.bodyGrid !== undefined) {
// make the columns fit the available space whenever the window resizes
// http://stackoverflow.com/a/29835739
this.bodyGrid.setColumns(this.bodyGrid.getColumns());
// Resize the slickgrid canvas for the new body size.
this.bodyGrid.resizeCanvas();
}
};
/**
* Converts the current instance into a JSON object.
*
* @return {Object} base object ready for JSON conversion.
*/
EmperorAttributeABC.prototype.toJSON = function() {
var json = {};
json.category = this.$select.val();
// Convert SlickGrid list of objects to single object
var gridData = this.bodyGrid.getData();
var jsonData = {};
for (var i = 0; i < gridData.length; i++) {
jsonData[gridData[i].category] = gridData[i].value;
}
json.data = jsonData;
return json;
};
/**
* Decodes JSON string and modifies its own instance variables accordingly.
*
* @param {Object} json Parsed JSON string representation of self.
*
*/
EmperorAttributeABC.prototype.fromJSON = function(json) {
this.$select.val(json.category);
this.$select.trigger('chosen:updated');
// fetch and set the SlickGrid-formatted data
var k = this.getActiveDecompViewKey();
var data = this.decompViewDict[k].setCategory(
json.data, this.setPlottableAttributes, json.category);
this.setSlickGridDataset(data);
// set all to needsUpdate
this.decompViewDict[k].needsUpdate = true;
};
return {'EmperorViewControllerABC': EmperorViewControllerABC,
'EmperorAttributeABC': EmperorAttributeABC};
});