<commit_before>Namespace('storybase.builder.views');

storybase.builder.views.mixins = {};

/**
 * @name save:section
 * @event
 * @param Section section Event triggered when a section has successfully
 *     been saved.
 * @param Boolean showAlert Should an alert be displayed by a callback 
 *     bound to this event.
 */

/**
 * @name save:story
 * @event
 * @param Story story Event triggered when a story has successfully
 *     been saved.
 * @param Boolean showAlert Should an alert be displayed by a callback 
 *     bound to this event.
 */

/**
 * Master view for the story builder
 *
 * Dispatches to sub-views.
 */
storybase.builder.views.AppView = Backbone.View.extend({
  initialize: function() {
    // Common options passed to sub-views
    var commonOptions = {};
    this.dispatcher = this.options.dispatcher;
    commonOptions.dispatcher = this.dispatcher;
    // The currently active step of the story building process
    // This will get set by an event callback 
    this.activeStep = null; 

    // Initialize the view for the workflow step indicator
    this.workflowStepView = new storybase.builder.views.WorkflowStepView(
      commonOptions
    );
    // TODO: Change the selector as the template changes
    this.$('header').first().children().first().append(this.workflowStepView.el);
    // Initialize a view for the tools menu
    this.toolsView = new storybase.builder.views.ToolsView(commonOptions);
    
    // TODO: Change the selector as the template changes
    this.$('header').first().children().first().append(this.toolsView.el);
    // Initialize view for moving back and forth through the workflow steps
    this.workflowNavView = new storybase.builder.views.WorkflowNavView(
      commonOptions
    );
    this.helpView = new storybase.builder.views.HelpView(commonOptions);

    // Store subviews in an object keyed with values of this.activeStep
    if (this.model) {
      commonOptions.model = this.model;
    }
    var buildViewOptions = _.extend(commonOptions, {
      assetTypes: this.options.assetTypes,
      layouts: this.options.layouts,
      help: this.options.help
    });
    this.subviews = {
      selecttemplate: new storybase.builder.views.SelectStoryTemplateView({
        dispatcher: this.dispatcher,
        collection: this.options.storyTemplates
      }),
      build: new storybase.builder.views.BuilderView(buildViewOptions),
      data: new storybase.builder.views.DataView(commonOptions),
      review: new storybase.builder.views.ReviewView(commonOptions),
      share: new storybase.builder.views.ShareView(commonOptions)
    };

    // Bind callbacks for custom events
    this.dispatcher.on("select:template", this.setTemplate, this);
    this.dispatcher.on("select:workflowstep", this.updateStep, this); 
    this.dispatcher.on("error", this.error, this);
    this.dispatcher.on("alert", this.showAlert, this);
  },

  /**
   * Set the template and move to the next step of the workflow
   */
  setTemplate: function(template) {
    this.activeTemplate = template;
    this.updateStep('build');
  },

  /**
   * Set the active step of the workflow and re-render the view
   */
  updateStep: function(step) {
    console.debug('Updating active view to ' + step);
    this.activeStep = step;
    this.render();
  },

  /**
   * Get the sub-view for the currently active step
   */
  getActiveView: function() {
    console.debug('Getting active view for step ' + this.activeStep);
    return this.subviews[this.activeStep];
  },

  render: function() {
    console.debug('Rendering main view');
    var activeView = this.getActiveView();
    this.$el.height($(window).height());
    this.$('#app').html(activeView.render().$el);
    this.$('#app').append(this.workflowNavView.render().el);
    this.workflowStepView.render();
    this.toolsView.render();
    return this;
  },

  /**
   * Generic error handler
   *
   * This is basically a stub that can later propogate error messages
   * up to the UI
   */
  error: function(msg) {
    console.error(msg);
    this.showAlert('error', msg);
  },

  showAlert: function(level, msg) {
    var view = new storybase.builder.views.AlertView({
      level: level,
      message: msg
    });
    this.$('.alerts').prepend(view.render().el);
    view.$el.fadeOut(5000, function() {
      $(this).remove();
    });
  }
});

storybase.builder.views.HelpView = Backbone.View.extend({
  tagName: 'div',

  className: 'help',

  templateSource: $('#help-template').html(),

  events: {
    'change .auto-show-help': 'setAutoShow'
  },

  initialize: function() {
    this.dispatcher = this.options.dispatcher;
    this.help = null;
    this.template = Handlebars.compile(this.templateSource);
    // Always show help when switching to a new section
    this.autoShow = true;

    this.dispatcher.on('do:show:help', this.show, this);
  },

  setAutoShow: function(evt) {
    this.autoShow = $(evt.target).prop('checked'); 
  },

  /**
   * Show the help text via a modal window.
   *
   * @param boolean force Force showing the help in a modal, overriding
   *     the value of this.autoShow.
   * @help object help Updated help information.  The object should have
   *     a body property and optionally a title property.
   *
   * @returns object This view.
   */
  show: function(force, help) {
    var show = this.autoShow || force;
    if (!_.isUndefined(help)) {
      // A new help object was sent with the signal, update
      // our internal value
      this.help = help;
    }
    if (this.help) {
      this.render();
      if (show) {
        this.delegateEvents();
        this.$el.modal();
      }
    }
    return this;
  },

  render: function() {
    var context = _.extend(this.help, {
      'autoShow': this.autoShow
    });
    this.$el.html(this.template(this.help));
    return this;
  }
});

/**
 * Base class for menu-like views
 */
storybase.builder.views.MenuView = Backbone.View.extend({
  items: [],

  itemTemplateSource: $('#menu-item-template').html(),

  getItemTemplate: function() {
    if (_.isUndefined(this.itemTemplate)) {
      this.itemTemplate = Handlebars.compile(this.itemTemplateSource);
    }
    return this.itemTemplate; 
  },

  events: function() {
    var events = {};
    _.each(this.items, function(item) {
      if (item.callback) {
        events["click ." + item.id] = item.callback;
      }
    });
    return events;
  },

  getItem: function(id) {
    return _.filter(this.items, function(item) {
      return item.id === id;
    })[0];
  },

  setVisibility: function(id, visible) {
    var item = this.getItem(id);
    item.visible = visible;
  },

  getVisibleItems: function() {
    return _.filter(this.items, function(item) {
      return item.visible == true;
    });
  }
});

/**
 * Shows current step of workflow 
 */
storybase.builder.views.WorkflowStepView = storybase.builder.views.MenuView.extend({
  tagName: 'ul',

  className: 'workflow nav',

  itemTemplateSource: $('#workflow-item-template').html(),

  items: [
    {
      id: 'build',
      title: 'Build',
      callback: 'selectStep',
      visible: true,
      path: ''
    },
    {
      id: 'data',
      title: 'Add Data',
      callback: 'selectStep',
      visible: false,
      path: 'data/'
    },
    {
      id: 'review',
      title: 'Review',
      callback: 'selectStep',
      visible: false,
      path: 'review/'
    },
    {
      id: 'share',
      title: 'Share',
      callback: 'selectStep',
      visible: false,
      path: 'share/legal/'
    }
  ],

  initialize: function() {
    this.dispatcher = this.options.dispatcher;
    this.template = Handlebars.compile(this.templateSource);
    this.storyId = null;

    this.dispatcher.on('ready:story', this.showWorkflowItems, this);
    this.dispatcher.on('save:story', this.showWorkflowItems, this);
    this.dispatcher.on('ready:story', this.setStoryId, this);
    this.dispatcher.on('save:story', this.setStoryId, this);
    this.dispatcher.on('select:workflowstep', this.highlightActive, this);
  },


  render: function() {
    var that = this;
    var template = this.getItemTemplate();
    var context = {
      storyId: this.storyId
    };
    this.$el.empty();
    _.each(this.getVisibleItems(), function(item) {
      that.$el.append(template(_.extend(item, context)));
    });
    return this;
  },


  setStoryId: function(story) {
    if (!story.isNew()) {
      this.storyId = story.id; 
    }
  },

  /**
   * Show the worfklow items that require a story.
   */
  showWorkflowItems: function(story) {
    if (!story.isNew()) {
      this.setVisibility('data', true);
      this.setVisibility('review', true);
      this.setVisibility('share', true);
      this.render();
    }
    return this;
  },

  selectStep: function(evt) {
    evt.preventDefault();
    var route = $(evt.target).attr("href");
    this.dispatcher.trigger('navigate', route, 
      {trigger: true, replace: true});
  },

  highlightActive: function(step) {
    _.each(this.items, function(item) {
      if (item.id === step) {
        item.active = true;
      }
      else {
        item.active = false;
      }
    });
    return this.render();
  }
});

storybase.builder.views.ToolsView = storybase.builder.views.MenuView.extend({
  tagName: 'ul',

  className: 'tools nav',

  items: [
    {
      id: 'help',
      title: 'Help',
      callback: 'toggleHelp', 
      href: '#',
      visible: true 
    },
    {
      id: 'assets',
      title: 'Assets',
      callback: 'toggleAssetList',
      href: '#',
      visible: false
    },
    {
      id: 'preview',
      title: 'Preview',
      callback: null,
      href: '#',
      visible: false
    },
    {
      id: 'exit',
      title: 'Exit',
      callback: null,
      href: '/',
      visible: true 
    },
  ],

  initialize: function() {
    this.dispatcher = this.options.dispatcher;

    this.dispatcher.on('has:assetlist', this.toggleAssetsItem, this);
    this.dispatcher.on('ready:story', this.setStoryId, this);
    this.dispatcher.on('save:story', this.setStoryId, this);
  },

  render: function() {
    var that = this;
    var template = this.getItemTemplate();
    this.$el.empty();
    _.each(this.getVisibleItems(), function(item) {
      that.$el.append(template(item));
    });
    return this;
  },

  toggleAssetsItem: function(visible) {
    this.setVisibility('assets', visible);
    this.render();
  },

  toggleAssetList: function(evt) {
    evt.preventDefault();
    this.dispatcher.trigger("toggle:assetlist");
  },

  toggleHelp: function(evt) {
    evt.preventDefault();
    this.dispatcher.trigger('do:show:help', true);
  },
  
  setStoryId: function(story) {
    if (!story.isNew() && _.isUndefined(this.storyId)) {
      var item = this.getItem('preview');
      this.storyId = story.id; 
      item.href = '/stories/' + this.storyId + '/viewer/';
      item.visible = true;
      this.render();
    }
  }
});

/**
 * A pair of links that lets the user move forward and back through the
 * different workflow steps.
 */
storybase.builder.views.WorkflowNavView = storybase.builder.views.MenuView.extend({
  tagName: 'div',

  className: 'workflow-nav',

  itemTemplateSource: $('#workflow-nav-item-template').html(),

  items: [
    {
      id: 'build',
      backTitle: 'Continue Writing Story',
      callback: 'selectStep',
      path: ''
    },
    {
      id: 'data',
      fwdTitle: 'Add Data',
      backTitle: 'Back to "Add Data"',
      callback: 'selectStep',
      path: 'data/'
    },
    {
      id: 'review',
      backTitle: 'Back to Review',
      fwdTitle: 'Review',
      callback: 'selectStep',
      path: 'review/'
    },
    {
      id: 'share-legal',
      backTitle: 'Back to Legal Agreements',
      fwdTitle: 'Share My Story',
      callback: 'selectStep',
      path: 'share/legal/'
    },
    {
      id: 'share-tagging',
      backTitle: 'Back to Tagging',
      fwdTitle: 'Continue',
      callback: 'selectStep',
      path: 'share/tagging/'
    },
    {
      id: 'share-publish',
      fwdTitle: 'Continue',
      callback: 'selectStep',
      path: 'share/publish/'
    },
    {
      id: 'new',
      fwdTitle: 'Tell Another Story',
      callback: null, 
      href: '/build'
    }
  ],

  visibility: {
    'selecttemplate': {
      back: null,
      forward: null
    },

    'build': {
      back: null,
      forward: 'data'
    },
    'data': {
      back: 'build',
      forward: 'review'
    },
    'review': {
      back: 'data',
      forward: 'share-legal'
    },
    'share-legal': {
      back: 'review',
      forward: 'share-tagging'
    },
    'share-tagging': {
      back: 'share-legal',
      forward: 'share-publish'
    },
    'share-publish': {
      back: 'share-tagging',
      forward: 'new'
    }
  },

  initialize: function() {
    this.dispatcher = this.options.dispatcher;
    this.forward = this.getItem('data');
    this.back = null;

    this.dispatcher.on('ready:story', this.setStoryId, this);
    this.dispatcher.on('save:story', this.setStoryId, this);
    this.dispatcher.on('select:workflowstep', this.selectVisible, this);
  },

  render: function() {
    console.info('Rendering navigation view');
    var that = this;
    var template = this.getItemTemplate();
    this.$el.empty();
    if (this.storyId) {
      if (this.back) {
        this.$el.append(template({
          id: this.back.id,
          title: this.back.backTitle,
          href: this.getHref(this.back) 
        }));
      }
      if (this.forward) {
        this.$el.append(template({
          id: this.forward.id,
          title: this.forward.fwdTitle,
          href: this.getHref(this.forward)
        }));
      }
    }
    this.delegateEvents();
    return this;
  },

  selectVisible: function(step, subStep) {
    console.debug('Selecting step ' + step);
    if (!_.isUndefined(subStep)) {
      console.debug('Selecting sub-step ' + subStep);
    }
    var key = _.isUndefined(subStep) ? step : step + '-' + subStep;
    this.forward = this.visibility[key].forward ? this.getItem(this.visibility[key].forward) : null;
    this.back = this.visibility[key].back ? this.getItem(this.visibility[key].back) : null;
    this.render();
    return this;
  },

  selectStep: function(evt) {
    evt.preventDefault();
    var route = $(evt.target).attr("href");
    this.dispatcher.trigger('navigate', route, 
      {trigger: true, replace: true});
  },

  setStoryId: function(story) {
    if (!story.isNew() && _.isUndefined(this.storyId)) {
      this.storyId = story.id; 
      this.render();
    }
  },

  getHref: function(item) {
    if (!_.isUndefined(item.href)) {
      return item.href;
    }
    if (!_.isUndefined(item.path)) {
      return this.storyId + '/' + item.path;
    }

    return '#';
  },
});

/**
 * Story template selector
 */
storybase.builder.views.SelectStoryTemplateView = Backbone.View.extend({
  tagName: 'ul',
 
  // TODO: Remove row from classes when we apply real styles.  The row
  // class is just used for my bootstrap layout based on the 1140 grid
  // system
  className: 'story-templates row',

  initialize: function() {
    this.dispatcher = this.options.dispatcher;
    _.bindAll(this, 'addTemplateEntry');
  },

  addTemplateEntry: function(template) {
      var view = new storybase.builder.views.StoryTemplateView({
        dispatcher: this.dispatcher,
        model: template
      });
      this.$el.append(view.render().el);
  },

  render: function() {
    this.collection.each(this.addTemplateEntry);
    return this;
  }
});

/**
 * Story template listing
 */
storybase.builder.views.StoryTemplateView = Backbone.View.extend({
  tagName: 'li',

  className: 'template',

  templateSource: $('#story-template-template').html(),

  events: {
    "click a": "select"
  },

  initialize: function() {
    console.info('initializing form');
    this.dispatcher = this.options.dispatcher;
    this.template = Handlebars.compile(this.templateSource);
  },

  render: function() {
    this.$el.html(this.template(this.model.toJSON()));
    return this;
  },

  /**
   * Event handler for clicking a template's link
   */
  select: function(e) {
    this.dispatcher.trigger("select:template", this.model);
    e.preventDefault();
  },

});

storybase.builder.views.AlertView = Backbone.View.extend({
  tagName: 'div',

  className: 'alert',

  initialize: function() {
    this.level = this.options.level;
    this.message = this.options.message;
  },

  render: function() {
    this.$el.addClass('alert-' + this.level);
    this.$el.html(this.message);
    return this;
  }
});

storybase.builder.views.BuilderView = Backbone.View.extend({
  tagName: 'div',

  className: 'builder',

  templateSource: $('#builder-template').html(),

  initialize: function() {
    var that = this;

    this.dispatcher = this.options.dispatcher;
    this.help = this.options.help;

    this._thumbnailViews = [];
    this._sectionsFetched = false;
    this._thumbnailsAdded = false;
    
    if (_.isUndefined(this.model)) {
      // Create a new story model instance
      this.model = new storybase.models.Story({
        title: ""
      });
    }
    else {
      // The view was initialized with a model
      this.dispatcher.trigger("ready:story", this.model);
    }

    this.unusedAssetView = new storybase.builder.views.UnusedAssetView({
      dispatcher: this.dispatcher,
      assets: this.model.unusedAssets
    });
    this.lastSavedView = new storybase.builder.views.LastSavedView({
      dispatcher: this.dispatcher,
      lastSaved: this.model.get('last_edited')
    });

    this.model.on("sync", this.triggerSaved, this);
    this.model.unusedAssets.on("sync reset add", this.hasAssetList, this);
    this.model.sections.on("reset", this.setSectionsFetched, this);

    this.dispatcher.on("select:template", this.setStoryTemplate, this);
    this.dispatcher.on("ready:templateSections", this.initializeStoryFromTemplate, this);
    this.dispatcher.on("do:save:story", this.save, this);
    this.dispatcher.on("select:thumbnail", this.showEditView, this);
    this.dispatcher.on("toggle:assetlist", this.toggleAssetList, this);
    this.dispatcher.on("add:sectionasset", this.showSaved, this);
    this.dispatcher.on("save:section", this.showSaved, this);
    this.dispatcher.on("save:story", this.showSaved, this);

    _.bindAll(this, 'addSectionThumbnail', 'setTemplateStory', 'setTemplateSections');

    this.template = Handlebars.compile(this.templateSource);

    if (!this.model.isNew()) {
      this.model.sections.fetch();
      this.model.unusedAssets.fetch();
    }
  },

  setSectionsFetched: function() {
    this._sectionsFetched = true;
  },

  addSectionThumbnail: function(section) {
    console.info("Adding section thumbnail");
    var view = new storybase.builder.views.SectionThumbnailView({
      dispatcher: this.dispatcher,
      model: section,
      editView: new storybase.builder.views.SectionEditView({
        dispatcher: this.dispatcher,
        model: section,
        story: this.model,
        assetTypes: this.options.assetTypes,
        layouts: this.options.layouts,
      })
    });
    this._thumbnailViews.splice(this._thumbnailViews.length - 1, 0, view);
  },

  addStoryInfoThumbnail: function() {
    var view = new storybase.builder.views.PseudoSectionThumbnailView({
      dispatcher: this.dispatcher,
      title: "Story Information",
      editView: new storybase.builder.views.StoryInfoEditView({
        dispatcher: this.dispatcher,
        help: this.help.where({slug: 'story-information'})[0],
        model: this.model
      })
    });
    this._thumbnailViews.push(view);
  },

  addCallToActionThumbnail: function() {
    var view = new storybase.builder.views.PseudoSectionThumbnailView({
      dispatcher: this.dispatcher,
      title: "Call to Action",
      editView: new storybase.builder.views.CallToActionEditView({
        dispatcher: this.dispatcher,
        help: this.help.where({slug: 'call-to-action'})[0],
        model: this.model
      })
    });
    this._thumbnailViews.push(view);
  },

  /**
   * Event handler for when the story model and sections are ready.
   *
   * The story will either be ready when it has been cloned from the
   * template or when it has been fetched from the server.
   */
  addSectionThumbnails: function(options) {
    options = _.isUndefined(options) ? {} : options;
    _.defaults(options, {
      render: true
    });
    console.debug('Entering addSectionThumbnails');
    this.addStoryInfoThumbnail();
    this.addCallToActionThumbnail();
    this.model.sections.each(this.addSectionThumbnail);
    this._thumbnailsAdded = true;
    if (options.render) {
      this.render();
    }
  },

  render: function() {
    console.info('Rendering builder view');
    var that = this;
    if (this._sectionsFetched) {
      if (!this._thumbnailsAdded) {
        this.addSectionThumbnails({render: false});
      }
    }
    else {
      this.model.sections.on("reset", this.addSectionThumbnails, this);
    }
    this.$el.html(this.template());
    this.$el.prepend(this.unusedAssetView.render().$el.hide());
    this.$el.prepend(this.lastSavedView.render().el);
    if (this._thumbnailViews.length) {
      _.each(this._thumbnailViews, function(view) {
        that.$(".sections").append(view.render().el);
        that.$('.sections').before(view.editView.render().el);
      });
      this.dispatcher.trigger("select:thumbnail", this._thumbnailViews[0]);
    }
    return this;
  },

  /**
   * Event callback that displays an alert indicating the story has been
   * saved.
   */
  showSaved: function(model, showAlert) {
    showAlert = _.isUndefined(showAlert) || showAlert;
    if (showAlert) {
      this.dispatcher.trigger('alert', 'success', "The story has been saved");
    }
  },

  triggerSaved: function() {
    this.dispatcher.trigger('save:story', this.model);
  },

  setStoryTemplate: function(template) {
    console.info("Setting story template");
    var that = this;
    this.storyTemplate = template;
    this.storyTemplate.getStory({
      success: this.setTemplateStory,
      error: function(story, response) {
        that.error("Failed fetching template story");
      }
    });
  },

  setTemplateStory: function(story) {
    console.info("Setting template story");
    var that = this;
    this.templateStory = story;
    this.templateStory.sections.on("reset", this.setTemplateSections, this);
    this.templateStory.sections.fetch();
  },

  setTemplateSections: function(sections) {
    console.info("Setting template sections"); 
    this.templateSections = sections;
    this.dispatcher.trigger("ready:templateSections");
  },

  initializeStoryFromTemplate: function() {
    console.info("Initializing sections");
    var that = this;
    this.model.set('structure_type',
                   this.templateStory.get('structure_type'));
    this.templateSections.each(function(section) {
      var sectionCopy = new storybase.models.Section();
      sectionCopy.set("title", section.get("title"));
      sectionCopy.set("layout", section.get("layout"));
      sectionCopy.set("root", section.get("root"));
      sectionCopy.set("weight", section.get("weight"));
      sectionCopy.set("layout_template", section.get("layout_template"));
      sectionCopy.set("help", section.get("help"));
      that.model.sections.push(sectionCopy);
    });
    this.dispatcher.trigger("ready:story", this.model);
    this.addSectionThumbnails();
  },

  save: function() {
    console.info("Saving story");
    var that = this;
    this.model.save(null, {
      success: function(model, response) {
        that.dispatcher.trigger('save:story', model);
        that.dispatcher.trigger('navigate', model.id + '/', {
          trigger: true
        });
        model.saveSections();
      }
    });
  },

  showEditView: function(thumbnailView) {
    this.$('.edit-section').hide();
    //thumbnailView.editView.$el.show();
    console.debug('Showing edit view');
    thumbnailView.editView.show();
  },

  toggleAssetList: function() {
    this.unusedAssetView.$el.toggle(); 
  },
<commit_msg>Don't trigger router event when setting URL on initial save.<commit_after>Namespace('storybase.builder.views');

storybase.builder.views.mixins = {};

/**
 * @name save:section
 * @event
 * @param Section section Event triggered when a section has successfully
 *     been saved.
 * @param Boolean showAlert Should an alert be displayed by a callback 
 *     bound to this event.
 */

/**
 * @name save:story
 * @event
 * @param Story story Event triggered when a story has successfully
 *     been saved.
 * @param Boolean showAlert Should an alert be displayed by a callback 
 *     bound to this event.
 */

/**
 * Master view for the story builder
 *
 * Dispatches to sub-views.
 */
storybase.builder.views.AppView = Backbone.View.extend({
  initialize: function() {
    // Common options passed to sub-views
    var commonOptions = {};
    this.dispatcher = this.options.dispatcher;
    commonOptions.dispatcher = this.dispatcher;
    // The currently active step of the story building process
    // This will get set by an event callback 
    this.activeStep = null; 

    // Initialize the view for the workflow step indicator
    this.workflowStepView = new storybase.builder.views.WorkflowStepView(
      commonOptions
    );
    // TODO: Change the selector as the template changes
    this.$('header').first().children().first().append(this.workflowStepView.el);
    // Initialize a view for the tools menu
    this.toolsView = new storybase.builder.views.ToolsView(commonOptions);
    
    // TODO: Change the selector as the template changes
    this.$('header').first().children().first().append(this.toolsView.el);
    // Initialize view for moving back and forth through the workflow steps
    this.workflowNavView = new storybase.builder.views.WorkflowNavView(
      commonOptions
    );
    this.helpView = new storybase.builder.views.HelpView(commonOptions);

    // Store subviews in an object keyed with values of this.activeStep
    if (this.model) {
      commonOptions.model = this.model;
    }
    var buildViewOptions = _.extend(commonOptions, {
      assetTypes: this.options.assetTypes,
      layouts: this.options.layouts,
      help: this.options.help
    });
    this.subviews = {
      selecttemplate: new storybase.builder.views.SelectStoryTemplateView({
        dispatcher: this.dispatcher,
        collection: this.options.storyTemplates
      }),
      build: new storybase.builder.views.BuilderView(buildViewOptions),
      data: new storybase.builder.views.DataView(commonOptions),
      review: new storybase.builder.views.ReviewView(commonOptions),
      share: new storybase.builder.views.ShareView(commonOptions)
    };

    // Bind callbacks for custom events
    this.dispatcher.on("select:template", this.setTemplate, this);
    this.dispatcher.on("select:workflowstep", this.updateStep, this); 
    this.dispatcher.on("error", this.error, this);
    this.dispatcher.on("alert", this.showAlert, this);
  },

  /**
   * Set the template and move to the next step of the workflow
   */
  setTemplate: function(template) {
    this.activeTemplate = template;
    this.updateStep('build');
  },

  /**
   * Set the active step of the workflow and re-render the view
   */
  updateStep: function(step) {
    console.debug('Updating active view to ' + step);
    this.activeStep = step;
    this.render();
  },

  /**
   * Get the sub-view for the currently active step
   */
  getActiveView: function() {
    console.debug('Getting active view for step ' + this.activeStep);
    return this.subviews[this.activeStep];
  },

  render: function() {
    console.debug('Rendering main view');
    var activeView = this.getActiveView();
    this.$el.height($(window).height());
    this.$('#app').html(activeView.render().$el);
    this.$('#app').append(this.workflowNavView.render().el);
    this.workflowStepView.render();
    this.toolsView.render();
    return this;
  },

  /**
   * Generic error handler
   *
   * This is basically a stub that can later propogate error messages
   * up to the UI
   */
  error: function(msg) {
    console.error(msg);
    this.showAlert('error', msg);
  },

  showAlert: function(level, msg) {
    var view = new storybase.builder.views.AlertView({
      level: level,
      message: msg
    });
    this.$('.alerts').prepend(view.render().el);
    view.$el.fadeOut(5000, function() {
      $(this).remove();
    });
  }
});

storybase.builder.views.HelpView = Backbone.View.extend({
  tagName: 'div',

  className: 'help',

  templateSource: $('#help-template').html(),

  events: {
    'change .auto-show-help': 'setAutoShow'
  },

  initialize: function() {
    this.dispatcher = this.options.dispatcher;
    this.help = null;
    this.template = Handlebars.compile(this.templateSource);
    // Always show help when switching to a new section
    this.autoShow = true;

    this.dispatcher.on('do:show:help', this.show, this);
  },

  setAutoShow: function(evt) {
    this.autoShow = $(evt.target).prop('checked'); 
  },

  /**
   * Show the help text via a modal window.
   *
   * @param boolean force Force showing the help in a modal, overriding
   *     the value of this.autoShow.
   * @help object help Updated help information.  The object should have
   *     a body property and optionally a title property.
   *
   * @returns object This view.
   */
  show: function(force, help) {
    var show = this.autoShow || force;
    if (!_.isUndefined(help)) {
      // A new help object was sent with the signal, update
      // our internal value
      this.help = help;
    }
    if (this.help) {
      this.render();
      if (show) {
        this.delegateEvents();
        this.$el.modal();
      }
    }
    return this;
  },

  render: function() {
    var context = _.extend(this.help, {
      'autoShow': this.autoShow
    });
    this.$el.html(this.template(this.help));
    return this;
  }
});

/**
 * Base class for menu-like views
 */
storybase.builder.views.MenuView = Backbone.View.extend({
  items: [],

  itemTemplateSource: $('#menu-item-template').html(),

  getItemTemplate: function() {
    if (_.isUndefined(this.itemTemplate)) {
      this.itemTemplate = Handlebars.compile(this.itemTemplateSource);
    }
    return this.itemTemplate; 
  },

  events: function() {
    var events = {};
    _.each(this.items, function(item) {
      if (item.callback) {
        events["click ." + item.id] = item.callback;
      }
    });
    return events;
  },

  getItem: function(id) {
    return _.filter(this.items, function(item) {
      return item.id === id;
    })[0];
  },

  setVisibility: function(id, visible) {
    var item = this.getItem(id);
    item.visible = visible;
  },

  getVisibleItems: function() {
    return _.filter(this.items, function(item) {
      return item.visible == true;
    });
  }
});

/**
 * Shows current step of workflow 
 */
storybase.builder.views.WorkflowStepView = storybase.builder.views.MenuView.extend({
  tagName: 'ul',

  className: 'workflow nav',

  itemTemplateSource: $('#workflow-item-template').html(),

  items: [
    {
      id: 'build',
      title: 'Build',
      callback: 'selectStep',
      visible: true,
      path: ''
    },
    {
      id: 'data',
      title: 'Add Data',
      callback: 'selectStep',
      visible: false,
      path: 'data/'
    },
    {
      id: 'review',
      title: 'Review',
      callback: 'selectStep',
      visible: false,
      path: 'review/'
    },
    {
      id: 'share',
      title: 'Share',
      callback: 'selectStep',
      visible: false,
      path: 'share/legal/'
    }
  ],

  initialize: function() {
    this.dispatcher = this.options.dispatcher;
    this.template = Handlebars.compile(this.templateSource);
    this.storyId = null;

    this.dispatcher.on('ready:story', this.showWorkflowItems, this);
    this.dispatcher.on('save:story', this.showWorkflowItems, this);
    this.dispatcher.on('ready:story', this.setStoryId, this);
    this.dispatcher.on('save:story', this.setStoryId, this);
    this.dispatcher.on('select:workflowstep', this.highlightActive, this);
  },


  render: function() {
    var that = this;
    var template = this.getItemTemplate();
    var context = {
      storyId: this.storyId
    };
    this.$el.empty();
    _.each(this.getVisibleItems(), function(item) {
      that.$el.append(template(_.extend(item, context)));
    });
    return this;
  },


  setStoryId: function(story) {
    if (!story.isNew()) {
      this.storyId = story.id; 
    }
  },

  /**
   * Show the worfklow items that require a story.
   */
  showWorkflowItems: function(story) {
    if (!story.isNew()) {
      this.setVisibility('data', true);
      this.setVisibility('review', true);
      this.setVisibility('share', true);
      this.render();
    }
    return this;
  },

  selectStep: function(evt) {
    evt.preventDefault();
    var route = $(evt.target).attr("href");
    this.dispatcher.trigger('navigate', route, 
      {trigger: true, replace: true});
  },

  highlightActive: function(step) {
    _.each(this.items, function(item) {
      if (item.id === step) {
        item.active = true;
      }
      else {
        item.active = false;
      }
    });
    return this.render();
  }
});

storybase.builder.views.ToolsView = storybase.builder.views.MenuView.extend({
  tagName: 'ul',

  className: 'tools nav',

  items: [
    {
      id: 'help',
      title: 'Help',
      callback: 'toggleHelp', 
      href: '#',
      visible: true 
    },
    {
      id: 'assets',
      title: 'Assets',
      callback: 'toggleAssetList',
      href: '#',
      visible: false
    },
    {
      id: 'preview',
      title: 'Preview',
      callback: null,
      href: '#',
      visible: false
    },
    {
      id: 'exit',
      title: 'Exit',
      callback: null,
      href: '/',
      visible: true 
    },
  ],

  initialize: function() {
    this.dispatcher = this.options.dispatcher;

    this.dispatcher.on('has:assetlist', this.toggleAssetsItem, this);
    this.dispatcher.on('ready:story', this.setStoryId, this);
    this.dispatcher.on('save:story', this.setStoryId, this);
  },

  render: function() {
    var that = this;
    var template = this.getItemTemplate();
    this.$el.empty();
    _.each(this.getVisibleItems(), function(item) {
      that.$el.append(template(item));
    });
    return this;
  },

  toggleAssetsItem: function(visible) {
    this.setVisibility('assets', visible);
    this.render();
  },

  toggleAssetList: function(evt) {
    evt.preventDefault();
    this.dispatcher.trigger("toggle:assetlist");
  },

  toggleHelp: function(evt) {
    evt.preventDefault();
    this.dispatcher.trigger('do:show:help', true);
  },
  
  setStoryId: function(story) {
    if (!story.isNew() && _.isUndefined(this.storyId)) {
      var item = this.getItem('preview');
      this.storyId = story.id; 
      item.href = '/stories/' + this.storyId + '/viewer/';
      item.visible = true;
      this.render();
    }
  }
});

/**
 * A pair of links that lets the user move forward and back through the
 * different workflow steps.
 */
storybase.builder.views.WorkflowNavView = storybase.builder.views.MenuView.extend({
  tagName: 'div',

  className: 'workflow-nav',

  itemTemplateSource: $('#workflow-nav-item-template').html(),

  items: [
    {
      id: 'build',
      backTitle: 'Continue Writing Story',
      callback: 'selectStep',
      path: ''
    },
    {
      id: 'data',
      fwdTitle: 'Add Data',
      backTitle: 'Back to "Add Data"',
      callback: 'selectStep',
      path: 'data/'
    },
    {
      id: 'review',
      backTitle: 'Back to Review',
      fwdTitle: 'Review',
      callback: 'selectStep',
      path: 'review/'
    },
    {
      id: 'share-legal',
      backTitle: 'Back to Legal Agreements',
      fwdTitle: 'Share My Story',
      callback: 'selectStep',
      path: 'share/legal/'
    },
    {
      id: 'share-tagging',
      backTitle: 'Back to Tagging',
      fwdTitle: 'Continue',
      callback: 'selectStep',
      path: 'share/tagging/'
    },
    {
      id: 'share-publish',
      fwdTitle: 'Continue',
      callback: 'selectStep',
      path: 'share/publish/'
    },
    {
      id: 'new',
      fwdTitle: 'Tell Another Story',
      callback: null, 
      href: '/build'
    }
  ],

  visibility: {
    'selecttemplate': {
      back: null,
      forward: null
    },

    'build': {
      back: null,
      forward: 'data'
    },
    'data': {
      back: 'build',
      forward: 'review'
    },
    'review': {
      back: 'data',
      forward: 'share-legal'
    },
    'share-legal': {
      back: 'review',
      forward: 'share-tagging'
    },
    'share-tagging': {
      back: 'share-legal',
      forward: 'share-publish'
    },
    'share-publish': {
      back: 'share-tagging',
      forward: 'new'
    }
  },

  initialize: function() {
    this.dispatcher = this.options.dispatcher;
    this.forward = this.getItem('data');
    this.back = null;

    this.dispatcher.on('ready:story', this.setStoryId, this);
    this.dispatcher.on('save:story', this.setStoryId, this);
    this.dispatcher.on('select:workflowstep', this.selectVisible, this);
  },

  render: function() {
    console.info('Rendering navigation view');
    var that = this;
    var template = this.getItemTemplate();
    this.$el.empty();
    if (this.storyId) {
      if (this.back) {
        this.$el.append(template({
          id: this.back.id,
          title: this.back.backTitle,
          href: this.getHref(this.back) 
        }));
      }
      if (this.forward) {
        this.$el.append(template({
          id: this.forward.id,
          title: this.forward.fwdTitle,
          href: this.getHref(this.forward)
        }));
      }
    }
    this.delegateEvents();
    return this;
  },

  selectVisible: function(step, subStep) {
    console.debug('Selecting step ' + step);
    if (!_.isUndefined(subStep)) {
      console.debug('Selecting sub-step ' + subStep);
    }
    var key = _.isUndefined(subStep) ? step : step + '-' + subStep;
    this.forward = this.visibility[key].forward ? this.getItem(this.visibility[key].forward) : null;
    this.back = this.visibility[key].back ? this.getItem(this.visibility[key].back) : null;
    this.render();
    return this;
  },

  selectStep: function(evt) {
    evt.preventDefault();
    var route = $(evt.target).attr("href");
    this.dispatcher.trigger('navigate', route, 
      {trigger: true, replace: true});
  },

  setStoryId: function(story) {
    if (!story.isNew() && _.isUndefined(this.storyId)) {
      this.storyId = story.id; 
      this.render();
    }
  },

  getHref: function(item) {
    if (!_.isUndefined(item.href)) {
      return item.href;
    }
    if (!_.isUndefined(item.path)) {
      return this.storyId + '/' + item.path;
    }

    return '#';
  },
});

/**
 * Story template selector
 */
storybase.builder.views.SelectStoryTemplateView = Backbone.View.extend({
  tagName: 'ul',
 
  // TODO: Remove row from classes when we apply real styles.  The row
  // class is just used for my bootstrap layout based on the 1140 grid
  // system
  className: 'story-templates row',

  initialize: function() {
    this.dispatcher = this.options.dispatcher;
    _.bindAll(this, 'addTemplateEntry');
  },

  addTemplateEntry: function(template) {
      var view = new storybase.builder.views.StoryTemplateView({
        dispatcher: this.dispatcher,
        model: template
      });
      this.$el.append(view.render().el);
  },

  render: function() {
    this.collection.each(this.addTemplateEntry);
    return this;
  }
});

/**
 * Story template listing
 */
storybase.builder.views.StoryTemplateView = Backbone.View.extend({
  tagName: 'li',

  className: 'template',

  templateSource: $('#story-template-template').html(),

  events: {
    "click a": "select"
  },

  initialize: function() {
    console.info('initializing form');
    this.dispatcher = this.options.dispatcher;
    this.template = Handlebars.compile(this.templateSource);
  },

  render: function() {
    this.$el.html(this.template(this.model.toJSON()));
    return this;
  },

  /**
   * Event handler for clicking a template's link
   */
  select: function(e) {
    this.dispatcher.trigger("select:template", this.model);
    e.preventDefault();
  },

});

storybase.builder.views.AlertView = Backbone.View.extend({
  tagName: 'div',

  className: 'alert',

  initialize: function() {
    this.level = this.options.level;
    this.message = this.options.message;
  },

  render: function() {
    this.$el.addClass('alert-' + this.level);
    this.$el.html(this.message);
    return this;
  }
});

storybase.builder.views.BuilderView = Backbone.View.extend({
  tagName: 'div',

  className: 'builder',

  templateSource: $('#builder-template').html(),

  initialize: function() {
    var that = this;

    this.dispatcher = this.options.dispatcher;
    this.help = this.options.help;

    this._thumbnailViews = [];
    this._sectionsFetched = false;
    this._thumbnailsAdded = false;
    
    if (_.isUndefined(this.model)) {
      // Create a new story model instance
      this.model = new storybase.models.Story({
        title: ""
      });
    }
    else {
      // The view was initialized with a model
      this.dispatcher.trigger("ready:story", this.model);
    }

    this.unusedAssetView = new storybase.builder.views.UnusedAssetView({
      dispatcher: this.dispatcher,
      assets: this.model.unusedAssets
    });
    this.lastSavedView = new storybase.builder.views.LastSavedView({
      dispatcher: this.dispatcher,
      lastSaved: this.model.get('last_edited')
    });

    this.model.on("sync", this.triggerSaved, this);
    this.model.unusedAssets.on("sync reset add", this.hasAssetList, this);
    this.model.sections.on("reset", this.setSectionsFetched, this);

    this.dispatcher.on("select:template", this.setStoryTemplate, this);
    this.dispatcher.on("ready:templateSections", this.initializeStoryFromTemplate, this);
    this.dispatcher.on("do:save:story", this.save, this);
    this.dispatcher.on("select:thumbnail", this.showEditView, this);
    this.dispatcher.on("toggle:assetlist", this.toggleAssetList, this);
    this.dispatcher.on("add:sectionasset", this.showSaved, this);
    this.dispatcher.on("save:section", this.showSaved, this);
    this.dispatcher.on("save:story", this.showSaved, this);

    _.bindAll(this, 'addSectionThumbnail', 'setTemplateStory', 'setTemplateSections');

    this.template = Handlebars.compile(this.templateSource);

    if (!this.model.isNew()) {
      this.model.sections.fetch();
      this.model.unusedAssets.fetch();
    }
  },

  setSectionsFetched: function() {
    this._sectionsFetched = true;
  },

  addSectionThumbnail: function(section) {
    console.info("Adding section thumbnail");
    var view = new storybase.builder.views.SectionThumbnailView({
      dispatcher: this.dispatcher,
      model: section,
      editView: new storybase.builder.views.SectionEditView({
        dispatcher: this.dispatcher,
        model: section,
        story: this.model,
        assetTypes: this.options.assetTypes,
        layouts: this.options.layouts,
      })
    });
    this._thumbnailViews.splice(this._thumbnailViews.length - 1, 0, view);
  },

  addStoryInfoThumbnail: function() {
    var view = new storybase.builder.views.PseudoSectionThumbnailView({
      dispatcher: this.dispatcher,
      title: "Story Information",
      editView: new storybase.builder.views.StoryInfoEditView({
        dispatcher: this.dispatcher,
        help: this.help.where({slug: 'story-information'})[0],
        model: this.model
      })
    });
    this._thumbnailViews.push(view);
  },

  addCallToActionThumbnail: function() {
    var view = new storybase.builder.views.PseudoSectionThumbnailView({
      dispatcher: this.dispatcher,
      title: "Call to Action",
      editView: new storybase.builder.views.CallToActionEditView({
        dispatcher: this.dispatcher,
        help: this.help.where({slug: 'call-to-action'})[0],
        model: this.model
      })
    });
    this._thumbnailViews.push(view);
  },

  /**
   * Event handler for when the story model and sections are ready.
   *
   * The story will either be ready when it has been cloned from the
   * template or when it has been fetched from the server.
   */
  addSectionThumbnails: function(options) {
    options = _.isUndefined(options) ? {} : options;
    _.defaults(options, {
      render: true
    });
    console.debug('Entering addSectionThumbnails');
    this.addStoryInfoThumbnail();
    this.addCallToActionThumbnail();
    this.model.sections.each(this.addSectionThumbnail);
    this._thumbnailsAdded = true;
    if (options.render) {
      this.render();
    }
  },

  render: function() {
    console.info('Rendering builder view');
    var that = this;
    if (this._sectionsFetched) {
      if (!this._thumbnailsAdded) {
        this.addSectionThumbnails({render: false});
      }
    }
    else {
      this.model.sections.on("reset", this.addSectionThumbnails, this);
    }
    this.$el.html(this.template());
    this.$el.prepend(this.unusedAssetView.render().$el.hide());
    this.$el.prepend(this.lastSavedView.render().el);
    if (this._thumbnailViews.length) {
      _.each(this._thumbnailViews, function(view) {
        that.$(".sections").append(view.render().el);
        that.$('.sections').before(view.editView.render().el);
      });
      this.dispatcher.trigger("select:thumbnail", this._thumbnailViews[0]);
    }
    return this;
  },

  /**
   * Event callback that displays an alert indicating the story has been
   * saved.
   */
  showSaved: function(model, showAlert) {
    showAlert = _.isUndefined(showAlert) || showAlert;
    if (showAlert) {
      this.dispatcher.trigger('alert', 'success', "The story has been saved");
    }
  },

  triggerSaved: function() {
    this.dispatcher.trigger('save:story', this.model);
  },

  setStoryTemplate: function(template) {
    console.info("Setting story template");
    var that = this;
    this.storyTemplate = template;
    this.storyTemplate.getStory({
      success: this.setTemplateStory,
      error: function(story, response) {
        that.error("Failed fetching template story");
      }
    });
  },

  setTemplateStory: function(story) {
    console.info("Setting template story");
    var that = this;
    this.templateStory = story;
    this.templateStory.sections.on("reset", this.setTemplateSections, this);
    this.templateStory.sections.fetch();
  },

  setTemplateSections: function(sections) {
    console.info("Setting template sections"); 
    this.templateSections = sections;
    this.dispatcher.trigger("ready:templateSections");
  },

  initializeStoryFromTemplate: function() {
    console.info("Initializing sections");
    var that = this;
    this.model.set('structure_type',
                   this.templateStory.get('structure_type'));
    this.templateSections.each(function(section) {
      var sectionCopy = new storybase.models.Section();
      sectionCopy.set("title", section.get("title"));
      sectionCopy.set("layout", section.get("layout"));
      sectionCopy.set("root", section.get("root"));
      sectionCopy.set("weight", section.get("weight"));
      sectionCopy.set("layout_template", section.get("layout_template"));
      sectionCopy.set("help", section.get("help"));
      that.model.sections.push(sectionCopy);
    });
    this.dispatcher.trigger("ready:story", this.model);
    this.addSectionThumbnails();
  },

  save: function() {
    console.info("Saving story");
    var that = this;
    this.model.save(null, {
      success: function(model, response) {
        that.dispatcher.trigger('save:story', model);
        that.dispatcher.trigger('navigate', model.id + '/', {
          trigger: false 
        });
        model.saveSections();
      }
    });
  },

  showEditView: function(thumbnailView) {
    this.$('.edit-section').hide();
    //thumbnailView.editView.$el.show();
    console.debug('Showing edit view');
    thumbnailView.editView.show();
  },

  toggleAssetList: function() {
    this.unusedAssetView.$el.toggle(); 
  },

{'fraction_non_alphanumeric': 0.14659121530584748, 'fraction_numerical': 0.0005803950832245092, 'mean_word_length': 2.836806107841578, 'pattern_counts': {'":': 2, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1974937', 'n_tokens_mistral': 15053, 'n_tokens_neox': 14770, 'n_words': 3077}