text: /*jshint multistr:true */
  
  this.recline = this.recline || {};
  this.recline.View = this.recline.View || {};
  
  (function($, my) {
  // ## SlickGrid Dataset View
  //
  // Provides a tabular view on a Dataset, based on SlickGrid.
  //
  // https://github.com/mleibman/SlickGrid
  //
  // Initialize it with a `recline.Model.Dataset`.
  //
  // Additional options to drive SlickGrid grid can be given through state.
  // The following keys allow for customization:
  // * gridOptions: to add options at grid level
  // * columnsEditor: to add editor for editable columns
  //
  // For example:
  //    var grid = new recline.View.SlickGrid({
  //         model: dataset,
  //         el: $el,
  //         state: {
  //          gridOptions: {editable: true},
  //          columnsEditor: [
  //            {column: 'date', editor: Slick.Editors.Date },
  //            {column: 'title', editor: Slick.Editors.Text}
  //          ]
  //        }
  //      });
  //// NB: you need an explicit height on the element for slickgrid to work
  my.SlickGrid = Backbone.View.extend({
    initialize: function(modelEtc) {
      var self = this;
      this.el = $(this.el);
      this.el.addClass('recline-slickgrid');
      _.bindAll(this, 'render');
      this.model.records.bind('add', this.render);
      this.model.records.bind('reset', this.render);
      this.model.records.bind('remove', this.render);
      this.model.records.bind('change', this.onRecordChanged, this);
  
      var state = _.extend({
          hiddenColumns: [],
          columnsOrder: [],
          columnsSort: {},
          columnsWidth: [],
          columnsEditor: [],
          options: {},
          fitColumns: false
        }, modelEtc.state
  
      );
      this.state = new recline.Model.ObjectState(state);
    },
  
    events: {
    },
  
    onRecordChanged: function(record) {
      // Ignore if the grid is not yet drawn
      if (!this.grid) {
        return;
      }
  
      // Let's find the row corresponding to the index
      var row_index = this.grid.getData().getModelRow( record );
      this.grid.invalidateRow(row_index);
      this.grid.getData().updateItem(record, row_index);
      this.grid.render();
    },
  
    render: function() {
      var self = this;
  
      var options = _.extend({
        enableCellNavigation: true,
        enableColumnReorder: true,
        explicitInitialization: true,
        syncColumnCellResize: true,
        forceFitColumns: this.state.get('fitColumns')
      }, self.state.get('gridOptions'));
  
      // We need all columns, even the hidden ones, to show on the column picker
      var columns = [];
      // custom formatter as default one escapes html
      // plus this way we distinguish between rendering/formatting and computed value (so e.g. sort still works ...)
      // row = row index, cell = cell index, value = value, columnDef = column definition, dataContext = full row values
      var formatter = function(row, cell, value, columnDef, dataContext) {
        var field = self.model.fields.get(columnDef.id);
        if (field.renderer) {
          return field.renderer(value, field, dataContext);
        } else {
          return value;
        }
      };
      _.each(this.model.fields.toJSON(),function(field){
        var column = {
          id: field.id,
          name: field.label,
          field: field.id,
          sortable: true,
          minWidth: 80,
          formatter: formatter
        };
  
        var widthInfo = _.find(self.state.get('columnsWidth'),function(c){return c.column === field.id;});
        if (widthInfo){
          column.width = widthInfo.width;
        }
  
        var editInfo = _.find(self.state.get('columnsEditor'),function(c){return c.column === field.id;});
        if (editInfo){
          column.editor = editInfo.editor;
        }
        columns.push(column);
      });
  
      // Restrict the visible columns
      var visibleColumns = columns.filter(function(column) {
        return _.indexOf(self.state.get('hiddenColumns'), column.id) === -1;
      });
  
      // Order them if there is ordering info on the state
      if (this.state.get('columnsOrder') && this.state.get('columnsOrder').length > 0) {
        visibleColumns = visibleColumns.sort(function(a,b){
          return _.indexOf(self.state.get('columnsOrder'),a.id) > _.indexOf(self.state.get('columnsOrder'),b.id) ? 1 : -1;
        });
        columns = columns.sort(function(a,b){
          return _.indexOf(self.state.get('columnsOrder'),a.id) > _.indexOf(self.state.get('columnsOrder'),b.id) ? 1 : -1;
        });
      }
  
      // Move hidden columns to the end, so they appear at the bottom of the
      // column picker
      var tempHiddenColumns = [];
      for (var i = columns.length -1; i >= 0; i--){
        if (_.indexOf(_.pluck(visibleColumns,'id'),columns[i].id) === -1){
          tempHiddenColumns.push(columns.splice(i,1)[0]);
        }
      }
      columns = columns.concat(tempHiddenColumns);
  
      // Transform a model object into a row
      function toRow(m) {
        var row = {};
        self.model.fields.each(function(field){
          row[field.id] = m.getFieldValueUnrendered(field);
        });
        return row;
      }
  
      function RowSet() {
        var models = [];
        var rows = [];
  
        this.push = function(model, row) {
          models.push(model);
          rows.push(row);
        };
  
        this.getLength = function() {return rows.length; };
        this.getItem = function(index) {return rows[index];};
        this.getItemMetadata = function(index) {return {};};
        this.getModel = function(index) {return models[index];};
        this.getModelRow = function(m) {return models.indexOf(m);};
        this.updateItem = function(m,i) {
          rows[i] = toRow(m);
          models[i] = m;
        };
      }
  
      var data = new RowSet();
  
      this.model.records.each(function(doc){
        data.push(doc, toRow(doc));
      });
  
      this.grid = new Slick.Grid(this.el, data, visibleColumns, options);
  
      // Column sorting
      var sortInfo = this.model.queryState.get('sort');
      if (sortInfo){
        var column = sortInfo[0].field;
        var sortAsc = sortInfo[0].order !== 'desc';
        this.grid.setSortColumn(column, sortAsc);
      }
  
      this.grid.onSort.subscribe(function(e, args){
        var order = (args.sortAsc) ? 'asc':'desc';
        var sort = [{
          field: args.sortCol.field,
          order: order
        }];
        self.model.query({sort: sort});
      });
  
      this.grid.onColumnsReordered.subscribe(function(e, args){
        self.state.set({columnsOrder: _.pluck(self.grid.getColumns(),'id')});
      });
  
      this.grid.onColumnsResized.subscribe(function(e, args){
          var columns = args.grid.getColumns();
          var defaultColumnWidth = args.grid.getOptions().defaultColumnWidth;
          var columnsWidth = [];
          _.each(columns,function(column){
            if (column.width != defaultColumnWidth){
              columnsWidth.push({column:column.id,width:column.width});
            }
          });
          self.state.set({columnsWidth:columnsWidth});
      });
  
      this.grid.onCellChange.subscribe(function (e, args) {
        // We need to change the model associated value
        //
        var grid = args.grid;
        var model = data.getModel(args.row);
        var field = grid.getColumns()[args.cell].id;
        var v = {};
        v[field] = args.item[field];
        model.set(v);
      });
  
      var columnpicker = new Slick.Controls.ColumnPicker(columns, this.grid,
                                                         _.extend(options,{state:this.state}));
  
      if (self.visible){
        self.grid.init();
        self.rendered = true;
      } else {
        // Defer rendering until the view is visible
        self.rendered = false;
      }
  
      return this;
   },
  
    show: function() {
      // If the div is hidden, SlickGrid will calculate wrongly some
      // sizes so we must render it explicitly when the view is visible
      if (!this.rendered){
        if (!this.grid){
          this.render();
        }
        this.grid.init();
        this.rendered = true;
      }
      this.visible = true;
    },
  
    hide: function() {
      this.visible = false;
    }
  });
  
  })(jQuery, recline.View);
  
  /*
  * Context menu for the column picker, adapted from
  * http://mleibman.github.com/SlickGrid/examples/example-grouping
  *
  */
  (function ($) {
    function SlickColumnPicker(columns, grid, options) {
      var $menu;
      var columnCheckboxes;
  
      var defaults = {
        fadeSpeed:250
      };
  
      function init() {
        grid.onHeaderContextMenu.subscribe(handleHeaderContextMenu);
        options = $.extend({}, defaults, options);
  
        $menu = $('<ul class="dropdown-menu slick-contextmenu" style="display:none;position:absolute;z-index:20;" />').appendTo(document.body);
  
        $menu.bind('mouseleave', function (e) {
          $(this).fadeOut(options.fadeSpeed);
        });
        $menu.bind('click', updateColumn);
  
      }
  
      function handleHeaderContextMenu(e, args) {
        e.preventDefault();
        $menu.empty();
        columnCheckboxes = [];
  
        var $li, $input;
        for (var i = 0; i < columns.length; i++) {
          $li = $('<li />').appendTo($menu);
          $input = $('<input type="checkbox" />').data('column-id', columns[i].id).attr('id','slick-column-vis-'+columns[i].id);
          columnCheckboxes.push($input);
  
          if (grid.getColumnIndex(columns[i].id) !== null) {
            $input.attr('checked', 'checked');
          }
          $input.appendTo($li);
          $('<label />')
              .text(columns[i].name)
              .attr('for','slick-column-vis-'+columns[i].id)
              .appendTo($li);
        }
        $('<li/>').addClass('divider').appendTo($menu);
        $li = $('<li />').data('option', 'autoresize').appendTo($menu);
        $input = $('<input type="checkbox" />').data('option', 'autoresize').attr('id','slick-option-autoresize');
        $input.appendTo($li);
        $('<label />')
            .text('Force fit columns')
            .attr('for','slick-option-autoresize')
            .appendTo($li);
        if (grid.getOptions().forceFitColumns) {
          $input.attr('checked', 'checked');
        }
  
        $menu.css('top', e.pageY - 10)
            .css('left', e.pageX - 10)
            .fadeIn(options.fadeSpeed);
      }
  
      function updateColumn(e) {
        var checkbox;
  
        if ($(e.target).data('option') === 'autoresize') {
          var checked;
          if ($(e.target).is('li')){
              checkbox = $(e.target).find('input').first();
              checked = !checkbox.is(':checked');
              checkbox.attr('checked',checked);
          } else {
            checked = e.target.checked;
          }
  
          if (checked) {
            grid.setOptions({forceFitColumns:true});
            grid.autosizeColumns();
          } else {
            grid.setOptions({forceFitColumns:false});
          }
          options.state.set({fitColumns:checked});
          return;
        }
  
        if (($(e.target).is('li') && !$(e.target).hasClass('divider')) ||
              $(e.target).is('input')) {
          if ($(e.target).is('li')){
              checkbox = $(e.target).find('input').first();
              checkbox.attr('checked',!checkbox.is(':checked'));
          }
          var visibleColumns = [];
          var hiddenColumnsIds = [];
          $.each(columnCheckboxes, function (i, e) {
            if ($(this).is(':checked')) {
              visibleColumns.push(columns[i]);
            } else {
              hiddenColumnsIds.push(columns[i].id);
            }
          });
  
          if (!visibleColumns.length) {
            $(e.target).attr('checked', 'checked');
            return;
          }
  
          grid.setColumns(visibleColumns);
          options.state.set({hiddenColumns:hiddenColumnsIds});
        }
      }
      init();
    }
  
    // Slick.Controls.ColumnPicker
    $.extend(true, window, { Slick:{ Controls:{ ColumnPicker:SlickColumnPicker }}});
  })(jQuery);
  