<commit_before>var $ = require('jquery-browserify');
var _ = require('underscore');
var Backbone = require('backbone');
var utils = require('.././util');

module.exports = Backbone.View.extend({
    className: 'application',

    events: {
      'click .post-views .edit': 'edit',
      'click .post-views .preview': 'preview',
      'click .post-views .settings': 'settings',
      'click .post-views .meta': 'meta',
      'click .logout': 'logout',
      'click a.item.removed': 'restoreFile',
      'click a.save': 'save',
      'click a.cancel': 'cancel',
      'click a.confirm': 'updateFile',
      'click a.delete': 'deleteFile',
      'click a.translate': 'translate',
      'click a.draft': 'draft',
      'click .mobile-menu .toggle': 'toggleMobileClass',
      'focus input.filepath': 'checkPlaceholder',
      'keypress input.filepath': 'saveFilePath'
    },

    initialize: function(options) {

      // Key Binding support accross the application.
      if (!window.shortcutsRegistered) {
        key('j, k, enter, o, ctrl+s', _.bind(function(e, handler) {
          if (!app.state.mode || app.state.mode === 'tree') {
            // We are in any navigation view
            if (handler.key === 'j' || handler.key === 'k') {
              utils.pageListing(handler.key);
            } else {
              utils.goToFile();
            }
          } else {
            // We are in state of the application
            // where we can edit a file
            if (handler.key === 'ctrl+s') {
              this.updateFile();
            }
          }
        }, this));

        window.shortcutsRegistered = true;
      }

      app.state = {
        user: '',
        repo: '',
        mode: '',
        branch: '',
        path: ''
      };

      this.eventRegister = app.eventRegister;

      _.bindAll(this, 'documentTitle', 'headerContext', 'sidebarContext', 'recentFiles', 'updateSaveState', 'closeSettings', 'filenameInput');
      this.eventRegister.bind('documentTitle', this.documentTitle);
      this.eventRegister.bind('headerContext', this.headerContext);
      this.eventRegister.bind('sidebarContext', this.sidebarContext);
      this.eventRegister.bind('recentFiles', this.recentFiles);
      this.eventRegister.bind('updateSaveState', this.updateSaveState);
      this.eventRegister.bind('filenameInput', this.filenameInput);
      this.eventRegister.bind('closeSettings', this.closeSettings);
    },

    render: function(options) {
      var view = this;
      var tmpl = _(window.app.templates.app).template();
      var isJekyll = false;
      var errorPage = false;
      var hideInterface = false; // Flag for unauthenticated landing
      this.noMenu = false; // Prevents a mobile toggle from appearing when nto required.
      this.viewing = app.state.mode;

      if (options) {
        if (options.hideInterface) hideInterface = options.hideInterface;
        if (options.jekyll) isJekyll = options.jekyll;
        if (options.noMenu) this.noMenu = options.noMenu;
        if (options.error) errorPage = options.error;
      }

      if (hideInterface) {
        $(this.el).toggleClass('disable-interface', true);
      } else {
        $(this.el).toggleClass('disable-interface', false);
      }

      $(this.el).empty().append(tmpl(_.extend(this.model, app.state, {
        jekyll: isJekyll,
        error: errorPage,
        noMenu: view.noMenu,
        lang: (app.state.file) ? _.mode(app.state.file) : undefined
      })));

      // When the sidebar should be open.
      // Fix this in re-factor, could be much tighter
      if (app.state.mode === 'tree' ||
          app.state.mode === '' && window.authenticated && app.state.user) {
        $('#prose').toggleClass('open', true);
        $('#prose').toggleClass('mobile', false);
      } else {
        $('#prose').toggleClass('open mobile', false);
      }

      return this;
    },

    toggleMobileClass: function(e) {
      $(e.target).toggleClass('active');
      $(this.el).toggleClass('mobile');
      return false;
    },

    documentTitle: function(title) {
      document.title = title + ' · Prose';
    },

    headerContext: function(data, alterable) {
      var heading = _(window.app.templates.heading).template();

      if (data.writable) this.writable = true;
      if (data.lang) this.lang = data.lang;
      if (data.metadata) this.metadata = data.metadata;

      $('#heading').empty().append(heading(_.extend(data, {
        alterable: alterable ? true : false
      })));
    },

    filenameInput: function() {
      $('.filepath', this.el).focus();
    },

    sidebarContext: function(data) {
      if (app.state.mode === 'tree') {
        var tmpl = _(app.templates.sidebarProject).template();

        // Branch Switching
        _.defer(function() {
          $('.chzn-select', this.el).chosen().change(function() {
              router.navigate($(this).val(), true);
          });
        });

        $('#drawer', this.el)
          .empty()
          .append(tmpl(data));
      }
    },

    recentFiles: function(data) {
      var sidebarTmpl = _(window.app.templates.recentFiles).template();
      $('#drawer', this.el).empty().append(sidebarTmpl(data));
    },

    // Event Triggering to other files
    edit: function(e) {
      this.viewing = 'edit';
      this.eventRegister.trigger('edit', e);
      return false;
    },

    preview: function(e) {
      this.viewing = 'preview';

      if ($(e.target).data('jekyll')) {
        this.eventRegister.trigger('preview', e);
      } else {
        this.eventRegister.trigger('preview', e);
        // Cancel propagation
        return false;
      }
    },

    // Event Triggering to other files
    meta: function(e) {
      this.viewing = 'meta';
      this.eventRegister.trigger('meta', e);
      return false;
    },

    settings: function(e) {
      var tmpl = _(app.templates.settings).template();
      var $navItems = $('.navigation a', this.el);

      if ($(e.target, this.el).hasClass('active')) {
        this.cancel();
      } else {
        $navItems.removeClass('active');
        $(e.target, this.el).addClass('active');

        $('#drawer', this.el)
          .empty()
          .append(tmpl({
            lang: this.lang,
            writable: this.writable,
            metadata: this.metadata,
            jekyll: this.model.jekyll,
            draft: (app.state.path.split('/')[0] === '_drafts') ? true : false
          }));

        $('#prose').toggleClass('open mobile', true);
      }

      return false;
    },

    closeSettings: function() {
      $('.post-views a', this.el).removeClass('active');

      if (app.state.mode === 'blob') {
        $('.post-views .preview', this.el).addClass('active');
      } else {
        $('.post-views .edit', this.el).addClass('active');
      }

      $('#prose').toggleClass('open mobile', false);
    },

    restoreFile: function(e) {
      var $target = $(e.currentTarget);
      var $overlay = $(e.currentTarget).find('.overlay');
      var path = $target.data('path');

      // Spinning icon
      var message = ' Restoring ' + path;
      $overlay.html(message);

      app.models.restoreFile(app.state.user, app.state.repo, app.state.branch, path, app.state.history.commits[path][0].url, function(err) {
        if (err) {
          message = ' Error Try again in 30 Seconds';
          $overlay.html(message);
        } else {
          message = ' Restored ' + path;
          $overlay.html(message);
          $overlay.removeClass('removed').addClass('restored');

          // Update the listing anchor link
          $target
            .removeClass('removed')
            .attr('title', 'Restored ' + path)
            .addClass('added');

          // Update the anchor listing icon
          $target.find('.removed')
            .removeClass('removed')
            .addClass('added');
        }
      });

      return false;
    },

    deleteFile: function(e) {
      this.eventRegister.trigger('deleteFile', e);
      return false;
    },

    translate: function(e) {
      this.eventRegister.trigger('translate', e);
      return false;
    },

    draft: function(e) {
      this.eventRegister.trigger('draft', e);
      return false;
    },

    save: function(e) {
      var tmpl = _(app.templates.sidebarSave).template();
      this.eventRegister.trigger('save', e);

      if ($(e.target, this.el).hasClass('active')) {
        this.cancel();
      } else {
        this.cancel();
        $('.navigation a', this.el).removeClass('active');
        $(e.target, this.el).addClass('active');

        $('#drawer', this.el)
          .empty()
          .append(tmpl({
            writable: this.writable
        }));

        $('#prose').toggleClass('open mobile', true);

        var $message = $('.commit-message', this.el);
        var filepath = $('input.filepath').val();
        var filename = _.extractFilename(filepath)[1];
        var placeholder = 'Updated ' + filename;
        if (app.state.mode === 'new') placeholder = 'Created ' + filename;
        $message.attr('placeholder', placeholder).focus();
      }

      return false;
    },

    cancel: function(e) {
      $('.navigation a', this.el).removeClass('active');
      $('.navigation .' + this.viewing, this.el).addClass('active');
      $('#prose').toggleClass('open mobile', false);
      this.eventRegister.trigger('cancelSave', e);
      return false;
    },

    updateFile: function(e) {
      this.eventRegister.trigger('updateFile', e);
      return false;
    },

    saveFilePath: function(e) {
      // Trigger updateFile when a return button has been pressed.
      if (e.which === 13) this.eventRegister.trigger('updateFile', e);
    },

    checkPlaceholder: function(e) {
      if (app.state.mode === 'new') {
        var $target = $(e.target, this.el);
        if (!$target.val()) {
          $target.val($target.attr('placeholder'));
        }
      }
    },

    logout: function() {
      app.models.logout();
      window.location.reload();
      return false;
    },

    updateSaveState: function(label, classes, kill) {
      var view = this;

      // Cancel if this condition is met
      if (classes === 'save' && $(this.el).hasClass('saving')) return;
      $('.button.save', this.el).html(label);

      // Pass a popover span to the avatar icon
      $('#heading', this.el).find('.popup').html(label);
      $('.action').find('.popup').html(label);

      $(this.el)
        .removeClass('error saving saved save')
        .addClass(classes);

      if (kill) {
        _.delay(function() {
          $(view.el).removeClass(classes);
        }, 1000);
      }
    },

    remove: function() {
      // Unbind pagehide event handler when View is removed
      this.eventRegister.unbind('documentTitle', this.documentTitle);
      this.eventRegister.unbind('sidebarContext', this.sidebarContext);
      this.eventRegister.unbind('headerContext', this.headerContext);
      this.eventRegister.unbind('recentFiles', this.recentFiles);
      this.eventRegister.unbind('updateSaveState', this.updateSaveState);
      this.eventRegister.unbind('filenameInput', this.filenameInput);
      this.eventRegister.unbind('closeSettings', this.closeSettings);
      Backbone.View.prototype.remove.call(this);
    }
});
<commit_msg>No need to keep track of state in this way<commit_after>var $ = require('jquery-browserify');
var _ = require('underscore');
var Backbone = require('backbone');
var utils = require('.././util');

module.exports = Backbone.View.extend({
    className: 'application',

    events: {
      'click .post-views .edit': 'edit',
      'click .post-views .preview': 'preview',
      'click .post-views .settings': 'settings',
      'click .post-views .meta': 'meta',
      'click .logout': 'logout',
      'click a.item.removed': 'restoreFile',
      'click a.save': 'save',
      'click a.cancel': 'cancel',
      'click a.confirm': 'updateFile',
      'click a.delete': 'deleteFile',
      'click a.translate': 'translate',
      'click a.draft': 'draft',
      'click .mobile-menu .toggle': 'toggleMobileClass',
      'focus input.filepath': 'checkPlaceholder',
      'keypress input.filepath': 'saveFilePath'
    },

    initialize: function(options) {

      // Key Binding support accross the application.
      if (!window.shortcutsRegistered) {
        key('j, k, enter, o, ctrl+s', _.bind(function(e, handler) {
          if (!app.state.mode || app.state.mode === 'tree') {
            // We are in any navigation view
            if (handler.key === 'j' || handler.key === 'k') {
              utils.pageListing(handler.key);
            } else {
              utils.goToFile();
            }
          } else {
            // We are in state of the application
            // where we can edit a file
            if (handler.key === 'ctrl+s') {
              this.updateFile();
            }
          }
        }, this));

        window.shortcutsRegistered = true;
      }

      app.state = {
        user: '',
        repo: '',
        mode: '',
        branch: '',
        path: ''
      };

      this.eventRegister = app.eventRegister;

      _.bindAll(this, 'documentTitle', 'headerContext', 'sidebarContext', 'recentFiles', 'updateSaveState', 'closeSettings', 'filenameInput');
      this.eventRegister.bind('documentTitle', this.documentTitle);
      this.eventRegister.bind('headerContext', this.headerContext);
      this.eventRegister.bind('sidebarContext', this.sidebarContext);
      this.eventRegister.bind('recentFiles', this.recentFiles);
      this.eventRegister.bind('updateSaveState', this.updateSaveState);
      this.eventRegister.bind('filenameInput', this.filenameInput);
      this.eventRegister.bind('closeSettings', this.closeSettings);
    },

    render: function(options) {
      var view = this;
      var tmpl = _(window.app.templates.app).template();
      var isJekyll = false;
      var errorPage = false;
      var hideInterface = false; // Flag for unauthenticated landing
      this.noMenu = false; // Prevents a mobile toggle from appearing when nto required.

      if (options) {
        if (options.hideInterface) hideInterface = options.hideInterface;
        if (options.jekyll) isJekyll = options.jekyll;
        if (options.noMenu) this.noMenu = options.noMenu;
        if (options.error) errorPage = options.error;
      }

      if (hideInterface) {
        $(this.el).toggleClass('disable-interface', true);
      } else {
        $(this.el).toggleClass('disable-interface', false);
      }

      $(this.el).empty().append(tmpl(_.extend(this.model, app.state, {
        jekyll: isJekyll,
        error: errorPage,
        noMenu: view.noMenu,
        lang: (app.state.file) ? _.mode(app.state.file) : undefined
      })));

      // When the sidebar should be open.
      // Fix this in re-factor, could be much tighter
      if (app.state.mode === 'tree' ||
          app.state.mode === '' && window.authenticated && app.state.user) {
        $('#prose').toggleClass('open', true);
        $('#prose').toggleClass('mobile', false);
      } else {
        $('#prose').toggleClass('open mobile', false);
      }

      return this;
    },

    toggleMobileClass: function(e) {
      $(e.target).toggleClass('active');
      $(this.el).toggleClass('mobile');
      return false;
    },

    documentTitle: function(title) {
      document.title = title + ' · Prose';
    },

    headerContext: function(data, alterable) {
      var heading = _(window.app.templates.heading).template();

      if (data.writable) this.writable = true;
      if (data.lang) this.lang = data.lang;
      if (data.metadata) this.metadata = data.metadata;

      $('#heading').empty().append(heading(_.extend(data, {
        alterable: alterable ? true : false
      })));
    },

    filenameInput: function() {
      $('.filepath', this.el).focus();
    },

    sidebarContext: function(data) {
      if (app.state.mode === 'tree') {
        var tmpl = _(app.templates.sidebarProject).template();

        // Branch Switching
        _.defer(function() {
          $('.chzn-select', this.el).chosen().change(function() {
              router.navigate($(this).val(), true);
          });
        });

        $('#drawer', this.el)
          .empty()
          .append(tmpl(data));
      }
    },

    recentFiles: function(data) {
      var sidebarTmpl = _(window.app.templates.recentFiles).template();
      $('#drawer', this.el).empty().append(sidebarTmpl(data));
    },

    // Event Triggering to other files
    edit: function(e) {
      this.eventRegister.trigger('edit', e);
      return false;
    },

    preview: function(e) {
      if ($(e.target).data('jekyll')) {
        this.eventRegister.trigger('preview', e);
      } else {
        this.eventRegister.trigger('preview', e);
        // Cancel propagation
        return false;
      }
    },

    // Event Triggering to other files
    meta: function(e) {
      this.eventRegister.trigger('meta', e);
      return false;
    },

    settings: function(e) {
      var tmpl = _(app.templates.settings).template();
      var $navItems = $('.navigation a', this.el);

      if ($(e.target, this.el).hasClass('active')) {
        this.cancel();
      } else {
        $navItems.removeClass('active');
        $(e.target, this.el).addClass('active');

        $('#drawer', this.el)
          .empty()
          .append(tmpl({
            lang: this.lang,
            writable: this.writable,
            metadata: this.metadata,
            jekyll: this.model.jekyll,
            draft: (app.state.path.split('/')[0] === '_drafts') ? true : false
          }));

        $('#prose').toggleClass('open mobile', true);
      }

      return false;
    },

    closeSettings: function() {
      $('.post-views a', this.el).removeClass('active');

      if (app.state.mode === 'blob') {
        $('.post-views .preview', this.el).addClass('active');
      } else {
        $('.post-views .edit', this.el).addClass('active');
      }

      $('#prose').toggleClass('open mobile', false);
    },

    restoreFile: function(e) {
      var $target = $(e.currentTarget);
      var $overlay = $(e.currentTarget).find('.overlay');
      var path = $target.data('path');

      // Spinning icon
      var message = ' Restoring ' + path;
      $overlay.html(message);

      app.models.restoreFile(app.state.user, app.state.repo, app.state.branch, path, app.state.history.commits[path][0].url, function(err) {
        if (err) {
          message = ' Error Try again in 30 Seconds';
          $overlay.html(message);
        } else {
          message = ' Restored ' + path;
          $overlay.html(message);
          $overlay.removeClass('removed').addClass('restored');

          // Update the listing anchor link
          $target
            .removeClass('removed')
            .attr('title', 'Restored ' + path)
            .addClass('added');

          // Update the anchor listing icon
          $target.find('.removed')
            .removeClass('removed')
            .addClass('added');
        }
      });

      return false;
    },

    deleteFile: function(e) {
      this.eventRegister.trigger('deleteFile', e);
      return false;
    },

    translate: function(e) {
      this.eventRegister.trigger('translate', e);
      return false;
    },

    draft: function(e) {
      this.eventRegister.trigger('draft', e);
      return false;
    },

    save: function(e) {
      var tmpl = _(app.templates.sidebarSave).template();
      this.eventRegister.trigger('save', e);

      if ($(e.target, this.el).hasClass('active')) {
        this.cancel();
      } else {
        this.cancel();
        $('.navigation a', this.el).removeClass('active');
        $(e.target, this.el).addClass('active');

        $('#drawer', this.el)
          .empty()
          .append(tmpl({
            writable: this.writable
        }));

        $('#prose').toggleClass('open mobile', true);

        var $message = $('.commit-message', this.el);
        var filepath = $('input.filepath').val();
        var filename = _.extractFilename(filepath)[1];
        var placeholder = 'Updated ' + filename;
        if (app.state.mode === 'new') placeholder = 'Created ' + filename;
        $message.attr('placeholder', placeholder).focus();
      }

      return false;
    },

    cancel: function(e) {
      $('.navigation a', this.el).removeClass('active');
      $('.navigation .' + app.state.mode, this.el).addClass('active');
      $('#prose').toggleClass('open mobile', false);
      this.eventRegister.trigger('cancelSave', e);
      return false;
    },

    updateFile: function(e) {
      this.eventRegister.trigger('updateFile', e);
      return false;
    },

    saveFilePath: function(e) {
      // Trigger updateFile when a return button has been pressed.
      if (e.which === 13) this.eventRegister.trigger('updateFile', e);
    },

    checkPlaceholder: function(e) {
      if (app.state.mode === 'new') {
        var $target = $(e.target, this.el);
        if (!$target.val()) {
          $target.val($target.attr('placeholder'));
        }
      }
    },

    logout: function() {
      app.models.logout();
      window.location.reload();
      return false;
    },

    updateSaveState: function(label, classes, kill) {
      var view = this;

      // Cancel if this condition is met
      if (classes === 'save' && $(this.el).hasClass('saving')) return;
      $('.button.save', this.el).html(label);

      // Pass a popover span to the avatar icon
      $('#heading', this.el).find('.popup').html(label);
      $('.action').find('.popup').html(label);

      $(this.el)
        .removeClass('error saving saved save')
        .addClass(classes);

      if (kill) {
        _.delay(function() {
          $(view.el).removeClass(classes);
        }, 1000);
      }
    },

    remove: function() {
      // Unbind pagehide event handler when View is removed
      this.eventRegister.unbind('documentTitle', this.documentTitle);
      this.eventRegister.unbind('sidebarContext', this.sidebarContext);
      this.eventRegister.unbind('headerContext', this.headerContext);
      this.eventRegister.unbind('recentFiles', this.recentFiles);
      this.eventRegister.unbind('updateSaveState', this.updateSaveState);
      this.eventRegister.unbind('filenameInput', this.filenameInput);
      this.eventRegister.unbind('closeSettings', this.closeSettings);
      Backbone.View.prototype.remove.call(this);
    }
});
<|endoftext|>


<commit_before>var project

function Project(name, place, config, docs) {
  this.name = name
  this.docs = Object.create(null)
  this.curDoc = null
  this.tabs = place.appendChild(document.createElement("ul"))
  this.tabs.className = "tabs"
  var self = this
  CodeMirror.on(this.tabs, "click", function(e) {
    var target = e.target || e.srcElement
    if (target.nodeName.toLowerCase() != "li") return
    var doc = self.findDoc(target.textContent)
    if (doc) self.selectDoc(doc)
  })

  var myConf = {
    switchToDoc: function(name) { self.selectDoc(self.findDoc(name)) },
    useWorker: false
  }
  for (var prop in config) myConf[prop] = config[prop]
  var server = this.server = new CodeMirror.TernServer(myConf)

  var firstDoc
  for (var name in docs) {
    var data = this.registerDoc(name, new CodeMirror.Doc(docs[name], "javascript"))
    if (!firstDoc) firstDoc = data
  }
  this.setSelectedTab(firstDoc)

  var keyMap = {
    "Ctrl-I": function(cm) { server.showType(cm); },
    "Ctrl-O": function(cm) { server.showDocs(cm); },
    "Ctrl-Space": function(cm) { server.complete(cm); },
    "Alt-.": function(cm) { server.jumpToDef(cm); },
    "Alt-,": function(cm) { server.jumpBack(cm); },
    "Ctrl-Q": function(cm) { server.rename(cm); }
  }
  this.editor = new CodeMirror(place, {
    lineNumbers: true,
    extraKeys: keyMap,
    matchBrackets: true,
    value: firstDoc.doc
  })
  this.editor.on("cursorActivity", function(cm) { server.updateArgHints(cm); })
}

Project.prototype = {
  findDoc: function(name) { return this.docs[name] },

  registerDoc: function(name, doc) {
    this.server.addDoc(name, doc)
    var data = this.docs[name] = {
      name: name,
      doc: doc,
      tab: this.tabs.appendChild(document.createElement("li"))
    }
    data.tab.textContent = name
    return data
  },

  unregisterDoc: function(doc) {
    this.server.delDoc(doc.name)
    delete this.docs[doc.name]
    this.tabs.removeChild(doc.tab)
    if (this.curDoc == doc) for (var n in this.docs) return this.selectDoc(this.docs[n])
  },

  setSelectedTab: function(doc) {
    for (var n = this.tabs.firstChild; n; n = n.nextSibling)
      n.className = n.textContent == doc.name ? "selected" : ""
  },

  selectDoc: function(doc) {
    if (doc == this.curDoc) return
    this.server.hideDoc(this.curDoc)
    this.setSelectedTab(doc)
    this.curDoc = doc
    this.editor.swapDoc(doc.doc)
  }
}

// Initialization

function load(file, c) {
  var xhr = new XMLHttpRequest();
  xhr.open("get", file, true);
  xhr.send();
  xhr.onreadystatechange = function() {
    if (xhr.readyState == 4) c(xhr.responseText, xhr.status);
  };
}

CodeMirror.on(window, "load", function() {
  var cmds = document.getElementById("commands")
  CodeMirror.on(cmds, "change", function() {
    if (!project || cmds.selectedIndex == 0) return
    var found = commands[cmds.value]
    cmds.selectedIndex = 0
    project.editor.focus()
    if (found) found()
  })

  var projects = document.getElementById("projects")
  var projectNames = []
  var projectTags = document.querySelectorAll("project")
  for (var i = 0; i < projectTags.length; i++) {
    var opt = projects.appendChild(document.createElement("option"))
    projectNames.push(opt.textContent = projectTags[i].id)
  }
  CodeMirror.on(projects, "change", function() {
    if (projects.selectedIndex != 0)
      initProject(projects.value, function() {
        projects.selectedIndex = 0
        project.editor.focus()
      })
  })
  function updateFromHash() {
    var name = location.hash.slice(1)
    if (projectNames.indexOf(name) > -1 &&
        (!project || project.name != name)) {
      initProject(name)
      return true
    }
  }
  CodeMirror.on(window, "hashchange", updateFromHash)

  updateFromHash() || initProject("simple")
})

function loadFiles(project, c) {
  var found = {}
  function inner(node) {
    while (node && node.nodeName != "PRE") node = node.nextSibling
    if (!node) return c(found)
    if (node.hasAttribute("file")) {
      load(node.getAttribute("file"), function(data) {
        found[node.id] = data
        inner(node.nextSibling)
      })
    } else {
      found[node.id] = node.textContent
      inner(node.nextSibling)
    }
  }
  inner(project.firstChild)
}

var defsLoaded = Object.create(null)
function loadDefs(defs, c) {
  var result = [], loaded = 0
  for (var i = 0; i < defs.length; ++i) (function(i) {
    var name = defs[i]
    if (defsLoaded[name]) {
      result[i] = defsLoaded[name]
      if (++loaded == defs.length) c(result)
    } else {
      load("../../defs/" + name + ".json", function(json) {
        defsLoaded[name] = result[i] = JSON.parse(json)
        if (++loaded == defs.length) c(result)
      })
    }
  })(i)
}

function words(str) {
  return str ? str.split(" ") : []
}

function initProject(name, c) {
  var node = document.getElementById(name)
  loadDefs(words(node.getAttribute("data-libs")), function(defs) {
    var plugins = {}
    words(node.getAttribute("data-plugins")).forEach(function(name) { plugins[name] = true })

    if (plugins.requirejs) plugins.requirejs = {override: {"jquery": "=$"}}

    loadFiles(node, function(files) {
      var place = document.getElementById("place")
      place.textContent = ""

      if (project) project.server.destroy()

      project = new Project(name, place, {
        defs: defs,
        plugins: plugins
      }, files)
      location.hash = "#" + name
      c && c()
    })
  })
}

var commands = {
  complete: function() { project.server.complete(project.editor) },
  jumptodef: function() { project.server.jumpToDef(project.editor) },
  finddocs: function() { project.server.showDocs(project.editor) },
  findtype: function() { project.server.showType(project.editor) },
  rename: function() { project.server.rename(project.editor) },
  addfile: function() {
    var name = prompt("Name of the new buffer", "")
    if (name == null) return
    if (!name) name = "test"
    var i = 0
    while (project.findDoc(name + (i || ""))) ++i
    project.selectDoc(project.registerDoc(name + (i || ""), new CodeMirror.Doc("", "javascript")))
  },
  delfile: function() {
    var hasDoc = false
    for (var _ in project.docs) { hasDoc = true; break }
    if (hasDoc) project.unregisterDoc(project.curDoc)
  }
}
<commit_msg>[demo] Fix initialization of project.curDoc<commit_after>var project

function Project(name, place, config, docs) {
  this.name = name
  this.docs = Object.create(null)
  this.tabs = place.appendChild(document.createElement("ul"))
  this.tabs.className = "tabs"
  var self = this
  CodeMirror.on(this.tabs, "click", function(e) {
    var target = e.target || e.srcElement
    if (target.nodeName.toLowerCase() != "li") return
    var doc = self.findDoc(target.textContent)
    if (doc) self.selectDoc(doc)
  })

  var myConf = {
    switchToDoc: function(name) { self.selectDoc(self.findDoc(name)) },
    useWorker: false
  }
  for (var prop in config) myConf[prop] = config[prop]
  var server = this.server = new CodeMirror.TernServer(myConf)

  var firstDoc
  for (var name in docs) {
    var data = this.registerDoc(name, new CodeMirror.Doc(docs[name], "javascript"))
    if (!firstDoc) firstDoc = data
  }
  this.curDoc = firstDoc
  this.setSelectedTab(firstDoc)

  var keyMap = {
    "Ctrl-I": function(cm) { server.showType(cm); },
    "Ctrl-O": function(cm) { server.showDocs(cm); },
    "Ctrl-Space": function(cm) { server.complete(cm); },
    "Alt-.": function(cm) { server.jumpToDef(cm); },
    "Alt-,": function(cm) { server.jumpBack(cm); },
    "Ctrl-Q": function(cm) { server.rename(cm); }
  }
  this.editor = new CodeMirror(place, {
    lineNumbers: true,
    extraKeys: keyMap,
    matchBrackets: true,
    value: firstDoc.doc
  })
  this.editor.on("cursorActivity", function(cm) { server.updateArgHints(cm); })
}

Project.prototype = {
  findDoc: function(name) { return this.docs[name] },

  registerDoc: function(name, doc) {
    this.server.addDoc(name, doc)
    var data = this.docs[name] = {
      name: name,
      doc: doc,
      tab: this.tabs.appendChild(document.createElement("li"))
    }
    data.tab.textContent = name
    return data
  },

  unregisterDoc: function(doc) {
    this.server.delDoc(doc.name)
    delete this.docs[doc.name]
    this.tabs.removeChild(doc.tab)
    if (this.curDoc == doc) for (var n in this.docs) return this.selectDoc(this.docs[n])
  },

  setSelectedTab: function(doc) {
    for (var n = this.tabs.firstChild; n; n = n.nextSibling)
      n.className = n.textContent == doc.name ? "selected" : ""
  },

  selectDoc: function(doc) {
    if (doc == this.curDoc) return
    this.server.hideDoc(this.curDoc)
    this.setSelectedTab(doc)
    this.curDoc = doc
    this.editor.swapDoc(doc.doc)
  }
}

// Initialization

function load(file, c) {
  var xhr = new XMLHttpRequest();
  xhr.open("get", file, true);
  xhr.send();
  xhr.onreadystatechange = function() {
    if (xhr.readyState == 4) c(xhr.responseText, xhr.status);
  };
}

CodeMirror.on(window, "load", function() {
  var cmds = document.getElementById("commands")
  CodeMirror.on(cmds, "change", function() {
    if (!project || cmds.selectedIndex == 0) return
    var found = commands[cmds.value]
    cmds.selectedIndex = 0
    project.editor.focus()
    if (found) found()
  })

  var projects = document.getElementById("projects")
  var projectNames = []
  var projectTags = document.querySelectorAll("project")
  for (var i = 0; i < projectTags.length; i++) {
    var opt = projects.appendChild(document.createElement("option"))
    projectNames.push(opt.textContent = projectTags[i].id)
  }
  CodeMirror.on(projects, "change", function() {
    if (projects.selectedIndex != 0)
      initProject(projects.value, function() {
        projects.selectedIndex = 0
        project.editor.focus()
      })
  })
  function updateFromHash() {
    var name = location.hash.slice(1)
    if (projectNames.indexOf(name) > -1 &&
        (!project || project.name != name)) {
      initProject(name)
      return true
    }
  }
  CodeMirror.on(window, "hashchange", updateFromHash)

  updateFromHash() || initProject("simple")
})

function loadFiles(project, c) {
  var found = {}
  function inner(node) {
    while (node && node.nodeName != "PRE") node = node.nextSibling
    if (!node) return c(found)
    if (node.hasAttribute("file")) {
      load(node.getAttribute("file"), function(data) {
        found[node.id] = data
        inner(node.nextSibling)
      })
    } else {
      found[node.id] = node.textContent
      inner(node.nextSibling)
    }
  }
  inner(project.firstChild)
}

var defsLoaded = Object.create(null)
function loadDefs(defs, c) {
  var result = [], loaded = 0
  for (var i = 0; i < defs.length; ++i) (function(i) {
    var name = defs[i]
    if (defsLoaded[name]) {
      result[i] = defsLoaded[name]
      if (++loaded == defs.length) c(result)
    } else {
      load("../../defs/" + name + ".json", function(json) {
        defsLoaded[name] = result[i] = JSON.parse(json)
        if (++loaded == defs.length) c(result)
      })
    }
  })(i)
}

function words(str) {
  return str ? str.split(" ") : []
}

function initProject(name, c) {
  var node = document.getElementById(name)
  loadDefs(words(node.getAttribute("data-libs")), function(defs) {
    var plugins = {}
    words(node.getAttribute("data-plugins")).forEach(function(name) { plugins[name] = true })

    if (plugins.requirejs) plugins.requirejs = {override: {"jquery": "=$"}}

    loadFiles(node, function(files) {
      var place = document.getElementById("place")
      place.textContent = ""

      if (project) project.server.destroy()

      project = new Project(name, place, {
        defs: defs,
        plugins: plugins
      }, files)
      location.hash = "#" + name
      c && c()
    })
  })
}

var commands = {
  complete: function() { project.server.complete(project.editor) },
  jumptodef: function() { project.server.jumpToDef(project.editor) },
  finddocs: function() { project.server.showDocs(project.editor) },
  findtype: function() { project.server.showType(project.editor) },
  rename: function() { project.server.rename(project.editor) },
  addfile: function() {
    var name = prompt("Name of the new buffer", "")
    if (name == null) return
    if (!name) name = "test"
    var i = 0
    while (project.findDoc(name + (i || ""))) ++i
    project.selectDoc(project.registerDoc(name + (i || ""), new CodeMirror.Doc("", "javascript")))
  },
  delfile: function() {
    var hasDoc = false
    for (var _ in project.docs) { hasDoc = true; break }
    if (hasDoc) project.unregisterDoc(project.curDoc)
  }
}
<|endoftext|>


<commit_before>// Generated by CoffeeScript 1.7.1
(function() {
  var __hasProp = {}.hasOwnProperty,
    __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };

  define(['backbone', 'underscore'], function(Backbone, _) {
    var Collection;
    return Collection = (function(_super) {
      __extends(Collection, _super);

      function Collection(models, options) {
        if (options == null) {
          options = {};
        }
        this.options = options;
        this.params = {};
        this.url = options.url;
        if (this.options.pageable) {
          this.options.pagination = {
            page: options.page || 1,
            pageSize: options.pageSize || 10,
            firstKey: options.firstKey || '_first',
            pageSizeKey: options.pageSizeKey || '_pageSize',
            recordCountKey: options.recordCountKey || 'recordCount'
          };
          this.turnPage = function(page, options) {
            if (page > this.options.pagination.pageCount || page < 1) {
              return;
            }
            this.options.pagination.page = page;
            return this.fetch(options);
          };
          this.nextPage = function(options) {
            var page;
            page = this.options.pagination.page + 1;
            return this.turnPage(page, options);
          };
          this.prevPage = function(options) {
            var page;
            page = this.options.pagination.page - 1;
            return this.turnPage(page, options);
          };
          this.getPageInfo = function() {
            var d, p;
            p = this.options.pagination;
            if (!p) {
              return {};
            }
            d = {
              start: (p.page - 1) * p.pageSize + 1,
              end: p.page * p.pageSize,
              total: p.recordCount
            };
            d.end = d.end > d.total ? d.total : d.end;
            return d;
          };
        }
        Collection.__super__.constructor.apply(this, arguments);
      }

      Collection.prototype.parse = function(resp, options) {
        var p;
        if (this.options.pageable) {
          p = this.options.pagination;
          p.recordCount = resp[p.recordCountKey];
          p.pageCount = Math.ceil(p.recordCount / p.pageSize);
        }
        if (this.options.root) {
          return resp[this.options.root];
        } else {
          return resp;
        }
      };

      Collection.prototype.fetch = function(options) {
        var data, p;
        data = _.extend({}, this.options.params, this.params);
        if (this.options.pageable) {
          p = this.options.pagination;
          data[p.firstKey] = p.pageSize * (p.page - 1);
          data[p.pageSizeKey] = p.pageSize;
        }
        options || (options = {});
        data = _.extend(data, options.data);
        options.data = data;
        return Collection.__super__.fetch.call(this, options);
      };

      return Collection;

    })(Backbone.Collection);
  });

}).call(this);
<commit_msg>compile to dist<commit_after>// Generated by CoffeeScript 1.7.1
(function() {
  var __hasProp = {}.hasOwnProperty,
    __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };

  define(['backbone', 'underscore'], function(Backbone, _) {
    var Collection;
    return Collection = (function(_super) {
      __extends(Collection, _super);

      function Collection(models, options) {
        if (options == null) {
          options = {};
        }
        this.options = options;
        this.params = {};
        this.url = options.url;
        if (this.options.pageable) {
          this.options.pagination = {
            page: options.page || 1,
            pageSize: options.pageSize || 10,
            pageKey: options.pageKey || '_page',
            pageSizeKey: options.pageSizeKey || '_pageSize',
            recordCountKey: options.recordCountKey || 'recordCount'
          };
          this.turnPage = function(page, options) {
            if (page > this.options.pagination.pageCount || page < 1) {
              return;
            }
            this.options.pagination.page = page;
            return this.fetch(options);
          };
          this.nextPage = function(options) {
            var page;
            page = this.options.pagination.page + 1;
            return this.turnPage(page, options);
          };
          this.prevPage = function(options) {
            var page;
            page = this.options.pagination.page - 1;
            return this.turnPage(page, options);
          };
          this.getPageInfo = function() {
            var d, p;
            p = this.options.pagination;
            if (!p) {
              return {};
            }
            d = {
              start: (p.page - 1) * p.pageSize + 1,
              end: p.page * p.pageSize,
              total: p.recordCount
            };
            d.end = d.end > d.total ? d.total : d.end;
            return d;
          };
        }
        Collection.__super__.constructor.apply(this, arguments);
      }

      Collection.prototype.parse = function(resp, options) {
        var p;
        if (this.options.pageable) {
          p = this.options.pagination;
          p.recordCount = resp[p.recordCountKey];
          p.pageCount = Math.ceil(p.recordCount / p.pageSize);
        }
        if (this.options.root) {
          return resp[this.options.root];
        } else {
          return resp;
        }
      };

      Collection.prototype.fetch = function(options) {
        var data, p;
        data = _.extend({}, this.options.params, this.params);
        if (this.options.pageable) {
          p = this.options.pagination;
          data[p.pageKey] = p.page;
          data[p.pageSizeKey] = p.pageSize;
        }
        options || (options = {});
        data = _.extend(data, options.data);
        options.data = data;
        return Collection.__super__.fetch.call(this, options);
      };

      return Collection;

    })(Backbone.Collection);
  });

}).call(this);
<|endoftext|>


<commit_before>/* JavaScript for DbFrontend List page
 * Script allows inline editing of the table contents
 * For any DbFrontend page using this javascript, check the following things to make if work properly:
 *  1. That the SiteDefs::OBJECT_TO_SCRIPT values for the page is 'Modal'
 *  2. Javascript classes are being added to the html elements properly
 *  3. The page which displays all the records return a single record if its 'id' is specified in GET params
 */

Ensembl.DbFrontendList = {

  // Row Class
  Row: Base.extend({
    constructor: function(row, panel, cellsOnly) {
      this.id = (row.className.match(/_dbf_row_([^\s]+)/) || []).pop();
      this.panel = panel;
      for (var i in this.panel.data) {
        if (this.panel.data[i]) {
          new Ensembl.DbFrontendList.Cell(row.cells[i], this.panel.data[i], this);
        }
        else if (row.cells[i].className.match(/_dbf_row_handle/) && !cellsOnly) {
          new Ensembl.DbFrontendList.HandleCell(row.cells[i], this);
        }
      }
    }
  }),

  // Cell Class - extended from js base class for DbFrontend
  Cell: Ensembl.DbFrontend.extend({

    // @override
    constructor: function(cell, data, row) {
      var self  = this;
      this.url  = data.url;
      this.name = data.name;
      this.row  = row;
      this.el   = $('
').html(cell.innerHTML).appendTo($(cell).empty()).append($('').click(function() {
        self.buttonClick(this);
      }));
    },

    // @override
    buttonClick: function(button) {
      this.initForm();
      this.makeRequest(button, this.form, {
        url:      this.url,
        data:     {id: this.row.id, _list: 1},
        success:  function(json) {
          var form   = $('form', this.getResponseNode(json));
          var field  = $('[name=' + this.name.replace('.', '\\.') + ']', form).first().parents('div').first().removeAttr('class');
          var button = $('[type=submit]', form).first().parents('div').first().attr('class', 'dbf-list-buttons');
          $('input', this.form.empty().append(form.empty().append(field, $('
').append(button)))).first().focus();
          this.validateForms(this.form);
        }
      });
      this.el.hide();
    },

    // @override
    showLoading: function(target, flag) {
      if (flag !== false) {
        target.empty().html('Loading…').show();
      }
    },

    // @override
    cancelButtonClick: function(button) {
      this.el.show('fast');
      this.form.hide('fast', function() {
        this.innerHTML = '';
      });
    },

    // @override
    formSubmit: function(form) {
      this.makeRequest(form, this.form, {
        data:    $(form).serialize() + '&id=' + this.row.id,
        success: function(json) {
          if (json.redirectURL) {
            var url = json.redirectURL;
            this.success = !url.match(/Problem/);
            if (this.success) {
              url = window.location.href.split('#')[0];
              url = url + (url.match(/\?/) ? '&' : '?') + 'id=' + this.row.id;
            }
            this.makeRequest({}, this.form, {
              async: false,
              url: url,
              success: function(json) {
                if (this.success) {
                  var resTr = $('._dbf_row_' + this.row.id, this.getResponseNode(json));
                  if (resTr.length) {
                    this.row.panel.updateRow(resTr.children().map(function(i, cell) {return cell.innerHTML;}), this.el.parents('tr')[0]);
                  }
                }
                else {
                  this.showError(this.form, this.getResponseNode(json).children('p').first().html(), this.el);
                }
                this.afterResponse(this.success);
              }
            });
          }
          else {
            this.showError(this.form, '', this.el);
          }
        }
      });
    }
  }),

  // HandleCell Class - cell that contains edit, delete and duplicate link
  HandleCell: Ensembl.DbFrontendRow.extend({

    // @override
    constructor: function(cell, row) {
      this.row = row;
      this.base(cell);
    },

    // @override
    createForm: function() {
      var tr = this.el[0].parentNode;
      return $('
').appendTo($('').appendTo($('
').append('').insertAfter(tr).attr('class', 'dbf-list-form')).attr('colspan', tr.cells.length - 1));
    },

    // @override
    buttonClick: function(button) {
      if (this.form) {
        this.form.parents('tr').first().remove();
        this.form = false;
      }
      this.base(button);
    },

    // @override
    cancelButtonClick: function(button) {
      var previous = $(button).parents('._dbf_form_wrap').prev()[0];
      if (previous) {
        $(previous).show().next().remove();
      }
      else {
        this.form.children().first().slideUp(function() {
          $(this).parents('tr').first().remove();
        });
      }
    },

    // @override
    deleteButtonClick: function(button) {
      this.makeRequest(button, this.form, {
        success: function(json) {
          if (json.redirectURL) {
            if (json.redirectURL.match(/Problem/)) {
              this.makeRequest({}, this.form, {
                async: false,
                url: json.redirectURL,
                success: function(json) {
                  this.form.html(this.getResponseNode(json).html());
                }
              });
            }
            else {
              this.form.parents('tr').first().remove();
              this.row.panel.deleteRow(this.el.parents('tr')[0]);
              for (var i in this) {
                delete this[i];
              }
            }
          }
        }
      });
    },

    // @override
    formSubmit: function(form) {
      this.action = form.className.match(/_dbf_add/) ? 'add' : 'edit';
      this.makeRequest(form, this.form.children(':last'), {
        success: function(json) {
          if (json.redirectURL) {
            var url = json.redirectURL;
            this.success = !url.match(/Problem/);
            if (this.success) {
              var id = (url.match(/(\?|&|;)id\=([0-9]+)/) || []).pop() || 0;
              if (id) {
                url = window.location.href.split('#')[0];
                url = url + (url.match(/\?/) ? '&' : '?') + 'id=' + id;
              }
            }
            this.makeRequest({}, this.form.children(':last'), {
              async   : false,
              url     : url,
              success : function(json) {
                if (this.success) {
                  var resTr = $('._dbf_row_' + id, this.getResponseNode(json));
                  if (resTr.length) {
                    this.row.panel[this.action === 'add' ? 'addRow' : 'updateRow'](resTr.children().map(function(i, cell) {return cell.innerHTML;}), this.el.parents('tr')[0], resTr[0].className);
                  }
                  this.afterResponse(this.success);
                  this.form.parents('tr').first().remove();
                }
                else {
                  this.showError(this.form, this.getResponseNode(json).children('p').first().html());
                }
              }
            });
          }
        }
      });
    }
  })
};

// DbFrontendList Panel class
Ensembl.Panel.DbFrontendList = Ensembl.Panel.extend({

  init: function() {
    var self = this;
    this.base();
    this.data = [];

    $('table._dbf_list', this.el).each(function() {
      $('thead th', this).each(function() {
        var inps = $('input[type=hidden]', this);
        self.data.push(inps.length ? {url: inps[0].value, name: inps[0].name, title: $(this).text(), type: inps[0].className} : false);
      });
      $('tbody tr', this).each(function () {
        self.initRow(this);
      });
      return false;
    });
  },

  // constructs a DbFrontendList.Row object from an already existing TR element
  initRow: function(row) {
    new Ensembl.DbFrontendList.Row(row, this);
  },

  // Adds a new TR element (after a given reference TR element) and initiates it as a DbFrontendList.Row object taking in account the DataTables settings
  addRow: function(data, refRow, classes) {
    var table = $(refRow).parents('table').first();
    var row   = false;
    if (table.hasClass('data_table')) {
      table = table.dataTable();
      row = table.fnGetNodes(table.fnAddData(data));
      $(row).addClass(classes.replace(/bg(1|2)/, ''));
    }
    else {
      row = $('
').attr('class', classes).append($.map(data, function(html) { return $('').html(html)[0]; })).insertAfter(refRow)[0];
      this.setRowBGs();
    }
    if (row) {
      $(row.cells).each(function(i) {this.className = refRow.cells[i].className});
      new Ensembl.DbFrontendList.Row(row, this);
      this.highlightRow(row);
    }
  },

  // Update an already existing TR element with given data and re-initiates it as DbFrontendList.Row taking in account the DataTables settings
  updateRow: function(data, row) {
    $(row.cells).each(function(i) {this.innerHTML = data[i]});
    var table = $(row).parents('table').first();
    if (table.hasClass('data_table')) {
      table.dataTable().fnUpdate($.map(row.cells, function(cell) {return cell.innerHTML; }), row);
    }
    new Ensembl.DbFrontendList.Row(row, this, true);
    this.highlightRow(row);
  },

  // Removes a TR element from the table, taking in account the DataTables settings
  deleteRow: function(row) {
    var table = $(row).parents('table').first();
    if (table.hasClass('data_table')) {
      table.dataTable().fnDeleteRow(row);
    }
    else {
      $(row).remove();
      this.setRowBGs();
    }
  },

  // Maintains the even-odd backgroundColor order of the TR elements
  setRowBGs: function() {
    var bg = ['bg1', 'bg2'];
    $('table._dbf_list', this.el).each(function() {
      $('tbody tr', this).each(function() {
        if (this.className.match(/_dbf_row_/)) {
          $(this).removeClass('bg1 bg2').addClass(bg[0]);
          bg = bg.reverse();
        }
      });
    });
  },

  // Highlights a row for 1 second
  highlightRow: function(row) {
    $(row).effect('highlight', {'color': '#ddddff'}, 1000);
  }
});<commit_msg>merging changes from 66 branch<commit_after>/* JavaScript for DbFrontend List page
 * Script allows inline editing of the table contents
 * For any DbFrontend page using this javascript, check the following things to make if work properly:
 *  1. That the SiteDefs::OBJECT_TO_SCRIPT values for the page is 'Modal'
 *  2. Javascript classes are being added to the html elements properly
 *  3. The page which displays all the records return a single record if its 'id' is specified in GET params
 */

Ensembl.DbFrontendList = {

  // Row Class
  Row: Base.extend({
    constructor: function(row, panel) {
      this.id = (row.className.match(/_dbf_row_([^\s]+)/) || []).pop();
      this.panel = panel;
      for (var i in this.panel.data) {
        if (this.panel.data[i]) {
          new Ensembl.DbFrontendList.Cell(row.cells[i], this.panel.data[i], this);
        }
        else if (row.cells[i].className.match(/_dbf_row_handle/)) {
          this.handleCell = new Ensembl.DbFrontendList.HandleCell(row.cells[i], this);
        }
      }
    }
  }),

  // Cell Class - extended from js base class for DbFrontend
  Cell: Ensembl.DbFrontend.extend({

    // @override
    constructor: function(cell, data, row) {
      var self  = this;
      this.url  = data.url;
      this.name = data.name;
      this.row  = row;
      this.el   = $('
').html(cell.innerHTML).appendTo($(cell).empty()).append($('').click(function() {
        self.buttonClick(this);
      }));
      $('._dbf_list_view', this.el).click(function(e) {
        e.preventDefault();
        var handle = self.row.handleCell;
        handle.initForm();
        handle.makeRequest(this, handle.form, {
          url:      this.href,
          data:     {},
          success:  function(json) {
            handle.getResponseNode(json)
              .children().wrapAll('
').end()
              .append($('Close'))
              .appendTo(handle.form.empty())
            ;
          }
        });
      });
    },

    // @override
    buttonClick: function(button) {
      this.initForm();
      this.makeRequest(button, this.form, {
        url:      this.url,
        data:     {id: this.row.id, _list: 1},
        success:  function(json) {
          var form   = $('form', this.getResponseNode(json));
          var field  = $('[name=' + this.name.replace('.', '\\.') + ']', form).first().parents('div').first().removeAttr('class');
          var button = $('[type=submit]', form).first().parents('div').first().attr('class', 'dbf-list-buttons');
          $('input', this.form.empty().append(form.empty().append(field, $('
').append(button)))).first().focus();
          this.validateForms(this.form);
        }
      });
      this.el.hide();
    },

    // @override
    showLoading: function(target, flag) {
      if (flag !== false) {
        target.empty().html('Loading…').show();
      }
    },

    // @override
    cancelButtonClick: function(button) {
      this.el.show('fast');
      this.form.hide('fast', function() {
        this.innerHTML = '';
      });
    },

    // @override
    formSubmit: function(form) {
      this.makeRequest(form, this.form, {
        data:    $(form).serialize() + '&id=' + this.row.id,
        success: function(json) {
          if (json.redirectURL) {
            var url = json.redirectURL;
            this.success = !url.match(/Problem/);
            if (this.success) {
              url = window.location.href.split('#')[0];
              url = url + (url.match(/\?/) ? '&' : '?') + 'id=' + this.row.id;
            }
            this.makeRequest({}, this.form, {
              async: false,
              url: url,
              success: function(json) {
                if (this.success) {
                  var resTr = $('._dbf_row_' + this.row.id, this.getResponseNode(json));
                  if (resTr.length) {
                    this.row.panel.updateRow(resTr.children().map(function(i, cell) {return cell.innerHTML;}), this.el.parents('tr')[0]);
                  }
                }
                else {
                  this.showError(this.form, this.getResponseNode(json).children('p').first().html(), this.el);
                }
                this.afterResponse(this.success);
              }
            });
          }
          else {
            this.showError(this.form, '', this.el);
          }
        }
      });
    }
  }),

  // HandleCell Class - cell that contains edit, delete and duplicate link
  HandleCell: Ensembl.DbFrontendRow.extend({

    // @override
    constructor: function(cell, row) {
      this.row = row;
      this.base(cell);
    },

    // @override
    createForm: function() {
      var tr = this.el[0].parentNode;
      return $('
').appendTo($('').appendTo($('
').insertAfter(tr).attr('class', 'dbf-list-form')).attr('colspan', tr.cells.length));
    },

    // @override
    buttonClick: function(button) {
      if (this.form) {
        this.form.parents('tr').first().remove();
        this.form = false;
      }
      this.base(button);
    },

    // @override
    cancelButtonClick: function(button) {
      var previous = $(button).parents('._dbf_form_wrap').prev()[0];
      if (previous) {
        $(previous).show().next().remove();
      }
      else {
        this.form.children().first().slideUp(function() {
          $(this).parents('tr').first().remove();
        });
        this.form = false;
      }
    },

    // @override
    deleteButtonClick: function(button) {
      this.makeRequest(button, this.form, {
        success: function(json) {
          if (json.redirectURL) {
            if (json.redirectURL.match(/Problem/)) {
              this.makeRequest({}, this.form, {
                async: false,
                url: json.redirectURL,
                success: function(json) {
                  this.form.html(this.getResponseNode(json).html());
                }
              });
            }
            else {
              this.form.parents('tr').first().remove();
              this.row.panel.deleteRow(this.el.parents('tr')[0]);
              for (var i in this) {
                delete this[i];
              }
            }
          }
        }
      });
    },

    // @override
    formSubmit: function(form) {
      this.action = form.className.match(/_dbf_add/) ? 'add' : 'edit';
      this.makeRequest(form, this.form.children(':last'), {
        success: function(json) {
          if (json.redirectURL) {
            var url = json.redirectURL;
            this.success = !url.match(/Problem/);
            if (this.success) {
              var id = (url.match(/(\?|&|;)id\=([0-9]+)/) || []).pop() || 0;
              if (id) {
                url = window.location.href.split('#')[0];
                url = url + (url.match(/\?/) ? '&' : '?') + 'id=' + id;
              }
            }
            this.makeRequest({}, this.form.children(':last'), {
              async   : false,
              url     : url,
              success : function(json) {
                if (this.success) {
                  var resTr = $('._dbf_row_' + id, this.getResponseNode(json));
                  if (resTr.length) {
                    this.row.panel[this.action === 'add' ? 'addRow' : 'updateRow'](resTr.children().map(function(i, cell) {return cell.innerHTML;}), this.el.parents('tr')[0], resTr[0].className);
                  }
                  this.afterResponse(this.success);
                  this.form.parents('tr').first().remove();
                }
                else {
                  this.showError(this.form, this.getResponseNode(json).children('p').first().html());
                }
              }
            });
          }
        }
      });
    }
  })
};

// DbFrontendList Panel class
Ensembl.Panel.DbFrontendList = Ensembl.Panel.extend({

  init: function() {
    var self = this;
    this.base();
    this.data = [];

    $('table._dbf_list', this.el).each(function() {
      $('thead th', this).each(function() {
        var inps = $('input[type=hidden]', this);
        self.data.push(inps.length ? {url: inps[0].value, name: inps[0].name, title: $(this).text(), type: inps[0].className} : false);
      });
      $('tbody tr', this).each(function () {
        self.initRow(this);
      });
      return false;
    });
  },

  // constructs a DbFrontendList.Row object from an already existing TR element
  initRow: function(row) {
    new Ensembl.DbFrontendList.Row(row, this);
  },

  // Adds a new TR element (after a given reference TR element) and initiates it as a DbFrontendList.Row object taking in account the DataTables settings
  addRow: function(data, refRow, classes) {
    var table = $(refRow).parents('table').first();
    var row   = false;
    if (table.hasClass('data_table')) {
      table = table.dataTable();
      row = table.fnGetNodes(table.fnAddData(data));
      $(row).addClass(classes.replace(/bg(1|2)/, ''));
    }
    else {
      row = $('
').attr('class', classes).append($.map(data, function(html) { return $('').html(html)[0]; })).insertAfter(refRow)[0];
      this.setRowBGs();
    }
    if (row) {
      $(row.cells).each(function(i) {this.className = refRow.cells[i].className});
      new Ensembl.DbFrontendList.Row(row, this);
      this.highlightRow(row);
    }
  },

  // Update an already existing TR element with given data and re-initiates it as DbFrontendList.Row taking in account the DataTables settings
  updateRow: function(data, row) {
    $(row.cells).each(function(i) {this.innerHTML = data[i]});
    var table = $(row).parents('table').first();
    if (table.hasClass('data_table')) {
      table.dataTable().fnUpdate($.map(row.cells, function(cell) {return cell.innerHTML; }), row);
    }
    new Ensembl.DbFrontendList.Row(row, this);
    this.highlightRow(row);
  },

  // Removes a TR element from the table, taking in account the DataTables settings
  deleteRow: function(row) {
    var table = $(row).parents('table').first();
    if (table.hasClass('data_table')) {
      table.dataTable().fnDeleteRow(row);
    }
    else {
      $(row).remove();
      this.setRowBGs();
    }
  },

  // Maintains the even-odd backgroundColor order of the TR elements
  setRowBGs: function() {
    var bg = ['bg1', 'bg2'];
    $('table._dbf_list', this.el).each(function() {
      $('tbody tr', this).each(function() {
        if (this.className.match(/_dbf_row_/)) {
          $(this).removeClass('bg1 bg2').addClass(bg[0]);
          bg = bg.reverse();
        }
      });
    });
  },

  // Highlights a row for 1 second
  highlightRow: function(row) {
    $(row).effect('highlight', {'color': '#ddddff'}, 1000);
  }
});<|endoftext|>


<commit_before>(function() {
'use strict';

angular.module('superdesk.editor2.ctrl', []).controller('SdTextEditorController', SdTextEditorController);

SdTextEditorController.$inject = ['lodash', 'EMBED_PROVIDERS', '$timeout', '$element', 'editor', 'config', '$q'];
function SdTextEditorController(_, EMBED_PROVIDERS, $timeout, $element, editor, config, $q) {
    var vm = this;
    function Block(attrs) {
        var self = this;
        angular.extend(self, {
            body: attrs && attrs.body || '',
            loading: attrs && attrs.loading || false,
            caption: attrs && attrs.caption || undefined,
            blockType: attrs && attrs.blockType || 'text',
            embedType: attrs && attrs.embedType || undefined,
            association: attrs && attrs.association || undefined,
            lowerAddEmbedIsExtended: undefined,
            showAndFocusLowerAddAnEmbedBox: function() {
                self.lowerAddEmbedIsExtended = true;
            },
        });
    }
    /**
    * For the given blocks, merge text blocks when there are following each other and add empty text block arround embeds if needed
    */
    function prepareBlocks(blocks) {
        var newBlocks = [];
        blocks.forEach(function(block, i) {
            // if not the first block and there is a text block following another one
            if (i > 0 && block.blockType === 'text' && blocks[i - 1].blockType === 'text') {
                // we merge the content with the previous block
                newBlocks[newBlocks.length - 1].body += block.body;
            } else {
                // otherwise we add the full block
                newBlocks.push(block);
            }
            // add emtpy text block if block is between 2 embed blocks or at the end
            if (blocks[i].blockType === 'embed') {
                if (i === blocks.length - 1 || blocks[i + 1].blockType === 'embed') {
                    newBlocks.push(new Block());
                }
            }
        });
        // add emtpy text block at the top if needed
        if (newBlocks[0].blockType === 'embed') {
            newBlocks.unshift(new Block());
        }
        return newBlocks;
    }
    function splitIntoBlock(bodyHtml) {
        var blocks = [], block;
        /**
        * push the current block into the blocks collection if it is not empty
        */
        function commitBlock() {
            if (block !== undefined && block.body.trim() !== '') {
                blocks.push(block);
                block = undefined;
            }
        }
        $('
' + bodyHtml + '
')
        .contents()
        .toArray()
        .forEach(function(element) {
            // if we get a
, we push the current block and create a new one
            // for the paragraph content
            if (element.nodeName === 'P') {
                commitBlock();
                if (angular.isDefined(element.innerHTML) && element.textContent !== '' && element.textContent !== '\n') {
                    blocks.push(new Block({body: element.outerHTML.trim()}));
                }
                // detect if it's an embed
            } else if (element.nodeName === '#comment') {
                if (element.nodeValue.indexOf('EMBED START') > -1) {
                    commitBlock();
                    // retrieve the embed type following the comment
                    var embedType;
                    var embedTypeRegex = /EMBED START ([\w-]+)/;
                    var match;
                    if ((match = embedTypeRegex.exec(angular.copy(element.nodeValue).trim())) !== null) {
                        embedType = match[1];
                    } else {
                        embedType = EMBED_PROVIDERS.custom;
                    }
                    // retrieve the association reference
                    var association;
                    var embedAssoKey = /{id: "(embedded\d+)"}/;
                    if ((match = embedAssoKey.exec(angular.copy(element.nodeValue).trim())) !== null) {
                        if (vm.associations) {
                            association = angular.copy(vm.associations[match[1]]);
                        }
                    }
                    // create the embed block
                    block = new Block({blockType: 'embed', embedType: embedType, association: association});
                }
                if (element.nodeValue.indexOf('EMBED END') > -1) {
                    commitBlock();
                }
                // if it's not a paragraph or an embed, we update the current block
            } else {
                if (block === undefined) {
                    block = new Block();
                }
                // we want the outerHTML (ex: 'text') or the node value for text and comment
                block.body += (element.outerHTML || element.nodeValue || '').trim();
            }
        });
        // at the end of the loop, we push the last current block
        if (block !== undefined && block.body.trim() !== '') {
            blocks.push(block);
        }
        // Complete embeds with metadata (from association datadata or html)
        blocks.forEach(function(block) {
            if (block.blockType === 'embed') {
                // for images that come from Superdesk, we use the association
                if (block.association && block.embedType === 'Image') {
                    block.caption = block.association.description_text;
                    var url;
                    // prefers "embed" for image url, otherwise "viewImage"
                    if (block.association.renditions.embed) {
                        url = block.association.renditions.embed.href;
                    } else {
                        url = block.association.renditions.viewImage.href;
                    }
                    block.body = '';
                    editor.generateMediaTag(block.association).then(function(img) {
                        block.body = img;
                    });
                } else {
                    // extract body and caption from embed block html
                    var original_body = angular.element(angular.copy(block.body));
                    if (original_body.get(0).nodeName === 'FIGURE') {
                        block.body = '';
                        original_body.contents().toArray().forEach(function(element) {
                            if (element.nodeName === 'FIGCAPTION') {
                                block.caption = element.innerHTML;
                            } else {
                                block.body += element.outerHTML || element.nodeValue || '';
                            }
                        });
                    }
                }
            }
        });
        // if no block, create an empty one to start
        if (blocks.length === 0) {
            blocks.push(new Block());
        }
        return blocks;
    }
    angular.extend(vm, {
        configuration: angular.extend({embeds: true}, config.editor || {}),
        blocks: [],
        initEditorWithOneBlock: function(model) {
            vm.model = model;
            vm.blocks = [new Block({body: model.$modelValue})];
        },
        initEditorWithMultipleBlock: function(model) {
            // save the model to update it later
            vm.model = model;
            // parse the given model and create blocks per paragraph and embed
            var content = model.$modelValue || '';
            // update the actual blocks value at the end to prevent more digest cycle as needed
            vm.blocks = splitIntoBlock(content);
            vm.renderBlocks();
        },
        serializeBlock: function(blocks) {
            blocks = angular.isDefined(blocks) ? blocks : vm.blocks;
            // in case blocks are not ready
            if (blocks.length === 0) {
                return '';
            }
            var new_body = '';
            if (vm.config.multiBlockEdition) {
                blocks.forEach(function(block) {
                    if (angular.isDefined(block.body) && block.body.trim() !== '') {
                        if (block.blockType === 'embed') {
                            var blockName = block.embedType.trim();
                            // add an id to the image in order to retrieve it in `assocations` field
                            if (block.association) {
                                blockName += ' {id: "embedded' + vm.generateBlockId(block) + '"}';
                            }
                            new_body += [
                                '\n',
                                '
',
                                block.body,
                                '
',
                                block.caption,
                                '
',
                                '
',
                                '\n\n'].join('');
                        } else {
                            new_body += block.body + '\n';
                        }
                    }
                });
            } else {
                new_body = (blocks.length > 0) ? blocks[0].body : '';
            }
            // strip
 and

            new_body = new_body.trim().replace(/

<\/p>$/, '');
            new_body = new_body.replace(/
$/, '');
            return new_body;
        },
        commitChanges: function() {
            var associations = angular.copy(vm.associations);
            // initialize associations if doesn't exist
            if (typeof associations !== 'object') {
                associations = {};
            }
            // remove older associations
            angular.forEach(associations, function(value, key) {
                if (_.startsWith(key, 'embedded')) {
                    associations[key] = null;
                }
            });
            // update associations with the ones stored in blocks
            vm.associations = angular.extend({}, associations, vm.getAssociations());
            // save model with latest state of blocks
            vm.model.$setViewValue(vm.serializeBlock());
        },
        /**
        * Return an object that contains the embedded images in the story
        */
        getAssociations: function() {
            var association = {};
            vm.blocks.forEach(function(block) {
                // we keep the association only for Superdesk images
                if (block.association) {
                    // add the association
                    association['embedded' + vm.generateBlockId(block)] = angular.copy(block.association);
                }
            });
            return association;
        },
        getBlockPosition: function(block) {
            return _.indexOf(vm.blocks, block);
        },
        splitAndInsert: function(textBlockCtrl, blocksToInsert) {
            // index where to add the new block
            var index = vm.getBlockPosition(textBlockCtrl.block) + 1;
            if (index === 0) {
                throw 'Block to split not found';
            }
            // cut the text that is after the caret in the block and save it in order to add it after the embed later
            var after = textBlockCtrl.extractEndOfBlock().innerHTML;
            return $q.when((function() {
                if (after) {
                    // save the blocks (with removed leading text)
                    textBlockCtrl.updateModel();
                    // add new text block for the remaining text
                    return vm.insertNewBlock(index, {
                        body: after
                    }, true);
                }
            })()).then(function() {
                if (angular.isDefined(blocksToInsert)) {
                    var isArray = true;
                    if (!angular.isArray(blocksToInsert)) {
                        isArray = false;
                        blocksToInsert = [blocksToInsert];
                    }
                    var waitFor = $q.when();
                    var createdBlocks = [];
                    blocksToInsert.forEach(function(bti) {
                        waitFor = waitFor.then(function() {
                            var newBlock = vm.insertNewBlock(index, bti);
                            createdBlocks.push(newBlock);
                            return newBlock;
                        });
                    });
                    return waitFor.then(function() {
                        if (isArray) {
                            return $q.all(createdBlocks);
                        } else {
                            return createdBlocks[0];
                        }
                    });
                }
            });
        },
        /**
        ** Merge text blocks when there are following each other and add empty text block arround embeds if needed
        ** @param {Integer} position
        ** @param {Object} block ; block attributes
        ** @param {boolean} doNotRenderBlocks ; if true, it won't merge text blocks and
        ** add empty text block if needed through the `renderBlocks()` function.
        ** @returns {object} this
        */
        insertNewBlock: function(position, attrs, doNotRenderBlocks) {
            var new_block = new Block(attrs);
            return $q(function(resolve) {
                $timeout(function() {
                    vm.blocks.splice(position, 0, new_block);
                    $timeout(function() {
                        vm.commitChanges();
                        if (!doNotRenderBlocks) {
                            $timeout(function() {
                                vm.renderBlocks();
                                resolve(new_block);
                            }, 0, false);
                        } else {
                            resolve(new_block);
                        }
                    }, 0, false);
                });
            });
        },
        /**
        * Merge text blocks when there are following each other and add empty text block arround embeds if needed
        */
        renderBlocks: function() {
            vm.blocks = prepareBlocks(vm.blocks);
        },
        removeBlock: function(block) {
            // remove block only if it's not the only one
            var block_position = vm.getBlockPosition(block);
            if (vm.blocks.length > 1) {
                vm.blocks.splice(block_position, 1);
            } else {
                // if it's the first block, just remove the content
                block.body = '';
            }
            vm.renderBlocks();
            return $timeout(vm.commitChanges);
        },
        clipboard: undefined,
        cutBlock: function(block) {
            vm.clipboard = angular.copy(block);
            return vm.removeBlock(block);
        },
        getCutBlock: function(remove) {
            var block = vm.clipboard;
            if (remove) {
                vm.clipboard = undefined;
            }
            return block;
        },
        /**
        * Compute an id for the block with its content and its position.
        * Used as `track by` value, it allows the blocks to be well rendered.
        */
        generateBlockId: function(block) {
            function hashCode(string) {
                var hash = 0, i, chr, len;
                if (string.length === 0) {
                    return hash;
                }
                for (i = 0, len = string.length; i < len; i++) {
                    chr   = string.charCodeAt(i);
                    /*jshint bitwise: false */
                    hash  = ((hash << 5) - hash) + chr;
                    hash |= 0; // Convert to 32bit integer
                }
                return hash;
            }
            return String(Math.abs(hashCode(block.body))) + String(vm.getBlockPosition(block));
        }
    });
}
})();
<commit_msg>Editor: prevent chrome from adding an empty
 at the beginning of a new item.<commit_after>(function() {
'use strict';

angular.module('superdesk.editor2.ctrl', []).controller('SdTextEditorController', SdTextEditorController);

SdTextEditorController.$inject = ['lodash', 'EMBED_PROVIDERS', '$timeout', '$element', 'editor', 'config', '$q'];
function SdTextEditorController(_, EMBED_PROVIDERS, $timeout, $element, editor, config, $q) {
    var vm = this;
    function Block(attrs) {
        var self = this;
        if (!angular.isDefined(attrs)) {
            attrs = {};
        }
        angular.extend(self, _.defaults({
            body: attrs.body,
            loading: attrs.loading,
            caption: attrs.caption,
            blockType: attrs.blockType,
            embedType: attrs.embedType,
            association: attrs.association,
            lowerAddEmbedIsExtended: undefined,
            showAndFocusLowerAddAnEmbedBox: function() {
                self.lowerAddEmbedIsExtended = true;
            }
        }, {
            body: '

',
            loading: false,
            blockType: 'text'
        }));
    }
    /**
    * For the given blocks, merge text blocks when there are following each other and add empty text block arround embeds if needed
    */
    function prepareBlocks(blocks) {
        var newBlocks = [];
        blocks.forEach(function(block, i) {
            // if not the first block and there is a text block following another one
            if (i > 0 && block.blockType === 'text' && blocks[i - 1].blockType === 'text') {
                // we merge the content with the previous block
                newBlocks[newBlocks.length - 1].body += block.body;
            } else {
                // otherwise we add the full block
                newBlocks.push(block);
            }
            // add emtpy text block if block is between 2 embed blocks or at the end
            if (blocks[i].blockType === 'embed') {
                if (i === blocks.length - 1 || blocks[i + 1].blockType === 'embed') {
                    newBlocks.push(new Block());
                }
            }
        });
        // add emtpy text block at the top if needed
        if (newBlocks[0].blockType === 'embed') {
            newBlocks.unshift(new Block());
        }
        return newBlocks;
    }
    function splitIntoBlock(bodyHtml) {
        var blocks = [], block;
        /**
        * push the current block into the blocks collection if it is not empty
        */
        function commitBlock() {
            if (block !== undefined && block.body.trim() !== '') {
                blocks.push(block);
                block = undefined;
            }
        }
        $('
' + bodyHtml + '
')
        .contents()
        .toArray()
        .forEach(function(element) {
            // if we get a
, we push the current block and create a new one
            // for the paragraph content
            if (element.nodeName === 'P') {
                commitBlock();
                if (angular.isDefined(element.innerHTML) && element.textContent !== '' && element.textContent !== '\n') {
                    blocks.push(new Block({body: element.outerHTML.trim()}));
                }
                // detect if it's an embed
            } else if (element.nodeName === '#comment') {
                if (element.nodeValue.indexOf('EMBED START') > -1) {
                    commitBlock();
                    // retrieve the embed type following the comment
                    var embedType;
                    var embedTypeRegex = /EMBED START ([\w-]+)/;
                    var match;
                    if ((match = embedTypeRegex.exec(angular.copy(element.nodeValue).trim())) !== null) {
                        embedType = match[1];
                    } else {
                        embedType = EMBED_PROVIDERS.custom;
                    }
                    // retrieve the association reference
                    var association;
                    var embedAssoKey = /{id: "(embedded\d+)"}/;
                    if ((match = embedAssoKey.exec(angular.copy(element.nodeValue).trim())) !== null) {
                        if (vm.associations) {
                            association = angular.copy(vm.associations[match[1]]);
                        }
                    }
                    // create the embed block
                    block = new Block({blockType: 'embed', embedType: embedType, association: association});
                }
                if (element.nodeValue.indexOf('EMBED END') > -1) {
                    commitBlock();
                }
                // if it's not a paragraph or an embed, we update the current block
            } else {
                if (block === undefined) {
                    block = new Block({body: ''});
                }
                // we want the outerHTML (ex: 'text') or the node value for text and comment
                block.body += (element.outerHTML || element.nodeValue || '').trim();
            }
        });
        // at the end of the loop, we push the last current block
        if (block !== undefined && block.body.trim() !== '') {
            blocks.push(block);
        }
        // Complete embeds with metadata (from association datadata or html)
        blocks.forEach(function(block) {
            if (block.blockType === 'embed') {
                // for images that come from Superdesk, we use the association
                if (block.association && block.embedType === 'Image') {
                    block.caption = block.association.description_text;
                    var url;
                    // prefers "embed" for image url, otherwise "viewImage"
                    if (block.association.renditions.embed) {
                        url = block.association.renditions.embed.href;
                    } else {
                        url = block.association.renditions.viewImage.href;
                    }
                    block.body = '';
                    editor.generateMediaTag(block.association).then(function(img) {
                        block.body = img;
                    });
                } else {
                    // extract body and caption from embed block html
                    var original_body = angular.element(angular.copy(block.body));
                    if (original_body.get(0).nodeName === 'FIGURE') {
                        block.body = '';
                        original_body.contents().toArray().forEach(function(element) {
                            if (element.nodeName === 'FIGCAPTION') {
                                block.caption = element.innerHTML;
                            } else {
                                block.body += element.outerHTML || element.nodeValue || '';
                            }
                        });
                    }
                }
            }
        });
        // if no block, create an empty one to start
        if (blocks.length === 0) {
            blocks.push(new Block());
        }
        return blocks;
    }
    angular.extend(vm, {
        configuration: angular.extend({embeds: true}, config.editor || {}),
        blocks: [],
        initEditorWithOneBlock: function(model) {
            vm.model = model;
            vm.blocks = [new Block({body: model.$modelValue})];
        },
        initEditorWithMultipleBlock: function(model) {
            // save the model to update it later
            vm.model = model;
            // parse the given model and create blocks per paragraph and embed
            var content = model.$modelValue || '';
            // update the actual blocks value at the end to prevent more digest cycle as needed
            vm.blocks = splitIntoBlock(content);
            vm.renderBlocks();
        },
        serializeBlock: function(blocks) {
            blocks = angular.isDefined(blocks) ? blocks : vm.blocks;
            // in case blocks are not ready
            if (blocks.length === 0) {
                return '';
            }
            var new_body = '';
            if (vm.config.multiBlockEdition) {
                blocks.forEach(function(block) {
                    if (angular.isDefined(block.body) && block.body.trim() !== '') {
                        if (block.blockType === 'embed') {
                            var blockName = block.embedType.trim();
                            // add an id to the image in order to retrieve it in `assocations` field
                            if (block.association) {
                                blockName += ' {id: "embedded' + vm.generateBlockId(block) + '"}';
                            }
                            new_body += [
                                '\n',
                                '
',
                                block.body,
                                '
',
                                block.caption,
                                '
',
                                '
',
                                '\n\n'].join('');
                        } else {
                            new_body += block.body + '\n';
                        }
                    }
                });
            } else {
                new_body = (blocks.length > 0) ? blocks[0].body : '';
            }
            // strip
 and

            new_body = new_body.trim().replace(/

<\/p>$/, '');
            new_body = new_body.replace(/
$/, '');
            return new_body;
        },
        commitChanges: function() {
            var associations = angular.copy(vm.associations);
            // initialize associations if doesn't exist
            if (typeof associations !== 'object') {
                associations = {};
            }
            // remove older associations
            angular.forEach(associations, function(value, key) {
                if (_.startsWith(key, 'embedded')) {
                    associations[key] = null;
                }
            });
            // update associations with the ones stored in blocks
            vm.associations = angular.extend({}, associations, vm.getAssociations());
            // save model with latest state of blocks
            vm.model.$setViewValue(vm.serializeBlock());
        },
        /**
        * Return an object that contains the embedded images in the story
        */
        getAssociations: function() {
            var association = {};
            vm.blocks.forEach(function(block) {
                // we keep the association only for Superdesk images
                if (block.association) {
                    // add the association
                    association['embedded' + vm.generateBlockId(block)] = angular.copy(block.association);
                }
            });
            return association;
        },
        getBlockPosition: function(block) {
            return _.indexOf(vm.blocks, block);
        },
        splitAndInsert: function(textBlockCtrl, blocksToInsert) {
            // index where to add the new block
            var index = vm.getBlockPosition(textBlockCtrl.block) + 1;
            if (index === 0) {
                throw 'Block to split not found';
            }
            // cut the text that is after the caret in the block and save it in order to add it after the embed later
            var after = textBlockCtrl.extractEndOfBlock().innerHTML;
            return $q.when((function() {
                if (after) {
                    // save the blocks (with removed leading text)
                    textBlockCtrl.updateModel();
                    // add new text block for the remaining text
                    return vm.insertNewBlock(index, {
                        body: after
                    }, true);
                }
            })()).then(function() {
                if (angular.isDefined(blocksToInsert)) {
                    var isArray = true;
                    if (!angular.isArray(blocksToInsert)) {
                        isArray = false;
                        blocksToInsert = [blocksToInsert];
                    }
                    var waitFor = $q.when();
                    var createdBlocks = [];
                    blocksToInsert.forEach(function(bti) {
                        waitFor = waitFor.then(function() {
                            var newBlock = vm.insertNewBlock(index, bti);
                            createdBlocks.push(newBlock);
                            return newBlock;
                        });
                    });
                    return waitFor.then(function() {
                        if (isArray) {
                            return $q.all(createdBlocks);
                        } else {
                            return createdBlocks[0];
                        }
                    });
                }
            });
        },
        /**
        ** Merge text blocks when there are following each other and add empty text block arround embeds if needed
        ** @param {Integer} position
        ** @param {Object} block ; block attributes
        ** @param {boolean} doNotRenderBlocks ; if true, it won't merge text blocks and
        ** add empty text block if needed through the `renderBlocks()` function.
        ** @returns {object} this
        */
        insertNewBlock: function(position, attrs, doNotRenderBlocks) {
            var new_block = new Block(attrs);
            return $q(function(resolve) {
                $timeout(function() {
                    vm.blocks.splice(position, 0, new_block);
                    $timeout(function() {
                        vm.commitChanges();
                        if (!doNotRenderBlocks) {
                            $timeout(function() {
                                vm.renderBlocks();
                                resolve(new_block);
                            }, 0, false);
                        } else {
                            resolve(new_block);
                        }
                    }, 0, false);
                });
            });
        },
        /**
        * Merge text blocks when there are following each other and add empty text block arround embeds if needed
        */
        renderBlocks: function() {
            vm.blocks = prepareBlocks(vm.blocks);
        },
        removeBlock: function(block) {
            // remove block only if it's not the only one
            var block_position = vm.getBlockPosition(block);
            if (vm.blocks.length > 1) {
                vm.blocks.splice(block_position, 1);
            } else {
                // if it's the first block, just remove the content
                block.body = '';
            }
            vm.renderBlocks();
            return $timeout(vm.commitChanges);
        },
        clipboard: undefined,
        cutBlock: function(block) {
            vm.clipboard = angular.copy(block);
            return vm.removeBlock(block);
        },
        getCutBlock: function(remove) {
            var block = vm.clipboard;
            if (remove) {
                vm.clipboard = undefined;
            }
            return block;
        },
        /**
        * Compute an id for the block with its content and its position.
        * Used as `track by` value, it allows the blocks to be well rendered.
        */
        generateBlockId: function(block) {
            function hashCode(string) {
                var hash = 0, i, chr, len;
                if (string.length === 0) {
                    return hash;
                }
                for (i = 0, len = string.length; i < len; i++) {
                    chr   = string.charCodeAt(i);
                    /*jshint bitwise: false */
                    hash  = ((hash << 5) - hash) + chr;
                    hash |= 0; // Convert to 32bit integer
                }
                return hash;
            }
            return String(Math.abs(hashCode(block.body))) + String(vm.getBlockPosition(block));
        }
    });
}
})();
<|endoftext|>