Source: src/js/views/MarkdownEditorView.js

define([
  "underscore",
  "jquery",
  "backbone",
  "woofmark",
  "models/metadata/eml220/EMLText",
  "views/ImageUploaderView",
  "views/MarkdownView",
  "views/TableEditorView",
  "text!templates/markdownEditor.html",
], function (
  _,
  $,
  Backbone,
  Woofmark,
  EMLText,
  ImageUploader,
  MarkdownView,
  TableEditor,
  Template,
) {
  /**
   * @class MarkdownEditorView
   * @classdesc A view of an HTML textarea with markdown editor UI and preview tab
   * @classcategory Views
   * @extends Backbone.View
   * @constructor
   */
  var MarkdownEditorView = Backbone.View.extend(
    /** @lends MarkdownEditorView.prototype */ {
      /**
       * The type of View this is
       * @type {string}
       * @readonly
       */
      type: "MarkdownEditor",

      /**
       * The HTML classes to use for this view's element
       * @type {string}
       */
      className: "markdown-editor",

      /**
       * References to templates for this view. HTML files are converted to
       * Underscore.js templates
       * @type {Underscore.Template}
       */
      template: _.template(Template),

      /*
       * Markdown to insert into the textarea when the view is first rendered
       * @type {string}
       */
      // markdown: "",

      /**
       * EMLText model that contains a markdown attribute to edit. The markdown is
       * inserted into the textarea when the view is first rendered. If there's no markdown,
       * then the view looks for markdown from the markdownExample attribute in the model.
       * Note that if there are multiple markdown strings in the model, only the first
       * is rendered/edited.
       * @type {EMLText}
       */
      model: null,

      /**
       * The placeholder text to display in the textarea when it's empty
       * @type {string}
       */
      markdownPlaceholder: "",

      /**
       * The placeholder text to display in the preview area when there's no
       * markdown
       * @type {string}
       */
      previewPlaceholder: "",

      /**
       * Indicates whether or not to render a table of contents for the markdown
       * preview. If set to true, a table of contents will be shown in the preview
       * if there two or more top-level headers are rendered from the markdown.
       * @type {boolean}
       */
      showTOC: false,

      /**
       * The maximum height for uploaded image files. If a file is taller than this, it
       * will be resized without warning before being uploaded. If set to null,
       * the image won't be resized based on height (but might be depending on
       * maxImageWidth).
       * @type {number}
       * @default 1200
       * @since 2.15.0
       */
      maxImageHeight: 1200,

      /**
       * The maximum width for uploaded image files. If a file is wider than this, it
       * will be resized without warning before being uploaded. If set to null,
       * the image won't be resized based on width (but might be depending on
       * maxImageHeight).
       * @type {number}
       * @default 1200
       * @since 2.15.0
       */
      maxImageWidth: 1200,

      /**
       * A jQuery selector for the HTML textarea element that will contain the
       * markdown text.
       * @type {string}
       */
      textarea: ".markdown-textarea",

      /**
       * The events this view will listen to and the associated function to call.
       * @type {Object}
       */
      events: {
        "click #markdown-preview-link": "previewMarkdown",
        "focusout .markdown-textarea": "updateMarkdown",
      },

      /**
       * Initialize is executed when a new markdownEditor is created.
       * @param {Object} options - A literal object with options to pass to the view
       */
      initialize: function (options) {
        if (typeof options !== "undefined") {
          this.model = options.model || new EMLText();
          this.markdownPlaceholder = options.markdownPlaceholder || "";
          this.previewPlaceholder = options.previewPlaceholder || "";
          this.showTOC = options.showTOC || false;
        }
      },

      /**
       * render - Renders the markdownEditor - add UI for adding and editing
       * markdown to a textarea
       */
      render: function () {
        try {
          // Save the view
          var view = this;

          // The markdown attribute in the model may be a string or an array of strings.
          // Although EML211 can comprise an array of markdown elements,
          // this view will only render/edit the first if there are multiple.
          var markdown = this.model.get("markdown");
          if (Array.isArray(markdown) && markdown.length) {
            markdown = markdown[0];
          }
          if (!markdown || !markdown.length) {
            markdown = this.model.get("markdownExample");
          }

          // Insert the template into the view
          this.$el
            .html(
              this.template({
                markdown: markdown || "",
                markdownPlaceholder: this.markdownPlaceholder || "",
                previewPlaceholder: this.previewPlaceholder || "",
                cid: this.cid,
              }),
            )
            .data("view", this);

          // The textarea element that the markdown editor buttons & functions will edit
          var textarea = this.$el.find(this.textarea);

          if (textarea && textarea.length) {
            textarea = textarea[0];
          }

          if (!textarea) {
            console.log(
              "error: the markdown editor view was not rendered because no textarea element was found.",
            );
            return;
          }

          // Set woofmark options. See https://github.com/bevacqua/woofmark
          var woofmarkOptions = {
            fencing: true,
            html: false,
            wysiwyg: false,
            defaultMode: "markdown",
            render: {
              // Hide buttons that switch between markdown, WYSIWYG, & HTML for now
              modes: function (button, id) {
                button.remove();
              },
            },
          };

          // Set options for all the buttons that will be shown in the toolbar.
          // Buttons will be shown in the order they are listed.
          // Defaults from Woofmark will be used unless they are replaced here,
          // see: https://github.com/bevacqua/woofmark/blob/master/src/strings.js.
          // They key is the ID for the button.
          //    remove: if set to true, the button will be removed (use this to hide default woofmark buttons)
          //    icon: the name of the font awesome icon to show in the button. If no button or svg is set, the ID/key will be displayed instead.
          //    svg: svg code to show in the button. If no button or svg is set, the ID/key will be displayed instead.
          //    title: The title to show on hover
          //    function: The function to call when the button is pressed. It will be passed chunks, cmd, e (see Woofmark docs), plus the ID/key. Called with view as the this (context).
          //    shortcut: The keyboard shortcut to use for the button. This will only work if there is also a custom function set.
          //    insertDividerAfter: If set to true, a visual divider will be placed after this button.
          var buttonOptions = {
            // Default woofmark buttons to remove
            attachment: {
              remove: true,
            },
            heading: {
              remove: true,
            },
            hr: {
              remove: true,
            },
            // Remove the default image uploader button so we can add our own that
            // uploads the image as a dataone object.
            image: {
              remove: true,
            },
            // Default woofmark buttons to keep, with custom properties, + custom buttons
            bold: {
              icon: "bold",
            },
            italic: {
              icon: "italic",
            },
            strike: {
              title: "Strikethrough",
              icon: "strikethrough",
              shortcut: "Ctrl+Shift+X",
              function: view.strikethrough,
              insertDividerAfter: true,
            },
            h1: {
              svg: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 2.9V0h7.8v2.9l-2 .5v7h7.7v-7l-2-.5V0h7.7v2.9l-2 .5v17.2l2 .5V24h-7.8v-2.9l2-.5V14H5.9v6.6l2 .5V24H0v-2.9l2-.5V3.4z"/><path fill-rule="nonzero" d="M24 16.4v-1.9h-1.4V5.8h-4.1v1.8H20v7h-1.4v1.8z"/></svg>`,
              title: "Top-level heading",
              function: view.addHeader,
            },
            h2: {
              svg: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill-rule="nonzero" d="M23.2 17.3l.1-3.1h-2v1.3H18c.1-.7.8-1.5 2-2.2L22 12a4 4 0 001-1l.3-1.5c0-.9-.3-1.6-1-2.1-.6-.5-1.5-.8-2.6-.8-1.3 0-2.2.3-2.9 1-.6.6-1 1.5-1 2.8l2.2.1c0-.8.2-1.3.4-1.7.3-.3.6-.5 1.1-.5.4 0 .7.1.9.3.2.2.3.5.3.8 0 .4-.1.7-.4 1-.2.4-.6.7-1.1 1a17 17 0 00-2.1 1.9c-.5.5-.8 1-1 1.6-.3.6-.4 1.4-.4 2.3h7.6z"/><path d="M.5 4.6V2.3h6.3v2.3L5.2 5v5.6h6.2V5l-1.6-.4V2.3H16v2.3l-1.6.4v14l1.6.4v2.3H9.8v-2.3l1.6-.4v-5.3H5.2V19l1.6.4v2.3H.5v-2.3l1.6-.4V5z"/></svg>`,
              title: "Second-level heading",
              function: view.addHeader,
            },
            h3: {
              svg: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill-rule="nonzero" d="M18.1 17.3c.7 0 1.4-.1 2-.4.6-.2 1-.6 1.4-1 .3-.6.5-1.1.5-1.7 0-1.2-.7-2-2-2.5 1-.5 1.6-1.2 1.6-2.2 0-.9-.4-1.6-1-2-.6-.6-1.5-.8-2.6-.8-1 0-1.9.3-2.5.8a3 3 0 00-1 2.2l2.1.1c0-.9.4-1.3 1.3-1.3.3 0 .6 0 .9.3.2.2.3.4.3.8s-.2.7-.5.9a3 3 0 01-1.5.3v1.9h.6c.5 0 1 0 1.2.3.3.3.5.7.5 1 0 .5-.2.8-.4 1.1-.3.3-.7.5-1.1.5-1 0-1.5-.6-1.5-1.8l-2.2.1c.1 2.3 1.4 3.4 4 3.4z"/><path d="M2 6.6V4.8h4.7v1.8l-1.2.3V11H10V6.9l-1.2-.3V4.8h4.6v1.8l-1.2.3V17l1.2.3v1.8H8.9v-1.8l1.2-.3v-3.9H5.5v4l1.2.2v1.8H2v-1.8l1.2-.3V7z"/></svg>`,
              title: "Tertiary heading",
              function: view.addHeader,
              insertDividerAfter: true,
            },
            divider: {
              svg: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><rect width="22" height="3" x="1" y="10.58" fill-rule="evenodd"/></svg>`,
              function: view.addDivider,
              title: "Top-level heading",
              shortcut: "Ctrl+Enter",
            },
            ol: {
              title: "Ordered list",
              icon: "list-ol",
            },
            ul: {
              title: "Un-ordered list",
              icon: "list-ul",
              insertDividerAfter: true,
            },
            quote: {
              icon: "quote-left",
            },
            code: {
              icon: "code",
              insertDividerAfter: true,
            },
            link: {
              icon: "link",
            },
            d1Image: {
              icon: "picture",
              function: view.addMdImage,
              title: "Image",
              // use Ctrl+G to overwrite the built-in woofmark image function
              shortcut: "Ctrl+G",
            },
            table: {
              icon: "table",
              function: view.addTable,
              insertDividerAfter: true,
            },
          };

          var buttonKeys = Object.keys(buttonOptions);

          // PRE-RENDER WOOFMARK
          // Set titles on buttons before the Woofmark text editor is rendered.
          // This way we can use Woofmark's build in functionality to convert "Ctrl"
          // to "Cmd" symbol if user is on mac.
          _.each(
            buttonKeys,
            function (key, i) {
              var options = buttonOptions[key],
                title =
                  options.title || key.charAt(0).toUpperCase() + key.slice(1),
                presetShortcut = "";

              if (
                Woofmark.strings.titles[key] &&
                Woofmark.strings.titles[key].match(/Ctrl\+.*$/)
              ) {
                presetShortcut =
                  Woofmark.strings.titles[key].match(/Ctrl\+.*$/)[0];
              }

              var shortcut = options.shortcut || presetShortcut;

              if (title) {
                Woofmark.strings.titles[key] = [title, shortcut].join(" ");
              }
              // So that we can identify buttons that we want to manipulate after
              // they are rendered, use the key as the button text for now.
              Woofmark.strings.buttons[key] = key;
            },
            this,
          );

          // RENDER WOOFMARK
          // Initialize the woofmark markdown editor
          this.markdownEditor = new Woofmark(textarea, woofmarkOptions);

          // POST-RENDER WOOFMARK
          // After the markdown editor is initialized..

          // Add custom functions
          _.each(buttonKeys, function (key, i) {
            var options = buttonOptions[key];
            if (options.function) {
              // addCommandButton uses cmd, not ctrl
              var shortcut = "";
              if (options.shortcut) {
                shortcut = options.shortcut.replace("Ctrl", "cmd");
              }
              view.markdownEditor.addCommandButton(
                key,
                shortcut,
                function (e, mode, chunks) {
                  options.function.call(view, e, mode, chunks, key);
                },
              );
            }
          });

          // Modify the button order & appearance
          var buttonContainer = $(view.markdownEditor.textarea)
            .parent()
            .find(".wk-commands");
          var buttons = buttonContainer.find(".wk-command");
          _.each(buttonKeys, function (key, i) {
            // Re-order buttons based on the order in buttonOptions, and remove
            // any that are marked for removal
            var options = buttonOptions[key];
            var buttonEl = buttonContainer
              .find(".wk-command")
              .filter(function () {
                return this.innerHTML == key;
              });
            if (options.remove !== true) {
              // Add tooltip
              buttonEl.tooltip({
                placement: "top",
                delay: 500,
                trigger: "hover",
              });
              // Add font awesome icon or SVG
              if (options.icon) {
                buttonEl.html("<i class='icon-" + options.icon + "'></i>");
              } else if (options.svg) {
                buttonEl.html(options.svg);
                buttonEl.find("svg").height("13px").width("auto");
              }
              buttonContainer.append(buttonEl);
              if (options.insertDividerAfter === true) {
                buttonContainer.append(
                  "<div class='wk-commands-divider'></div>",
                );
              }
            } else {
              buttonEl.remove();
            }
          });
        } catch (e) {
          console.log(e);
          console.log(
            "Failed to render the markdown editor UI, error message: " + e,
          );
        }
      },

      /**
       * addHeader - description
       *
       * @param  {event}  e      is the original event object
       * @param  {string} mode   can be markdown, html, or wysiwyg
       * @param  {object} chunks is a chunks object, describing the current state of the editor, see https://github.com/bevacqua/woofmark#chunks
       * @param  {string} id     the ID of the function, set as they key in buttonOptions in the render function
       */
      addHeader: function (e, mode, chunks, id) {
        // Get the header level from the ID
        var levelToCreate = parseInt(id.replace(/^\D+/g, ""));

        chunks.selection = chunks.selection
          .replace(/\s+/g, " ")
          .replace(/(^\s+|\s+$)/g, "");

        if (!chunks.selection) {
          chunks.startTag = new Array(levelToCreate + 1).join("#") + " ";
          chunks.selection = Woofmark.strings.placeholders.heading;
          chunks.endTag = "";
          chunks.skip({ before: 1, after: 1 });
          return;
        }

        chunks.findTags(/#+[ ]*/, /[ ]*#+/);

        if (/#+/.test(chunks.startTag)) {
          level = RegExp.lastMatch.length;
        }

        chunks.startTag = chunks.endTag = "";
        chunks.findTags(null, /\s?(-+|=+)/);

        if (/=+/.test(chunks.endTag)) {
          level = 1;
        }

        if (/-+/.test(chunks.endTag)) {
          level = 2;
        }

        chunks.startTag = chunks.endTag = "";
        chunks.skip({ before: 1, after: 1 });

        if (levelToCreate > 0) {
          chunks.startTag = new Array(levelToCreate + 1).join("#") + " ";
        }
      },

      /**
       * addDivider - Add or remove a divider
       *
       * @param  {event} e      is the original event object
       * @param  {string} mode   can be markdown, html, or wysiwyg
       * @param  {object} chunks is a chunks object, describing the current state of the editor, see https://github.com/bevacqua/woofmark#chunks
       */
      addDivider: function (e, mode, chunks) {
        // If the selection includes a divider, remove it
        var markdown = chunks.before + chunks.selection + chunks.after;
        var startSel = chunks.before.length;
        var endSel = startSel + chunks.selection.length + 1;
        var dividerRE = /(\r\n|\r|\n){2}-{3,}/gm;
        var dividerDeleted = false;
        while ((match = dividerRE.exec(markdown)) !== null) {
          // +1 so that we don't delete the divider if selection is at the newlines before a divider
          var startDivider = match.index + 2;
          // +1 so that if the selection is at the end of a divider, it will still be deleted
          var endDivider = match.index + match[0].length + 1;
          if (
            (endSel > startDivider && endSel <= endDivider) ||
            (startSel < endDivider && startSel >= startDivider)
          ) {
            tableString = match[0];
            chunks.before = markdown.slice(0, startDivider - 2) + "\n";
            chunks.selection = "";
            chunks.after = markdown.slice(endDivider);
            dividerDeleted = true;
            break;
          }
        }
        // If the divider was not deleted (therefore not detected), then add one
        if (!dividerDeleted) {
          var dividerToAdd = "\n\n--------------------\n";
          if (/(\r\n|\r|\n){1}$/.test(chunks.before)) {
            dividerToAdd = "\n--------------------\n";
          }
          chunks.before = chunks.before + dividerToAdd;
        }
      },

      /**
       * addTable - Creates the UI for editing and adding tables to the textarea.
       * Detects whether the selection contained any part of a markdown table,
       * then opens a woofmark dialog box and inserts a table editor view. If a
       * table was selected, the table information is imported into the table
       * editor where the user can edit it. If no table was selected, then it
       * creates an empty table where the user can add data.
       *
       * @param  {event} e      is the original event object
       * @param  {string} mode   can be markdown, html, or wysiwyg
       * @param  {object} chunks is a chunks object, describing the current state of the editor, see https://github.com/bevacqua/woofmark#chunks
       */
      addTable: function (e, mode, chunks) {
        // Use a modified version of the link dialog
        this.markdownEditor.showLinkDialog();

        // Select the image upload dialog elements so that we can customize it
        var dialog = $(".wk-prompt"),
          dialogContent = dialog.find(".wk-prompt-input-container"),
          dialogTitle = dialog.find(".wk-prompt-title"),
          dialogDescription = dialog.find(".wk-prompt-description"),
          dialogOkBtn = dialog.find(".wk-prompt-ok");

        // Detect whether the selection includes a markdown table.
        // If it does, ensure the complete table is selected, and save the
        // markdown table string segment to be parsed.
        var markdown = chunks.before + chunks.selection + chunks.after;
        var startSel = chunks.before.length;
        var endSel = startSel + chunks.selection.length + 1;
        var tableRE =
          /((\|[^|\r\n]*)+\|(\r?\n|\r)?)+((?:\s*\|\s*:?\s*[-=]+\s*:?\s*)+\|)(\n\s*(?:\|[^\n]+\|\r?\n?)*)?$/gm;
        // The regular expression used by showdown to detect tables:
        // var tableRE = /^ {0,3}\|?.+\|.+\n {0,3}\|?[ \t]*:?[ \t]*(?:[-=]){2,}[ \t]*:?[ \t]*\|[ \t]*:?[ \t]*(?:[-=]){2,}[\s\S]+?(?:\n\n|ยจ0)/gm;
        var tables = markdown.match(tableRE);
        var tableString = "";
        while ((match = tableRE.exec(markdown)) !== null) {
          var startTab = match.index;
          var endTab = match.index + match[0].length;
          if (
            (endSel > startTab && endSel <= endTab) ||
            (startSel < endTab && startSel >= startTab)
          ) {
            tableString = match[0];
            chunks.before = markdown.slice(0, startTab);
            chunks.selection = markdown.slice(startTab, endTab);
            chunks.after = markdown.slice(endTab);
            // Just use the first table match in which there is also at least partial selection
            break;
          }
        }

        // Clone the chunks object at this point in case the textarea loses focus
        // and the selection changes before the "ok" buttons is pressed
        const chunksClone = JSON.parse(JSON.stringify(chunks));

        // Add a table editor view.
        // Pass the parsesd markdown table, if there is one
        var tableEditor = new TableEditor({
          markdown: tableString,
        });
        // Render the table editor
        tableEditor.render();
        // Add the rendered table editor to the dialog, update the dialog.
        dialogContent.html(tableEditor.el);
        dialogDescription.remove();
        dialogTitle.text("Insert Table");

        // Listen for when the OK button is clicked. Attach listener to the dialog
        // so that it's destroyed when the dialog is destroyed. It won't be called
        // if the user presses cancel.
        var view = this;
        dialogOkBtn.off("click");
        dialogOkBtn.on("click", function insertText(event) {
          var tableMarkdown = tableEditor.getMarkdown();
          view.markdownEditor.runCommand(function (chunks, mode) {
            chunks.before = chunksClone.before;
            chunks.after = chunksClone.after;
            chunks.selection = tableMarkdown;
          });
        });
      },

      /**
       * addMdImage - The function that gets called when a user clicks the custom
       * add image button added to the markdown editor. It uses the UI created by
       * the ImageUploaderView to allow a user to select & upload an image to the
       * repository, and uses Woofmark's built-in add image functionality to
       * insert the correct markdown into the textarea. This function must be
       * called such that "this" is the markdownEditor view.
       */
      addMdImage: function () {
        try {
          var view = this;

          // Show woofmark's default image upload dialog, inserted at the end of body
          view.markdownEditor.showImageDialog();

          // Select the image upload dialog elements so that we can customize it
          var imageDialog = $(".wk-prompt"),
            imageDialogInput = imageDialog.find(".wk-prompt-input"),
            imageDialogDescription = imageDialog.find(".wk-prompt-description"),
            imageDialogOkBtn = imageDialog.find(".wk-prompt-ok"),
            // Save the inner HTML of the button for when we replace it
            // temporarily during image upload
            imageDialogOkBtnTxt = imageDialogOkBtn.html();

          // Create an ImageUploaderView and insert into this view.
          mdImageUploader = new ImageUploader({
            uploadInstructions: "Drag & drop an image here or click to upload",
            imageTagName: "img",
            height: "175",
            width: "300",
            maxHeight: view.maxImageHeight || null,
            maxWidth: view.maxImageWidth || null,
          });

          // Show when image is uploading; temporarily disable the OK button
          view.stopListening(mdImageUploader, "addedfile");
          view.listenTo(mdImageUploader, "addedfile", function () {
            // Disable the button during upload;
            imageDialogOkBtn.prop("disabled", true);
            imageDialogOkBtn.css({ opacity: "0.5", cursor: "not-allowed" });
            imageDialogOkBtn.html(
              "<i class='icon-spinner icon-spin icon-large loading icon'></i> " +
                "Uploading...",
            );
          });

          // Update the image input URL when the image is successfully uploaded
          view.stopListening(mdImageUploader, "successSaving");
          view.listenTo(
            mdImageUploader,
            "successSaving",
            function (dataONEObject) {
              //Execute the DataONEObject function that performs various functions after
              // a successful save
              dataONEObject.onSuccessfulSave();

              // Re-enable the button
              imageDialogOkBtn.prop("disabled", false);
              imageDialogOkBtn.html(imageDialogOkBtnTxt);
              imageDialogOkBtn.css({ opacity: "1", cursor: "pointer" });

              // Get the uploaded image's url.
              //var url = dataONEObject.url();
              var url = "";

              if (MetacatUI.appModel.get("isCN")) {
                var sourceRepo;

                //Use the object service URL from the origin MN/datasource
                if (dataONEObject.get("datasource")) {
                  sourceRepo = MetacatUI.nodeModel.getMember(
                    dataONEObject.get("datasource"),
                  );
                }
                //Use the object service URL from the alt repo
                if (!sourceRepo) {
                  sourceRepo = MetacatUI.appModel.getActiveAltRepo();
                }

                if (sourceRepo) {
                  url = sourceRepo.objectServiceUrl;
                }
              }

              //If this MetacatUI deployment is pointing to a MN, use the meta service URL from the AppModel
              if (!url) {
                url =
                  MetacatUI.appModel.get("objectServiceUrl") ||
                  MetacatUI.appModel.get("resolveServiceUrl");
              }

              url = url + dataONEObject.get("id");

              // Create title out of file name without extension.
              var title = dataONEObject.get("fileName");
              if (title && title.lastIndexOf(".") > 0) {
                title = title.substring(0, title.lastIndexOf("."));
              }

              // Add the url + title to the input
              imageDialogInput.val(url + ' "' + title + '"');
            },
          );

          // Clear the input when the image is removed
          view.stopListening(mdImageUploader, "removedfile");
          view.listenTo(mdImageUploader, "removedfile", function () {
            imageDialogInput.val("");
          });

          // Render the image uploader and insert it just after the upload
          // instructions in the image upload dialog box.
          mdImageUploader.render();
          // The instructions for uploading in image that displays in the prompt/dialog
          imageDialogDescription.text(
            "Click or drag & drop to upload an image",
          );
          $(mdImageUploader.el).insertAfter(imageDialogDescription);
          // Hide the input box for now, to keep the uploader simple
          imageDialogInput.hide();
        } catch (e) {
          console.log(
            "Failed to load the UI for adding markdown images. Error: " + e,
          );
        }
      },

      /**
       * strikethrough - Add or remove the markdown syntax for strike through to
       * the textarea. If there is text selected, then strike through formatting
       * will be added or removed from that selection. If no selection,
       * some placeholder text will be added surrounded by the strikethrough
       * delimiters.
       *
       * @param  {event} e      is the original event object
       * @param  {string} mode   can be markdown, html, or wysiwyg
       * @param  {object} chunks is a chunks object, describing the current state of the editor, see https://github.com/bevacqua/woofmark#chunks
       */
      strikethrough: function (e, mode, chunks) {
        try {
          var markup = "~~";
          // exactly two tiles
          var tildes = "\\~{2}";
          // 2 tildes at the start of a string
          var rleading = /^(\~{2})/;
          // 2 tildes at the end of a string
          var rtrailing = /(\~{2}$)/;
          // 0-1 spaces at the end of a string
          var rtrailingspace = /(\s?)$/;
          // 2+ line breaks
          var rnewlines = /\n{2,}/g;
          // the text to add when no text is selected
          var placeholder = "strikethrough text";

          // Remove leading & trailing white space from selection
          // (but do not remove from the user's text)
          chunks.trim();
          // Replace 2+ consecutive line breaks with 1 linebreak, otherwise
          // strikethrough syntax is incorrect and won't render HTML as expected
          chunks.selection = chunks.selection.replace(rnewlines, "\n");

          // See if the text before or after already contains ~~ at the start/end
          var leadTildes = rtrailing.exec(chunks.before);
          var trailTildes = rleading.exec(chunks.after);
          // See if the selected text already contains ~~ at start or end
          var selectLeadTildes = rleading.exec(chunks.selection);
          var selectTrailTildes = rtrailing.exec(chunks.selection);

          // If the selection is already surrounded by ~~, remove them
          if (leadTildes && trailTildes) {
            chunks.before = chunks.before.replace(rtrailing, "");
            chunks.after = chunks.after.replace(rleading, "");
            // If the selection starts & ends with ~~, remove them
          } else if (selectLeadTildes && selectTrailTildes) {
            chunks.selection = chunks.selection.replace(rleading, "");
            chunks.selection = chunks.selection.replace(rtrailing, "");
            // Otherwise, add a set of ~~
          } else {
            chunks.before = chunks.before + markup;
            chunks.after = markup + chunks.after;
            // Add the placeholder text if there was no selection
            if (chunks.selection.length <= 0) {
              chunks.selection = placeholder;
            }
          }
        } catch (e) {
          console.log(
            "Failed to add or remove strikethrough formatting from markdown. Error: " +
              e,
          );
        }
      },

      /**
       * updateMarkdown - Update the markdown attribute in this view using the
       * value of the markdown textarea
       */
      updateMarkdown: function () {
        try {
          newMarkdown = this.$(this.textarea).val();

          // The markdown attribute in the model may be a string or an array of strings.
          // Although EML211 can comprise an array of markdown elements,
          // this view will only edit the first if there are multiple.
          if (Array.isArray(this.model.get("markdown")) && markdown.length) {
            // Clone then update arary before setting it on the model
            // so that the backbone "change" event is fired.
            // See https://stackoverflow.com/a/10240697
            var newMarkdownArray = _.clone(this.model.get("markdown"));
            newMarkdownArray[0] = newMarkdown;
            this.model.set("markdown", newMarkdownArray);
          } else {
            this.model.set("markdown", newMarkdown);
          }
        } catch (e) {
          console.log("Failed to the view's markdown attribute, error: " + e);
        }
      },

      /**
       * previewMarkdown - render the markdown preview.
       */
      previewMarkdown: function () {
        try {
          var markdown = this.model.get("markdown");
          if (Array.isArray(markdown)) {
            markdown = markdown[0];
          }

          var markdownPreview = new MarkdownView({
            markdown: markdown || this.previewPlaceholder,
            showTOC: this.showTOC || false,
          });

          // Render the preview
          markdownPreview.render();
          // Add the rendered markdown to the preview tab
          this.$("#markdown-preview-" + this.cid).html(markdownPreview.el);
        } catch (e) {
          console.log(
            "Failed to preview markdown content. Error message: " + e,
          );
        }
      },
    },
  );

  return MarkdownEditorView;
});