kai-jacobsen/kontentblocks

View on GitHub
js/src/frontend/Views/EditModalModules.js

Summary

Maintainability
D
2 days
Test Coverage
var Logger = require('common/Logger');
var ModalFieldCollection = require('frontend/Collections/ModalFieldCollection');
var LoadingAnimation = require('frontend/Views/LoadingAnimation');
var Config = require('common/Config');
var Ui = require('common/UI');
var TinyMCE = require('common/TinyMCE');
var Notice = require('common/Notice');
var Ajax = require('common/Ajax');
var tplModuleEditForm = require('templates/frontend/module-edit-form.hbs');
var History = require('frontend/Views/EditModalHistory');
/**
 * This is the modal which wraps the modules input form
 * and loads when the user clicks on "edit" while in frontend editing mode
 * @type {*|void|Object}
 *
 */
//KB.Backbone.EditModalModules
module.exports = Backbone.View.extend({
  tagName: 'div',
  id: 'onsite-modal',
  timerId: null,
  viewStack: {},
  /**
   * Init method
   */
  initialize: function () {
    var that = this;
    this.FieldModels = new ModalFieldCollection();

    // add form skeleton to modal
    this.$el.append(tplModuleEditForm({
      model: {},
      i18n: KB.i18n.jsFrontend
    }));

    this.history = new History({modal: this});

    // cache elements
    this.LoadingAnimation = new LoadingAnimation({
      el: this.$el
    });

    this.setupElements();
    this.bindHandlers();


    return this;
  },

  bindHandlers: function () {
    var that = this;
    // use this event to refresh the modal on demand
    this.listenTo(KB.Events, 'modal.recalibrate', this.recalibrate);
    // use this event to trigger preview
    this.listenTo(KB.Events, 'modal.preview', this.preview);
    // use this event to trigger update
    this.listenTo(KB.Events, 'modal.update', this.update);

    // Attach resize event handler
    jQuery(window).on('resize', function () {
      that.recalibrate();
    });

    this.listenTo(KB.Events, 'kb.tinymce.new-editor', function (ed) {
      if (ed.settings && ed.settings.kblive) {
        that.attachEditorEvents(ed);
      }
    });

  },
  setupElements: function () {
    this.$form = jQuery('#onsite-form', this.$el);
    this.$formContent = jQuery('#onsite-content', this.$el);
    this.$inner = jQuery('.os-content-inner', this.$formContent);
    this.$title = jQuery('.controls-title', this.$el);
    this.$draft = jQuery('.kb-modal__draft-notice', this.$el);
  },
  events: {
    'click .close-controls': 'destroy',
    'click .kb-save-form': 'update',
    'click .kb-preview-form': 'preview',
    'change .kb-template-select': 'viewfileChange'
  },
  /**
   * Main method to open the modal
   * If modal is already opened and ModuleView differs from active
   * the modal reloads the view
   * else it's a noop
   * @param ModuleView Backbone View
   * @param force
   * @param keepinhistory
   * @returns {KB.Backbone.EditModalModules}
   */
  openView: function (ModuleView, force, keepinhistory) {
    //force = (_.isUndefined(force)) ? false : true;
    if (this.ModuleView && this.ModuleView.cid === ModuleView.cid) {
      return this;
    }

    if (keepinhistory) {
      this.history.prepend(this.ModuleView);
    }

    this.listenTo(KB.Events, 'modal.refresh', this.reload);

    this.ModuleView = ModuleView;
    this.model = ModuleView.model;
    this.realmid = this.setupModuleId();
    this.setupWindow();
    this.attach();
    this.render();
    this.recalibrate();
  },

  setupModuleId: function () {
    return this.model.get('mid');
  },
  /**
   * Attach events to Module View
   */
  attach: function () {
    //when update gets called from module controls, notify this view
    this.listenTo(this.ModuleView, 'kb.frontend.module.inline.saved', this.frontendViewUpdated);
    this.listenTo(this.model, 'data.updated', this.preview);
    this.listenTo(this.model, 'remove', this.destroy);
  },

  detach: function () {
    // reset listeners and ModuleView
    this.FieldModels.reset();
    this.stopListening();
    KB.Events.trigger('modal.close', this);
  },
  /**
   * reload the modal
   */
  reload: function () {
    this.render(true);
  },
  /**
   * Destroy and remove the modal
   */
  destroy: function () {
    var that = this;
    this.stopListening(KB.Events, 'modal.refresh', this.reload);
    that.detach();
    that.history.reset();
    jQuery('.wp-editor-area', this.$el).each(function (i, item) {
      tinymce.remove('#' + item.id);
    });
    that.ModuleView = null;
    that.initialized = false;
    that.unbind();
    that.$el.detach();

    if (KB.Sidebar.visible) {
      KB.Sidebar.$el.css('width', "");
    }

  },

  /**
   * Append element and restore position
   */
  setupWindow: function () {
    var that = this;
    if (KB.Sidebar.visible) {
      this.$el.appendTo(KB.Sidebar.$el);
      this.mode = 'sidebar';
      this.listenToOnce(KB.Sidebar, 'sidebar.close', function () {
        this.mode = 'body';
        this.$el.removeClass('kb-modal-sidebar');
        this.destroy();
      });
      KB.Sidebar.clearTimer();
    } else {
      this.mode = 'body';
      this.$el.appendTo('body').show();
    }
    // init draggable container and store position in config var
    if (this.mode === 'body') {
      this.$el.css('position', 'fixed').draggable({
        handle: '._controls-title',
        containment: 'window',
        helper: 'clone',
        stop: function (eve, ui) {
          // fit modal to window in size and position
          that.recalibrate(ui.position);
        }
      }).resizable();
    }
  },

  /**
   * Callback handler for update events triggered from module controls
   */
  frontendViewUpdated: function () {
    this.$el.removeClass('isDirty');
    this.reload();
  },
  /**
   * Calls serialize in preview mode
   * No data gets saved
   */
  preview: function (options) {
    if (options && options.hasOwnProperty('silent')) {
      if (options.silent === true) {
        return;
      }
    }
    this.serialize(false, false);
  },
  /**
   * Wrapper to serialize()
   * Calls serialize in save mode
   */
  update: function () {
    this.serialize(true, true);
    this.switchDraftOff();
  },
  /**
   * Main render method of the modal content
   */
  render: function (reload) {
    var that = this,
      json;
    Logger.User.info('Frontend modal retrieves data from the server');
    json = this.model.toJSON();

    // apply settings for the modal from the active module, if any
    this.applyControlsSettings(this.$el);

    //this.updateViewClassTo = false;

    // get the form
    jQuery.ajax({
      url: ajaxurl,
      data: {
        action: 'getModuleForm',
        module: json,
        entityData: json.entityData,
        //overloadData: overloadData,
        _ajax_nonce: Config.getNonce('read')
      },
      type: 'POST',
      dataType: 'json',
      beforeSend: function () {
        that.LoadingAnimation.show();
      },
      success: function (res) {
        // clear form content
        that.$inner.empty();
        // clear fields on ModuleView
        that.ModuleView.clearFields();
        // set id to module id
        that.$inner.attr('id', that.model.get('mid'));
        // append the html to the inner form container
        that.$inner.append(res.data.html);

        if (that.model.get('state').draft) {
          that.$draft.show(150);
        } else {
          that.$draft.hide();
        }

        var tinymce = window.tinymce;
        var $$ = tinymce.$;
        jQuery(document).on('click', function (event) {
          var id, mode,
            target = $$(event.target);
          if (target.hasClass('wp-switch-editor')) {
            id = target.attr('data-wp-editor-id');
            mode = target.hasClass('switch-tmce') ? 'tmce' : 'html';
            window.switchEditors.go(id, mode);
          }
        });


        if (res.data.json) {
          KB.payload = _.extend(KB.payload, res.data.json);
          _.defer(function () {
            if (res.data.json.Fields) {
              that.FieldModels.add(_.toArray(res.data.json.Fields));
            }
          });
        }
        // (Re)Init UI widgets
        Ui.initTabs();
        Ui.initToggleBoxes();
        TinyMCE.addEditor(that.$form);

        // -----------------------------------------------
        Logger.User.info('Frontend modal done.');

        that.$title.text(that.model.get('settings').name);

        if (reload) {
          if (that.FieldModels.length > 0) {
            KB.Events.trigger('modal.reload');
          }
        }
        Logger.User.info('Frontend modal.reload triggered.');

        // delayed recalibration
        setTimeout(function () {
          that.$el.show();
          that.LoadingAnimation.hide();
          that.ModuleView.renderStatusBar(that.$el);
          that.$('.kb-template-select').select2({
            templateResult: function (state) {
              if (!state.id) {
                return state.text;
              }
              var desc = state.element.dataset.tpldesc;
              return jQuery(
                '<span>' + state.text + '<br><span class="kb-tpl-desc">' + desc + '</span></span>'
              );
            }
          });
          that.recalibrate();
        }, 550);
      },
      error: function () {
        Notice.notice('There went something wrong', 'error');
      }
    });
  },


  /**
   * position and height of the modal may change depending on user action resp. contents
   * if the contents fits easily,  modal height will be set to the minimum required height
   * if contents take too much height, modal height will be set to maximum possible height
   * scrollbars are added as necessary
   */
  recalibrate: function () {
    var winH,
      conH,
      position,
      winDiff;
    // get window height
    winH = (jQuery(window).height() - 16);
    // get height of modal contents
    conH = jQuery('.os-content-inner').height();
    //get position of modal
    position = this.$el.position();

    // calculate if the modal contents overlap the window height
    // i.e. if part of the modal is out of view
    winDiff = (conH + position.top) - winH;
    // if the modal overlaps the height of the window
    // calculate possible height and set
    // nanoScroller needs an re-init after every change
    if (winDiff > 0) {
      this.initScrollbars(conH - (winDiff + 30));
    }
    //
    else if ((conH - position.top ) < winH) {
      this.initScrollbars(conH);

    } else {
      this.initScrollbars((winH - position.top));
    }

    // be aware of WP admin bar
    // TODO maybe check if admin bar is around
    if (position.top < 32) {
      this.$el.css('top', '32px');
      this.$el.css('right', '20px');

    }

    //if (KB.Sidebar.visible) {
    //  var sw = KB.Sidebar.$el.width();
    //  this.$el.css('left', sw + 'px');
    //  this.$el.css('height', winH + 'px');
    //}

    if (this.mode === 'sidebar') {
      var settings = this.model.get('settings');
      var cWidth = (settings.controls && settings.controls.width) || 600;
      KB.Sidebar.$el.width(cWidth);
      this.$el.addClass('kb-modal-sidebar');
      this.$el.width(cWidth);
    }

  },
  /**
   * (Re) Init Nano scrollbars
   * @param height
   */
  initScrollbars: function (height) {
    jQuery('.kb-nano', this.$el).height(height + 20);
    jQuery('.kb-nano').nanoScroller({preventPageScrolling: true, contentClass: 'kb-nano-content'});
  },

  /**
   * Serialize the form data
   * @param mode update or preview
   * @param showNotice show update notice or don't
   */
  serialize: function (mode, showNotice) {
    var that = this, moddata,
      save = mode || false,
      notice = (showNotice !== false);
    tinymce.triggerSave();

    moddata = this.formdataForId(this.realmid);
    this.model.set('entityData', moddata);
    this.LoadingAnimation.show();
    var ViewAnimation = new LoadingAnimation({
      el: this.ModuleView.$el
    });
    ViewAnimation.show();

    this.model.sync(save, this).done(function (res, b, c) {
      that.moduleUpdated(res, b, c, save, notice);
      ViewAnimation.remove();
    });
  },
  /**
   * This will update the actual DOM element of the edited module
   * @param res
   * @param b
   * @param c
   * @param save
   * @param notice
   */
  moduleUpdated: function (res, b, c, save, notice) {
    var that = this;

    that.model.trigger('modal.serialize.before');


    // change the container class if viewfile changed
    if (that.updateViewClassTo !== false) {
      that.updateContainerClass(that.updateViewClassTo);
    }

    // replace module html with new html
    that.ModuleView.trigger('modal.before.nodeupdate');
    that.ModuleView.$el.html(res.data.html);
    that.ModuleView.trigger('modal.after.nodeupdate');

    if (res.data.json && res.data.json.Fields) {
      KB.FieldControls.updateModels(res.data.json.Fields);
    }

    that.model.set('entityData', res.data.newModuleData);
    if (save) {
      that.model.trigger('module.model.updated', that.model);
      KB.Events.trigger('modal.saved');
    }
    jQuery(document).trigger('kb.module-update', that.model.get('settings').id, that.ModuleView);
    jQuery(document).trigger('kb.refresh');
    that.ModuleView.delegateEvents();

    // (re)attach inline editors and handle module controls
    // delay action to be safe
    _.defer(function () {
      that.ModuleView.render();
      that.model.trigger('modal.serialize');
      that.recalibrate();
    });
    //
    if (save) {
      if (notice) {
        Notice.notice(KB.i18n.jsFrontend.frontendModal.noticeDataSaved, 'success');
      }
      that.$el.removeClass('isDirty');
      that.ModuleView.getClean();
    } else {
      if (notice) {
        Notice.notice(KB.i18n.jsFrontend.frontendModal.noticePreviewUpdated, 'success');
      }
      that.$el.addClass('isDirty');
    }

    that.ModuleView.trigger('kb.view.module.HTMLChanged');
    that.LoadingAnimation.hide();
  },
  /**
   * Callback handler when the viewfile select field triggers change
   * @param e $ event
   */
  viewfileChange: function (e) {
    this.updateViewClassTo = {
      current: this.ModuleView.model.get('viewfile'),
      target: e.currentTarget.value
    };
    this.model.set('viewfile', e.currentTarget.value, {silent: true});
    this.preview();
    this.reload();
  },
  /**
   * Update modules element class to new view to
   * respect view dependent styles on the fly
   * @param viewfile string
   */
  updateContainerClass: function (viewfile) {
    if (!viewfile || !viewfile.current || !viewfile.target) {
      return false;
    }
    this.ModuleView.$el.removeClass(this._classifyView(viewfile.current));
    this.ModuleView.$el.addClass(this._classifyView(viewfile.target));
    this.updateViewClassTo = false;
  },
  /**
   * Delay key up events on form inputs
   * only fires the last event after 750ms
   */
  delayInput: function () {
    var that = this;
    if (this.timerId) {
      clearTimeout(this.timerId);
    }
    this.timerId = setTimeout(function () {
      that.timerId = null;
      that.serialize(false, false);
    }, 750);
  },
  attachEditorEvents: function (ed) {

  },


  /**
   * Modules can pass special settings to manipulate the modal
   * By now it's limited to the width
   * Maybe extended as usecases arise
   * @param $el
   */
  applyControlsSettings: function ($el) {
    var settings = this.model.get('settings');
    var cWidth = settings.controls && settings.controls.width;
    if (cWidth) {
      $el.css('width', settings.controls.width + 'px');
    }

    if (this.mode === 'sidebar' && cWidth) {
      KB.Sidebar.$el.width(cWidth);
    }

    if (settings.controls && settings.controls.fullscreen) {
      $el.width('100%').height('100%').addClass('fullscreen');
    } else {
      $el.height('').removeClass('fullscreen');
    }
  },
  /**
   * Helper method to create a element class from viewfile
   * @param str
   * @returns {string}
   * @private
   */
  _classifyView: function (str) {
    return 'view-' + str.replace('.twig', '');
  },
  switchDraftOff: function () {
    var json = this.model.toJSON();
    var that = this;

    if (!this.model.get('state').draft) {
      return;
    }
    // get the form
    Ajax.send({
      action: 'undraftModule',
      module: json,
      postId: this.model.get('parentObjectId'),
      _ajax_nonce: Config.getNonce('update')
    }, function (res) {
      if (res.success) {
        that.$draft.hide(150);
      }
    }, this);
  },
  formdataForId: function (mid) {
    var formdata;
    if (!mid) {
      return null;
    }
    formdata = this.$form.serializeJSON();
    if (formdata[mid]) {
      return _.clone(formdata[mid]);
    }
    return null;
  }
});