publiclab/plots2

View on GitHub
app/assets/javascripts/editor.js

Summary

Maintainability
A
4 hrs
Test Coverage
class Editor {
  // default parameters:
  //   defaultForm - when the Editor is initialized, there needs to be a default editor form:
  //     1. the main comment form in multi-comment wikis, questions, & research notes.
  //     2. the only editor form on /wiki/new and /wiki/edit
  //   isSingleFormPage - to distinguish between a) pages with multiple comments b) pages like /wiki/new, and /features/new with only one comment form
  //     elements have different ID naming conventions on the two kinds of pages:
  //     1. multi-form pages with multiple comments: #comment-preview-123
  //     2. /wiki/new and /wiki/edit: #preview-main
  constructor(defaultForm = "main", isSingleFormPage = false) {
    this.commentFormID = defaultForm;
    this.isSingleFormPage = isSingleFormPage;
    // this will get deleted in the next few PRs, so collapsing into one line to pass codeclimate
      
    marked.setOptions({
      gfm: true,
      tables: true,
      breaks: true,
      pedantic: false,
      sanitize: false,
      smartLists: true,
      langPrefix: 'language-',
      highlight: function(code, lang) {
        if (lang === 'js') {
          return highlighter.javascript(code);
        }
        return code;
      }
    });
  }
  setState(commentFormID) {
    this.commentFormID = commentFormID;
    this.attachSaveListener();
  }
  get textAreaElement() {
    const textAreaID = "#text-input-" + this.commentFormID;
    return $(textAreaID);
  }
  get textAreaValue() { 
    return this.textAreaElement.val(); 
  }
  get previewElement() {
    // eg. on /wiki/new & /wiki/edit, the preview element is called #preview-main
    const previewIDPrefix = this.isSingleFormPage ? "#preview-" : "#comment-preview-"
    const previewID = previewIDPrefix + this.commentFormID;
    return $(previewID);
  }
  // wraps currently selected text in textarea with strings startString & endString
  wrap(startString, endString, newlineDesired = false, fallback) {
    const selectionStart = this.textAreaElement[0].selectionStart;
    const selectionEnd = this.textAreaElement[0].selectionEnd;
    const selection = fallback || this.textAreaValue.substring(selectionStart, selectionEnd); // fallback if nothing has been selected, and we're simply dealing with an insertion point

    let newText = newlineDesired ? startString + selection + endString + "\n\n" : startString + selection + endString; // ie. ** + selection + ** (wrapping selection in bold)
    const selectionStartsMidText = this.textAreaElement[0].selectionStart > 0;
    if (newlineDesired && selectionStartsMidText) { newText = "\n" + newText; }

    const textLength = this.textAreaValue.length;
    const textBeforeSelection = this.textAreaValue.substring(0, selectionStart);
    const textAfterSelection = this.textAreaValue.substring(selectionEnd, textLength);
    this.textAreaElement.val(textBeforeSelection + newText + textAfterSelection);
  }
  bold() {
    this.wrap('**', '**');
  }
  italic() {
    this.wrap('_', '_');
  }
  link(uri) {
    uri = prompt('Enter a URL');
    if (!uri) { uri = ""; }
    this.wrap(
      '[', 
      '](' + uri + ')'
    );
  }
  image(src) {
    this.wrap(
      '\n![', 
      '](' + src + ')\n'
    );
  }
  
  h2() {
    this.wrap('##', '');
  }

  //debounce function addition
  debounce(func, wait, immediate) {
    let timeout;
    return function () {
      let context = this,
        args = arguments;
      let later = function () {
        timeout = null;
        if (!immediate) func.apply(context, args);
      };
      let callNow = immediate && !timeout;
      clearTimeout(timeout);
      timeout = setTimeout(later, wait);
      if (callNow) func.apply(context, args);
    };
  }

  // this function is dedicated to Don Blair https://github.com/donblair
  attachSaveListener() {
    // remove any other existing eventHandler
    $("textarea").off("input.save"); // input.save is a custom jQuery eventHandler
    const thisEditor = this; // save a reference to this editor, because inside the eventListener, "this" points to e.target
    //implementing a debounce function on save method
    this.textAreaElement.on(
    "input.save",
    debounce(function () {
      //changing styles and text
      //explicitly handling main comment form
      if ($('#text-input-main').is(':focus')) {
       
       $("#comment-form-main .btn-toolbar #save-button-main").find("i").removeClass("fa fa-save").addClass("fas fa-sync fa-spin");
     
       let saving_text = $('<p id="saving-text" style="padding-bottom: 8px"> Saving... </p>');
       $("#comment-form-main .imagebar").prepend(saving_text);
       $("#comment-form-main .imagebar p").not("#saving-text").hide();
        
       //adding delay and revering the styles
        setTimeout(() => {
          $("#comment-form-main .btn-toolbar #save-button-main").find("i").removeClass("fas fa-sync fa-spin").addClass("fa fa-save");
          
          $("#comment-form-main .imagebar").find("#saving-text").remove();
          $("#comment-form-main .imagebar p").not("#saving-text").show();
        }, 400);
    }
    else { 
        //handling other comment forms
        let comment_temp_id = (document.activeElement.parentElement.parentElement.id);
        let imager_bar = (document.activeElement.nextElementSibling.className);

        $('#'+comment_temp_id).find('.btn-toolbar').find(".save-button").find("i").removeClass("fa fa-save").addClass("fas fa-sync fa-spin");

        let saving_text = $('<p id="saving-text" style="padding-bottom: 8px"> Saving... </p>');
        $('#'+comment_temp_id).find('.'+imager_bar).prepend(saving_text);
        $('#'+comment_temp_id).find('.'+imager_bar).find("p").not("#saving-text").hide();

        setTimeout(() => {
          $('#'+comment_temp_id).find('.btn-toolbar').find(".save-button").find("i").removeClass("fas fa-sync fa-spin").addClass("fa fa-save");
          
          $('#'+comment_temp_id).find('.'+imager_bar).find("#saving-text").remove();
          $('#'+comment_temp_id).find('.'+imager_bar).find("p").not("#saving-text").show();
        }, 400);
    }
      thisEditor.save(thisEditor);
      }, 700)
    );
  }  
    save(thisEditor) {
    const storageKey = "plots:" + window.location.pathname + ":" + thisEditor.commentFormID;
    localStorage.setItem(storageKey, thisEditor.textAreaValue);
  }
  recover() {
    const storageKey = "plots:" + window.location.pathname + ":" + this.commentFormID;
    this.textAreaElement.val(localStorage.getItem(storageKey));
  }
  apply_template(template) {
    if(this.textAreaValue == ""){
      this.textAreaElement.val(this.templates[template])
    }else if((this.textAreaValue == this.templates['event']) || (this.textAreaValue == this.templates['default']) || (this.textAreaValue == this.templates['support'])){
      this.textAreaElement.val(this.templates[template])
    }else{
      this.textAreaElement.val(this.textAreaValue+'\n\n'+this.templates[template])
    }
  }
  toggle_preview() {
    // if the element is part of a multi-comment page,
    // ensure to grab the current element and not the other comment element.
    const previewBtn = $("#toggle-preview-button-" + this.commentFormID);
    const formIdPrefix = this.isSingleFormPage ? "#form-body-" : "#comment-form-body-";
    const commentFormBody = $(formIdPrefix + this.commentFormID);

    this.previewElement[0].innerHTML = marked.parse(this.textAreaValue);
    this.previewElement.toggle();
    commentFormBody.toggle();
    commentFormBody.parent().find(".btn-toolbar").toggle();    // toggles comment toolbar

    this.toggleButtonPreviewMode(previewBtn);
  }
  toggleButtonPreviewMode(previewBtn) {
    let isPreviewing = previewBtn.attr('data-previewing');

    // If data-previewing attribute is not present -> we are not in "preview" mode
    if (!isPreviewing) {
      previewBtn.attr('data-previewing', 'false');
      isPreviewing = 'false';
    }

    if (isPreviewing === 'false') {
      previewBtn.attr('data-previewing', 'true');

      let previewText = previewBtn.attr('data-previewing-text');
      previewBtn.text(previewText);
    } else {
      previewBtn.attr('data-previewing', 'false');
      previewBtn.text('Preview');
    }
  }
}