UNPKG

vue2-medium-editor

Version:
1,425 lines (1,281 loc) 326 kB
(function webpackUniversalModuleDefinition(root, factory) { if(typeof exports === 'object' && typeof module === 'object') module.exports = factory(); else if(typeof define === 'function' && define.amd) define([], factory); else if(typeof exports === 'object') exports["vueMediumEditor"] = factory(); else root["vueMediumEditor"] = factory(); })(this, function() { return /******/ (function(modules) { // webpackBootstrap /******/ // The module cache /******/ var installedModules = {}; /******/ /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ /******/ // Check if module is in cache /******/ if(installedModules[moduleId]) { /******/ return installedModules[moduleId].exports; /******/ } /******/ // Create a new module (and put it into the cache) /******/ var module = installedModules[moduleId] = { /******/ i: moduleId, /******/ l: false, /******/ exports: {} /******/ }; /******/ /******/ // Execute the module function /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ /******/ // Flag the module as loaded /******/ module.l = true; /******/ /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ /******/ /******/ // expose the modules object (__webpack_modules__) /******/ __webpack_require__.m = modules; /******/ /******/ // expose the module cache /******/ __webpack_require__.c = installedModules; /******/ /******/ // identity function for calling harmony imports with the correct context /******/ __webpack_require__.i = function(value) { return value; }; /******/ /******/ // define getter function for harmony exports /******/ __webpack_require__.d = function(exports, name, getter) { /******/ if(!__webpack_require__.o(exports, name)) { /******/ Object.defineProperty(exports, name, { /******/ configurable: false, /******/ enumerable: true, /******/ get: getter /******/ }); /******/ } /******/ }; /******/ /******/ // getDefaultExport function for compatibility with non-harmony modules /******/ __webpack_require__.n = function(module) { /******/ var getter = module && module.__esModule ? /******/ function getDefault() { return module['default']; } : /******/ function getModuleExports() { return module; }; /******/ __webpack_require__.d(getter, 'a', getter); /******/ return getter; /******/ }; /******/ /******/ // Object.prototype.hasOwnProperty.call /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; /******/ /******/ // __webpack_public_path__ /******/ __webpack_require__.p = ""; /******/ /******/ // Load entry module and return exports /******/ return __webpack_require__(__webpack_require__.s = 3); /******/ }) /************************************************************************/ /******/ ([ /* 0 */ /***/ (function(module, exports) { // shim for using process in browser var process = module.exports = {}; // cached from whatever global is present so that test runners that stub it // don't break things. But we need to wrap it in a try catch in case it is // wrapped in strict mode code which doesn't define any globals. It's inside a // function because try/catches deoptimize in certain engines. var cachedSetTimeout; var cachedClearTimeout; function defaultSetTimout() { throw new Error('setTimeout has not been defined'); } function defaultClearTimeout () { throw new Error('clearTimeout has not been defined'); } (function () { try { if (typeof setTimeout === 'function') { cachedSetTimeout = setTimeout; } else { cachedSetTimeout = defaultSetTimout; } } catch (e) { cachedSetTimeout = defaultSetTimout; } try { if (typeof clearTimeout === 'function') { cachedClearTimeout = clearTimeout; } else { cachedClearTimeout = defaultClearTimeout; } } catch (e) { cachedClearTimeout = defaultClearTimeout; } } ()) function runTimeout(fun) { if (cachedSetTimeout === setTimeout) { //normal enviroments in sane situations return setTimeout(fun, 0); } // if setTimeout wasn't available but was latter defined if ((cachedSetTimeout === defaultSetTimout || !cachedSetTimeout) && setTimeout) { cachedSetTimeout = setTimeout; return setTimeout(fun, 0); } try { // when when somebody has screwed with setTimeout but no I.E. maddness return cachedSetTimeout(fun, 0); } catch(e){ try { // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally return cachedSetTimeout.call(null, fun, 0); } catch(e){ // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error return cachedSetTimeout.call(this, fun, 0); } } } function runClearTimeout(marker) { if (cachedClearTimeout === clearTimeout) { //normal enviroments in sane situations return clearTimeout(marker); } // if clearTimeout wasn't available but was latter defined if ((cachedClearTimeout === defaultClearTimeout || !cachedClearTimeout) && clearTimeout) { cachedClearTimeout = clearTimeout; return clearTimeout(marker); } try { // when when somebody has screwed with setTimeout but no I.E. maddness return cachedClearTimeout(marker); } catch (e){ try { // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally return cachedClearTimeout.call(null, marker); } catch (e){ // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error. // Some versions of I.E. have different rules for clearTimeout vs setTimeout return cachedClearTimeout.call(this, marker); } } } var queue = []; var draining = false; var currentQueue; var queueIndex = -1; function cleanUpNextTick() { if (!draining || !currentQueue) { return; } draining = false; if (currentQueue.length) { queue = currentQueue.concat(queue); } else { queueIndex = -1; } if (queue.length) { drainQueue(); } } function drainQueue() { if (draining) { return; } var timeout = runTimeout(cleanUpNextTick); draining = true; var len = queue.length; while(len) { currentQueue = queue; queue = []; while (++queueIndex < len) { if (currentQueue) { currentQueue[queueIndex].run(); } } queueIndex = -1; len = queue.length; } currentQueue = null; draining = false; runClearTimeout(timeout); } process.nextTick = function (fun) { var args = new Array(arguments.length - 1); if (arguments.length > 1) { for (var i = 1; i < arguments.length; i++) { args[i - 1] = arguments[i]; } } queue.push(new Item(fun, args)); if (queue.length === 1 && !draining) { runTimeout(drainQueue); } }; // v8 likes predictible objects function Item(fun, array) { this.fun = fun; this.array = array; } Item.prototype.run = function () { this.fun.apply(null, this.array); }; process.title = 'browser'; process.browser = true; process.env = {}; process.argv = []; process.version = ''; // empty string to avoid regexp issues process.versions = {}; function noop() {} process.on = noop; process.addListener = noop; process.once = noop; process.off = noop; process.removeListener = noop; process.removeAllListeners = noop; process.emit = noop; process.prependListener = noop; process.prependOnceListener = noop; process.listeners = function (name) { return [] } process.binding = function (name) { throw new Error('process.binding is not supported'); }; process.cwd = function () { return '/' }; process.chdir = function (dir) { throw new Error('process.chdir is not supported'); }; process.umask = function() { return 0; }; /***/ }), /* 1 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var _mediumEditor = __webpack_require__(4); var _mediumEditor2 = _interopRequireDefault(_mediumEditor); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } exports.default = { name: 'medium-editor', props: { text: [String], customTag: { type: [String], default: function _default() { return 'div'; } }, options: { type: [Object], default: function _default() {} } }, render: function render(h) { return h(this.customTag, { ref: 'element' }); }, mounted: function mounted(evt) { this.createAndSubscribe(); }, beforeDestroy: function beforeDestroy(evt) { this.tearDown(); }, methods: { tearDown: function tearDown() { this.api.unsubscribe('editableInput', this.emit); this.api.destroy(); }, createAndSubscribe: function createAndSubscribe() { var _this = this; this.$refs.element.innerHTML = this.text; this.api = new _mediumEditor2.default(this.$refs.element, this.options); // bind edit operations to model // we need to store the handler in order to later on detach it again this.emit = function (event) { return _this.$emit('edit', { event: event, api: _this.api }); }; this.api.subscribe('editableInput', this.emit); // emit event to give parent access to MediumEditor instance this.$emit('editorCreated', this.api); } }, watch: { text: function text(newText) { // innerHTML MUST not be performed if the text did not actually change. // otherwise, the caret position will be reset. if (newText !== this.$refs.element.innerHTML) { this.api.setContent(this.text, 0); this.$refs.element.innerHTML = this.text; } }, /** * There is currently no way to change the options of a medium editor * without destroying and re-setting up the MediumEditor object. * We only tear down the editor, if the options actually changed. * See: https://github.com/yabwe/medium-editor/issues/1129 */ options: function options(newOptions) { this.tearDown(); this.createAndSubscribe(); } }, MediumEditor: _mediumEditor2.default }; /***/ }), /* 2 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = { name: 'medium-editor-nuxt', props: { text: [String], customTag: { type: [String], default: function _default() { return 'div'; } } }, render: function render(h) { return h(this.customTag, { ref: 'element' }); } }; /***/ }), /* 3 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; /* WEBPACK VAR INJECTION */(function(process) { Object.defineProperty(exports, "__esModule", { value: true }); var _interactive = __webpack_require__(1); var _interactive2 = _interopRequireDefault(_interactive); var _renderOnly = __webpack_require__(2); var _renderOnly2 = _interopRequireDefault(_renderOnly); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } // Because Medium Editor does not support SSR, we just render in that case exports.default = process.browser ? _interactive2.default : _renderOnly2.default; /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(0))) /***/ }), /* 4 */ /***/ (function(module, exports, __webpack_require__) { /* WEBPACK VAR INJECTION */(function(process) {var __WEBPACK_AMD_DEFINE_RESULT__;/*global self, document, DOMException */ /*! @source http://purl.eligrey.com/github/classList.js/blob/master/classList.js */ // Full polyfill for browsers with no classList support if (!("classList" in document.createElement("_"))) { (function (view) { "use strict"; if (!('Element' in view)) return; var classListProp = "classList" , protoProp = "prototype" , elemCtrProto = view.Element[protoProp] , objCtr = Object , strTrim = String[protoProp].trim || function () { return this.replace(/^\s+|\s+$/g, ""); } , arrIndexOf = Array[protoProp].indexOf || function (item) { var i = 0 , len = this.length ; for (; i < len; i++) { if (i in this && this[i] === item) { return i; } } return -1; } // Vendors: please allow content code to instantiate DOMExceptions , DOMEx = function (type, message) { this.name = type; this.code = DOMException[type]; this.message = message; } , checkTokenAndGetIndex = function (classList, token) { if (token === "") { throw new DOMEx( "SYNTAX_ERR" , "An invalid or illegal string was specified" ); } if (/\s/.test(token)) { throw new DOMEx( "INVALID_CHARACTER_ERR" , "String contains an invalid character" ); } return arrIndexOf.call(classList, token); } , ClassList = function (elem) { var trimmedClasses = strTrim.call(elem.getAttribute("class") || "") , classes = trimmedClasses ? trimmedClasses.split(/\s+/) : [] , i = 0 , len = classes.length ; for (; i < len; i++) { this.push(classes[i]); } this._updateClassName = function () { elem.setAttribute("class", this.toString()); }; } , classListProto = ClassList[protoProp] = [] , classListGetter = function () { return new ClassList(this); } ; // Most DOMException implementations don't allow calling DOMException's toString() // on non-DOMExceptions. Error's toString() is sufficient here. DOMEx[protoProp] = Error[protoProp]; classListProto.item = function (i) { return this[i] || null; }; classListProto.contains = function (token) { token += ""; return checkTokenAndGetIndex(this, token) !== -1; }; classListProto.add = function () { var tokens = arguments , i = 0 , l = tokens.length , token , updated = false ; do { token = tokens[i] + ""; if (checkTokenAndGetIndex(this, token) === -1) { this.push(token); updated = true; } } while (++i < l); if (updated) { this._updateClassName(); } }; classListProto.remove = function () { var tokens = arguments , i = 0 , l = tokens.length , token , updated = false , index ; do { token = tokens[i] + ""; index = checkTokenAndGetIndex(this, token); while (index !== -1) { this.splice(index, 1); updated = true; index = checkTokenAndGetIndex(this, token); } } while (++i < l); if (updated) { this._updateClassName(); } }; classListProto.toggle = function (token, force) { token += ""; var result = this.contains(token) , method = result ? force !== true && "remove" : force !== false && "add" ; if (method) { this[method](token); } if (force === true || force === false) { return force; } else { return !result; } }; classListProto.toString = function () { return this.join(" "); }; if (objCtr.defineProperty) { var classListPropDesc = { get: classListGetter , enumerable: true , configurable: true }; try { objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc); } catch (ex) { // IE 8 doesn't support enumerable:true if (ex.number === -0x7FF5EC54) { classListPropDesc.enumerable = false; objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc); } } } else if (objCtr[protoProp].__defineGetter__) { elemCtrProto.__defineGetter__(classListProp, classListGetter); } }(self)); } /* Blob.js * A Blob implementation. * 2014-07-24 * * By Eli Grey, http://eligrey.com * By Devin Samarin, https://github.com/dsamarin * License: X11/MIT * See https://github.com/eligrey/Blob.js/blob/master/LICENSE.md */ /*global self, unescape */ /*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true, plusplus: true */ /*! @source http://purl.eligrey.com/github/Blob.js/blob/master/Blob.js */ (function (view) { "use strict"; view.URL = view.URL || view.webkitURL; if (view.Blob && view.URL) { try { new Blob; return; } catch (e) {} } // Internally we use a BlobBuilder implementation to base Blob off of // in order to support older browsers that only have BlobBuilder var BlobBuilder = view.BlobBuilder || view.WebKitBlobBuilder || view.MozBlobBuilder || (function(view) { var get_class = function(object) { return Object.prototype.toString.call(object).match(/^\[object\s(.*)\]$/)[1]; } , FakeBlobBuilder = function BlobBuilder() { this.data = []; } , FakeBlob = function Blob(data, type, encoding) { this.data = data; this.size = data.length; this.type = type; this.encoding = encoding; } , FBB_proto = FakeBlobBuilder.prototype , FB_proto = FakeBlob.prototype , FileReaderSync = view.FileReaderSync , FileException = function(type) { this.code = this[this.name = type]; } , file_ex_codes = ( "NOT_FOUND_ERR SECURITY_ERR ABORT_ERR NOT_READABLE_ERR ENCODING_ERR " + "NO_MODIFICATION_ALLOWED_ERR INVALID_STATE_ERR SYNTAX_ERR" ).split(" ") , file_ex_code = file_ex_codes.length , real_URL = view.URL || view.webkitURL || view , real_create_object_URL = real_URL.createObjectURL , real_revoke_object_URL = real_URL.revokeObjectURL , URL = real_URL , btoa = view.btoa , atob = view.atob , ArrayBuffer = view.ArrayBuffer , Uint8Array = view.Uint8Array , origin = /^[\w-]+:\/*\[?[\w\.:-]+\]?(?::[0-9]+)?/ ; FakeBlob.fake = FB_proto.fake = true; while (file_ex_code--) { FileException.prototype[file_ex_codes[file_ex_code]] = file_ex_code + 1; } // Polyfill URL if (!real_URL.createObjectURL) { URL = view.URL = function(uri) { var uri_info = document.createElementNS("http://www.w3.org/1999/xhtml", "a") , uri_origin ; uri_info.href = uri; if (!("origin" in uri_info)) { if (uri_info.protocol.toLowerCase() === "data:") { uri_info.origin = null; } else { uri_origin = uri.match(origin); uri_info.origin = uri_origin && uri_origin[1]; } } return uri_info; }; } URL.createObjectURL = function(blob) { var type = blob.type , data_URI_header ; if (type === null) { type = "application/octet-stream"; } if (blob instanceof FakeBlob) { data_URI_header = "data:" + type; if (blob.encoding === "base64") { return data_URI_header + ";base64," + blob.data; } else if (blob.encoding === "URI") { return data_URI_header + "," + decodeURIComponent(blob.data); } if (btoa) { return data_URI_header + ";base64," + btoa(blob.data); } else { return data_URI_header + "," + encodeURIComponent(blob.data); } } else if (real_create_object_URL) { return real_create_object_URL.call(real_URL, blob); } }; URL.revokeObjectURL = function(object_URL) { if (object_URL.substring(0, 5) !== "data:" && real_revoke_object_URL) { real_revoke_object_URL.call(real_URL, object_URL); } }; FBB_proto.append = function(data/*, endings*/) { var bb = this.data; // decode data to a binary string if (Uint8Array && (data instanceof ArrayBuffer || data instanceof Uint8Array)) { var str = "" , buf = new Uint8Array(data) , i = 0 , buf_len = buf.length ; for (; i < buf_len; i++) { str += String.fromCharCode(buf[i]); } bb.push(str); } else if (get_class(data) === "Blob" || get_class(data) === "File") { if (FileReaderSync) { var fr = new FileReaderSync; bb.push(fr.readAsBinaryString(data)); } else { // async FileReader won't work as BlobBuilder is sync throw new FileException("NOT_READABLE_ERR"); } } else if (data instanceof FakeBlob) { if (data.encoding === "base64" && atob) { bb.push(atob(data.data)); } else if (data.encoding === "URI") { bb.push(decodeURIComponent(data.data)); } else if (data.encoding === "raw") { bb.push(data.data); } } else { if (typeof data !== "string") { data += ""; // convert unsupported types to strings } // decode UTF-16 to binary string bb.push(unescape(encodeURIComponent(data))); } }; FBB_proto.getBlob = function(type) { if (!arguments.length) { type = null; } return new FakeBlob(this.data.join(""), type, "raw"); }; FBB_proto.toString = function() { return "[object BlobBuilder]"; }; FB_proto.slice = function(start, end, type) { var args = arguments.length; if (args < 3) { type = null; } return new FakeBlob( this.data.slice(start, args > 1 ? end : this.data.length) , type , this.encoding ); }; FB_proto.toString = function() { return "[object Blob]"; }; FB_proto.close = function() { this.size = 0; delete this.data; }; return FakeBlobBuilder; }(view)); view.Blob = function(blobParts, options) { var type = options ? (options.type || "") : ""; var builder = new BlobBuilder(); if (blobParts) { for (var i = 0, len = blobParts.length; i < len; i++) { if (Uint8Array && blobParts[i] instanceof Uint8Array) { builder.append(blobParts[i].buffer); } else { builder.append(blobParts[i]); } } } var blob = builder.getBlob(type); if (!blob.slice && blob.webkitSlice) { blob.slice = blob.webkitSlice; } return blob; }; var getPrototypeOf = Object.getPrototypeOf || function(object) { return object.__proto__; }; view.Blob.prototype = getPrototypeOf(new view.Blob()); }(typeof self !== "undefined" && self || typeof window !== "undefined" && window || this.content || this)); (function (root, factory) { 'use strict'; var isElectron = typeof module === 'object' && typeof process !== 'undefined' && process && process.versions && process.versions.electron; if (!isElectron && typeof module === 'object') { module.exports = factory; } else if (true) { !(__WEBPACK_AMD_DEFINE_RESULT__ = function () { return factory; }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); } else { root.MediumEditor = factory; } }(this, function () { 'use strict'; function MediumEditor(elements, options) { 'use strict'; return this.init(elements, options); } MediumEditor.extensions = {}; /*jshint unused: true */ (function (window) { 'use strict'; function copyInto(overwrite, dest) { var prop, sources = Array.prototype.slice.call(arguments, 2); dest = dest || {}; for (var i = 0; i < sources.length; i++) { var source = sources[i]; if (source) { for (prop in source) { if (source.hasOwnProperty(prop) && typeof source[prop] !== 'undefined' && (overwrite || dest.hasOwnProperty(prop) === false)) { dest[prop] = source[prop]; } } } } return dest; } // https://developer.mozilla.org/en-US/docs/Web/API/Node/contains // Some browsers (including phantom) don't return true for Node.contains(child) // if child is a text node. Detect these cases here and use a fallback // for calls to Util.isDescendant() var nodeContainsWorksWithTextNodes = false; try { var testParent = document.createElement('div'), testText = document.createTextNode(' '); testParent.appendChild(testText); nodeContainsWorksWithTextNodes = testParent.contains(testText); } catch (exc) {} var Util = { // http://stackoverflow.com/questions/17907445/how-to-detect-ie11#comment30165888_17907562 // by rg89 isIE: ((navigator.appName === 'Microsoft Internet Explorer') || ((navigator.appName === 'Netscape') && (new RegExp('Trident/.*rv:([0-9]{1,}[.0-9]{0,})').exec(navigator.userAgent) !== null))), isEdge: (/Edge\/\d+/).exec(navigator.userAgent) !== null, // if firefox isFF: (navigator.userAgent.toLowerCase().indexOf('firefox') > -1), // http://stackoverflow.com/a/11752084/569101 isMac: (window.navigator.platform.toUpperCase().indexOf('MAC') >= 0), // https://github.com/jashkenas/underscore // Lonely letter MUST USE the uppercase code keyCode: { BACKSPACE: 8, TAB: 9, ENTER: 13, ESCAPE: 27, SPACE: 32, DELETE: 46, K: 75, // K keycode, and not k M: 77, V: 86 }, /** * Returns true if it's metaKey on Mac, or ctrlKey on non-Mac. * See #591 */ isMetaCtrlKey: function (event) { if ((Util.isMac && event.metaKey) || (!Util.isMac && event.ctrlKey)) { return true; } return false; }, /** * Returns true if the key associated to the event is inside keys array * * @see : https://github.com/jquery/jquery/blob/0705be475092aede1eddae01319ec931fb9c65fc/src/event.js#L473-L484 * @see : http://stackoverflow.com/q/4471582/569101 */ isKey: function (event, keys) { var keyCode = Util.getKeyCode(event); // it's not an array let's just compare strings! if (false === Array.isArray(keys)) { return keyCode === keys; } if (-1 === keys.indexOf(keyCode)) { return false; } return true; }, getKeyCode: function (event) { var keyCode = event.which; // getting the key code from event if (null === keyCode) { keyCode = event.charCode !== null ? event.charCode : event.keyCode; } return keyCode; }, blockContainerElementNames: [ // elements our editor generates 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre', 'ul', 'li', 'ol', // all other known block elements 'address', 'article', 'aside', 'audio', 'canvas', 'dd', 'dl', 'dt', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'header', 'hgroup', 'main', 'nav', 'noscript', 'output', 'section', 'video', 'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td' ], emptyElementNames: ['br', 'col', 'colgroup', 'hr', 'img', 'input', 'source', 'wbr'], extend: function extend(/* dest, source1, source2, ...*/) { var args = [true].concat(Array.prototype.slice.call(arguments)); return copyInto.apply(this, args); }, defaults: function defaults(/*dest, source1, source2, ...*/) { var args = [false].concat(Array.prototype.slice.call(arguments)); return copyInto.apply(this, args); }, /* * Create a link around the provided text nodes which must be adjacent to each other and all be * descendants of the same closest block container. If the preconditions are not met, unexpected * behavior will result. */ createLink: function (document, textNodes, href, target) { var anchor = document.createElement('a'); Util.moveTextRangeIntoElement(textNodes[0], textNodes[textNodes.length - 1], anchor); anchor.setAttribute('href', href); if (target) { if (target === '_blank') { anchor.setAttribute('rel', 'noopener noreferrer'); } anchor.setAttribute('target', target); } return anchor; }, /* * Given the provided match in the format {start: 1, end: 2} where start and end are indices into the * textContent of the provided element argument, modify the DOM inside element to ensure that the text * identified by the provided match can be returned as text nodes that contain exactly that text, without * any additional text at the beginning or end of the returned array of adjacent text nodes. * * The only DOM manipulation performed by this function is splitting the text nodes, non-text nodes are * not affected in any way. */ findOrCreateMatchingTextNodes: function (document, element, match) { var treeWalker = document.createTreeWalker(element, NodeFilter.SHOW_ALL, null, false), matchedNodes = [], currentTextIndex = 0, startReached = false, currentNode = null, newNode = null; while ((currentNode = treeWalker.nextNode()) !== null) { if (currentNode.nodeType > 3) { continue; } else if (currentNode.nodeType === 3) { if (!startReached && match.start < (currentTextIndex + currentNode.nodeValue.length)) { startReached = true; newNode = Util.splitStartNodeIfNeeded(currentNode, match.start, currentTextIndex); } if (startReached) { Util.splitEndNodeIfNeeded(currentNode, newNode, match.end, currentTextIndex); } if (startReached && currentTextIndex === match.end) { break; // Found the node(s) corresponding to the link. Break out and move on to the next. } else if (startReached && currentTextIndex > (match.end + 1)) { throw new Error('PerformLinking overshot the target!'); // should never happen... } if (startReached) { matchedNodes.push(newNode || currentNode); } currentTextIndex += currentNode.nodeValue.length; if (newNode !== null) { currentTextIndex += newNode.nodeValue.length; // Skip the newNode as we'll already have pushed it to the matches treeWalker.nextNode(); } newNode = null; } else if (currentNode.tagName.toLowerCase() === 'img') { if (!startReached && (match.start <= currentTextIndex)) { startReached = true; } if (startReached) { matchedNodes.push(currentNode); } } } return matchedNodes; }, /* * Given the provided text node and text coordinates, split the text node if needed to make it align * precisely with the coordinates. * * This function is intended to be called from Util.findOrCreateMatchingTextNodes. */ splitStartNodeIfNeeded: function (currentNode, matchStartIndex, currentTextIndex) { if (matchStartIndex !== currentTextIndex) { return currentNode.splitText(matchStartIndex - currentTextIndex); } return null; }, /* * Given the provided text node and text coordinates, split the text node if needed to make it align * precisely with the coordinates. The newNode argument should from the result of Util.splitStartNodeIfNeeded, * if that function has been called on the same currentNode. * * This function is intended to be called from Util.findOrCreateMatchingTextNodes. */ splitEndNodeIfNeeded: function (currentNode, newNode, matchEndIndex, currentTextIndex) { var textIndexOfEndOfFarthestNode, endSplitPoint; textIndexOfEndOfFarthestNode = currentTextIndex + currentNode.nodeValue.length + (newNode ? newNode.nodeValue.length : 0) - 1; endSplitPoint = matchEndIndex - currentTextIndex - (newNode ? currentNode.nodeValue.length : 0); if (textIndexOfEndOfFarthestNode >= matchEndIndex && currentTextIndex !== textIndexOfEndOfFarthestNode && endSplitPoint !== 0) { (newNode || currentNode).splitText(endSplitPoint); } }, /* * Take an element, and break up all of its text content into unique pieces such that: * 1) All text content of the elements are in separate blocks. No piece of text content should span * across multiple blocks. This means no element return by this function should have * any blocks as children. * 2) The union of the textcontent of all of the elements returned here covers all * of the text within the element. * * * EXAMPLE: * In the event that we have something like: * * <blockquote> * <p>Some Text</p> * <ol> * <li>List Item 1</li> * <li>List Item 2</li> * </ol> * </blockquote> * * This function would return these elements as an array: * [ <p>Some Text</p>, <li>List Item 1</li>, <li>List Item 2</li> ] * * Since the <blockquote> and <ol> elements contain blocks within them they are not returned. * Since the <p> and <li>'s don't contain block elements and cover all the text content of the * <blockquote> container, they are the elements returned. */ splitByBlockElements: function (element) { if (element.nodeType !== 3 && element.nodeType !== 1) { return []; } var toRet = [], blockElementQuery = MediumEditor.util.blockContainerElementNames.join(','); if (element.nodeType === 3 || element.querySelectorAll(blockElementQuery).length === 0) { return [element]; } for (var i = 0; i < element.childNodes.length; i++) { var child = element.childNodes[i]; if (child.nodeType === 3) { toRet.push(child); } else if (child.nodeType === 1) { var blockElements = child.querySelectorAll(blockElementQuery); if (blockElements.length === 0) { toRet.push(child); } else { toRet = toRet.concat(MediumEditor.util.splitByBlockElements(child)); } } } return toRet; }, // Find the next node in the DOM tree that represents any text that is being // displayed directly next to the targetNode (passed as an argument) // Text that appears directly next to the current node can be: // - A sibling text node // - A descendant of a sibling element // - A sibling text node of an ancestor // - A descendant of a sibling element of an ancestor findAdjacentTextNodeWithContent: function findAdjacentTextNodeWithContent(rootNode, targetNode, ownerDocument) { var pastTarget = false, nextNode, nodeIterator = ownerDocument.createNodeIterator(rootNode, NodeFilter.SHOW_TEXT, null, false); // Use a native NodeIterator to iterate over all the text nodes that are descendants // of the rootNode. Once past the targetNode, choose the first non-empty text node nextNode = nodeIterator.nextNode(); while (nextNode) { if (nextNode === targetNode) { pastTarget = true; } else if (pastTarget) { if (nextNode.nodeType === 3 && nextNode.nodeValue && nextNode.nodeValue.trim().length > 0) { break; } } nextNode = nodeIterator.nextNode(); } return nextNode; }, // Find an element's previous sibling within a medium-editor element // If one doesn't exist, find the closest ancestor's previous sibling findPreviousSibling: function (node) { if (!node || Util.isMediumEditorElement(node)) { return false; } var previousSibling = node.previousSibling; while (!previousSibling && !Util.isMediumEditorElement(node.parentNode)) { node = node.parentNode; previousSibling = node.previousSibling; } return previousSibling; }, isDescendant: function isDescendant(parent, child, checkEquality) { if (!parent || !child) { return false; } if (parent === child) { return !!checkEquality; } // If parent is not an element, it can't have any descendants if (parent.nodeType !== 1) { return false; } if (nodeContainsWorksWithTextNodes || child.nodeType !== 3) { return parent.contains(child); } var node = child.parentNode; while (node !== null) { if (node === parent) { return true; } node = node.parentNode; } return false; }, // https://github.com/jashkenas/underscore isElement: function isElement(obj) { return !!(obj && obj.nodeType === 1); }, // https://github.com/jashkenas/underscore throttle: function (func, wait) { var THROTTLE_INTERVAL = 50, context, args, result, timeout = null, previous = 0, later = function () { previous = Date.now(); timeout = null; result = func.apply(context, args); if (!timeout) { context = args = null; } }; if (!wait && wait !== 0) { wait = THROTTLE_INTERVAL; } return function () { var now = Date.now(), remaining = wait - (now - previous); context = this; args = arguments; if (remaining <= 0 || remaining > wait) { if (timeout) { clearTimeout(timeout); timeout = null; } previous = now; result = func.apply(context, args); if (!timeout) { context = args = null; } } else if (!timeout) { timeout = setTimeout(later, remaining); } return result; }; }, traverseUp: function (current, testElementFunction) { if (!current) { return false; } do { if (current.nodeType === 1) { if (testElementFunction(current)) { return current; } // do not traverse upwards past the nearest containing editor if (Util.isMediumEditorElement(current)) { return false; } } current = current.parentNode; } while (current); return false; }, htmlEntities: function (str) { // converts special characters (like <) into their escaped/encoded values (like &lt;). // This allows you to show to display the string without the browser reading it as HTML. return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'); }, // http://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div insertHTMLCommand: function (doc, html) { var selection, range, el, fragment, node, lastNode, toReplace, res = false, ecArgs = ['insertHTML', false, html]; /* Edge's implementation of insertHTML is just buggy right now: * - Doesn't allow leading white space at the beginning of an element * - Found a case when a <font size="2"> tag was inserted when calling alignCenter inside a blockquote * * There are likely other bugs, these are just the ones we found so far. * For now, let's just use the same fallback we did for IE */ if (!MediumEditor.util.isEdge && doc.queryCommandSupported('insertHTML')) { try { return doc.execCommand.apply(doc, ecArgs); } catch (ignore) {} } selection = doc.getSelection(); if (selection.rangeCount) { range = selection.getRangeAt(0); toReplace = range.commonAncestorContainer; // https://github.com/yabwe/medium-editor/issues/748 // If the selection is an empty editor element, create a temporary text node inside of the editor // and select it so that we don't delete the editor element if (Util.isMediumEditorElement(toReplace) && !toReplace.firstChild) { range.selectNode(toReplace.appendChild(doc.createTextNode(''))); } else if ((toReplace.nodeType === 3 && range.startOffset === 0 && range.endOffset === toReplace.nodeValue.length) || (toReplace.nodeType !== 3 && toReplace.innerHTML === range.toString())) { // Ensure range covers maximum amount of nodes as possible // By moving up the DOM and selecting ancestors whose only child is the range while (!Util.isMediumEditorElement(toReplace) && toReplace.parentNode && toReplace.parentNode.childNodes.length === 1 && !Util.isMediumEditorElement(toReplace.parentNode)) { toReplace = toReplace.parentNode; } range.selectNode(toReplace); } range.deleteContents(); el = doc.createElement('div'); el.innerHTML = html; fragment = doc.createDocumentFragment(); while (el.firstChild) { node = el.firstChild; lastNode = fragment.appendChild(node); } range.insertNode(fragment); // Preserve the selection: if (lastNode) { range = range.cloneRange(); range.setStartAfter(lastNode); range.collapse(true); MediumEditor.selection.selectRange(doc, range); } res = true; } // https://github.com/yabwe/medium-editor/issues/992 // If we're monitoring calls to execCommand, notify listeners as if a real call had happened if (doc.execCommand.callListeners) { doc.execCommand.callListeners(ecArgs, res); } return res; }, execFormatBlock: function (doc, tagName) { // Get the top level block element that contains the selection var blockContainer = Util.getTopBlockContainer(MediumEditor.selection.getSelectionStart(doc)), childNodes; // Special handling for blockquote if (tagName === 'blockquote') { if (blockContainer) { childNodes = Array.prototype.slice.call(blockContainer.childNodes); // Check if the blockquote has a block element as a child (nested blocks) if (childNodes.some(function (childNode) { return Util.isBlockContainer(childNode); })) { // FF handles blockquote differently on formatBlock // allowing nesting, we need to use outdent // https://developer.mozilla.org/en-US/docs/Rich-Text_Editing_in_Mozilla return doc.execCommand('outdent', false, null); } } // When IE blockquote needs to be called as indent // http://stackoverflow.com/questions/1816223/rich-text-editor-with-blockquote-function/1821777#1821777 if (Util.isIE) { return doc.execCommand('indent', false, tagName); } } // If the blockContainer is already the element type being passed in // treat it as 'undo' formatting and just convert it to a <p> if (blockContainer && tagName === blockContainer.nodeName.toLowerCase()) { tagName = 'p'; } // When IE we need to add <> to heading elements // http://stackoverflow.com/questions/10741831/execcommand-formatblock-headings-in-ie if (Util.isIE) { tagName = '<' + tagName + '>'; } // When FF, IE and Edge, we have to handle blockquote node seperately as 'formatblock' does not work. // https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand#Commands if (blockContainer && blockContainer.nodeName.toLowerCase() === 'blockquote') { // For IE, just use outdent if (Util.isIE && tagName === '<p>') { return doc.execCommand('outdent', false, tagName); } // For Firefox and Edge, make sure there's a nested block element before calling outdent if ((Util.isFF || Util.isEdge) && tagName === 'p') { childNodes = Array.prototype.slice.call(blockContainer.childNodes); // If there are some non-block elements we need to wrap everything in a <p> before we outdent if (childNodes.some(function (childNode) { return !Util.isBlockContainer(childNode); })) { doc.execCommand('formatBlock', false, tagName); } return doc.execCommand('outdent', false, tagName); } } return doc.execCommand('formatBlock', false, tagName); }, /** * Set target to blank on the given el element * * TODO: not sure if this should be here * * When creating a link (using core -> createLink) the selection returned by Firefox will be the parent of the created link * instead of the created link itself (as it is for Chrome for example), so we retrieve all "a" children to grab the good one by * using `anchorUrl` to ensure that we are adding target="_blank" on the good one. * This isn't a bulletproof solution anyway .. */ setTargetBlank: function (el, anchorUrl) { var i, url = anchorUrl || false; if (el.nodeName.toLowerCase() === 'a') { el.target = '_blank'; el.rel = 'noopener noreferrer'; } else { el = el.getElementsByTagName('a'); for (i = 0; i < el.length; i += 1) { if (false === url || url === el[i].attributes.href.value) { el[i].target = '_blank'; el[i].rel = 'noopener noreferrer'; } } } }, /* * this function is called to explicitly remove the target='_blank' as FF holds on to _blank value even * after unchecking the checkbox on anchor form */ r