diff --git a/markdownx/static/markdownx/js/markdownx.js b/markdownx/static/markdownx/js/markdownx.js index 83b580f..19bc645 100644 --- a/markdownx/static/markdownx/js/markdownx.js +++ b/markdownx/static/markdownx/js/markdownx.js @@ -13,7 +13,7 @@ "use strict"; exports.__esModule = true; var utils_1 = require("./utils"); -var UPLOAD_URL_ATTRIBUTE = "data-markdownx-upload-urls-path", PROCESSING_URL_ATTRIBUTE = "data-markdownx-urls-path"; +var UPLOAD_URL_ATTRIBUTE = "data-markdownx-upload-urls-path", PROCESSING_URL_ATTRIBUTE = "data-markdownx-urls-path", RESIZABILITY_ATTRIBUTE = "data-markdownx-editor-resizable", LATENCY_ATTRIBUTE = "data-markdownx-latency"; // --------------------------------------------------------------------------------------------------------------------- /** * @@ -22,7 +22,7 @@ var UPLOAD_URL_ATTRIBUTE = "data-markdownx-upload-urls-path", PROCESSING_URL_ATT * @param {string} value * @returns {string} */ -function tabKeyEvent(start, end, value) { +function applyIndentation(start, end, value) { return value.substring(0, start) + (value.substring(start, end).match(/\n/g) === null ? "\t" + value.substring(start) : value.substring(start, end).replace(/^/gm, '\t') + value.substring(end)); @@ -34,7 +34,7 @@ function tabKeyEvent(start, end, value) { * @param {string} value * @returns {string} */ -function shiftTabKeyEvent(start, end, value) { +function removeIndentation(start, end, value) { var endString = null, lineNumbers = (value.substring(start, end).match(/\n/g) || []).length; if (start === end) { // Replacing `\t` at a specific location (+/- 1 chars) where there is no selection. @@ -51,6 +51,34 @@ function shiftTabKeyEvent(start, end, value) { } return value.substring(0, start) + endString; } +/** + * + * @param start + * @param end + * @param value + * @returns {string} + */ +function applyDuplication(start, end, value) { + var pattern = new RegExp("(?:.|\n){0," + end + "}\n([^].+)(?:.|\n)*", 'm'); + switch (start) { + case end: + var line_1 = ''; + value.replace(pattern, function (match, p1) { return line_1 += p1; }); + return value.replace(line_1, line_1 + "\n" + line_1); + default: + return (value.substring(0, start) + + value.substring(start, end) + + (~value.charAt(start - 1).indexOf('\n') || ~value.charAt(start).indexOf('\n') ? '\n' : '') + + value.substring(start, end) + + value.substring(end)); + } +} +function getHeight(element) { + return Math.max(// Maximum of computed or set heights. + parseInt(window.getComputedStyle(element).height), // Height is not set in styles. + (parseInt(element.style.height) || 0) // Property's own height if set, otherwise 0. + ); +} /** * @example * @@ -64,92 +92,155 @@ function shiftTabKeyEvent(start, end, value) { */ var MarkdownX = function (editor, preview) { var _this = this; - this.editor = editor; - this.preview = preview; - this._editorIsResizable = this.editor.style.resize == 'none'; - this.timeout = null; - this.getEditorHeight = function (editor) { return editor.scrollHeight + "px"; }; + var properties = { + editor: editor, + preview: preview, + _editorIsResizable: null, + _latency: null + }; + var _initialize = function () { + _this.timeout = null; + // Events + // ---------------------------------------------------------------------------------------------- + var documentListeners = { + // ToDo: Deprecate. + object: document, + listeners: [ + { type: "drop", capture: false, listener: onHtmlEvents }, + { type: "dragover", capture: false, listener: onHtmlEvents }, + { type: "dragenter", capture: false, listener: onHtmlEvents }, + { type: "dragleave", capture: false, listener: onHtmlEvents } + ] + }, editorListeners = { + object: properties.editor, + listeners: [ + { type: "drop", capture: false, listener: onDrop }, + { type: "input", capture: true, listener: inputChanged }, + { type: "keydown", capture: true, listener: onKeyDown }, + { type: "dragover", capture: false, listener: onDragEnter }, + { type: "dragenter", capture: false, listener: onDragEnter }, + { type: "dragleave", capture: false, listener: onDragLeave }, + { type: "compositionstart", capture: true, listener: onKeyDown } + ] + }; + // Initialise + // -------------------------------------------------------- + utils_1.mountEvents(editorListeners); + utils_1.mountEvents(documentListeners); // ToDo: Deprecate. + properties.editor.style.transition = "opacity 1s ease"; + properties.editor.style.webkitTransition = "opacity 1s ease"; + // Latency must be a value >= 500 microseconds. + properties._latency = Math.max(parseInt(properties.editor.getAttribute(LATENCY_ATTRIBUTE)) || 0, 500); + properties._editorIsResizable = + (properties.editor.getAttribute(RESIZABILITY_ATTRIBUTE).match(/True/) || []).length > 0; + getMarkdown(); + inputChanged(); + utils_1.triggerCustomEvent("markdownx.init"); + }; /** * settings for ``timeout``. * * @private */ - this._markdownify = function () { + var _markdownify = function () { clearTimeout(_this.timeout); - _this.timeout = setTimeout(_this.getMarkdown, 500); + _this.timeout = setTimeout(getMarkdown, properties._latency); }; - this.updateHeight = function () { - _this._editorIsResizable ? _this.editor.style.height = _this.getEditorHeight(_this.editor) : null; - }; - this.inputChanged = function () { - _this.updateHeight(); - _this._markdownify(); - }; - // ToDo: Deprecate. - this.onHtmlEvents = function (event) { return _this._routineEventResponse(event); }; /** * Routine tasks for event handlers (e.g. default preventions). * * @param {Event} event * @private */ - this._routineEventResponse = function (event) { + var _inhibitDefault = function (event) { event.preventDefault(); event.stopPropagation(); }; - this.onDragEnter = function (event) { + var updateHeight = function () { + // Ensure that the editor is resizable before anything else. + // Change size if scroll is larger that height, otherwise do nothing. + if (properties._editorIsResizable && getHeight(properties.editor) < properties.editor.scrollHeight) + properties.editor.style.height = properties.editor.scrollHeight + "px"; + }; + var inputChanged = function () { + updateHeight(); + _markdownify(); + }; + // ToDo: Deprecate. + var onHtmlEvents = function (event) { return _inhibitDefault(event); }; + var onDragEnter = function (event) { event.dataTransfer.dropEffect = 'copy'; - _this._routineEventResponse(event); + _inhibitDefault(event); }; - this.onDragLeave = function (event) { return _this._routineEventResponse(event); }; - this.onDrop = function (event) { + var onDragLeave = function (event) { return _inhibitDefault(event); }; + var onDrop = function (event) { if (event.dataTransfer && event.dataTransfer.files.length) - Object.keys(event.dataTransfer.files).map(function (fileKey) { return _this.sendFile(event.dataTransfer.files[fileKey]); }); - _this._routineEventResponse(event); + Object.keys(event.dataTransfer.files).map(function (fileKey) { return sendFile(event.dataTransfer.files[fileKey]); }); + _inhibitDefault(event); }; - this.onKeyDown = function (event) { - // ASCII code references: - var TAB_KEY = 9, SELECTION_START = _this.editor.selectionStart; - if (event.keyCode !== TAB_KEY) + /** + * + * @param event + * @returns {KeyboardEvent} + */ + var onKeyDown = function (event) { + // `Tab` for indentation, `d` for duplication. + if (event.key !== 'Tab' && event.key !== 'd') return null; - event.preventDefault(); - var handlerFunc = event.shiftKey && event.keyCode === TAB_KEY ? shiftTabKeyEvent : tabKeyEvent; - _this.editor.value = handlerFunc(_this.editor.selectionStart, _this.editor.selectionEnd, _this.editor.value); - _this._markdownify(); - _this.editor.focus(); - _this.editor.selectionEnd = _this.editor.selectionStart = SELECTION_START; + _inhibitDefault(event); + var handlerFunc = null; + switch (event.key) { + case "Tab": + // Shift pressed: un-indent, otherwise indent. + handlerFunc = event.shiftKey ? removeIndentation : applyIndentation; + break; + case "d": + if (event.ctrlKey || event.metaKey) + handlerFunc = applyDuplication; + else + return null; + break; + default: + return null; + } + // Holding the start location before anything changes. + var SELECTION_START = properties.editor.selectionStart; + properties.editor.value = handlerFunc(properties.editor.selectionStart, properties.editor.selectionEnd, properties.editor.value); + _markdownify(); + properties.editor.focus(); + // Set the cursor location to the start location of the selection. + properties.editor.selectionEnd = properties.editor.selectionStart = SELECTION_START; return false; }; /** * * @param file - * @returns {Request} */ - this.sendFile = function (file) { - _this.editor.style.opacity = "0.3"; - var xhr = new utils_1.Request(_this.editor.getAttribute(UPLOAD_URL_ATTRIBUTE), // URL + var sendFile = function (file) { + properties.editor.style.opacity = "0.3"; + var xhr = new utils_1.Request(properties.editor.getAttribute(UPLOAD_URL_ATTRIBUTE), // URL utils_1.preparePostData({ image: file }) // Data ); xhr.success = function (resp) { var response = JSON.parse(resp); if (response.image_code) { - _this.insertImage(response.image_code); + insertImage(response.image_code); utils_1.triggerCustomEvent('markdownx.fileUploadEnd', [response]); } else if (response.image_path) { // ToDo: Deprecate. - _this.insertImage("![](\"" + response.image_path + "\")"); + insertImage("![](\"" + response.image_path + "\")"); utils_1.triggerCustomEvent('markdownx.fileUploadEnd', [response]); } else { console.error('Wrong response', response); utils_1.triggerCustomEvent('markdownx.fileUploadError', [response]); } - _this.preview.innerHTML = _this.response; - _this.editor.style.opacity = "1"; + properties.preview.innerHTML = _this.response; + properties.editor.style.opacity = "1"; }; xhr.error = function (response) { - _this.editor.style.opacity = "1"; + properties.editor.style.opacity = "1"; console.error(response); utils_1.triggerCustomEvent('fileUploadError', [response]); }; @@ -157,15 +248,14 @@ var MarkdownX = function (editor, preview) { }; /** * - * @returns {Request} */ - this.getMarkdown = function () { - var xhr = new utils_1.Request(_this.editor.getAttribute(PROCESSING_URL_ATTRIBUTE), // URL - utils_1.preparePostData({ content: _this.editor.value }) // Data + var getMarkdown = function () { + var xhr = new utils_1.Request(properties.editor.getAttribute(PROCESSING_URL_ATTRIBUTE), // URL + utils_1.preparePostData({ content: properties.editor.value }) // Data ); xhr.success = function (response) { - _this.preview.innerHTML = response; - _this.updateHeight(); + properties.preview.innerHTML = response; + updateHeight(); utils_1.triggerCustomEvent('markdownx.update', [response]); }; xhr.error = function (response) { @@ -174,46 +264,15 @@ var MarkdownX = function (editor, preview) { }; return xhr.send(); }; - this.insertImage = function (textToInsert) { - var cursorPosition = _this.editor.selectionStart, text = _this.editor.value, textBeforeCursor = text.substring(0, cursorPosition), textAfterCursor = text.substring(cursorPosition, text.length); - _this.editor.value = "" + textBeforeCursor + textToInsert + textAfterCursor; - _this.editor.selectionStart = cursorPosition + textToInsert.length; - _this.editor.selectionEnd = cursorPosition + textToInsert.length; - utils_1.triggerEvent(_this.editor, 'keyup'); - _this.inputChanged(); + var insertImage = function (textToInsert) { + var cursorPosition = properties.editor.selectionStart, text = properties.editor.value, textBeforeCursor = text.substring(0, cursorPosition), textAfterCursor = text.substring(cursorPosition, text.length); + properties.editor.value = "" + textBeforeCursor + textToInsert + textAfterCursor; + properties.editor.selectionStart = cursorPosition + textToInsert.length; + properties.editor.selectionEnd = cursorPosition + textToInsert.length; + utils_1.triggerEvent(properties.editor, 'keyup'); + inputChanged(); }; - // Events - // ---------------------------------------------------------------------------------------------- - var documentListeners = { - // ToDo: Deprecate. - object: document, - listeners: [ - { type: 'drop', capture: false, listener: this.onHtmlEvents }, - { type: 'dragover', capture: false, listener: this.onHtmlEvents }, - { type: 'dragenter', capture: false, listener: this.onHtmlEvents }, - { type: 'dragleave', capture: false, listener: this.onHtmlEvents } - ] - }, editorListeners = { - object: this.editor, - listeners: [ - { type: 'drop', capture: false, listener: this.onDrop }, - { type: 'input', capture: true, listener: this.inputChanged }, - { type: 'keydown', capture: true, listener: this.onKeyDown }, - { type: 'dragover', capture: false, listener: this.onDragEnter }, - { type: 'dragenter', capture: false, listener: this.onDragEnter }, - { type: 'dragleave', capture: false, listener: this.onDragLeave }, - { type: 'compositionstart', capture: true, listener: this.onKeyDown } - ] - }; - // Initialise - // ---------------------------------------------------------------------------------------------- - utils_1.mountEvents(editorListeners); - utils_1.mountEvents(documentListeners); // ToDo: Deprecate. - utils_1.triggerCustomEvent('markdownx.init'); - this.editor.style.transition = "opacity 1s ease"; - this.editor.style.webkitTransition = "opacity 1s ease"; - this.getMarkdown(); - this.inputChanged(); + _initialize(); }; (function (funcName, baseObj) { // The public function name defaults to window.docReady diff --git a/markdownx/static/markdownx/js/markdownx.min.js b/markdownx/static/markdownx/js/markdownx.min.js index cffd877..61bd683 100644 --- a/markdownx/static/markdownx/js/markdownx.min.js +++ b/markdownx/static/markdownx/js/markdownx.min.js @@ -1 +1 @@ -!function e(t,n,r){function o(s,u){if(!n[s]){if(!t[s]){var a="function"==typeof require&&require;if(!u&&a)return a(s,!0);if(i)return i(s,!0);var c=new Error("Cannot find module '"+s+"'");throw c.code="MODULE_NOT_FOUND",c}var d=n[s]={exports:{}};t[s][0].call(d.exports,function(e){var n=t[s][1][e];return o(n?n:e)},d,d.exports,e,t,n,r)}return n[s].exports}for(var i="function"==typeof require&&require,s=0;s0&&null!==n[e-1].match(/\t/)?e-1:e,r=n.substring(e).replace(/\t/,"")):r=o?n.substring(e,t).replace(/^\t/gm,"")+n.substring(t,n.length):n.substring(e).replace(/\t/,""),n.substring(0,e)+r}n.__esModule=!0;var i=e("./utils"),s=function(e,t){var n=this;this.editor=e,this.preview=t,this._editorIsResizable="none"==this.editor.style.resize,this.timeout=null,this.getEditorHeight=function(e){return e.scrollHeight+"px"},this._markdownify=function(){clearTimeout(n.timeout),n.timeout=setTimeout(n.getMarkdown,500)},this.updateHeight=function(){n._editorIsResizable&&(n.editor.style.height=n.getEditorHeight(n.editor))},this.inputChanged=function(){n.updateHeight(),n._markdownify()},this.onHtmlEvents=function(e){return n._routineEventResponse(e)},this._routineEventResponse=function(e){e.preventDefault(),e.stopPropagation()},this.onDragEnter=function(e){e.dataTransfer.dropEffect="copy",n._routineEventResponse(e)},this.onDragLeave=function(e){return n._routineEventResponse(e)},this.onDrop=function(e){e.dataTransfer&&e.dataTransfer.files.length&&Object.keys(e.dataTransfer.files).map(function(t){return n.sendFile(e.dataTransfer.files[t])}),n._routineEventResponse(e)},this.onKeyDown=function(e){var t=n.editor.selectionStart;if(9!==e.keyCode)return null;e.preventDefault();var i=e.shiftKey&&9===e.keyCode?o:r;return n.editor.value=i(n.editor.selectionStart,n.editor.selectionEnd,n.editor.value),n._markdownify(),n.editor.focus(),n.editor.selectionEnd=n.editor.selectionStart=t,!1},this.sendFile=function(e){n.editor.style.opacity="0.3";var t=new i.Request(n.editor.getAttribute("data-markdownx-upload-urls-path"),i.preparePostData({image:e}));return t.success=function(e){var t=JSON.parse(e);t.image_code?(n.insertImage(t.image_code),i.triggerCustomEvent("markdownx.fileUploadEnd",[t])):t.image_path?(n.insertImage('![]("'+t.image_path+'")'),i.triggerCustomEvent("markdownx.fileUploadEnd",[t])):(console.error("Wrong response",t),i.triggerCustomEvent("markdownx.fileUploadError",[t])),n.preview.innerHTML=n.response,n.editor.style.opacity="1"},t.error=function(e){n.editor.style.opacity="1",console.error(e),i.triggerCustomEvent("fileUploadError",[e])},t.send()},this.getMarkdown=function(){var e=new i.Request(n.editor.getAttribute("data-markdownx-urls-path"),i.preparePostData({content:n.editor.value}));return e.success=function(e){n.preview.innerHTML=e,n.updateHeight(),i.triggerCustomEvent("markdownx.update",[e])},e.error=function(e){console.error(e),i.triggerCustomEvent("markdownx.updateError",[e])},e.send()},this.insertImage=function(e){var t=n.editor.selectionStart,r=n.editor.value,o=r.substring(0,t),s=r.substring(t,r.length);n.editor.value=""+o+e+s,n.editor.selectionStart=t+e.length,n.editor.selectionEnd=t+e.length,i.triggerEvent(n.editor,"keyup"),n.inputChanged()};var s={object:document,listeners:[{type:"drop",capture:!1,listener:this.onHtmlEvents},{type:"dragover",capture:!1,listener:this.onHtmlEvents},{type:"dragenter",capture:!1,listener:this.onHtmlEvents},{type:"dragleave",capture:!1,listener:this.onHtmlEvents}]},u={object:this.editor,listeners:[{type:"drop",capture:!1,listener:this.onDrop},{type:"input",capture:!0,listener:this.inputChanged},{type:"keydown",capture:!0,listener:this.onKeyDown},{type:"dragover",capture:!1,listener:this.onDragEnter},{type:"dragenter",capture:!1,listener:this.onDragEnter},{type:"dragleave",capture:!1,listener:this.onDragLeave},{type:"compositionstart",capture:!0,listener:this.onKeyDown}]};i.mountEvents(u),i.mountEvents(s),i.triggerCustomEvent("markdownx.init"),this.editor.style.transition="opacity 1s ease",this.editor.style.webkitTransition="opacity 1s ease",this.getMarkdown(),this.inputChanged()};!function(e,t){e=e||"docReady",t=t||window;var n=[],r=!1,o=!1,i=function(){r||(r=!0,n.map(function(e){return e.fn.call(window,e.ctx)}),n=[])};t[e]=function(e,t){if(r)return void setTimeout(function(){return e(t)},1);n.push({fn:e,ctx:t}),"complete"===document.readyState?setTimeout(i,1):o||(document.addEventListener("DOMContentLoaded",i,!1),window.addEventListener("load",i,!1),o=!0)}}("docReady",window),docReady(function(){var e=document.querySelectorAll(".markdownx > .markdownx-editor"),t=document.querySelectorAll(".markdownx > .markdownx-preview");return i.zip(e,t).map(function(e){return new s(e[0],e[1])})})},{"./utils":2}],2:[function(e,t,n){"use strict";function r(e){if(document.cookie&&document.cookie.length){var t=document.cookie.split(";").filter(function(t){return t.indexOf(e+"=")!==-1})[0];try{return decodeURIComponent(t.trim().substring(e.length+1))}catch(t){if(t instanceof TypeError)return console.info('No cookie with key "'+e+'". Wrong name?'),null;throw t}}return null}function o(){for(var e=[],t=0;t-1&&n.splice(r,1),e.className=n.join(" ")}})}n.__esModule=!0,n.getCookie=r,n.zip=o,n.mountEvents=i,n.preparePostData=s;var l=function(){if("XMLHttpRequest"in window)return new XMLHttpRequest;try{return new ActiveXObject("Msxml2.XMLHTTP.6.0")}catch(e){}try{return new ActiveXObject("Msxml2.XMLHTTP.3.0")}catch(e){}try{return new ActiveXObject("Microsoft.XMLHTTP")}catch(e){}throw alert("Your browser belongs to history!"),new TypeError("This browser does not support AJAX requests.")},p=function(){function e(e,t){this.xhr=l(),this.url=e,this.data=t}return e.prototype.progress=function(e){e.lengthComputable&&console.log(e.loaded/e.total*100+"% uploaded")},e.prototype.error=function(e){console.error(e)},e.prototype.success=function(e){console.info(e)},e.prototype.send=function(){var e=this,t=this.success,n=this.error,r=this.progress;this.xhr.open("POST",this.url,!0),this.xhr.setRequestHeader("X-Requested-With","XMLHttpRequest"),this.xhr.upload.onprogress=function(e){return r(e)},this.xhr.onerror=function(t){n(e.xhr.responseText)},this.xhr.onload=function(n){var r=null;e.xhr.readyState==XMLHttpRequest.DONE&&(r=e.xhr.responseType&&"text"!==e.xhr.responseType?"document"===e.xhr.responseType?e.xhr.responseXML:e.xhr.response:e.xhr.responseText),t(r)},this.xhr.send(this.data)},e}();n.Request=p,n.triggerEvent=u,n.triggerCustomEvent=a,n.addClass=c,n.removeClass=d},{}]},{},[1]); \ No newline at end of file +!function e(t,r,n){function o(s,a){if(!r[s]){if(!t[s]){var u="function"==typeof require&&require;if(!a&&u)return u(s,!0);if(i)return i(s,!0);var c=new Error("Cannot find module '"+s+"'");throw c.code="MODULE_NOT_FOUND",c}var l=r[s]={exports:{}};t[s][0].call(l.exports,function(e){var r=t[s][1][e];return o(r?r:e)},l,l.exports,e,t,r,n)}return r[s].exports}for(var i="function"==typeof require&&require,s=0;s0&&null!==r[e-1].match(/\t/)?e-1:e,n=r.substring(e).replace(/\t/,"")):n=o?r.substring(e,t).replace(/^\t/gm,"")+r.substring(t,r.length):r.substring(e).replace(/\t/,""),r.substring(0,e)+n}function i(e,t,r){var n=new RegExp("(?:.|\n){0,"+t+"}\n([^].+)(?:.|\n)*","m");switch(e){case t:var o="";return r.replace(n,function(e,t){return o+=t}),r.replace(o,o+"\n"+o);default:return r.substring(0,e)+r.substring(e,t)+(~r.charAt(e-1).indexOf("\n")||~r.charAt(e).indexOf("\n")?"\n":"")+r.substring(e,t)+r.substring(t)}}function s(e){return Math.max(parseInt(window.getComputedStyle(e).height),parseInt(e.style.height)||0)}r.__esModule=!0;var a=e("./utils"),u=function(e,t){var r=this,u={editor:e,preview:t,_editorIsResizable:null,_latency:null},c=function(){clearTimeout(r.timeout),r.timeout=setTimeout(w,u._latency)},l=function(e){e.preventDefault(),e.stopPropagation()},d=function(){u._editorIsResizable&&s(u.editor)0,w(),p(),a.triggerCustomEvent("markdownx.init")}()};!function(e,t){e=e||"docReady",t=t||window;var r=[],n=!1,o=!1,i=function(){n||(n=!0,r.map(function(e){return e.fn.call(window,e.ctx)}),r=[])};t[e]=function(e,t){if(n)return void setTimeout(function(){return e(t)},1);r.push({fn:e,ctx:t}),"complete"===document.readyState?setTimeout(i,1):o||(document.addEventListener("DOMContentLoaded",i,!1),window.addEventListener("load",i,!1),o=!0)}}("docReady",window),docReady(function(){var e=document.querySelectorAll(".markdownx > .markdownx-editor"),t=document.querySelectorAll(".markdownx > .markdownx-preview");return a.zip(e,t).map(function(e){return new u(e[0],e[1])})})},{"./utils":2}],2:[function(e,t,r){"use strict";function n(e){if(document.cookie&&document.cookie.length){var t=document.cookie.split(";").filter(function(t){return t.indexOf(e+"=")!==-1})[0];try{return decodeURIComponent(t.trim().substring(e.length+1))}catch(t){if(t instanceof TypeError)return console.info('No cookie with key "'+e+'". Wrong name?'),null;throw t}}return null}function o(){for(var e=[],t=0;t-1&&r.splice(n,1),e.className=r.join(" ")}})}r.__esModule=!0,r.getCookie=n,r.zip=o,r.mountEvents=i,r.preparePostData=s;var d=function(){if("XMLHttpRequest"in window)return new XMLHttpRequest;try{return new ActiveXObject("Msxml2.XMLHTTP.6.0")}catch(e){}try{return new ActiveXObject("Msxml2.XMLHTTP.3.0")}catch(e){}try{return new ActiveXObject("Microsoft.XMLHTTP")}catch(e){}throw alert("Your browser belongs to history!"),new TypeError("This browser does not support AJAX requests.")},p=function(){function e(e,t){this.xhr=d(),this.url=e,this.data=t}return e.prototype.progress=function(e){e.lengthComputable&&console.log(e.loaded/e.total*100+"% uploaded")},e.prototype.error=function(e){console.error(e)},e.prototype.success=function(e){console.info(e)},e.prototype.send=function(){var e=this,t=this.success,r=this.error,n=this.progress;this.xhr.open("POST",this.url,!0),this.xhr.setRequestHeader("X-Requested-With","XMLHttpRequest"),this.xhr.upload.onprogress=function(e){return n(e)},this.xhr.onerror=function(t){r(e.xhr.responseText)},this.xhr.onload=function(r){var n=null;e.xhr.readyState==XMLHttpRequest.DONE&&(n=e.xhr.responseType&&"text"!==e.xhr.responseType?"document"===e.xhr.responseType?e.xhr.responseXML:e.xhr.response:e.xhr.responseText),t(n)},this.xhr.send(this.data)},e}();r.Request=p,r.triggerEvent=a,r.triggerCustomEvent=u,r.addClass=c,r.removeClass=l},{}]},{},[1]); \ No newline at end of file diff --git a/markdownx/static/markdownx/js/src/markdownx.ts b/markdownx/static/markdownx/js/src/markdownx.ts index d74b9ba..dbb5e2e 100644 --- a/markdownx/static/markdownx/js/src/markdownx.ts +++ b/markdownx/static/markdownx/js/src/markdownx.ts @@ -25,7 +25,9 @@ import { } from "./utils"; const UPLOAD_URL_ATTRIBUTE: string = "data-markdownx-upload-urls-path", - PROCESSING_URL_ATTRIBUTE: string = "data-markdownx-urls-path"; + PROCESSING_URL_ATTRIBUTE: string = "data-markdownx-urls-path", + RESIZABILITY_ATTRIBUTE: string = "data-markdownx-editor-resizable", + LATENCY_ATTRIBUTE: string = "data-markdownx-latency"; // --------------------------------------------------------------------------------------------------------------------- @@ -37,7 +39,7 @@ const UPLOAD_URL_ATTRIBUTE: string = "data-markdownx-upload-urls-path", * @param {string} value * @returns {string} */ -function tabKeyEvent(start: number, end: number, value: string): string { +function applyIndentation(start: number, end: number, value: string): string { return value.substring(0, start) + ( value.substring(start, end).match(/\n/g) === null ? @@ -55,7 +57,7 @@ function tabKeyEvent(start: number, end: number, value: string): string { * @param {string} value * @returns {string} */ -function shiftTabKeyEvent(start: number, end: number, value: string): string { +function removeIndentation(start: number, end: number, value: string): string { let endString: string = null, lineNumbers: number = (value.substring(start, end).match(/\n/g) || []).length; @@ -84,6 +86,45 @@ function shiftTabKeyEvent(start: number, end: number, value: string): string { } +/** + * + * @param start + * @param end + * @param value + * @returns {string} + */ +function applyDuplication(start, end, value): string { + + const pattern = new RegExp(`(?:.|\n){0,${end}}\n([^].+)(?:.|\n)*`, 'm'); + + switch (start) { + case end: // not selected. + let line: string = ''; + value.replace(pattern, (match, p1) => line += p1); + return value.replace(line, `${line}\n${line}`); + + default: // selected. + return ( + value.substring(0, start) + + value.substring(start, end) + + (~value.charAt(start - 1).indexOf('\n') || ~value.charAt(start).indexOf('\n') ? '\n' : '') + + value.substring(start, end) + + value.substring(end) + ) + } + +} + +function getHeight (element: HTMLElement): number { + + return Math.max( // Maximum of computed or set heights. + parseInt(window.getComputedStyle(element).height), // Height is not set in styles. + (parseInt(element.style.height) || 0) // Property's own height if set, otherwise 0. + ) + +} + + /** * @example * @@ -95,97 +136,176 @@ function shiftTabKeyEvent(start: number, end: number, value: string): string { * @param {HTMLTextAreaElement} editor - Markdown editor element. * @param {HTMLElement} preview - Markdown preview element. */ -const MarkdownX = function (editor: HTMLTextAreaElement, preview: Element) { +const MarkdownX = function (editor: HTMLTextAreaElement, preview: Element): void { - this.editor = editor; - this.preview = preview; - this._editorIsResizable = this.editor.style.resize == 'none'; - this.timeout = null; + const properties = { - this.getEditorHeight = (editor: HTMLTextAreaElement) => `${editor.scrollHeight}px`; + editor: editor, + preview: preview, + _editorIsResizable: null, + _latency: null + + }; + + const _initialize = () => { + + this.timeout = null; + + // Events + // ---------------------------------------------------------------------------------------------- + let documentListeners = { + // ToDo: Deprecate. + object: document, + listeners: [ + { type: "drop" , capture: false, listener: onHtmlEvents }, + { type: "dragover" , capture: false, listener: onHtmlEvents }, + { type: "dragenter", capture: false, listener: onHtmlEvents }, + { type: "dragleave", capture: false, listener: onHtmlEvents } + ] + }, + editorListeners = { + object: properties.editor, + listeners: [ + { type: "drop", capture: false, listener: onDrop }, + { type: "input", capture: true , listener: inputChanged }, + { type: "keydown", capture: true , listener: onKeyDown }, + { type: "dragover", capture: false, listener: onDragEnter }, + { type: "dragenter", capture: false, listener: onDragEnter }, + { type: "dragleave", capture: false, listener: onDragLeave }, + { type: "compositionstart", capture: true , listener: onKeyDown } + ] + }; + + // Initialise + // -------------------------------------------------------- + mountEvents(editorListeners); + mountEvents(documentListeners); // ToDo: Deprecate. + + properties.editor.style.transition = "opacity 1s ease"; + properties.editor.style.webkitTransition = "opacity 1s ease"; + + // Latency must be a value >= 500 microseconds. + properties._latency = Math.max(parseInt(properties.editor.getAttribute(LATENCY_ATTRIBUTE)) || 0, 500); + + properties._editorIsResizable = + (properties.editor.getAttribute(RESIZABILITY_ATTRIBUTE).match(/True/) || []).length > 0; + + getMarkdown(); + inputChanged(); + + triggerCustomEvent("markdownx.init"); + + }; /** * settings for ``timeout``. * * @private */ - this._markdownify = (): void => { + const _markdownify = (): void => { clearTimeout(this.timeout); - this.timeout = setTimeout(this.getMarkdown, 500) + this.timeout = setTimeout(getMarkdown, properties._latency) }; - this.updateHeight = (): void => { - - this._editorIsResizable ? this.editor.style.height = this.getEditorHeight(this.editor) : null - - }; - - this.inputChanged = (): void => { - - this.updateHeight(); - this._markdownify() - - }; - - // ToDo: Deprecate. - this.onHtmlEvents = (event: Event): void => this._routineEventResponse(event); - /** * Routine tasks for event handlers (e.g. default preventions). * * @param {Event} event * @private */ - this._routineEventResponse = (event: any): void => { + const _inhibitDefault = (event: any): void => { event.preventDefault(); event.stopPropagation() }; - this.onDragEnter = (event: any): void => { + const updateHeight = (): void => { + // Ensure that the editor is resizable before anything else. + // Change size if scroll is larger that height, otherwise do nothing. + if (properties._editorIsResizable && getHeight(properties.editor) < properties.editor.scrollHeight) + properties.editor.style.height = `${properties.editor.scrollHeight}px`; + + }; + + const inputChanged = (): void => { + + updateHeight(); + _markdownify() + + }; + + // ToDo: Deprecate. + const onHtmlEvents = (event: Event): void => _inhibitDefault(event); + + const onDragEnter = (event: any): void => { event.dataTransfer.dropEffect = 'copy'; - this._routineEventResponse(event) + _inhibitDefault(event) }; - this.onDragLeave = (event: Event): void => this._routineEventResponse(event); + const onDragLeave = (event: Event): void => _inhibitDefault(event); - this.onDrop = (event: any): void => { + const onDrop = (event: any): void => { if (event.dataTransfer && event.dataTransfer.files.length) - Object.keys(event.dataTransfer.files).map(fileKey => this.sendFile(event.dataTransfer.files[fileKey])); + Object.keys(event.dataTransfer.files).map(fileKey => sendFile(event.dataTransfer.files[fileKey])); - this._routineEventResponse(event); + _inhibitDefault(event); }; - this.onKeyDown = (event: any): Boolean | null => { + /** + * + * @param event + * @returns {KeyboardEvent} + */ + const onKeyDown = (event: KeyboardEvent): Boolean | null => { - // ASCII code references: - const TAB_KEY: number = 9, - SELECTION_START: number = this.editor.selectionStart; + // `Tab` for indentation, `d` for duplication. + if (event.key !== 'Tab' && event.key !== 'd') return null; - if (event.keyCode !== TAB_KEY) return null; + _inhibitDefault(event); - event.preventDefault(); - let handlerFunc = event.shiftKey && event.keyCode === TAB_KEY ? shiftTabKeyEvent : tabKeyEvent; + let handlerFunc = null; - this.editor.value = handlerFunc( - this.editor.selectionStart, - this.editor.selectionEnd, - this.editor.value + switch (event.key) { + case "Tab": // For indentation. + // Shift pressed: un-indent, otherwise indent. + handlerFunc = event.shiftKey ? removeIndentation : applyIndentation; + break; + + case "d": // For duplication. + if (event.ctrlKey || event.metaKey) // Is CTRL or CMD (on Mac) pressed? + handlerFunc = applyDuplication; + else + return null; + + break; + + default: + return null + } + + // Holding the start location before anything changes. + const SELECTION_START: number = properties.editor.selectionStart; + + properties.editor.value = handlerFunc( + properties.editor.selectionStart, + properties.editor.selectionEnd, + properties.editor.value ); - this._markdownify(); + _markdownify(); - this.editor.focus(); + properties.editor.focus(); - this.editor.selectionEnd = this.editor.selectionStart = SELECTION_START; + // Set the cursor location to the start location of the selection. + properties.editor.selectionEnd = properties.editor.selectionStart = SELECTION_START; return false @@ -194,14 +314,13 @@ const MarkdownX = function (editor: HTMLTextAreaElement, preview: Element) { /** * * @param file - * @returns {Request} */ - this.sendFile = (file: File): Request => { + const sendFile = (file: File) => { - this.editor.style.opacity = "0.3"; + properties.editor.style.opacity = "0.3"; const xhr = new Request( - this.editor.getAttribute(UPLOAD_URL_ATTRIBUTE), // URL + properties.editor.getAttribute(UPLOAD_URL_ATTRIBUTE), // URL preparePostData({image: file}) // Data ); @@ -211,13 +330,13 @@ const MarkdownX = function (editor: HTMLTextAreaElement, preview: Element) { if (response.image_code) { - this.insertImage(response.image_code); + insertImage(response.image_code); triggerCustomEvent('markdownx.fileUploadEnd', [response]) } else if (response.image_path) { // ToDo: Deprecate. - this.insertImage(`![]("${response.image_path}")`); + insertImage(`![]("${response.image_path}")`); triggerCustomEvent('markdownx.fileUploadEnd', [response]) } else { @@ -227,14 +346,14 @@ const MarkdownX = function (editor: HTMLTextAreaElement, preview: Element) { } - this.preview.innerHTML = this.response; - this.editor.style.opacity = "1"; + properties.preview.innerHTML = this.response; + properties.editor.style.opacity = "1"; }; xhr.error = (response: string): void => { - this.editor.style.opacity = "1"; + properties.editor.style.opacity = "1"; console.error(response); triggerCustomEvent('fileUploadError', [response]) @@ -246,18 +365,17 @@ const MarkdownX = function (editor: HTMLTextAreaElement, preview: Element) { /** * - * @returns {Request} */ - this.getMarkdown = (): Request => { + const getMarkdown = () => { const xhr = new Request( - this.editor.getAttribute(PROCESSING_URL_ATTRIBUTE), // URL - preparePostData({content: this.editor.value}) // Data + properties.editor.getAttribute(PROCESSING_URL_ATTRIBUTE), // URL + preparePostData({content: properties.editor.value}) // Data ); xhr.success = (response: string): void => { - this.preview.innerHTML = response; - this.updateHeight(); + properties.preview.innerHTML = response; + updateHeight(); triggerCustomEvent('markdownx.update', [response]) }; @@ -270,57 +388,23 @@ const MarkdownX = function (editor: HTMLTextAreaElement, preview: Element) { }; - this.insertImage = (textToInsert): void => { + const insertImage = (textToInsert): void => { - let cursorPosition = this.editor.selectionStart, - text = this.editor.value, + let cursorPosition = properties.editor.selectionStart, + text = properties.editor.value, textBeforeCursor = text.substring(0, cursorPosition), textAfterCursor = text.substring(cursorPosition, text.length); - this.editor.value = `${textBeforeCursor}${textToInsert}${textAfterCursor}`; - this.editor.selectionStart = cursorPosition + textToInsert.length; - this.editor.selectionEnd = cursorPosition + textToInsert.length; + properties.editor.value = `${textBeforeCursor}${textToInsert}${textAfterCursor}`; + properties.editor.selectionStart = cursorPosition + textToInsert.length; + properties.editor.selectionEnd = cursorPosition + textToInsert.length; - triggerEvent(this.editor, 'keyup'); - this.inputChanged(); + triggerEvent(properties.editor, 'keyup'); + inputChanged(); }; - // Events - // ---------------------------------------------------------------------------------------------- - let documentListeners = { - // ToDo: Deprecate. - object: document, - listeners: [ - { type: 'drop' , capture: false, listener: this.onHtmlEvents }, - { type: 'dragover' , capture: false, listener: this.onHtmlEvents }, - { type: 'dragenter', capture: false, listener: this.onHtmlEvents }, - { type: 'dragleave', capture: false, listener: this.onHtmlEvents } - ] - }, - editorListeners = { - object: this.editor, - listeners: [ - { type: 'drop', capture: false, listener: this.onDrop }, - { type: 'input', capture: true , listener: this.inputChanged }, - { type: 'keydown', capture: true , listener: this.onKeyDown }, - { type: 'dragover', capture: false, listener: this.onDragEnter }, - { type: 'dragenter', capture: false, listener: this.onDragEnter }, - { type: 'dragleave', capture: false, listener: this.onDragLeave }, - { type: 'compositionstart', capture: true , listener: this.onKeyDown } - ] - }; - - // Initialise - // ---------------------------------------------------------------------------------------------- - - mountEvents(editorListeners); - mountEvents(documentListeners); // ToDo: Deprecate. - triggerCustomEvent('markdownx.init'); - this.editor.style.transition = "opacity 1s ease"; - this.editor.style.webkitTransition = "opacity 1s ease"; - this.getMarkdown(); - this.inputChanged() + _initialize(); };