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;
});