// needs Markdown.Converter.js at the moment (function () { var util = {}, position = {}, ui = {}, doc = window.document, re = window.RegExp, nav = window.navigator, SETTINGS = { lineLength: 72 }, // Used to work around some browser bugs where we can't use feature testing. uaSniffed = { isIE: /msie/.test(nav.userAgent.toLowerCase()), isIE_5or6: /msie 6/.test(nav.userAgent.toLowerCase()) || /msie 5/.test(nav.userAgent.toLowerCase()), isOpera: /opera/.test(nav.userAgent.toLowerCase()) }; // ------------------------------------------------------------------- // YOUR CHANGES GO HERE // // I've tried to localize the things you are likely to change to // this area. // ------------------------------------------------------------------- // The text that appears on the upper part of the dialog box when // entering links. var linkDialogText = "
http://example.com/ \"optional title\"
"; var imageDialogText = "http://example.com/images/diagram.jpg \"optional title\"
"; // The default text that appears in the dialog input box when entering // links. var imageDefaultText = "http://"; var linkDefaultText = "http://"; var defaultHelpHoverTitle = "Markdown Editing Help"; // ------------------------------------------------------------------- // END OF YOUR CHANGES // ------------------------------------------------------------------- // The buttons parameter allows configuring which buttons appear // See Markdown.Editor.getDefaultButtons() for the format of the buttons parameter. Buttons // can be removed by acquiring the default buttons object and deleting the appropriate button // properties before passing the object here. Moving spacers and pointing to different sprites // can also be accomplished by changing properties in the button object. // // A help button is not displayed by default to preserve previous functionality. To include a help // button, add a "help" property to the buttons object using Markdown.Editor.MakeHelpButton(), passing // the onClick handler and optional title. Title defaults to "Markdown Editing Help". // The previous method for specifying a help button continues to be supported, so this class // remains completely backward compatible. // // The "old" method for specifying a help button: // help, if given, should have a property "handler", the click handler for the help button, // and can have an optional property "title" for the button's tooltip (defaults to "Markdown Editing Help"). // If help isn't given, no help button is created. // // The constructed editor object has the methods: // - getConverter() returns the markdown converter object that was passed to the constructor // - run() actually starts the editor; should be called after all necessary plugins are registered. Calling this more than once is a no-op. // - refreshPreview() forces the preview to be updated. This method is only available after run() was called. Markdown.Editor = function (markdownConverter, inputId, idPostfix, buttons) { if (typeof(idPostfix) === "object") { buttons = idPostfix; idPostfix = null; } idPostfix = idPostfix || ""; inputId = inputId || "wmd-input"; var hooks = this.hooks = new Markdown.HookCollection(); hooks.addNoop("onPreviewRefresh"); // called with no arguments after the preview has been refreshed hooks.addNoop("postBlockquoteCreation"); // called with the user's selection *after* the blockquote was created; should return the actual to-be-inserted text hooks.addFalse("insertImageDialog"); /* called with one parameter: a callback to be called with the URL of the image. If the application creates * its own image insertion dialog, this hook should return true, and the callback should be called with the chosen * image url (or null if the user cancelled). If this hook returns false, the default dialog will be used. */ this.getConverter = function () { return markdownConverter; } var that = this, panels; this.run = function () { if (panels) return; // already initialized panels = new PanelCollection(inputId, idPostfix); var commandManager = new CommandManager(hooks); var previewManager = new PreviewManager(markdownConverter, panels, function () { hooks.onPreviewRefresh(); }); var undoManager, uiManager; if (!/\?noundo/.test(doc.location.href)) { undoManager = new UndoManager(function () { previewManager.refresh(); if (uiManager) // not available on the first call uiManager.setUndoRedoButtonStates(); }, panels); this.textOperation = function (f) { undoManager.setCommandMode(); f(); that.refreshPreview(); } } uiManager = new UIManager(idPostfix, panels, undoManager, previewManager, commandManager, buttons); uiManager.setUndoRedoButtonStates(); var forceRefresh = that.refreshPreview = function () { previewManager.refresh(true); }; forceRefresh(); }; } // before: contains all the text in the input box BEFORE the selection. // after: contains all the text in the input box AFTER the selection. function Chunks() { } // startRegex: a regular expression to find the start tag // endRegex: a regular expresssion to find the end tag Chunks.prototype.findTags = function (startRegex, endRegex) { var chunkObj = this; var regex; if (startRegex) { regex = util.extendRegExp(startRegex, "", "$"); this.before = this.before.replace(regex, function (match) { chunkObj.startTag = chunkObj.startTag + match; return ""; }); regex = util.extendRegExp(startRegex, "^", ""); this.selection = this.selection.replace(regex, function (match) { chunkObj.startTag = chunkObj.startTag + match; return ""; }); } if (endRegex) { regex = util.extendRegExp(endRegex, "", "$"); this.selection = this.selection.replace(regex, function (match) { chunkObj.endTag = match + chunkObj.endTag; return ""; }); regex = util.extendRegExp(endRegex, "^", ""); this.after = this.after.replace(regex, function (match) { chunkObj.endTag = match + chunkObj.endTag; return ""; }); } }; // If remove is false, the whitespace is transferred // to the before/after regions. // // If remove is true, the whitespace disappears. Chunks.prototype.trimWhitespace = function (remove) { var beforeReplacer, afterReplacer, that = this; if (remove) { beforeReplacer = afterReplacer = ""; } else { beforeReplacer = function (s) { that.before += s; return ""; } afterReplacer = function (s) { that.after = s + that.after; return ""; } } this.selection = this.selection.replace(/^(\s*)/, beforeReplacer).replace(/(\s*)$/, afterReplacer); }; Chunks.prototype.skipLines = function (nLinesBefore, nLinesAfter, findExtraNewlines) { if (nLinesBefore === undefined) { nLinesBefore = 1; } if (nLinesAfter === undefined) { nLinesAfter = 1; } nLinesBefore++; nLinesAfter++; var regexText; var replacementText; // chrome bug ... documented at: http://meta.stackoverflow.com/questions/63307/blockquote-glitch-in-editor-in-chrome-6-and-7/65985#65985 if (navigator.userAgent.match(/Chrome/)) { "X".match(/()./); } this.selection = this.selection.replace(/(^\n*)/, ""); this.startTag = this.startTag + re.$1; this.selection = this.selection.replace(/(\n*$)/, ""); this.endTag = this.endTag + re.$1; this.startTag = this.startTag.replace(/(^\n*)/, ""); this.before = this.before + re.$1; this.endTag = this.endTag.replace(/(\n*$)/, ""); this.after = this.after + re.$1; if (this.before) { regexText = replacementText = ""; while (nLinesBefore--) { regexText += "\\n?"; replacementText += "\n"; } if (findExtraNewlines) { regexText = "\\n*"; } this.before = this.before.replace(new re(regexText + "$", ""), replacementText); } if (this.after) { regexText = replacementText = ""; while (nLinesAfter--) { regexText += "\\n?"; replacementText += "\n"; } if (findExtraNewlines) { regexText = "\\n*"; } this.after = this.after.replace(new re(regexText, ""), replacementText); } }; // end of Chunks // A collection of the important regions on the page. // Cached so we don't have to keep traversing the DOM. // Also holds ieCachedRange and ieCachedScrollTop, where necessary; working around // this issue: // Internet explorer has problems with CSS sprite buttons that use HTML // lists. When you click on the background image "button", IE will // select the non-existent link text and discard the selection in the // textarea. The solution to this is to cache the textarea selection // on the button's mousedown event and set a flag. In the part of the // code where we need to grab the selection, we check for the flag // and, if it's set, use the cached area instead of querying the // textarea. // // This ONLY affects Internet Explorer (tested on versions 6, 7 // and 8) and ONLY on button clicks. Keyboard shortcuts work // normally since the focus never leaves the textarea. function PanelCollection(inputId, postfix) { this.buttonBar = doc.getElementById("wmd-button-bar" + postfix); this.preview = doc.getElementById("wmd-preview" + postfix); this.input = doc.getElementById(inputId + postfix); }; // Returns true if the DOM element is visible, false if it's hidden. // Checks if display is anything other than none. util.isVisible = function (elem) { if (window.getComputedStyle) { // Most browsers return window.getComputedStyle(elem, null).getPropertyValue("display") !== "none"; } else if (elem.currentStyle) { // IE return elem.currentStyle["display"] !== "none"; } }; // Adds a listener callback to a DOM element which is fired on a specified // event. util.addEvent = function (elem, event, listener) { if (elem.attachEvent) { // IE only. The "on" is mandatory. elem.attachEvent("on" + event, listener); } else { // Other browsers. elem.addEventListener(event, listener, false); } }; // Removes a listener callback from a DOM element which is fired on a specified // event. util.removeEvent = function (elem, event, listener) { if (elem.detachEvent) { // IE only. The "on" is mandatory. elem.detachEvent("on" + event, listener); } else { // Other browsers. elem.removeEventListener(event, listener, false); } }; // Converts \r\n and \r to \n. util.fixEolChars = function (text) { text = text.replace(/\r\n/g, "\n"); text = text.replace(/\r/g, "\n"); return text; }; // Extends a regular expression. Returns a new RegExp // using pre + regex + post as the expression. // Used in a few functions where we have a base // expression and we want to pre- or append some // conditions to it (e.g. adding "$" to the end). // The flags are unchanged. // // regex is a RegExp, pre and post are strings. util.extendRegExp = function (regex, pre, post) { if (pre === null || pre === undefined) { pre = ""; } if (post === null || post === undefined) { post = ""; } var pattern = regex.toString(); var flags; // Replace the flags with empty space and store them. pattern = pattern.replace(/\/([gim]*)$/, function (wholeMatch, flagsPart) { flags = flagsPart; return ""; }); // Remove the slash delimiters on the regular expression. pattern = pattern.replace(/(^\/|\/$)/g, ""); pattern = pre + pattern + post; return new re(pattern, flags); } // UNFINISHED // The assignment in the while loop makes jslint cranky. // I'll change it to a better loop later. position.getTop = function (elem, isInner) { var result = elem.offsetTop; if (!isInner) { while (elem = elem.offsetParent) { result += elem.offsetTop; } } return result; }; position.getHeight = function (elem) { return elem.offsetHeight || elem.scrollHeight; }; position.getWidth = function (elem) { return elem.offsetWidth || elem.scrollWidth; }; position.getPageSize = function () { var scrollWidth, scrollHeight; var innerWidth, innerHeight; // It's not very clear which blocks work with which browsers. if (self.innerHeight && self.scrollMaxY) { scrollWidth = doc.body.scrollWidth; scrollHeight = self.innerHeight + self.scrollMaxY; } else if (doc.body.scrollHeight > doc.body.offsetHeight) { scrollWidth = doc.body.scrollWidth; scrollHeight = doc.body.scrollHeight; } else { scrollWidth = doc.body.offsetWidth; scrollHeight = doc.body.offsetHeight; } if (self.innerHeight) { // Non-IE browser innerWidth = self.innerWidth; innerHeight = self.innerHeight; } else if (doc.documentElement && doc.documentElement.clientHeight) { // Some versions of IE (IE 6 w/ a DOCTYPE declaration) innerWidth = doc.documentElement.clientWidth; innerHeight = doc.documentElement.clientHeight; } else if (doc.body) { // Other versions of IE innerWidth = doc.body.clientWidth; innerHeight = doc.body.clientHeight; } var maxWidth = Math.max(scrollWidth, innerWidth); var maxHeight = Math.max(scrollHeight, innerHeight); return [maxWidth, maxHeight, innerWidth, innerHeight]; }; // Handles pushing and popping TextareaStates for undo/redo commands. // I should rename the stack variables to list. function UndoManager(callback, panels) { var undoObj = this; var undoStack = []; // A stack of undo states var stackPtr = 0; // The index of the current state var mode = "none"; var lastState; // The last state var timer; // The setTimeout handle for cancelling the timer var inputStateObj; // Set the mode for later logic steps. var setMode = function (newMode, noSave) { if (mode != newMode) { mode = newMode; if (!noSave) { saveState(); } } if (!uaSniffed.isIE || mode != "moving") { timer = setTimeout(refreshState, 1); } else { inputStateObj = null; } }; var refreshState = function (isInitialState) { inputStateObj = new TextareaState(panels, isInitialState); timer = undefined; }; this.setCommandMode = function () { mode = "command"; saveState(); timer = setTimeout(refreshState, 0); }; this.canUndo = function () { return stackPtr > 1; }; this.canRedo = function () { if (undoStack[stackPtr + 1]) { return true; } return false; }; // Removes the last state and restores it. this.undo = function () { if (undoObj.canUndo()) { if (lastState) { // What about setting state -1 to null or checking for undefined? lastState.restore(); lastState = null; } else { undoStack[stackPtr] = new TextareaState(panels); undoStack[--stackPtr].restore(); if (callback) { callback(); } } } mode = "none"; panels.input.focus(); refreshState(); }; // Redo an action. this.redo = function () { if (undoObj.canRedo()) { undoStack[++stackPtr].restore(); if (callback) { callback(); } } mode = "none"; panels.input.focus(); refreshState(); }; // Push the input area state to the stack. var saveState = function () { var currState = inputStateObj || new TextareaState(panels); if (!currState) { return false; } if (mode == "moving") { if (!lastState) { lastState = currState; } return; } if (lastState) { if (undoStack[stackPtr - 1].text != lastState.text) { undoStack[stackPtr++] = lastState; } lastState = null; } undoStack[stackPtr++] = currState; undoStack[stackPtr + 1] = null; if (callback) { callback(); } }; var handleCtrlYZ = function (event) { var handled = false; if (event.ctrlKey || event.metaKey) { // IE and Opera do not support charCode. var keyCode = event.charCode || event.keyCode; var keyCodeChar = String.fromCharCode(keyCode); switch (keyCodeChar) { case "y": undoObj.redo(); handled = true; break; case "z": if (!event.shiftKey) { undoObj.undo(); } else { undoObj.redo(); } handled = true; break; } } if (handled) { if (event.preventDefault) { event.preventDefault(); } if (window.event) { window.event.returnValue = false; } return; } }; // Set the mode depending on what is going on in the input area. var handleModeChange = function (event) { if (!event.ctrlKey && !event.metaKey) { var keyCode = event.keyCode; if ((keyCode >= 33 && keyCode <= 40) || (keyCode >= 63232 && keyCode <= 63235)) { // 33 - 40: page up/dn and arrow keys // 63232 - 63235: page up/dn and arrow keys on safari setMode("moving"); } else if (keyCode == 8 || keyCode == 46 || keyCode == 127) { // 8: backspace // 46: delete // 127: delete setMode("deleting"); } else if (keyCode == 13) { // 13: Enter setMode("newlines"); } else if (keyCode == 27) { // 27: escape setMode("escape"); } else if ((keyCode < 16 || keyCode > 20) && keyCode != 91) { // 16-20 are shift, etc. // 91: left window key // I think this might be a little messed up since there are // a lot of nonprinting keys above 20. setMode("typing"); } } }; var setEventHandlers = function () { util.addEvent(panels.input, "keypress", function (event) { // keyCode 89: y // keyCode 90: z if ((event.ctrlKey || event.metaKey) && (event.keyCode == 89 || event.keyCode == 90)) { event.preventDefault(); } }); var handlePaste = function () { if (uaSniffed.isIE || (inputStateObj && inputStateObj.text != panels.input.value)) { if (timer == undefined) { mode = "paste"; saveState(); refreshState(); } } }; util.addEvent(panels.input, "keydown", handleCtrlYZ); util.addEvent(panels.input, "keydown", handleModeChange); util.addEvent(panels.input, "mousedown", function () { setMode("moving"); }); panels.input.onpaste = handlePaste; panels.input.ondrop = handlePaste; }; var init = function () { setEventHandlers(); refreshState(true); saveState(); }; init(); } // end of UndoManager // The input textarea state/contents. // This is used to implement undo/redo by the undo manager. function TextareaState(panels, isInitialState) { // Aliases var stateObj = this; var inputArea = panels.input; this.init = function () { if (!util.isVisible(inputArea)) { return; } if (!isInitialState && doc.activeElement && doc.activeElement !== inputArea) { // this happens when tabbing out of the input box return; } this.setInputAreaSelectionStartEnd(); this.scrollTop = inputArea.scrollTop; if (!this.text && inputArea.selectionStart || inputArea.selectionStart === 0) { this.text = inputArea.value; } } // Sets the selected text in the input box after we've performed an // operation. this.setInputAreaSelection = function () { if (!util.isVisible(inputArea)) { return; } if (inputArea.selectionStart !== undefined && !uaSniffed.isOpera) { inputArea.focus(); inputArea.selectionStart = stateObj.start; inputArea.selectionEnd = stateObj.end; inputArea.scrollTop = stateObj.scrollTop; } else if (doc.selection) { if (doc.activeElement && doc.activeElement !== inputArea) { return; } inputArea.focus(); var range = inputArea.createTextRange(); range.moveStart("character", -inputArea.value.length); range.moveEnd("character", -inputArea.value.length); range.moveEnd("character", stateObj.end); range.moveStart("character", stateObj.start); range.select(); } }; this.setInputAreaSelectionStartEnd = function () { if (!panels.ieCachedRange && (inputArea.selectionStart || inputArea.selectionStart === 0)) { stateObj.start = inputArea.selectionStart; stateObj.end = inputArea.selectionEnd; } else if (doc.selection) { stateObj.text = util.fixEolChars(inputArea.value); // IE loses the selection in the textarea when buttons are // clicked. On IE we cache the selection. Here, if something is cached, // we take it. var range = panels.ieCachedRange || doc.selection.createRange(); var fixedRange = util.fixEolChars(range.text); var marker = "\x07"; var markedRange = marker + fixedRange + marker; range.text = markedRange; var inputText = util.fixEolChars(inputArea.value); range.moveStart("character", -markedRange.length); range.text = fixedRange; stateObj.start = inputText.indexOf(marker); stateObj.end = inputText.lastIndexOf(marker) - marker.length; var len = stateObj.text.length - util.fixEolChars(inputArea.value).length; if (len) { range.moveStart("character", -fixedRange.length); while (len--) { fixedRange += "\n"; stateObj.end += 1; } range.text = fixedRange; } if (panels.ieCachedRange) stateObj.scrollTop = panels.ieCachedScrollTop; // this is set alongside with ieCachedRange panels.ieCachedRange = null; this.setInputAreaSelection(); } }; // Restore this state into the input area. this.restore = function () { if (stateObj.text != undefined && stateObj.text != inputArea.value) { inputArea.value = stateObj.text; } this.setInputAreaSelection(); inputArea.scrollTop = stateObj.scrollTop; }; // Gets a collection of HTML chunks from the inptut textarea. this.getChunks = function () { var chunk = new Chunks(); chunk.before = util.fixEolChars(stateObj.text.substring(0, stateObj.start)); chunk.startTag = ""; chunk.selection = util.fixEolChars(stateObj.text.substring(stateObj.start, stateObj.end)); chunk.endTag = ""; chunk.after = util.fixEolChars(stateObj.text.substring(stateObj.end)); chunk.scrollTop = stateObj.scrollTop; return chunk; }; // Sets the TextareaState properties given a chunk of markdown. this.setChunks = function (chunk) { chunk.before = chunk.before + chunk.startTag; chunk.after = chunk.endTag + chunk.after; this.start = chunk.before.length; this.end = chunk.before.length + chunk.selection.length; this.text = chunk.before + chunk.selection + chunk.after; this.scrollTop = chunk.scrollTop; }; this.init(); }; function PreviewManager(converter, panels, previewRefreshCallback) { var managerObj = this; var timeout; var elapsedTime; var oldInputText; var maxDelay = 3000; var startType = "delayed"; // The other legal value is "manual" // Adds event listeners to elements var setupEvents = function (inputElem, listener) { util.addEvent(inputElem, "input", listener); inputElem.onpaste = listener; inputElem.ondrop = listener; util.addEvent(inputElem, "keypress", listener); util.addEvent(inputElem, "keydown", listener); }; var getDocScrollTop = function () { var result = 0; if (window.innerHeight) { result = window.pageYOffset; } else if (doc.documentElement && doc.documentElement.scrollTop) { result = doc.documentElement.scrollTop; } else if (doc.body) { result = doc.body.scrollTop; } return result; }; var makePreviewHtml = function () { // If there is no registered preview panel // there is nothing to do. if (!panels.preview) return; var text = panels.input.value; if (text && text == oldInputText) { return; // Input text hasn't changed. } else { oldInputText = text; } var prevTime = new Date().getTime(); text = converter.makeHtml(text); // Calculate the processing time of the HTML creation. // It's used as the delay time in the event listener. var currTime = new Date().getTime(); elapsedTime = currTime - prevTime; pushPreviewHtml(text); }; // setTimeout is already used. Used as an event listener. var applyTimeout = function () { if (timeout) { clearTimeout(timeout); timeout = undefined; } if (startType !== "manual") { var delay = 0; if (startType === "delayed") { delay = elapsedTime; } if (delay > maxDelay) { delay = maxDelay; } timeout = setTimeout(makePreviewHtml, delay); } }; var getScaleFactor = function (panel) { if (panel.scrollHeight <= panel.clientHeight) { return 1; } return panel.scrollTop / (panel.scrollHeight - panel.clientHeight); }; var setPanelScrollTops = function () { if (panels.preview) { panels.preview.scrollTop = (panels.preview.scrollHeight - panels.preview.clientHeight) * getScaleFactor(panels.preview); } }; this.refresh = function (requiresRefresh) { if (requiresRefresh) { oldInputText = ""; makePreviewHtml(); } else { applyTimeout(); } }; this.processingTime = function () { return elapsedTime; }; var isFirstTimeFilled = true; // IE doesn't let you use innerHTML if the element is contained somewhere in a table // (which is the case for inline editing) -- in that case, detach the element, set the // value, and reattach. Yes, that *is* ridiculous. var ieSafePreviewSet = function (text) { var preview = panels.preview; var parent = preview.parentNode; var sibling = preview.nextSibling; parent.removeChild(preview); preview.innerHTML = text; if (!sibling) parent.appendChild(preview); else parent.insertBefore(preview, sibling); } var nonSuckyBrowserPreviewSet = function (text) { panels.preview.innerHTML = text; } var previewSetter; var previewSet = function (text) { if (previewSetter) return previewSetter(text); try { nonSuckyBrowserPreviewSet(text); previewSetter = nonSuckyBrowserPreviewSet; } catch (e) { previewSetter = ieSafePreviewSet; previewSetter(text); } }; var pushPreviewHtml = function (text) { var emptyTop = position.getTop(panels.input) - getDocScrollTop(); if (panels.preview) { previewSet(text); previewRefreshCallback(); } setPanelScrollTops(); if (isFirstTimeFilled) { isFirstTimeFilled = false; return; } var fullTop = position.getTop(panels.input) - getDocScrollTop(); if (uaSniffed.isIE) { setTimeout(function () { window.scrollBy(0, fullTop - emptyTop); }, 0); } else { window.scrollBy(0, fullTop - emptyTop); } }; var init = function () { setupEvents(panels.input, applyTimeout); makePreviewHtml(); if (panels.preview) { panels.preview.scrollTop = 0; } }; init(); }; // This simulates a modal dialog box and asks for the URL when you // click the hyperlink or image buttons. // // text: The html for the input box. // defaultInputText: The default value that appears in the input box. // callback: The function which is executed when the prompt is dismissed, either via OK or Cancel. // It receives a single argument; either the entered text (if OK was chosen) or null (if Cancel // was chosen). ui.prompt = function (title, text, defaultInputText, callback) { // These variables need to be declared at this level since they are used // in multiple functions. var dialog; // The dialog box. var input; // The text box where you enter the hyperlink. if (defaultInputText === undefined) { defaultInputText = ""; } // Used as a keydown event handler. Esc dismisses the prompt. // Key code 27 is ESC. var checkEscape = function (key) { var code = (key.charCode || key.keyCode); if (code === 27) { close(true); } }; // Dismisses the hyperlink input box. // isCancel is true if we don't care about the input text. // isCancel is false if we are going to keep the text. var close = function (isCancel) { util.removeEvent(doc.body, "keydown", checkEscape); var text = input.value; if (isCancel) { text = null; } else { // Fixes common pasting errors. text = text.replace(/^http:\/\/(https?|ftp):\/\//, '$1://'); if (!/^(?:https?|ftp):\/\//.test(text)) text = 'http://' + text; } $(dialog).modal('hide'); callback(text); return false; }; // Create the text input box form/window. var createDialog = function () { //One fine body…
//Ctrl+Q", icon: "icon-blockquote" }, code: { id: "wmd-code-button", title: "Code SampleCtrl+K", icon: "icon-code" }, image: { id: "wmd-image-button", title: "Image Ctrl+G", icon: "icon-picture" }, olist: { id: "wmd-olist-button", title: "Numbered List
Ctrl+O", icon: "icon-list" }, ulist: { id: "wmd-ulist-button", title: "Bulleted List
Ctrl+U", icon: "icon-bullet-list" }, heading: { id: "wmd-heading-button", title: "Heading
/
Ctrl+H", icon: "icon-header" }, hr: { id: "wmd-hr-button", title: "Horizontal Rule
Ctrl+R", icon: "icon-hr-line" }, undo: { id: "wmd-undo-button", title: "Undo - Ctrl+Z", icon: "icon-undo" }, redo: { id: "wmd-redo-button", title: /win/.test(nav.platform.toLowerCase()) ? "Redo - Ctrl+Y" : "Redo - Ctrl+Shift+Z" /* mac and other non-Windows platforms*/, icon: "icon-share-alt" } }; return result; } Markdown.Editor.getDefaultHelpButton = function() { var result = { id: "wmd-help-button", title: defaultHelpHoverTitle, isHelp: true } return result; } Markdown.Editor.MakeHelpButton = function(handler, title) { button = Markdown.Editor.getDefaultHelpButton(); if (handler) button.handler = handler; if (title) button.title = title; return button; } })();