Last active
December 20, 2017 14:20
-
-
Save thedavidmeister/f55b91c8e5e0c0e64e11e0c25f0ff3fe to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
(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["BalloonEditor"] = factory(); | |
else | |
root["BalloonEditor"] = factory(); | |
})(typeof self !== 'undefined' ? self : 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; | |
/******/ | |
/******/ // 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 = 4); | |
/******/ }) | |
/************************************************************************/ | |
/******/ ([ | |
/* 0 */ | |
/***/ (function(module, exports) { | |
/* | |
MIT License http://www.opensource.org/licenses/mit-license.php | |
Author Tobias Koppers @sokra | |
*/ | |
// css base code, injected by the css-loader | |
module.exports = function(useSourceMap) { | |
var list = []; | |
// return the list of modules as css string | |
list.toString = function toString() { | |
return this.map(function (item) { | |
var content = cssWithMappingToString(item, useSourceMap); | |
if(item[2]) { | |
return "@media " + item[2] + "{" + content + "}"; | |
} else { | |
return content; | |
} | |
}).join(""); | |
}; | |
// import a list of modules into the list | |
list.i = function(modules, mediaQuery) { | |
if(typeof modules === "string") | |
modules = [[null, modules, ""]]; | |
var alreadyImportedModules = {}; | |
for(var i = 0; i < this.length; i++) { | |
var id = this[i][0]; | |
if(typeof id === "number") | |
alreadyImportedModules[id] = true; | |
} | |
for(i = 0; i < modules.length; i++) { | |
var item = modules[i]; | |
// skip already imported module | |
// this implementation is not 100% perfect for weird media query combinations | |
// when a module is imported multiple times with different media queries. | |
// I hope this will never occur (Hey this way we have smaller bundles) | |
if(typeof item[0] !== "number" || !alreadyImportedModules[item[0]]) { | |
if(mediaQuery && !item[2]) { | |
item[2] = mediaQuery; | |
} else if(mediaQuery) { | |
item[2] = "(" + item[2] + ") and (" + mediaQuery + ")"; | |
} | |
list.push(item); | |
} | |
} | |
}; | |
return list; | |
}; | |
function cssWithMappingToString(item, useSourceMap) { | |
var content = item[1] || ''; | |
var cssMapping = item[3]; | |
if (!cssMapping) { | |
return content; | |
} | |
if (useSourceMap && typeof btoa === 'function') { | |
var sourceMapping = toComment(cssMapping); | |
var sourceURLs = cssMapping.sources.map(function (source) { | |
return '/*# sourceURL=' + cssMapping.sourceRoot + source + ' */' | |
}); | |
return [content].concat(sourceURLs).concat([sourceMapping]).join('\n'); | |
} | |
return [content].join('\n'); | |
} | |
// Adapted from convert-source-map (MIT) | |
function toComment(sourceMap) { | |
// eslint-disable-next-line no-undef | |
var base64 = btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap)))); | |
var data = 'sourceMappingURL=data:application/json;charset=utf-8;base64,' + base64; | |
return '/*# ' + data + ' */'; | |
} | |
/***/ }), | |
/* 1 */ | |
/***/ (function(module, exports, __webpack_require__) { | |
/* | |
MIT License http://www.opensource.org/licenses/mit-license.php | |
Author Tobias Koppers @sokra | |
*/ | |
var stylesInDom = {}; | |
var memoize = function (fn) { | |
var memo; | |
return function () { | |
if (typeof memo === "undefined") memo = fn.apply(this, arguments); | |
return memo; | |
}; | |
}; | |
var isOldIE = memoize(function () { | |
// Test for IE <= 9 as proposed by Browserhacks | |
// @see http://browserhacks.com/#hack-e71d8692f65334173fee715c222cb805 | |
// Tests for existence of standard globals is to allow style-loader | |
// to operate correctly into non-standard environments | |
// @see https://github.com/webpack-contrib/style-loader/issues/177 | |
return window && document && document.all && !window.atob; | |
}); | |
var getElement = (function (fn) { | |
var memo = {}; | |
return function(selector) { | |
if (typeof memo[selector] === "undefined") { | |
memo[selector] = fn.call(this, selector); | |
} | |
return memo[selector] | |
}; | |
})(function (target) { | |
return document.querySelector(target) | |
}); | |
var singleton = null; | |
var singletonCounter = 0; | |
var stylesInsertedAtTop = []; | |
var fixUrls = __webpack_require__(11); | |
module.exports = function(list, options) { | |
if (typeof DEBUG !== "undefined" && DEBUG) { | |
if (typeof document !== "object") throw new Error("The style-loader cannot be used in a non-browser environment"); | |
} | |
options = options || {}; | |
options.attrs = typeof options.attrs === "object" ? options.attrs : {}; | |
// Force single-tag solution on IE6-9, which has a hard limit on the # of <style> | |
// tags it will allow on a page | |
if (!options.singleton) options.singleton = isOldIE(); | |
// By default, add <style> tags to the <head> element | |
if (!options.insertInto) options.insertInto = "head"; | |
// By default, add <style> tags to the bottom of the target | |
if (!options.insertAt) options.insertAt = "bottom"; | |
var styles = listToStyles(list, options); | |
addStylesToDom(styles, options); | |
return function update (newList) { | |
var mayRemove = []; | |
for (var i = 0; i < styles.length; i++) { | |
var item = styles[i]; | |
var domStyle = stylesInDom[item.id]; | |
domStyle.refs--; | |
mayRemove.push(domStyle); | |
} | |
if(newList) { | |
var newStyles = listToStyles(newList, options); | |
addStylesToDom(newStyles, options); | |
} | |
for (var i = 0; i < mayRemove.length; i++) { | |
var domStyle = mayRemove[i]; | |
if(domStyle.refs === 0) { | |
for (var j = 0; j < domStyle.parts.length; j++) domStyle.parts[j](); | |
delete stylesInDom[domStyle.id]; | |
} | |
} | |
}; | |
}; | |
function addStylesToDom (styles, options) { | |
for (var i = 0; i < styles.length; i++) { | |
var item = styles[i]; | |
var domStyle = stylesInDom[item.id]; | |
if(domStyle) { | |
domStyle.refs++; | |
for(var j = 0; j < domStyle.parts.length; j++) { | |
domStyle.parts[j](item.parts[j]); | |
} | |
for(; j < item.parts.length; j++) { | |
domStyle.parts.push(addStyle(item.parts[j], options)); | |
} | |
} else { | |
var parts = []; | |
for(var j = 0; j < item.parts.length; j++) { | |
parts.push(addStyle(item.parts[j], options)); | |
} | |
stylesInDom[item.id] = {id: item.id, refs: 1, parts: parts}; | |
} | |
} | |
} | |
function listToStyles (list, options) { | |
var styles = []; | |
var newStyles = {}; | |
for (var i = 0; i < list.length; i++) { | |
var item = list[i]; | |
var id = options.base ? item[0] + options.base : item[0]; | |
var css = item[1]; | |
var media = item[2]; | |
var sourceMap = item[3]; | |
var part = {css: css, media: media, sourceMap: sourceMap}; | |
if(!newStyles[id]) styles.push(newStyles[id] = {id: id, parts: [part]}); | |
else newStyles[id].parts.push(part); | |
} | |
return styles; | |
} | |
function insertStyleElement (options, style) { | |
var target = getElement(options.insertInto) | |
if (!target) { | |
throw new Error("Couldn't find a style target. This probably means that the value for the 'insertInto' parameter is invalid."); | |
} | |
var lastStyleElementInsertedAtTop = stylesInsertedAtTop[stylesInsertedAtTop.length - 1]; | |
if (options.insertAt === "top") { | |
if (!lastStyleElementInsertedAtTop) { | |
target.insertBefore(style, target.firstChild); | |
} else if (lastStyleElementInsertedAtTop.nextSibling) { | |
target.insertBefore(style, lastStyleElementInsertedAtTop.nextSibling); | |
} else { | |
target.appendChild(style); | |
} | |
stylesInsertedAtTop.push(style); | |
} else if (options.insertAt === "bottom") { | |
target.appendChild(style); | |
} else { | |
throw new Error("Invalid value for parameter 'insertAt'. Must be 'top' or 'bottom'."); | |
} | |
} | |
function removeStyleElement (style) { | |
if (style.parentNode === null) return false; | |
style.parentNode.removeChild(style); | |
var idx = stylesInsertedAtTop.indexOf(style); | |
if(idx >= 0) { | |
stylesInsertedAtTop.splice(idx, 1); | |
} | |
} | |
function createStyleElement (options) { | |
var style = document.createElement("style"); | |
options.attrs.type = "text/css"; | |
addAttrs(style, options.attrs); | |
insertStyleElement(options, style); | |
return style; | |
} | |
function createLinkElement (options) { | |
var link = document.createElement("link"); | |
options.attrs.type = "text/css"; | |
options.attrs.rel = "stylesheet"; | |
addAttrs(link, options.attrs); | |
insertStyleElement(options, link); | |
return link; | |
} | |
function addAttrs (el, attrs) { | |
Object.keys(attrs).forEach(function (key) { | |
el.setAttribute(key, attrs[key]); | |
}); | |
} | |
function addStyle (obj, options) { | |
var style, update, remove, result; | |
// If a transform function was defined, run it on the css | |
if (options.transform && obj.css) { | |
result = options.transform(obj.css); | |
if (result) { | |
// If transform returns a value, use that instead of the original css. | |
// This allows running runtime transformations on the css. | |
obj.css = result; | |
} else { | |
// If the transform function returns a falsy value, don't add this css. | |
// This allows conditional loading of css | |
return function() { | |
// noop | |
}; | |
} | |
} | |
if (options.singleton) { | |
var styleIndex = singletonCounter++; | |
style = singleton || (singleton = createStyleElement(options)); | |
update = applyToSingletonTag.bind(null, style, styleIndex, false); | |
remove = applyToSingletonTag.bind(null, style, styleIndex, true); | |
} else if ( | |
obj.sourceMap && | |
typeof URL === "function" && | |
typeof URL.createObjectURL === "function" && | |
typeof URL.revokeObjectURL === "function" && | |
typeof Blob === "function" && | |
typeof btoa === "function" | |
) { | |
style = createLinkElement(options); | |
update = updateLink.bind(null, style, options); | |
remove = function () { | |
removeStyleElement(style); | |
if(style.href) URL.revokeObjectURL(style.href); | |
}; | |
} else { | |
style = createStyleElement(options); | |
update = applyToTag.bind(null, style); | |
remove = function () { | |
removeStyleElement(style); | |
}; | |
} | |
update(obj); | |
return function updateStyle (newObj) { | |
if (newObj) { | |
if ( | |
newObj.css === obj.css && | |
newObj.media === obj.media && | |
newObj.sourceMap === obj.sourceMap | |
) { | |
return; | |
} | |
update(obj = newObj); | |
} else { | |
remove(); | |
} | |
}; | |
} | |
var replaceText = (function () { | |
var textStore = []; | |
return function (index, replacement) { | |
textStore[index] = replacement; | |
return textStore.filter(Boolean).join('\n'); | |
}; | |
})(); | |
function applyToSingletonTag (style, index, remove, obj) { | |
var css = remove ? "" : obj.css; | |
if (style.styleSheet) { | |
style.styleSheet.cssText = replaceText(index, css); | |
} else { | |
var cssNode = document.createTextNode(css); | |
var childNodes = style.childNodes; | |
if (childNodes[index]) style.removeChild(childNodes[index]); | |
if (childNodes.length) { | |
style.insertBefore(cssNode, childNodes[index]); | |
} else { | |
style.appendChild(cssNode); | |
} | |
} | |
} | |
function applyToTag (style, obj) { | |
var css = obj.css; | |
var media = obj.media; | |
if(media) { | |
style.setAttribute("media", media) | |
} | |
if(style.styleSheet) { | |
style.styleSheet.cssText = css; | |
} else { | |
while(style.firstChild) { | |
style.removeChild(style.firstChild); | |
} | |
style.appendChild(document.createTextNode(css)); | |
} | |
} | |
function updateLink (link, options, obj) { | |
var css = obj.css; | |
var sourceMap = obj.sourceMap; | |
/* | |
If convertToAbsoluteUrls isn't defined, but sourcemaps are enabled | |
and there is no publicPath defined then lets turn convertToAbsoluteUrls | |
on by default. Otherwise default to the convertToAbsoluteUrls option | |
directly | |
*/ | |
var autoFixUrls = options.convertToAbsoluteUrls === undefined && sourceMap; | |
if (options.convertToAbsoluteUrls || autoFixUrls) { | |
css = fixUrls(css); | |
} | |
if (sourceMap) { | |
// http://stackoverflow.com/a/26603875 | |
css += "\n/*# sourceMappingURL=data:application/json;base64," + btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap)))) + " */"; | |
} | |
var blob = new Blob([css], { type: "text/css" }); | |
var oldSrc = link.href; | |
link.href = URL.createObjectURL(blob); | |
if(oldSrc) URL.revokeObjectURL(oldSrc); | |
} | |
/***/ }), | |
/* 2 */ | |
/***/ (function(module, __webpack_exports__, __webpack_require__) { | |
"use strict"; | |
/* WEBPACK VAR INJECTION */(function(module, global) {/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__checkGlobal__ = __webpack_require__(6); | |
/** Used to determine if values are of the language type `Object`. */ | |
var objectTypes = { | |
'function': true, | |
'object': true | |
}; | |
/** Detect free variable `exports`. */ | |
var freeExports = (objectTypes[typeof exports] && exports && !exports.nodeType) | |
? exports | |
: undefined; | |
/** Detect free variable `module`. */ | |
var freeModule = (objectTypes[typeof module] && module && !module.nodeType) | |
? module | |
: undefined; | |
/** Detect free variable `global` from Node.js. */ | |
var freeGlobal = Object(__WEBPACK_IMPORTED_MODULE_0__checkGlobal__["a" /* default */])(freeExports && freeModule && typeof global == 'object' && global); | |
/** Detect free variable `self`. */ | |
var freeSelf = Object(__WEBPACK_IMPORTED_MODULE_0__checkGlobal__["a" /* default */])(objectTypes[typeof self] && self); | |
/** Detect free variable `window`. */ | |
var freeWindow = Object(__WEBPACK_IMPORTED_MODULE_0__checkGlobal__["a" /* default */])(objectTypes[typeof window] && window); | |
/** Detect `this` as the global object. */ | |
var thisGlobal = Object(__WEBPACK_IMPORTED_MODULE_0__checkGlobal__["a" /* default */])(objectTypes[typeof this] && this); | |
/** | |
* Used as a reference to the global object. | |
* | |
* The `this` value is used if it's the global object to avoid Greasemonkey's | |
* restricted `window` object, otherwise the `window` object is used. | |
*/ | |
var root = freeGlobal || | |
((freeWindow !== (thisGlobal && thisGlobal.window)) && freeWindow) || | |
freeSelf || thisGlobal || Function('return this')(); | |
/* harmony default export */ __webpack_exports__["a"] = (root); | |
/* WEBPACK VAR INJECTION */}.call(__webpack_exports__, __webpack_require__(3)(module), __webpack_require__(5))) | |
/***/ }), | |
/* 3 */ | |
/***/ (function(module, exports) { | |
module.exports = function(originalModule) { | |
if(!originalModule.webpackPolyfill) { | |
var module = Object.create(originalModule); | |
// module.parent = undefined by default | |
if(!module.children) module.children = []; | |
Object.defineProperty(module, "loaded", { | |
enumerable: true, | |
get: function() { | |
return module.l; | |
} | |
}); | |
Object.defineProperty(module, "id", { | |
enumerable: true, | |
get: function() { | |
return module.i; | |
} | |
}); | |
Object.defineProperty(module, "exports", { | |
enumerable: true, | |
}); | |
module.webpackPolyfill = 1; | |
} | |
return module; | |
}; | |
/***/ }), | |
/* 4 */ | |
/***/ (function(module, __webpack_exports__, __webpack_require__) { | |
"use strict"; | |
Object.defineProperty(__webpack_exports__, "__esModule", { value: true }); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_getPrototype.js | |
/* Built-in method references for those with the same name as other `lodash` methods. */ | |
var nativeGetPrototype = Object.getPrototypeOf; | |
/** | |
* Gets the `[[Prototype]]` of `value`. | |
* | |
* @private | |
* @param {*} value The value to query. | |
* @returns {null|Object} Returns the `[[Prototype]]`. | |
*/ | |
function getPrototype(value) { | |
return nativeGetPrototype(Object(value)); | |
} | |
/* harmony default export */ var _getPrototype = (getPrototype); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_isHostObject.js | |
/** | |
* Checks if `value` is a host object in IE < 9. | |
* | |
* @private | |
* @param {*} value The value to check. | |
* @returns {boolean} Returns `true` if `value` is a host object, else `false`. | |
*/ | |
function isHostObject(value) { | |
// Many host objects are `Object` objects that can coerce to strings | |
// despite having improperly defined `toString` methods. | |
var result = false; | |
if (value != null && typeof value.toString != 'function') { | |
try { | |
result = !!(value + ''); | |
} catch (e) {} | |
} | |
return result; | |
} | |
/* harmony default export */ var _isHostObject = (isHostObject); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/isObjectLike.js | |
/** | |
* Checks if `value` is object-like. A value is object-like if it's not `null` | |
* and has a `typeof` result of "object". | |
* | |
* @static | |
* @memberOf _ | |
* @since 4.0.0 | |
* @category Lang | |
* @param {*} value The value to check. | |
* @returns {boolean} Returns `true` if `value` is object-like, else `false`. | |
* @example | |
* | |
* _.isObjectLike({}); | |
* // => true | |
* | |
* _.isObjectLike([1, 2, 3]); | |
* // => true | |
* | |
* _.isObjectLike(_.noop); | |
* // => false | |
* | |
* _.isObjectLike(null); | |
* // => false | |
*/ | |
function isObjectLike(value) { | |
return !!value && typeof value == 'object'; | |
} | |
/* harmony default export */ var lodash_isObjectLike = (isObjectLike); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/isPlainObject.js | |
/** `Object#toString` result references. */ | |
var objectTag = '[object Object]'; | |
/** Used for built-in method references. */ | |
var objectProto = Object.prototype; | |
/** Used to resolve the decompiled source of functions. */ | |
var funcToString = Function.prototype.toString; | |
/** Used to check objects for own properties. */ | |
var isPlainObject_hasOwnProperty = objectProto.hasOwnProperty; | |
/** Used to infer the `Object` constructor. */ | |
var objectCtorString = funcToString.call(Object); | |
/** | |
* Used to resolve the | |
* [`toStringTag`](http://ecma-international.org/ecma-262/6.0/#sec-object.prototype.tostring) | |
* of values. | |
*/ | |
var objectToString = objectProto.toString; | |
/** | |
* Checks if `value` is a plain object, that is, an object created by the | |
* `Object` constructor or one with a `[[Prototype]]` of `null`. | |
* | |
* @static | |
* @memberOf _ | |
* @since 0.8.0 | |
* @category Lang | |
* @param {*} value The value to check. | |
* @returns {boolean} Returns `true` if `value` is a plain object, | |
* else `false`. | |
* @example | |
* | |
* function Foo() { | |
* this.a = 1; | |
* } | |
* | |
* _.isPlainObject(new Foo); | |
* // => false | |
* | |
* _.isPlainObject([1, 2, 3]); | |
* // => false | |
* | |
* _.isPlainObject({ 'x': 0, 'y': 0 }); | |
* // => true | |
* | |
* _.isPlainObject(Object.create(null)); | |
* // => true | |
*/ | |
function isPlainObject(value) { | |
if (!lodash_isObjectLike(value) || | |
objectToString.call(value) != objectTag || _isHostObject(value)) { | |
return false; | |
} | |
var proto = _getPrototype(value); | |
if (proto === null) { | |
return true; | |
} | |
var Ctor = isPlainObject_hasOwnProperty.call(proto, 'constructor') && proto.constructor; | |
return (typeof Ctor == 'function' && | |
Ctor instanceof Ctor && funcToString.call(Ctor) == objectCtorString); | |
} | |
/* harmony default export */ var lodash_isPlainObject = (isPlainObject); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/config.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module utils/config | |
*/ | |
/** | |
* Handles a configuration dictionary. | |
*/ | |
class config_Config { | |
/** | |
* Creates an instance of the {@link ~Config} class. | |
* | |
* @param {Object} [configurations] The initial configurations to be set. Usually, provided by the user. | |
* @param {Object} [defaultConfigurations] The default configurations. Usually, provided by the system. | |
*/ | |
constructor( configurations, defaultConfigurations ) { | |
/** | |
* Store for the whole configuration. | |
* | |
* @private | |
* @member {Object} | |
*/ | |
this._config = {}; | |
// Set default configuration. | |
if ( defaultConfigurations ) { | |
this.define( defaultConfigurations ); | |
} | |
// Set initial configuration. | |
if ( configurations ) { | |
this._setObjectToTarget( this._config, configurations ); | |
} | |
} | |
/** | |
* Set configuration values. | |
* | |
* It accepts both a name/value pair or an object, which properties and values will be used to set | |
* configurations. | |
* | |
* It also accepts setting a "deep configuration" by using dots in the name. For example, `'resize.width'` sets | |
* the value for the `width` configuration in the `resize` subset. | |
* | |
* config.set( 'width', 500 ); | |
* config.set( 'toolbar.collapsed', true ); | |
* | |
* // Equivalent to: | |
* config.set( { | |
* width: 500 | |
* toolbar: { | |
* collapsed: true | |
* } | |
* } ); | |
* | |
* Passing an object as the value will amend the configuration, not replace it. | |
* | |
* config.set( 'toolbar', { | |
* collapsed: true, | |
* } ); | |
* | |
* config.set( 'toolbar', { | |
* color: 'red', | |
* } ); | |
* | |
* config.get( 'toolbar.collapsed' ); // true | |
* config.get( 'toolbar.color' ); // 'red' | |
* | |
* @param {String|Object} name The configuration name or an object from which take properties as | |
* configuration entries. Configuration names are case-sensitive. | |
* @param {*} value The configuration value. Used if a name is passed. | |
*/ | |
set( name, value ) { | |
this._setToTarget( this._config, name, value ); | |
} | |
/** | |
* Does exactly the same as {@link #set} with one exception – passed configuration extends | |
* existing one, but does not overwrite already defined values. | |
* | |
* This method is supposed to be called by plugin developers to setup plugin's configurations. It would be | |
* rarely used for other needs. | |
* | |
* @param {String|Object} name The configuration name or an object from which take properties as | |
* configuration entries. Configuration names are case-sensitive. | |
* @param {*} value The configuration value. Used if a name is passed. | |
*/ | |
define( name, value ) { | |
const isDefine = true; | |
this._setToTarget( this._config, name, value, isDefine ); | |
} | |
/** | |
* Gets the value for a configuration entry. | |
* | |
* config.get( 'name' ); | |
* | |
* Deep configurations can be retrieved by separating each part with a dot. | |
* | |
* config.get( 'toolbar.collapsed' ); | |
* | |
* @param {String} name The configuration name. Configuration names are case-sensitive. | |
* @returns {*} The configuration value or `undefined` if the configuration entry was not found. | |
*/ | |
get( name ) { | |
return this._getFromSource( this._config, name ); | |
} | |
/** | |
* Saves passed configuration to the specified target (nested object). | |
* | |
* @private | |
* @param {Object} target Nested config object. | |
* @param {String|Object} name The configuration name or an object from which take properties as | |
* configuration entries. Configuration names are case-sensitive. | |
* @param {*} value The configuration value. Used if a name is passed. | |
* @param {Boolean} [isDefine=false] Define if passed configuration should overwrite existing one. | |
*/ | |
_setToTarget( target, name, value, isDefine = false ) { | |
// In case of an object, iterate through it and call `_setToTarget` again for each property. | |
if ( lodash_isPlainObject( name ) ) { | |
this._setObjectToTarget( target, name, isDefine ); | |
return; | |
} | |
// The configuration name should be split into parts if it has dots. E.g. `resize.width` -> [`resize`, `width`]. | |
const parts = name.split( '.' ); | |
// Take the name of the configuration out of the parts. E.g. `resize.width` -> `width`. | |
name = parts.pop(); | |
// Iterate over parts to check if currently stored configuration has proper structure. | |
for ( const part of parts ) { | |
// If there is no object for specified part then create one. | |
if ( !lodash_isPlainObject( target[ part ] ) ) { | |
target[ part ] = {}; | |
} | |
// Nested object becomes a target. | |
target = target[ part ]; | |
} | |
// In case of value is an object. | |
if ( lodash_isPlainObject( value ) ) { | |
// We take care of proper config structure. | |
if ( !lodash_isPlainObject( target[ name ] ) ) { | |
target[ name ] = {}; | |
} | |
target = target[ name ]; | |
// And iterate through this object calling `_setToTarget` again for each property. | |
this._setObjectToTarget( target, value, isDefine ); | |
return; | |
} | |
// Do nothing if we are defining configuration for non empty name. | |
if ( isDefine && typeof target[ name ] != 'undefined' ) { | |
return; | |
} | |
target[ name ] = value; | |
} | |
/** | |
* Get specified configuration from specified source (nested object). | |
* | |
* @private | |
* @param {Object} source level of nested object. | |
* @param {String} name The configuration name. Configuration names are case-sensitive. | |
* @returns {*} The configuration value or `undefined` if the configuration entry was not found. | |
*/ | |
_getFromSource( source, name ) { | |
// The configuration name should be split into parts if it has dots. E.g. `resize.width` -> [`resize`, `width`]. | |
const parts = name.split( '.' ); | |
// Take the name of the configuration out of the parts. E.g. `resize.width` -> `width`. | |
name = parts.pop(); | |
// Iterate over parts to check if currently stored configuration has proper structure. | |
for ( const part of parts ) { | |
if ( !lodash_isPlainObject( source[ part ] ) ) { | |
source = null; | |
break; | |
} | |
// Nested object becomes a source. | |
source = source[ part ]; | |
} | |
// Always returns undefined for non existing configuration | |
return source ? source[ name ] : undefined; | |
} | |
/** | |
* Iterates through passed object and calls {@link #_setToTarget} method with object key and value for each property. | |
* | |
* @private | |
* @param {Object} target Nested config object. | |
* @param {Object} configuration Configuration data set | |
* @param {Boolean} [isDefine] Defines if passed configuration is default configuration or not. | |
*/ | |
_setObjectToTarget( target, configuration, isDefine ) { | |
Object.keys( configuration ).forEach( key => { | |
this._setToTarget( target, key, configuration[ key ], isDefine ); | |
} ); | |
} | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/ckeditorerror.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module utils/ckeditorerror | |
*/ | |
/** | |
* URL to the documentation with error codes. | |
*/ | |
const DOCUMENTATION_URL = | |
'https://ckeditor5.github.io/docs/nightly/ckeditor5/latest/framework/guides/support/error-codes.html'; | |
/** | |
* The CKEditor error class. | |
* | |
* All errors will be shortened during the minification process in order to reduce the code size. | |
* Therefore, all error messages should be documented in the same way as those in {@link module:utils/log}. | |
* | |
* Read more in the {@link module:utils/log} module. | |
* | |
* @extends Error | |
*/ | |
class CKEditorError extends Error { | |
/** | |
* Creates an instance of the CKEditorError class. | |
* | |
* Read more about error logging in the {@link module:utils/log} module. | |
* | |
* @param {String} message The error message in an `error-name: Error message.` format. | |
* During the minification process the "Error message" part will be removed to limit the code size | |
* and a link to this error documentation will be added to the `message`. | |
* @param {Object} [data] Additional data describing the error. A stringified version of this object | |
* will be appended to the error message, so the data are quickly visible in the console. The original | |
* data object will also be later available under the {@link #data} property. | |
*/ | |
constructor( message, data ) { | |
message = attachLinkToDocumentation( message ); | |
if ( data ) { | |
message += ' ' + JSON.stringify( data ); | |
} | |
super( message ); | |
/** | |
* @member {String} | |
*/ | |
this.name = 'CKEditorError'; | |
/** | |
* The additional error data passed to the constructor. | |
* | |
* @member {Object} | |
*/ | |
this.data = data; | |
} | |
/** | |
* Checks if error is an instance of CKEditorError class. | |
* | |
* @param {Object} error Object to check. | |
* @returns {Boolean} | |
*/ | |
static isCKEditorError( error ) { | |
return error instanceof CKEditorError; | |
} | |
} | |
/** | |
* Attaches link to the documentation at the end of the error message. | |
* | |
* @param {String} message Message to be logged. | |
* @returns {String} | |
*/ | |
function attachLinkToDocumentation( message ) { | |
const matchedErrorName = message.match( /^([^:]+):/ ); | |
if ( !matchedErrorName ) { | |
return message; | |
} | |
return message + ` Read more: ${ DOCUMENTATION_URL }#${ matchedErrorName[ 1 ] }\n`; | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/log.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/* global console */ | |
/** | |
* @module utils/log | |
*/ | |
/** | |
* The logging module. | |
* | |
* This object features two functions that should be used across CKEditor code base to log errors and warnings. | |
* Despite being an overridable interface for native `console.*` this module serves also the goal to limit the | |
* code size of a minified CKEditor package. During minification process the messages will be shortened and | |
* links to their documentation will be logged to the console. | |
* | |
* All errors and warning should be documented in the following way: | |
* | |
* /** | |
* * Error thrown when a plugin cannot be loaded due to JavaScript errors, lack of plugins with a given name, etc. | |
* * | |
* * @error plugin-load | |
* * @param pluginName The name of the plugin that could not be loaded. | |
* * @param moduleName The name of the module which tried to load this plugin. | |
* * / | |
* log.error( 'plugin-load: It was not possible to load the "{$pluginName}" plugin in module "{$moduleName}', { | |
* pluginName: 'foo', | |
* moduleName: 'bar' | |
* } ); | |
* | |
* ### Warning vs Error vs Throw | |
* | |
* * Whenever a potentially incorrect situation occurs, which does not directly lead to an incorrect behavior, | |
* log a warning. | |
* * Whenever an incorrect situation occurs, but the app may continue working (although perhaps incorrectly), | |
* log an error. | |
* * Whenever it's really bad and it does not make sense to continue working, throw a {@link module:utils/ckeditorerror~CKEditorError}. | |
* | |
* @namespace | |
*/ | |
const log = { | |
/** | |
* Logs an error to the console. | |
* | |
* Read more about error logging in the {@link module:utils/log} module. | |
* | |
* @param {String} message The error message in an `error-name: Error message.` format. | |
* During the minification process the "Error message" part will be removed to limit the code size | |
* and a link to this error documentation will be logged to the console. | |
* @param {Object} [data] Additional data describing the error. | |
*/ | |
error( message, data ) { | |
console.error( attachLinkToDocumentation( message ), data ); | |
}, | |
/** | |
* Logs a warning to the console. | |
* | |
* Read more about error logging in the {@link module:utils/log} module. | |
* | |
* @param {String} message The warning message in a `warning-name: Warning message.` format. | |
* During the minification process the "Warning message" part will be removed to limit the code size | |
* and a link to this error documentation will be logged to the console. | |
* @param {Object} [data] Additional data describing the warning. | |
*/ | |
warn( message, data ) { | |
console.warn( attachLinkToDocumentation( message ), data ); | |
} | |
}; | |
/* harmony default export */ var src_log = (log); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-core/src/plugincollection.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module core/plugincollection | |
*/ | |
/** | |
* Manages a list of CKEditor plugins, including loading, resolving dependencies and initialization. | |
*/ | |
class plugincollection_PluginCollection { | |
/** | |
* Creates an instance of the PluginCollection class. | |
* Allows loading and initializing plugins and their dependencies. | |
* | |
* @param {module:core/editor/editor~Editor} editor | |
* @param {Array.<Function>} [availablePlugins] Plugins (constructors) which the collection will be able to use | |
* when {@link module:core/plugincollection~PluginCollection#load} is used with plugin names (strings, instead of constructors). | |
* Usually, the editor will pass its built-in plugins to the collection so they can later be | |
* used in `config.plugins` or `config.removePlugins` by names. | |
*/ | |
constructor( editor, availablePlugins = [] ) { | |
/** | |
* @protected | |
* @member {module:core/editor/editor~Editor} module:core/plugin~PluginCollection#_editor | |
*/ | |
this._editor = editor; | |
/** | |
* Map of plugin constructors which can be retrieved by their names. | |
* | |
* @protected | |
* @member {Map.<String|Function,Function>} module:core/plugin~PluginCollection#_availablePlugins | |
*/ | |
this._availablePlugins = new Map(); | |
/** | |
* @protected | |
* @member {Map} module:core/plugin~PluginCollection#_plugins | |
*/ | |
this._plugins = new Map(); | |
for ( const PluginConstructor of availablePlugins ) { | |
this._availablePlugins.set( PluginConstructor, PluginConstructor ); | |
if ( PluginConstructor.pluginName ) { | |
this._availablePlugins.set( PluginConstructor.pluginName, PluginConstructor ); | |
} | |
} | |
} | |
/** | |
* Collection iterator. Returns `[ PluginConstructor, pluginInstance ]` pairs. | |
*/ | |
* [ Symbol.iterator ]() { | |
for ( const entry of this._plugins ) { | |
if ( typeof entry[ 0 ] == 'function' ) { | |
yield entry; | |
} | |
} | |
} | |
/** | |
* Gets the plugin instance by its constructor or name. | |
* | |
* @param {Function|String} key The plugin constructor or {@link module:core/plugin~PluginInterface.pluginName name}. | |
* @returns {module:core/plugin~PluginInterface} | |
*/ | |
get( key ) { | |
return this._plugins.get( key ); | |
} | |
/** | |
* Loads a set of plugins and adds them to the collection. | |
* | |
* @param {Array.<Function|String>} plugins An array of {@link module:core/plugin~PluginInterface plugin constructors} | |
* or {@link module:core/plugin~PluginInterface.pluginName plugin names}. The second option (names) work only if | |
* `availablePlugins` were passed to the {@link #constructor}. | |
* @param {Array.<String|Function>} [removePlugins] Names of plugins or plugin constructors | |
* which should not be loaded (despite being specified in the `plugins` array). | |
* @returns {Promise} A promise which gets resolved once all plugins are loaded and available into the | |
* collection. | |
* @returns {Promise.<Array.<module:core/plugin~PluginInterface>>} returns.loadedPlugins The array of loaded plugins. | |
*/ | |
load( plugins, removePlugins = [] ) { | |
const that = this; | |
const editor = this._editor; | |
const loading = new Set(); | |
const loaded = []; | |
const pluginConstructors = mapToAvailableConstructors( plugins ); | |
const removePluginConstructors = mapToAvailableConstructors( removePlugins ); | |
const missingPlugins = getMissingPluginNames( plugins ); | |
if ( missingPlugins ) { | |
// TODO update this error docs with links to docs because it will be a frequent problem. | |
/** | |
* Some plugins are not available and could not be loaded. | |
* | |
* Plugin classes (constructors) need to be provided to the editor before they can be loaded by name. | |
* This is usually done by the builder by setting the {@link module:core/editor/editor~Editor.build} | |
* property. | |
* | |
* **If you see this warning when using one of the {@glink builds/index CKEditor 5 Builds}** it means | |
* that you try to enable a plugin which was not included into that build. This may a be due to a typo | |
* in the plugin name or simply because that plugin is not part of this build. In the latter scenario, | |
* read more about {@glink builds/guides/development/custom-builds custom builds}. | |
* | |
* **If you see this warning when using one of the editor creators directly** (not a build), then it means | |
* that you tried loading plugins by name. However, unlike CKEditor 4, CKEditor 5 does not implement a "plugin loader". | |
* This means that CKEditor 5 does not know where to load the plugin modules from. Therefore, you need to | |
* provide each plugin through reference (as a constructor function). Check out the examples in | |
* {@glink builds/guides/integration/advanced-setup#Scenario-2-Building-from-source "Building from source"}. | |
* | |
* @error plugincollection-plugin-not-found | |
* @param {Array.<String>} plugins The name of the plugins which could not be loaded. | |
*/ | |
const errorMsg = 'plugincollection-plugin-not-found: Some plugins are not available and could not be loaded.'; | |
// Log the error so it's more visible on the console. Hopefully, for better DX. | |
src_log.error( errorMsg, { plugins: missingPlugins } ); | |
return Promise.reject( new CKEditorError( errorMsg, { plugins: missingPlugins } ) ); | |
} | |
return Promise.all( pluginConstructors.map( loadPlugin ) ) | |
.then( () => loaded ); | |
function loadPlugin( PluginConstructor ) { | |
if ( removePluginConstructors.includes( PluginConstructor ) ) { | |
return; | |
} | |
// The plugin is already loaded or being loaded - do nothing. | |
if ( that.get( PluginConstructor ) || loading.has( PluginConstructor ) ) { | |
return; | |
} | |
return instantiatePlugin( PluginConstructor ) | |
.catch( err => { | |
/** | |
* It was not possible to load the plugin. | |
* | |
* This is a generic error logged to the console when a JavaSript error is thrown during one of | |
* the plugins initialization. | |
* | |
* If you correctly handled a promise returned by the editor's `create()` method (like shown below) | |
* you will find the original error logged on the console too: | |
* | |
* ClassicEditor.create( document.getElementById( 'editor' ) ) | |
* .then( editor => { | |
* // ... | |
* } ) | |
* .catch( error => { | |
* console.error( error ); | |
* } ); | |
* | |
* @error plugincollection-load | |
* @param {String} plugin The name of the plugin that could not be loaded. | |
*/ | |
src_log.error( 'plugincollection-load: It was not possible to load the plugin.', { plugin: PluginConstructor } ); | |
throw err; | |
} ); | |
} | |
function instantiatePlugin( PluginConstructor ) { | |
return new Promise( resolve => { | |
loading.add( PluginConstructor ); | |
if ( PluginConstructor.requires ) { | |
PluginConstructor.requires.forEach( RequiredPluginConstructorOrName => { | |
const RequiredPluginConstructor = getPluginConstructor( RequiredPluginConstructorOrName ); | |
if ( removePlugins.includes( RequiredPluginConstructor ) ) { | |
/** | |
* Cannot load a plugin because one of its dependencies is listed in the `removePlugins` option. | |
* | |
* @error plugincollection-required | |
* @param {Function} plugin The required plugin. | |
* @param {Function} requiredBy The parent plugin. | |
*/ | |
throw new CKEditorError( | |
'plugincollection-required: Cannot load a plugin because one of its dependencies is listed in' + | |
'the `removePlugins` option.', | |
{ plugin: RequiredPluginConstructor, requiredBy: PluginConstructor } | |
); | |
} | |
loadPlugin( RequiredPluginConstructor ); | |
} ); | |
} | |
const plugin = new PluginConstructor( editor ); | |
that._add( PluginConstructor, plugin ); | |
loaded.push( plugin ); | |
resolve(); | |
} ); | |
} | |
function getPluginConstructor( PluginConstructorOrName ) { | |
if ( typeof PluginConstructorOrName == 'function' ) { | |
return PluginConstructorOrName; | |
} | |
return that._availablePlugins.get( PluginConstructorOrName ); | |
} | |
function getMissingPluginNames( plugins ) { | |
const missingPlugins = []; | |
for ( const pluginNameOrConstructor of plugins ) { | |
if ( !getPluginConstructor( pluginNameOrConstructor ) ) { | |
missingPlugins.push( pluginNameOrConstructor ); | |
} | |
} | |
return missingPlugins.length ? missingPlugins : null; | |
} | |
function mapToAvailableConstructors( plugins ) { | |
return plugins | |
.map( pluginNameOrConstructor => getPluginConstructor( pluginNameOrConstructor ) ) | |
.filter( PluginConstructor => !!PluginConstructor ); | |
} | |
} | |
/** | |
* Destroys all loaded plugins. | |
* | |
* @returns {Promise} | |
*/ | |
destroy() { | |
const promises = Array.from( this ) | |
.map( ( [ , pluginInstance ] ) => pluginInstance ) | |
.filter( pluginInstance => typeof pluginInstance.destroy == 'function' ) | |
.map( pluginInstance => pluginInstance.destroy() ); | |
return Promise.all( promises ); | |
} | |
/** | |
* Adds the plugin to the collection. Exposed mainly for testing purposes. | |
* | |
* @protected | |
* @param {Function} PluginConstructor The plugin constructor. | |
* @param {module:core/plugin~PluginInterface} plugin The instance of the plugin. | |
*/ | |
_add( PluginConstructor, plugin ) { | |
this._plugins.set( PluginConstructor, plugin ); | |
const pluginName = PluginConstructor.pluginName; | |
if ( !pluginName ) { | |
return; | |
} | |
if ( this._plugins.has( pluginName ) ) { | |
/** | |
* Two plugins with the same {@link module:core/plugin~PluginInterface.pluginName} were loaded. | |
* This may lead to runtime conflicts between these plugins. This usually means that incorrect | |
* params were passed to {@link module:core/editor/editor~Editor.create}. | |
* | |
* @error plugincollection-plugin-name-conflict | |
* @param {String} pluginName The duplicated plugin name. | |
* @param {Function} plugin1 The first plugin constructor. | |
* @param {Function} plugin2 The second plugin constructor. | |
*/ | |
src_log.warn( | |
'plugincollection-plugin-name-conflict: Two plugins with the same name were loaded.', | |
{ pluginName, plugin1: this._plugins.get( pluginName ).constructor, plugin2: PluginConstructor } | |
); | |
} else { | |
this._plugins.set( pluginName, plugin ); | |
} | |
} | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-core/src/commandcollection.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module core/commandcollection | |
*/ | |
/** | |
* Collection of commands. Its instance is available in {@link module:core/editor/editor~Editor#commands `editor.commands`}. | |
*/ | |
class commandcollection_CommandCollection { | |
/** | |
* Creates collection instance. | |
*/ | |
constructor() { | |
/** | |
* Command map. | |
* | |
* @private | |
* @member {Map} | |
*/ | |
this._commands = new Map(); | |
} | |
/** | |
* Registers a new command. | |
* | |
* @param {String} commandName The name of the command. | |
* @param {module:core/command~Command} command | |
*/ | |
add( commandName, command ) { | |
this._commands.set( commandName, command ); | |
} | |
/** | |
* Retrieves a command from the collection. | |
* | |
* @param {String} commandName The name of the command. | |
* @returns {module:core/command~Command} | |
*/ | |
get( commandName ) { | |
return this._commands.get( commandName ); | |
} | |
/** | |
* Executes a command. | |
* | |
* @param {String} commandName The name of the command. | |
*/ | |
execute( commandName, ...args ) { | |
const command = this.get( commandName ); | |
if ( !command ) { | |
/** | |
* Command does not exist. | |
* | |
* @error commandcollection-command-not-found | |
* @param {String} commandName Name of the command. | |
*/ | |
throw new CKEditorError( 'commandcollection-command-not-found: Command does not exist.', { commandName } ); | |
} | |
command.execute( ...args ); | |
} | |
/** | |
* Returns iterator of command names. | |
* | |
* @returns {Iterator.<String>} | |
*/ | |
* names() { | |
yield* this._commands.keys(); | |
} | |
/** | |
* Returns iterator of command instances. | |
* | |
* @returns {Iterator.<module:core/command~Command>} | |
*/ | |
* commands() { | |
yield* this._commands.values(); | |
} | |
/** | |
* Collection iterator. | |
*/ | |
[ Symbol.iterator ]() { | |
return this._commands[ Symbol.iterator ](); | |
} | |
/** | |
* Destroys all collection commands. | |
*/ | |
destroy() { | |
for ( const command of this.commands() ) { | |
command.destroy(); | |
} | |
} | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/translation-service.js | |
/** | |
* @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module utils/translation-service | |
*/ | |
let dictionaries = {}; | |
/** | |
* Adds package translations to existing ones. | |
* These translations will later be available for {@link module:utils/translation-service~translate translate}. | |
* | |
* add( 'pl', { | |
* 'OK': 'OK', | |
* 'Cancel [context: reject]': 'Anuluj' | |
* } ); | |
* | |
* @param {String} lang Target language. | |
* @param {Object.<String, String>} translations Translations which will be added to the dictionary. | |
*/ | |
function translation_service_add( lang, translations ) { | |
dictionaries[ lang ] = dictionaries[ lang ] || {}; | |
Object.assign( dictionaries[ lang ], translations ); | |
} | |
/** | |
* Translates string if the translation of the string was previously {@link module:utils/translation-service~add added} | |
* to the dictionary. This happens in a multi-language mode were translation modules are created by the bundler. | |
* | |
* When no translation is defined in the dictionary or the dictionary doesn't exist this function returns | |
* the original string without the `'[context: ]'` (happens in development and single-language modes). | |
* | |
* In a single-language mode (when values passed to `t()` were replaced with target languange strings) the dictionary | |
* is left empty, so this function will return the original strings always. | |
* | |
* translate( 'pl', 'Cancel [context: reject]' ); | |
* | |
* @param {String} lang Target language. | |
* @param {String} translationKey String which is going to be translated. | |
* @returns {String} Translated sentence. | |
*/ | |
function translate( lang, translationKey ) { | |
if ( !hasTranslation( lang, translationKey ) ) { | |
return translationKey.replace( / \[context: [^\]]+\]$/, '' ); | |
} | |
return dictionaries[ lang ][ translationKey ]; | |
} | |
// Checks whether the dictionary exists and translaiton in that dictionary exists. | |
function hasTranslation( lang, translationKey ) { | |
return ( | |
( lang in dictionaries ) && | |
( translationKey in dictionaries[ lang ] ) | |
); | |
} | |
/** | |
* Clears dictionaries for test purposes. | |
* | |
* @protected | |
*/ | |
function _clear() { | |
dictionaries = {}; | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/locale.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module utils/locale | |
*/ | |
/** | |
* Represents the localization services. | |
*/ | |
class locale_Locale { | |
/** | |
* Creates a new instance of the Locale class. | |
* | |
* @param {String} [lang='en'] The language code in [ISO 639-1](https://en.wikipedia.org/wiki/ISO_639-1) format. | |
*/ | |
constructor( lang ) { | |
/** | |
* The language code in [ISO 639-1](https://en.wikipedia.org/wiki/ISO_639-1) format. | |
* | |
* @readonly | |
* @member {String} | |
*/ | |
this.lang = lang || 'en'; | |
/** | |
* Translates the given string to the {@link #lang}. This method is also availble in {@link module:core/editor/editor~Editor#t} and | |
* {@link module:ui/view~View#t}. | |
* | |
* The strings may contain placeholders (`%<index>`) for values which are passed as the second argument. | |
* `<index>` is the index in the `values` array. | |
* | |
* editor.t( 'Created file "%0" in %1ms.', [ fileName, timeTaken ] ); | |
* | |
* This method's context is statically bound to Locale instance, | |
* so it can be called as a function: | |
* | |
* const t = this.t; | |
* t( 'Label' ); | |
* | |
* @method #t | |
* @param {String} str The string to translate. | |
* @param {String[]} values Values that should be used to interpolate the string. | |
*/ | |
this.t = ( ...args ) => this._t( ...args ); | |
} | |
/** | |
* Base for the {@link #t} method. | |
* | |
* @private | |
*/ | |
_t( str, values ) { | |
let translatedString = translate( this.lang, str ); | |
if ( values ) { | |
translatedString = translatedString.replace( /%(\d+)/g, ( match, index ) => { | |
return ( index < values.length ) ? values[ index ] : match; | |
} ); | |
} | |
return translatedString; | |
} | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/mix.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module utils/mix | |
*/ | |
/** | |
* Copies enumerable properties and symbols from the objects given as 2nd+ parameters to the | |
* prototype of first object (a constructor). | |
* | |
* class Editor { | |
* ... | |
* } | |
* | |
* const SomeMixin = { | |
* a() { | |
* return 'a'; | |
* } | |
* }; | |
* | |
* mix( Editor, SomeMixin, ... ); | |
* | |
* new Editor().a(); // -> 'a' | |
* | |
* Note: Properties which already exist in the base class will not be overriden. | |
* | |
* @param {Function} [baseClass] Class which prototype will be extended. | |
* @param {Object} [...mixins] Objects from which to get properties. | |
*/ | |
function mix( baseClass, ...mixins ) { | |
mixins.forEach( mixin => { | |
Object.getOwnPropertyNames( mixin ).concat( Object.getOwnPropertySymbols( mixin ) ) | |
.forEach( key => { | |
if ( key in baseClass.prototype ) { | |
return; | |
} | |
const sourceDescriptor = Object.getOwnPropertyDescriptor( mixin, key ); | |
sourceDescriptor.enumerable = false; | |
Object.defineProperty( baseClass.prototype, key, sourceDescriptor ); | |
} ); | |
} ); | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/spy.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module utils/spy | |
*/ | |
/** | |
* Creates a spy function (ala Sinon.js) that can be used to inspect call to it. | |
* | |
* The following are the present features: | |
* | |
* * spy.called: property set to `true` if the function has been called at least once. | |
* | |
* @returns {Function} The spy function. | |
*/ | |
function spy() { | |
return function spy() { | |
spy.called = true; | |
}; | |
} | |
/* harmony default export */ var src_spy = (spy); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/eventinfo.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module utils/eventinfo | |
*/ | |
/** | |
* The event object passed to event callbacks. It is used to provide information about the event as well as a tool to | |
* manipulate it. | |
*/ | |
class eventinfo_EventInfo { | |
/** | |
* @param {Object} source The emitter. | |
* @param {String} name The event name. | |
*/ | |
constructor( source, name ) { | |
/** | |
* The object that fired the event. | |
* | |
* @readonly | |
* @member {Object} | |
*/ | |
this.source = source; | |
/** | |
* The event name. | |
* | |
* @readonly | |
* @member {String} | |
*/ | |
this.name = name; | |
/** | |
* Path this event has followed. See {@link module:utils/emittermixin~EmitterMixin#delegate}. | |
* | |
* @readonly | |
* @member {Array.<Object>} | |
*/ | |
this.path = []; | |
// The following methods are defined in the constructor because they must be re-created per instance. | |
/** | |
* Stops the event emitter to call further callbacks for this event interaction. | |
* | |
* @method #stop | |
*/ | |
this.stop = src_spy(); | |
/** | |
* Removes the current callback from future interactions of this event. | |
* | |
* @method #off | |
*/ | |
this.off = src_spy(); | |
/** | |
* The value which will be returned by {@link module:utils/emittermixin~EmitterMixin#fire}. | |
* | |
* It's `undefined` by default and can be changed by an event listener: | |
* | |
* dataController.fire( 'getSelectedContent', ( evt ) => { | |
* // This listener will make `dataController.fire( 'getSelectedContent' )` | |
* // always return an empty DocumentFragment. | |
* evt.return = new DocumentFragment(); | |
* | |
* // Make sure no other listeners are executed. | |
* evt.stop(); | |
* } ); | |
* | |
* @member #return | |
*/ | |
} | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/uid.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module utils/uid | |
*/ | |
/** | |
* Returns a unique id. This id is a number (starting from 1) which will never get repeated on successive calls | |
* to this method. | |
* | |
* @returns {String} A number representing the id. | |
*/ | |
function uid() { | |
let uuid = 'e'; // Make sure that id does not start with number. | |
for ( let i = 0; i < 8; i++ ) { | |
uuid += Math.floor( ( 1 + Math.random() ) * 0x10000 ).toString( 16 ).substring( 1 ); | |
} | |
return uuid; | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/priorities.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module utils/priorities | |
*/ | |
/** | |
* String representing a priority value. | |
* | |
* @typedef {'highest'|'high'|'normal'|'low'|'lowest'} module:utils/priorities~PriorityString | |
*/ | |
/** | |
* Provides group of constants to use instead of hardcoding numeric priority values. | |
* | |
* @namespace | |
*/ | |
const priorities = { | |
/** | |
* Converts a string with priority name to it's numeric value. If `Number` is given, it just returns it. | |
* | |
* @static | |
* @param {module:utils/priorities~PriorityString|Number} priority Priority to convert. | |
* @returns {Number} Converted priority. | |
*/ | |
get( priority ) { | |
if ( typeof priority != 'number' ) { | |
return this[ priority ] || this.normal; | |
} else { | |
return priority; | |
} | |
}, | |
highest: 100000, | |
high: 1000, | |
normal: 0, | |
low: -1000, | |
lowest: -100000 | |
}; | |
/* harmony default export */ var src_priorities = (priorities); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/emittermixin.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module utils/emittermixin | |
*/ | |
const _listeningTo = Symbol( 'listeningTo' ); | |
const _emitterId = Symbol( 'emitterId' ); | |
/** | |
* Mixin that injects the events API into its host. | |
* | |
* @mixin EmitterMixin | |
* @implements module:utils/emittermixin~Emitter | |
*/ | |
const EmitterMixin = { | |
/** | |
* Registers a callback function to be executed when an event is fired. | |
* | |
* Events can be grouped in namespaces using `:`. | |
* When namespaced event is fired, it additionally fires all callbacks for that namespace. | |
* | |
* myEmitter.on( 'myGroup', genericCallback ); | |
* myEmitter.on( 'myGroup:myEvent', specificCallback ); | |
* | |
* // genericCallback is fired. | |
* myEmitter.fire( 'myGroup' ); | |
* // both genericCallback and specificCallback are fired. | |
* myEmitter.fire( 'myGroup:myEvent' ); | |
* // genericCallback is fired even though there are no callbacks for "foo". | |
* myEmitter.fire( 'myGroup:foo' ); | |
* | |
* An event callback can {@link module:utils/eventinfo~EventInfo#stop stop the event} and | |
* set the {@link module:utils/eventinfo~EventInfo#return return value} of the {@link #fire} method. | |
* | |
* @method #on | |
* @param {String} event The name of the event. | |
* @param {Function} callback The function to be called on event. | |
* @param {Object} [options={}] Additional options. | |
* @param {module:utils/priorities~PriorityString|Number} [options.priority='normal'] The priority of this event callback. The higher | |
* the priority value the sooner the callback will be fired. Events having the same priority are called in the | |
* order they were added. | |
*/ | |
on( event, callback, options = {} ) { | |
createEventNamespace( this, event ); | |
const lists = getCallbacksListsForNamespace( this, event ); | |
const priority = src_priorities.get( options.priority ); | |
callback = { | |
callback, | |
priority | |
}; | |
// Add the callback to all callbacks list. | |
for ( const callbacks of lists ) { | |
// Add the callback to the list in the right priority position. | |
let added = false; | |
for ( let i = 0; i < callbacks.length; i++ ) { | |
if ( callbacks[ i ].priority < priority ) { | |
callbacks.splice( i, 0, callback ); | |
added = true; | |
break; | |
} | |
} | |
// Add at the end, if right place was not found. | |
if ( !added ) { | |
callbacks.push( callback ); | |
} | |
} | |
}, | |
/** | |
* Registers a callback function to be executed on the next time the event is fired only. This is similar to | |
* calling {@link #on} followed by {@link #off} in the callback. | |
* | |
* @method #once | |
* @param {String} event The name of the event. | |
* @param {Function} callback The function to be called on event. | |
* @param {Object} [options={}] Additional options. | |
* @param {module:utils/priorities~PriorityString|Number} [options.priority='normal'] The priority of this event callback. The higher | |
* the priority value the sooner the callback will be fired. Events having the same priority are called in the | |
* order they were added. | |
*/ | |
once( event, callback, options ) { | |
const onceCallback = function( event, ...args ) { | |
// Go off() at the first call. | |
event.off(); | |
// Go with the original callback. | |
callback.call( this, event, ...args ); | |
}; | |
// Make a similar on() call, simply replacing the callback. | |
this.on( event, onceCallback, options ); | |
}, | |
/** | |
* Stops executing the callback on the given event. | |
* | |
* @method #off | |
* @param {String} event The name of the event. | |
* @param {Function} callback The function to stop being called. | |
*/ | |
off( event, callback ) { | |
const lists = getCallbacksListsForNamespace( this, event ); | |
for ( const callbacks of lists ) { | |
for ( let i = 0; i < callbacks.length; i++ ) { | |
if ( callbacks[ i ].callback == callback ) { | |
// Remove the callback from the list (fixing the next index). | |
callbacks.splice( i, 1 ); | |
i--; | |
} | |
} | |
} | |
}, | |
/** | |
* Registers a callback function to be executed when an event is fired in a specific (emitter) object. | |
* | |
* @method #listenTo | |
* @param {module:utils/emittermixin~Emitter} emitter The object that fires the event. | |
* @param {String} event The name of the event. | |
* @param {Function} callback The function to be called on event. | |
* @param {Object} [options={}] Additional options. | |
* @param {module:utils/priorities~PriorityString|Number} [options.priority='normal'] The priority of this event callback. The higher | |
* the priority value the sooner the callback will be fired. Events having the same priority are called in the | |
* order they were added. | |
*/ | |
listenTo( emitter, event, callback, options ) { | |
let emitterInfo, eventCallbacks; | |
// _listeningTo contains a list of emitters that this object is listening to. | |
// This list has the following format: | |
// | |
// _listeningTo: { | |
// emitterId: { | |
// emitter: emitter, | |
// callbacks: { | |
// event1: [ callback1, callback2, ... ] | |
// .... | |
// } | |
// }, | |
// ... | |
// } | |
if ( !this[ _listeningTo ] ) { | |
this[ _listeningTo ] = {}; | |
} | |
const emitters = this[ _listeningTo ]; | |
if ( !_getEmitterId( emitter ) ) { | |
_setEmitterId( emitter ); | |
} | |
const emitterId = _getEmitterId( emitter ); | |
if ( !( emitterInfo = emitters[ emitterId ] ) ) { | |
emitterInfo = emitters[ emitterId ] = { | |
emitter, | |
callbacks: {} | |
}; | |
} | |
if ( !( eventCallbacks = emitterInfo.callbacks[ event ] ) ) { | |
eventCallbacks = emitterInfo.callbacks[ event ] = []; | |
} | |
eventCallbacks.push( callback ); | |
// Finally register the callback to the event. | |
emitter.on( event, callback, options ); | |
}, | |
/** | |
* Stops listening for events. It can be used at different levels: | |
* | |
* * To stop listening to a specific callback. | |
* * To stop listening to a specific event. | |
* * To stop listening to all events fired by a specific object. | |
* * To stop listening to all events fired by all object. | |
* | |
* @method #stopListening | |
* @param {module:utils/emittermixin~Emitter} [emitter] The object to stop listening to. If omitted, stops it for all objects. | |
* @param {String} [event] (Requires the `emitter`) The name of the event to stop listening to. If omitted, stops it | |
* for all events from `emitter`. | |
* @param {Function} [callback] (Requires the `event`) The function to be removed from the call list for the given | |
* `event`. | |
*/ | |
stopListening( emitter, event, callback ) { | |
const emitters = this[ _listeningTo ]; | |
let emitterId = emitter && _getEmitterId( emitter ); | |
const emitterInfo = emitters && emitterId && emitters[ emitterId ]; | |
const eventCallbacks = emitterInfo && event && emitterInfo.callbacks[ event ]; | |
// Stop if nothing has been listened. | |
if ( !emitters || ( emitter && !emitterInfo ) || ( event && !eventCallbacks ) ) { | |
return; | |
} | |
// All params provided. off() that single callback. | |
if ( callback ) { | |
emitter.off( event, callback ); | |
} | |
// Only `emitter` and `event` provided. off() all callbacks for that event. | |
else if ( eventCallbacks ) { | |
while ( ( callback = eventCallbacks.pop() ) ) { | |
emitter.off( event, callback ); | |
} | |
delete emitterInfo.callbacks[ event ]; | |
} | |
// Only `emitter` provided. off() all events for that emitter. | |
else if ( emitterInfo ) { | |
for ( event in emitterInfo.callbacks ) { | |
this.stopListening( emitter, event ); | |
} | |
delete emitters[ emitterId ]; | |
} | |
// No params provided. off() all emitters. | |
else { | |
for ( emitterId in emitters ) { | |
this.stopListening( emitters[ emitterId ].emitter ); | |
} | |
delete this[ _listeningTo ]; | |
} | |
}, | |
/** | |
* Fires an event, executing all callbacks registered for it. | |
* | |
* The first parameter passed to callbacks is an {@link module:utils/eventinfo~EventInfo} object, | |
* followed by the optional `args` provided in the `fire()` method call. | |
* | |
* @method #fire | |
* @param {String|module:utils/eventinfo~EventInfo} eventOrInfo The name of the event or `EventInfo` object if event is delegated. | |
* @param {...*} [args] Additional arguments to be passed to the callbacks. | |
* @returns {*} By default the method returns `undefined`. However, the return value can be changed by listeners | |
* through modification of the {@link module:utils/eventinfo~EventInfo#return}'s value (the event info | |
* is the first param of every callback). | |
*/ | |
fire( eventOrInfo, ...args ) { | |
const eventInfo = eventOrInfo instanceof eventinfo_EventInfo ? eventOrInfo : new eventinfo_EventInfo( this, eventOrInfo ); | |
const event = eventInfo.name; | |
let callbacks = getCallbacksForEvent( this, event ); | |
// Record that the event passed this emitter on its path. | |
eventInfo.path.push( this ); | |
// Handle event listener callbacks first. | |
if ( callbacks ) { | |
// Arguments passed to each callback. | |
const callbackArgs = [ eventInfo, ...args ]; | |
// Copying callbacks array is the easiest and most secure way of preventing infinite loops, when event callbacks | |
// are added while processing other callbacks. Previous solution involved adding counters (unique ids) but | |
// failed if callbacks were added to the queue before currently processed callback. | |
// If this proves to be too inefficient, another method is to change `.on()` so callbacks are stored if same | |
// event is currently processed. Then, `.fire()` at the end, would have to add all stored events. | |
callbacks = Array.from( callbacks ); | |
for ( let i = 0; i < callbacks.length; i++ ) { | |
callbacks[ i ].callback.apply( this, callbackArgs ); | |
// Remove the callback from future requests if off() has been called. | |
if ( eventInfo.off.called ) { | |
// Remove the called mark for the next calls. | |
delete eventInfo.off.called; | |
this.off( event, callbacks[ i ].callback ); | |
} | |
// Do not execute next callbacks if stop() was called. | |
if ( eventInfo.stop.called ) { | |
break; | |
} | |
} | |
} | |
// Delegate event to other emitters if needed. | |
if ( this._delegations ) { | |
const destinations = this._delegations.get( event ); | |
const passAllDestinations = this._delegations.get( '*' ); | |
if ( destinations ) { | |
fireDelegatedEvents( destinations, eventInfo, args ); | |
} | |
if ( passAllDestinations ) { | |
fireDelegatedEvents( passAllDestinations, eventInfo, args ); | |
} | |
} | |
return eventInfo.return; | |
}, | |
/** | |
* Delegates selected events to another {@link module:utils/emittermixin~Emitter}. For instance: | |
* | |
* emitterA.delegate( 'eventX' ).to( emitterB ); | |
* emitterA.delegate( 'eventX', 'eventY' ).to( emitterC ); | |
* | |
* then `eventX` is delegated (fired by) `emitterB` and `emitterC` along with `data`: | |
* | |
* emitterA.fire( 'eventX', data ); | |
* | |
* and `eventY` is delegated (fired by) `emitterC` along with `data`: | |
* | |
* emitterA.fire( 'eventY', data ); | |
* | |
* @method #delegate | |
* @param {...String} events Event names that will be delegated to another emitter. | |
* @returns {module:utils/emittermixin~EmitterMixinDelegateChain} | |
*/ | |
delegate( ...events ) { | |
return { | |
to: ( emitter, nameOrFunction ) => { | |
if ( !this._delegations ) { | |
this._delegations = new Map(); | |
} | |
for ( const eventName of events ) { | |
const destinations = this._delegations.get( eventName ); | |
if ( !destinations ) { | |
this._delegations.set( eventName, new Map( [ [ emitter, nameOrFunction ] ] ) ); | |
} else { | |
destinations.set( emitter, nameOrFunction ); | |
} | |
} | |
} | |
}; | |
}, | |
/** | |
* Stops delegating events. It can be used at different levels: | |
* | |
* * To stop delegating all events. | |
* * To stop delegating a specific event to all emitters. | |
* * To stop delegating a specific event to a specific emitter. | |
* | |
* @method #stopDelegating | |
* @param {String} [event] The name of the event to stop delegating. If omitted, stops it all delegations. | |
* @param {module:utils/emittermixin~Emitter} [emitter] (requires `event`) The object to stop delegating a particular event to. | |
* If omitted, stops delegation of `event` to all emitters. | |
*/ | |
stopDelegating( event, emitter ) { | |
if ( !this._delegations ) { | |
return; | |
} | |
if ( !event ) { | |
this._delegations.clear(); | |
} else if ( !emitter ) { | |
this._delegations.delete( event ); | |
} else { | |
const destinations = this._delegations.get( event ); | |
if ( destinations ) { | |
destinations.delete( emitter ); | |
} | |
} | |
} | |
}; | |
/* harmony default export */ var emittermixin = (EmitterMixin); | |
/** | |
* Checks if `listeningEmitter` listens to an emitter with given `listenedToEmitterId` and if so, returns that emitter. | |
* If not, returns `null`. | |
* | |
* @protected | |
* @param {module:utils/emittermixin~EmitterMixin} listeningEmitter Emitter that listens. | |
* @param {String} listenedToEmitterId Unique emitter id of emitter listened to. | |
* @returns {module:utils/emittermixin~EmitterMixin|null} | |
*/ | |
function _getEmitterListenedTo( listeningEmitter, listenedToEmitterId ) { | |
if ( listeningEmitter[ _listeningTo ] && listeningEmitter[ _listeningTo ][ listenedToEmitterId ] ) { | |
return listeningEmitter[ _listeningTo ][ listenedToEmitterId ].emitter; | |
} | |
return null; | |
} | |
/** | |
* Sets emitter's unique id. | |
* | |
* **Note:** `_emitterId` can be set only once. | |
* | |
* @protected | |
* @param {module:utils/emittermixin~EmitterMixin} emitter Emitter for which id will be set. | |
* @param {String} [id] Unique id to set. If not passed, random unique id will be set. | |
*/ | |
function _setEmitterId( emitter, id ) { | |
if ( !emitter[ _emitterId ] ) { | |
emitter[ _emitterId ] = id || uid(); | |
} | |
} | |
/** | |
* Returns emitter's unique id. | |
* | |
* @protected | |
* @param {module:utils/emittermixin~EmitterMixin} emitter Emitter which id will be returned. | |
*/ | |
function _getEmitterId( emitter ) { | |
return emitter[ _emitterId ]; | |
} | |
// Gets the internal `_events` property of the given object. | |
// `_events` property store all lists with callbacks for registered event names. | |
// If there were no events registered on the object, empty `_events` object is created. | |
function getEvents( source ) { | |
if ( !source._events ) { | |
Object.defineProperty( source, '_events', { | |
value: {} | |
} ); | |
} | |
return source._events; | |
} | |
// Creates event node for generic-specific events relation architecture. | |
function makeEventNode() { | |
return { | |
callbacks: [], | |
childEvents: [] | |
}; | |
} | |
// Creates an architecture for generic-specific events relation. | |
// If needed, creates all events for given eventName, i.e. if the first registered event | |
// is foo:bar:abc, it will create foo:bar:abc, foo:bar and foo event and tie them together. | |
// It also copies callbacks from more generic events to more specific events when | |
// specific events are created. | |
function createEventNamespace( source, eventName ) { | |
const events = getEvents( source ); | |
// First, check if the event we want to add to the structure already exists. | |
if ( events[ eventName ] ) { | |
// If it exists, we don't have to do anything. | |
return; | |
} | |
// In other case, we have to create the structure for the event. | |
// Note, that we might need to create intermediate events too. | |
// I.e. if foo:bar:abc is being registered and we only have foo in the structure, | |
// we need to also register foo:bar. | |
// Currently processed event name. | |
let name = eventName; | |
// Name of the event that is a child event for currently processed event. | |
let childEventName = null; | |
// Array containing all newly created specific events. | |
const newEventNodes = []; | |
// While loop can't check for ':' index because we have to handle generic events too. | |
// In each loop, we truncate event name, going from the most specific name to the generic one. | |
// I.e. foo:bar:abc -> foo:bar -> foo. | |
while ( name !== '' ) { | |
if ( events[ name ] ) { | |
// If the currently processed event name is already registered, we can be sure | |
// that it already has all the structure created, so we can break the loop here | |
// as no more events need to be registered. | |
break; | |
} | |
// If this event is not yet registered, create a new object for it. | |
events[ name ] = makeEventNode(); | |
// Add it to the array with newly created events. | |
newEventNodes.push( events[ name ] ); | |
// Add previously processed event name as a child of this event. | |
if ( childEventName ) { | |
events[ name ].childEvents.push( childEventName ); | |
} | |
childEventName = name; | |
// If `.lastIndexOf()` returns -1, `.substr()` will return '' which will break the loop. | |
name = name.substr( 0, name.lastIndexOf( ':' ) ); | |
} | |
if ( name !== '' ) { | |
// If name is not empty, we found an already registered event that was a parent of the | |
// event we wanted to register. | |
// Copy that event's callbacks to newly registered events. | |
for ( const node of newEventNodes ) { | |
node.callbacks = events[ name ].callbacks.slice(); | |
} | |
// Add last newly created event to the already registered event. | |
events[ name ].childEvents.push( childEventName ); | |
} | |
} | |
// Gets an array containing callbacks list for a given event and it's more specific events. | |
// I.e. if given event is foo:bar and there is also foo:bar:abc event registered, this will | |
// return callback list of foo:bar and foo:bar:abc (but not foo). | |
// Returns empty array if given event has not been yet registered. | |
function getCallbacksListsForNamespace( source, eventName ) { | |
const eventNode = getEvents( source )[ eventName ]; | |
if ( !eventNode ) { | |
return []; | |
} | |
let callbacksLists = [ eventNode.callbacks ]; | |
for ( let i = 0; i < eventNode.childEvents.length; i++ ) { | |
const childCallbacksLists = getCallbacksListsForNamespace( source, eventNode.childEvents[ i ] ); | |
callbacksLists = callbacksLists.concat( childCallbacksLists ); | |
} | |
return callbacksLists; | |
} | |
// Get the list of callbacks for a given event, but only if there any callbacks have been registered. | |
// If there are no callbacks registered for given event, it checks if this is a specific event and looks | |
// for callbacks for it's more generic version. | |
function getCallbacksForEvent( source, eventName ) { | |
let event; | |
if ( !source._events || !( event = source._events[ eventName ] ) || !event.callbacks.length ) { | |
// There are no callbacks registered for specified eventName. | |
// But this could be a specific-type event that is in a namespace. | |
if ( eventName.indexOf( ':' ) > -1 ) { | |
// If the eventName is specific, try to find callback lists for more generic event. | |
return getCallbacksForEvent( source, eventName.substr( 0, eventName.lastIndexOf( ':' ) ) ); | |
} else { | |
// If this is a top-level generic event, return null; | |
return null; | |
} | |
} | |
return event.callbacks; | |
} | |
// Fires delegated events for given map of destinations. | |
// | |
// @private | |
// * @param {Map.<utils.Emitter>} destinations A map containing `[ {@link utils.Emitter}, "event name" ]` pair destinations. | |
// * @param {utils.EventInfo} eventInfo The original event info object. | |
// * @param {Array.<*>} fireArgs Arguments the original event was fired with. | |
function fireDelegatedEvents( destinations, eventInfo, fireArgs ) { | |
for ( let [ emitter, name ] of destinations ) { | |
if ( !name ) { | |
name = eventInfo.name; | |
} else if ( typeof name == 'function' ) { | |
name = name( eventInfo.name ); | |
} | |
const delegatedInfo = new eventinfo_EventInfo( eventInfo.source, name ); | |
delegatedInfo.path = [ ...eventInfo.path ]; | |
emitter.fire( delegatedInfo, ...fireArgs ); | |
} | |
} | |
/** | |
* Interface representing classes which mix in {@link module:utils/emittermixin~EmitterMixin}. | |
* | |
* @interface Emitter | |
*/ | |
/** | |
* The return value of {@link ~EmitterMixin#delegate}. | |
* | |
* @interface module:utils/emittermixin~EmitterMixinDelegateChain | |
*/ | |
/** | |
* Selects destination for {@link module:utils/emittermixin~EmitterMixin#delegate} events. | |
* | |
* @method #to | |
* @param {module:utils/emittermixin~Emitter} emitter An `EmitterMixin` instance which is the destination for delegated events. | |
* @param {String|Function} nameOrFunction A custom event name or function which converts the original name string. | |
*/ | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/eq.js | |
/** | |
* Performs a | |
* [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero) | |
* comparison between two values to determine if they are equivalent. | |
* | |
* @static | |
* @memberOf _ | |
* @since 4.0.0 | |
* @category Lang | |
* @param {*} value The value to compare. | |
* @param {*} other The other value to compare. | |
* @returns {boolean} Returns `true` if the values are equivalent, else `false`. | |
* @example | |
* | |
* var object = { 'user': 'fred' }; | |
* var other = { 'user': 'fred' }; | |
* | |
* _.eq(object, object); | |
* // => true | |
* | |
* _.eq(object, other); | |
* // => false | |
* | |
* _.eq('a', 'a'); | |
* // => true | |
* | |
* _.eq('a', Object('a')); | |
* // => false | |
* | |
* _.eq(NaN, NaN); | |
* // => true | |
*/ | |
function eq(value, other) { | |
return value === other || (value !== value && other !== other); | |
} | |
/* harmony default export */ var lodash_eq = (eq); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_assignValue.js | |
/** Used for built-in method references. */ | |
var _assignValue_objectProto = Object.prototype; | |
/** Used to check objects for own properties. */ | |
var _assignValue_hasOwnProperty = _assignValue_objectProto.hasOwnProperty; | |
/** | |
* Assigns `value` to `key` of `object` if the existing value is not equivalent | |
* using [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero) | |
* for equality comparisons. | |
* | |
* @private | |
* @param {Object} object The object to modify. | |
* @param {string} key The key of the property to assign. | |
* @param {*} value The value to assign. | |
*/ | |
function assignValue(object, key, value) { | |
var objValue = object[key]; | |
if (!(_assignValue_hasOwnProperty.call(object, key) && lodash_eq(objValue, value)) || | |
(value === undefined && !(key in object))) { | |
object[key] = value; | |
} | |
} | |
/* harmony default export */ var _assignValue = (assignValue); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_copyObject.js | |
/** | |
* Copies properties of `source` to `object`. | |
* | |
* @private | |
* @param {Object} source The object to copy properties from. | |
* @param {Array} props The property identifiers to copy. | |
* @param {Object} [object={}] The object to copy properties to. | |
* @param {Function} [customizer] The function to customize copied values. | |
* @returns {Object} Returns `object`. | |
*/ | |
function copyObject(source, props, object, customizer) { | |
object || (object = {}); | |
var index = -1, | |
length = props.length; | |
while (++index < length) { | |
var key = props[index]; | |
var newValue = customizer | |
? customizer(object[key], source[key], key, object, source) | |
: source[key]; | |
_assignValue(object, key, newValue); | |
} | |
return object; | |
} | |
/* harmony default export */ var _copyObject = (copyObject); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseProperty.js | |
/** | |
* The base implementation of `_.property` without support for deep paths. | |
* | |
* @private | |
* @param {string} key The key of the property to get. | |
* @returns {Function} Returns the new accessor function. | |
*/ | |
function baseProperty(key) { | |
return function(object) { | |
return object == null ? undefined : object[key]; | |
}; | |
} | |
/* harmony default export */ var _baseProperty = (baseProperty); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_getLength.js | |
/** | |
* Gets the "length" property value of `object`. | |
* | |
* **Note:** This function is used to avoid a | |
* [JIT bug](https://bugs.webkit.org/show_bug.cgi?id=142792) that affects | |
* Safari on at least iOS 8.1-8.3 ARM64. | |
* | |
* @private | |
* @param {Object} object The object to query. | |
* @returns {*} Returns the "length" value. | |
*/ | |
var getLength = _baseProperty('length'); | |
/* harmony default export */ var _getLength = (getLength); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/isObject.js | |
/** | |
* Checks if `value` is the | |
* [language type](http://www.ecma-international.org/ecma-262/6.0/#sec-ecmascript-language-types) | |
* of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) | |
* | |
* @static | |
* @memberOf _ | |
* @since 0.1.0 | |
* @category Lang | |
* @param {*} value The value to check. | |
* @returns {boolean} Returns `true` if `value` is an object, else `false`. | |
* @example | |
* | |
* _.isObject({}); | |
* // => true | |
* | |
* _.isObject([1, 2, 3]); | |
* // => true | |
* | |
* _.isObject(_.noop); | |
* // => true | |
* | |
* _.isObject(null); | |
* // => false | |
*/ | |
function isObject(value) { | |
var type = typeof value; | |
return !!value && (type == 'object' || type == 'function'); | |
} | |
/* harmony default export */ var lodash_isObject = (isObject); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/isFunction.js | |
/** `Object#toString` result references. */ | |
var funcTag = '[object Function]', | |
genTag = '[object GeneratorFunction]'; | |
/** Used for built-in method references. */ | |
var isFunction_objectProto = Object.prototype; | |
/** | |
* Used to resolve the | |
* [`toStringTag`](http://ecma-international.org/ecma-262/6.0/#sec-object.prototype.tostring) | |
* of values. | |
*/ | |
var isFunction_objectToString = isFunction_objectProto.toString; | |
/** | |
* Checks if `value` is classified as a `Function` object. | |
* | |
* @static | |
* @memberOf _ | |
* @since 0.1.0 | |
* @category Lang | |
* @param {*} value The value to check. | |
* @returns {boolean} Returns `true` if `value` is correctly classified, | |
* else `false`. | |
* @example | |
* | |
* _.isFunction(_); | |
* // => true | |
* | |
* _.isFunction(/abc/); | |
* // => false | |
*/ | |
function isFunction(value) { | |
// The use of `Object#toString` avoids issues with the `typeof` operator | |
// in Safari 8 which returns 'object' for typed array and weak map constructors, | |
// and PhantomJS 1.9 which returns 'function' for `NodeList` instances. | |
var tag = lodash_isObject(value) ? isFunction_objectToString.call(value) : ''; | |
return tag == funcTag || tag == genTag; | |
} | |
/* harmony default export */ var lodash_isFunction = (isFunction); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/isLength.js | |
/** Used as references for various `Number` constants. */ | |
var MAX_SAFE_INTEGER = 9007199254740991; | |
/** | |
* Checks if `value` is a valid array-like length. | |
* | |
* **Note:** This function is loosely based on | |
* [`ToLength`](http://ecma-international.org/ecma-262/6.0/#sec-tolength). | |
* | |
* @static | |
* @memberOf _ | |
* @since 4.0.0 | |
* @category Lang | |
* @param {*} value The value to check. | |
* @returns {boolean} Returns `true` if `value` is a valid length, | |
* else `false`. | |
* @example | |
* | |
* _.isLength(3); | |
* // => true | |
* | |
* _.isLength(Number.MIN_VALUE); | |
* // => false | |
* | |
* _.isLength(Infinity); | |
* // => false | |
* | |
* _.isLength('3'); | |
* // => false | |
*/ | |
function isLength(value) { | |
return typeof value == 'number' && | |
value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER; | |
} | |
/* harmony default export */ var lodash_isLength = (isLength); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/isArrayLike.js | |
/** | |
* Checks if `value` is array-like. A value is considered array-like if it's | |
* not a function and has a `value.length` that's an integer greater than or | |
* equal to `0` and less than or equal to `Number.MAX_SAFE_INTEGER`. | |
* | |
* @static | |
* @memberOf _ | |
* @since 4.0.0 | |
* @category Lang | |
* @param {*} value The value to check. | |
* @returns {boolean} Returns `true` if `value` is array-like, else `false`. | |
* @example | |
* | |
* _.isArrayLike([1, 2, 3]); | |
* // => true | |
* | |
* _.isArrayLike(document.body.children); | |
* // => true | |
* | |
* _.isArrayLike('abc'); | |
* // => true | |
* | |
* _.isArrayLike(_.noop); | |
* // => false | |
*/ | |
function isArrayLike(value) { | |
return value != null && lodash_isLength(_getLength(value)) && !lodash_isFunction(value); | |
} | |
/* harmony default export */ var lodash_isArrayLike = (isArrayLike); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_isIndex.js | |
/** Used as references for various `Number` constants. */ | |
var _isIndex_MAX_SAFE_INTEGER = 9007199254740991; | |
/** Used to detect unsigned integer values. */ | |
var reIsUint = /^(?:0|[1-9]\d*)$/; | |
/** | |
* Checks if `value` is a valid array-like index. | |
* | |
* @private | |
* @param {*} value The value to check. | |
* @param {number} [length=MAX_SAFE_INTEGER] The upper bounds of a valid index. | |
* @returns {boolean} Returns `true` if `value` is a valid index, else `false`. | |
*/ | |
function isIndex(value, length) { | |
length = length == null ? _isIndex_MAX_SAFE_INTEGER : length; | |
return !!length && | |
(typeof value == 'number' || reIsUint.test(value)) && | |
(value > -1 && value % 1 == 0 && value < length); | |
} | |
/* harmony default export */ var _isIndex = (isIndex); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_isIterateeCall.js | |
/** | |
* Checks if the given arguments are from an iteratee call. | |
* | |
* @private | |
* @param {*} value The potential iteratee value argument. | |
* @param {*} index The potential iteratee index or key argument. | |
* @param {*} object The potential iteratee object argument. | |
* @returns {boolean} Returns `true` if the arguments are from an iteratee call, | |
* else `false`. | |
*/ | |
function isIterateeCall(value, index, object) { | |
if (!lodash_isObject(object)) { | |
return false; | |
} | |
var type = typeof index; | |
if (type == 'number' | |
? (lodash_isArrayLike(object) && _isIndex(index, object.length)) | |
: (type == 'string' && index in object) | |
) { | |
return lodash_eq(object[index], value); | |
} | |
return false; | |
} | |
/* harmony default export */ var _isIterateeCall = (isIterateeCall); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_apply.js | |
/** | |
* A faster alternative to `Function#apply`, this function invokes `func` | |
* with the `this` binding of `thisArg` and the arguments of `args`. | |
* | |
* @private | |
* @param {Function} func The function to invoke. | |
* @param {*} thisArg The `this` binding of `func`. | |
* @param {Array} args The arguments to invoke `func` with. | |
* @returns {*} Returns the result of `func`. | |
*/ | |
function apply(func, thisArg, args) { | |
var length = args.length; | |
switch (length) { | |
case 0: return func.call(thisArg); | |
case 1: return func.call(thisArg, args[0]); | |
case 2: return func.call(thisArg, args[0], args[1]); | |
case 3: return func.call(thisArg, args[0], args[1], args[2]); | |
} | |
return func.apply(thisArg, args); | |
} | |
/* harmony default export */ var _apply = (apply); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/isSymbol.js | |
/** `Object#toString` result references. */ | |
var symbolTag = '[object Symbol]'; | |
/** Used for built-in method references. */ | |
var isSymbol_objectProto = Object.prototype; | |
/** | |
* Used to resolve the | |
* [`toStringTag`](http://ecma-international.org/ecma-262/6.0/#sec-object.prototype.tostring) | |
* of values. | |
*/ | |
var isSymbol_objectToString = isSymbol_objectProto.toString; | |
/** | |
* Checks if `value` is classified as a `Symbol` primitive or object. | |
* | |
* @static | |
* @memberOf _ | |
* @since 4.0.0 | |
* @category Lang | |
* @param {*} value The value to check. | |
* @returns {boolean} Returns `true` if `value` is correctly classified, | |
* else `false`. | |
* @example | |
* | |
* _.isSymbol(Symbol.iterator); | |
* // => true | |
* | |
* _.isSymbol('abc'); | |
* // => false | |
*/ | |
function isSymbol(value) { | |
return typeof value == 'symbol' || | |
(lodash_isObjectLike(value) && isSymbol_objectToString.call(value) == symbolTag); | |
} | |
/* harmony default export */ var lodash_isSymbol = (isSymbol); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/toNumber.js | |
/** Used as references for various `Number` constants. */ | |
var NAN = 0 / 0; | |
/** Used to match leading and trailing whitespace. */ | |
var reTrim = /^\s+|\s+$/g; | |
/** Used to detect bad signed hexadecimal string values. */ | |
var reIsBadHex = /^[-+]0x[0-9a-f]+$/i; | |
/** Used to detect binary string values. */ | |
var reIsBinary = /^0b[01]+$/i; | |
/** Used to detect octal string values. */ | |
var reIsOctal = /^0o[0-7]+$/i; | |
/** Built-in method references without a dependency on `root`. */ | |
var freeParseInt = parseInt; | |
/** | |
* Converts `value` to a number. | |
* | |
* @static | |
* @memberOf _ | |
* @since 4.0.0 | |
* @category Lang | |
* @param {*} value The value to process. | |
* @returns {number} Returns the number. | |
* @example | |
* | |
* _.toNumber(3.2); | |
* // => 3.2 | |
* | |
* _.toNumber(Number.MIN_VALUE); | |
* // => 5e-324 | |
* | |
* _.toNumber(Infinity); | |
* // => Infinity | |
* | |
* _.toNumber('3.2'); | |
* // => 3.2 | |
*/ | |
function toNumber(value) { | |
if (typeof value == 'number') { | |
return value; | |
} | |
if (lodash_isSymbol(value)) { | |
return NAN; | |
} | |
if (lodash_isObject(value)) { | |
var other = lodash_isFunction(value.valueOf) ? value.valueOf() : value; | |
value = lodash_isObject(other) ? (other + '') : other; | |
} | |
if (typeof value != 'string') { | |
return value === 0 ? value : +value; | |
} | |
value = value.replace(reTrim, ''); | |
var isBinary = reIsBinary.test(value); | |
return (isBinary || reIsOctal.test(value)) | |
? freeParseInt(value.slice(2), isBinary ? 2 : 8) | |
: (reIsBadHex.test(value) ? NAN : +value); | |
} | |
/* harmony default export */ var lodash_toNumber = (toNumber); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/toFinite.js | |
/** Used as references for various `Number` constants. */ | |
var INFINITY = 1 / 0, | |
MAX_INTEGER = 1.7976931348623157e+308; | |
/** | |
* Converts `value` to a finite number. | |
* | |
* @static | |
* @memberOf _ | |
* @since 4.12.0 | |
* @category Lang | |
* @param {*} value The value to convert. | |
* @returns {number} Returns the converted number. | |
* @example | |
* | |
* _.toFinite(3.2); | |
* // => 3.2 | |
* | |
* _.toFinite(Number.MIN_VALUE); | |
* // => 5e-324 | |
* | |
* _.toFinite(Infinity); | |
* // => 1.7976931348623157e+308 | |
* | |
* _.toFinite('3.2'); | |
* // => 3.2 | |
*/ | |
function toFinite(value) { | |
if (!value) { | |
return value === 0 ? value : 0; | |
} | |
value = lodash_toNumber(value); | |
if (value === INFINITY || value === -INFINITY) { | |
var sign = (value < 0 ? -1 : 1); | |
return sign * MAX_INTEGER; | |
} | |
return value === value ? value : 0; | |
} | |
/* harmony default export */ var lodash_toFinite = (toFinite); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/toInteger.js | |
/** | |
* Converts `value` to an integer. | |
* | |
* **Note:** This function is loosely based on | |
* [`ToInteger`](http://www.ecma-international.org/ecma-262/6.0/#sec-tointeger). | |
* | |
* @static | |
* @memberOf _ | |
* @since 4.0.0 | |
* @category Lang | |
* @param {*} value The value to convert. | |
* @returns {number} Returns the converted integer. | |
* @example | |
* | |
* _.toInteger(3.2); | |
* // => 3 | |
* | |
* _.toInteger(Number.MIN_VALUE); | |
* // => 0 | |
* | |
* _.toInteger(Infinity); | |
* // => 1.7976931348623157e+308 | |
* | |
* _.toInteger('3.2'); | |
* // => 3 | |
*/ | |
function toInteger(value) { | |
var result = lodash_toFinite(value), | |
remainder = result % 1; | |
return result === result ? (remainder ? result - remainder : result) : 0; | |
} | |
/* harmony default export */ var lodash_toInteger = (toInteger); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/rest.js | |
/** Used as the `TypeError` message for "Functions" methods. */ | |
var FUNC_ERROR_TEXT = 'Expected a function'; | |
/* Built-in method references for those with the same name as other `lodash` methods. */ | |
var nativeMax = Math.max; | |
/** | |
* Creates a function that invokes `func` with the `this` binding of the | |
* created function and arguments from `start` and beyond provided as | |
* an array. | |
* | |
* **Note:** This method is based on the | |
* [rest parameter](https://mdn.io/rest_parameters). | |
* | |
* @static | |
* @memberOf _ | |
* @since 4.0.0 | |
* @category Function | |
* @param {Function} func The function to apply a rest parameter to. | |
* @param {number} [start=func.length-1] The start position of the rest parameter. | |
* @returns {Function} Returns the new function. | |
* @example | |
* | |
* var say = _.rest(function(what, names) { | |
* return what + ' ' + _.initial(names).join(', ') + | |
* (_.size(names) > 1 ? ', & ' : '') + _.last(names); | |
* }); | |
* | |
* say('hello', 'fred', 'barney', 'pebbles'); | |
* // => 'hello fred, barney, & pebbles' | |
*/ | |
function rest(func, start) { | |
if (typeof func != 'function') { | |
throw new TypeError(FUNC_ERROR_TEXT); | |
} | |
start = nativeMax(start === undefined ? (func.length - 1) : lodash_toInteger(start), 0); | |
return function() { | |
var args = arguments, | |
index = -1, | |
length = nativeMax(args.length - start, 0), | |
array = Array(length); | |
while (++index < length) { | |
array[index] = args[start + index]; | |
} | |
switch (start) { | |
case 0: return func.call(this, array); | |
case 1: return func.call(this, args[0], array); | |
case 2: return func.call(this, args[0], args[1], array); | |
} | |
var otherArgs = Array(start + 1); | |
index = -1; | |
while (++index < start) { | |
otherArgs[index] = args[index]; | |
} | |
otherArgs[start] = array; | |
return _apply(func, this, otherArgs); | |
}; | |
} | |
/* harmony default export */ var lodash_rest = (rest); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_createAssigner.js | |
/** | |
* Creates a function like `_.assign`. | |
* | |
* @private | |
* @param {Function} assigner The function to assign values. | |
* @returns {Function} Returns the new assigner function. | |
*/ | |
function createAssigner(assigner) { | |
return lodash_rest(function(object, sources) { | |
var index = -1, | |
length = sources.length, | |
customizer = length > 1 ? sources[length - 1] : undefined, | |
guard = length > 2 ? sources[2] : undefined; | |
customizer = (assigner.length > 3 && typeof customizer == 'function') | |
? (length--, customizer) | |
: undefined; | |
if (guard && _isIterateeCall(sources[0], sources[1], guard)) { | |
customizer = length < 3 ? undefined : customizer; | |
length = 1; | |
} | |
object = Object(object); | |
while (++index < length) { | |
var source = sources[index]; | |
if (source) { | |
assigner(object, source, index, customizer); | |
} | |
} | |
return object; | |
}); | |
} | |
/* harmony default export */ var _createAssigner = (createAssigner); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_isPrototype.js | |
/** Used for built-in method references. */ | |
var _isPrototype_objectProto = Object.prototype; | |
/** | |
* Checks if `value` is likely a prototype object. | |
* | |
* @private | |
* @param {*} value The value to check. | |
* @returns {boolean} Returns `true` if `value` is a prototype, else `false`. | |
*/ | |
function isPrototype(value) { | |
var Ctor = value && value.constructor, | |
proto = (typeof Ctor == 'function' && Ctor.prototype) || _isPrototype_objectProto; | |
return value === proto; | |
} | |
/* harmony default export */ var _isPrototype = (isPrototype); | |
// EXTERNAL MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_root.js | |
var _root = __webpack_require__(2); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_Reflect.js | |
/** Built-in value references. */ | |
var Reflect = _root["a" /* default */].Reflect; | |
/* harmony default export */ var _Reflect = (Reflect); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_iteratorToArray.js | |
/** | |
* Converts `iterator` to an array. | |
* | |
* @private | |
* @param {Object} iterator The iterator to convert. | |
* @returns {Array} Returns the converted array. | |
*/ | |
function iteratorToArray(iterator) { | |
var data, | |
result = []; | |
while (!(data = iterator.next()).done) { | |
result.push(data.value); | |
} | |
return result; | |
} | |
/* harmony default export */ var _iteratorToArray = (iteratorToArray); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseKeysIn.js | |
/** Used for built-in method references. */ | |
var _baseKeysIn_objectProto = Object.prototype; | |
/** Built-in value references. */ | |
var enumerate = _Reflect ? _Reflect.enumerate : undefined, | |
propertyIsEnumerable = _baseKeysIn_objectProto.propertyIsEnumerable; | |
/** | |
* The base implementation of `_.keysIn` which doesn't skip the constructor | |
* property of prototypes or treat sparse arrays as dense. | |
* | |
* @private | |
* @param {Object} object The object to query. | |
* @returns {Array} Returns the array of property names. | |
*/ | |
function baseKeysIn(object) { | |
object = object == null ? object : Object(object); | |
var result = []; | |
for (var key in object) { | |
result.push(key); | |
} | |
return result; | |
} | |
// Fallback for IE < 9 with es6-shim. | |
if (enumerate && !propertyIsEnumerable.call({ 'valueOf': 1 }, 'valueOf')) { | |
baseKeysIn = function(object) { | |
return _iteratorToArray(enumerate(object)); | |
}; | |
} | |
/* harmony default export */ var _baseKeysIn = (baseKeysIn); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseTimes.js | |
/** | |
* The base implementation of `_.times` without support for iteratee shorthands | |
* or max array length checks. | |
* | |
* @private | |
* @param {number} n The number of times to invoke `iteratee`. | |
* @param {Function} iteratee The function invoked per iteration. | |
* @returns {Array} Returns the array of results. | |
*/ | |
function baseTimes(n, iteratee) { | |
var index = -1, | |
result = Array(n); | |
while (++index < n) { | |
result[index] = iteratee(index); | |
} | |
return result; | |
} | |
/* harmony default export */ var _baseTimes = (baseTimes); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/isArrayLikeObject.js | |
/** | |
* This method is like `_.isArrayLike` except that it also checks if `value` | |
* is an object. | |
* | |
* @static | |
* @memberOf _ | |
* @since 4.0.0 | |
* @category Lang | |
* @param {*} value The value to check. | |
* @returns {boolean} Returns `true` if `value` is an array-like object, | |
* else `false`. | |
* @example | |
* | |
* _.isArrayLikeObject([1, 2, 3]); | |
* // => true | |
* | |
* _.isArrayLikeObject(document.body.children); | |
* // => true | |
* | |
* _.isArrayLikeObject('abc'); | |
* // => false | |
* | |
* _.isArrayLikeObject(_.noop); | |
* // => false | |
*/ | |
function isArrayLikeObject(value) { | |
return lodash_isObjectLike(value) && lodash_isArrayLike(value); | |
} | |
/* harmony default export */ var lodash_isArrayLikeObject = (isArrayLikeObject); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/isArguments.js | |
/** `Object#toString` result references. */ | |
var argsTag = '[object Arguments]'; | |
/** Used for built-in method references. */ | |
var isArguments_objectProto = Object.prototype; | |
/** Used to check objects for own properties. */ | |
var isArguments_hasOwnProperty = isArguments_objectProto.hasOwnProperty; | |
/** | |
* Used to resolve the | |
* [`toStringTag`](http://ecma-international.org/ecma-262/6.0/#sec-object.prototype.tostring) | |
* of values. | |
*/ | |
var isArguments_objectToString = isArguments_objectProto.toString; | |
/** Built-in value references. */ | |
var isArguments_propertyIsEnumerable = isArguments_objectProto.propertyIsEnumerable; | |
/** | |
* Checks if `value` is likely an `arguments` object. | |
* | |
* @static | |
* @memberOf _ | |
* @since 0.1.0 | |
* @category Lang | |
* @param {*} value The value to check. | |
* @returns {boolean} Returns `true` if `value` is correctly classified, | |
* else `false`. | |
* @example | |
* | |
* _.isArguments(function() { return arguments; }()); | |
* // => true | |
* | |
* _.isArguments([1, 2, 3]); | |
* // => false | |
*/ | |
function isArguments(value) { | |
// Safari 8.1 incorrectly makes `arguments.callee` enumerable in strict mode. | |
return lodash_isArrayLikeObject(value) && isArguments_hasOwnProperty.call(value, 'callee') && | |
(!isArguments_propertyIsEnumerable.call(value, 'callee') || isArguments_objectToString.call(value) == argsTag); | |
} | |
/* harmony default export */ var lodash_isArguments = (isArguments); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/isArray.js | |
/** | |
* Checks if `value` is classified as an `Array` object. | |
* | |
* @static | |
* @memberOf _ | |
* @since 0.1.0 | |
* @type {Function} | |
* @category Lang | |
* @param {*} value The value to check. | |
* @returns {boolean} Returns `true` if `value` is correctly classified, | |
* else `false`. | |
* @example | |
* | |
* _.isArray([1, 2, 3]); | |
* // => true | |
* | |
* _.isArray(document.body.children); | |
* // => false | |
* | |
* _.isArray('abc'); | |
* // => false | |
* | |
* _.isArray(_.noop); | |
* // => false | |
*/ | |
var isArray = Array.isArray; | |
/* harmony default export */ var lodash_isArray = (isArray); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/isString.js | |
/** `Object#toString` result references. */ | |
var stringTag = '[object String]'; | |
/** Used for built-in method references. */ | |
var isString_objectProto = Object.prototype; | |
/** | |
* Used to resolve the | |
* [`toStringTag`](http://ecma-international.org/ecma-262/6.0/#sec-object.prototype.tostring) | |
* of values. | |
*/ | |
var isString_objectToString = isString_objectProto.toString; | |
/** | |
* Checks if `value` is classified as a `String` primitive or object. | |
* | |
* @static | |
* @since 0.1.0 | |
* @memberOf _ | |
* @category Lang | |
* @param {*} value The value to check. | |
* @returns {boolean} Returns `true` if `value` is correctly classified, | |
* else `false`. | |
* @example | |
* | |
* _.isString('abc'); | |
* // => true | |
* | |
* _.isString(1); | |
* // => false | |
*/ | |
function isString(value) { | |
return typeof value == 'string' || | |
(!lodash_isArray(value) && lodash_isObjectLike(value) && isString_objectToString.call(value) == stringTag); | |
} | |
/* harmony default export */ var lodash_isString = (isString); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_indexKeys.js | |
/** | |
* Creates an array of index keys for `object` values of arrays, | |
* `arguments` objects, and strings, otherwise `null` is returned. | |
* | |
* @private | |
* @param {Object} object The object to query. | |
* @returns {Array|null} Returns index keys, else `null`. | |
*/ | |
function indexKeys(object) { | |
var length = object ? object.length : undefined; | |
if (lodash_isLength(length) && | |
(lodash_isArray(object) || lodash_isString(object) || lodash_isArguments(object))) { | |
return _baseTimes(length, String); | |
} | |
return null; | |
} | |
/* harmony default export */ var _indexKeys = (indexKeys); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/keysIn.js | |
/** Used for built-in method references. */ | |
var keysIn_objectProto = Object.prototype; | |
/** Used to check objects for own properties. */ | |
var keysIn_hasOwnProperty = keysIn_objectProto.hasOwnProperty; | |
/** | |
* Creates an array of the own and inherited enumerable property names of `object`. | |
* | |
* **Note:** Non-object values are coerced to objects. | |
* | |
* @static | |
* @memberOf _ | |
* @since 3.0.0 | |
* @category Object | |
* @param {Object} object The object to query. | |
* @returns {Array} Returns the array of property names. | |
* @example | |
* | |
* function Foo() { | |
* this.a = 1; | |
* this.b = 2; | |
* } | |
* | |
* Foo.prototype.c = 3; | |
* | |
* _.keysIn(new Foo); | |
* // => ['a', 'b', 'c'] (iteration order is not guaranteed) | |
*/ | |
function keysIn(object) { | |
var index = -1, | |
isProto = _isPrototype(object), | |
props = _baseKeysIn(object), | |
propsLength = props.length, | |
indexes = _indexKeys(object), | |
skipIndexes = !!indexes, | |
result = indexes || [], | |
length = result.length; | |
while (++index < propsLength) { | |
var key = props[index]; | |
if (!(skipIndexes && (key == 'length' || _isIndex(key, length))) && | |
!(key == 'constructor' && (isProto || !keysIn_hasOwnProperty.call(object, key)))) { | |
result.push(key); | |
} | |
} | |
return result; | |
} | |
/* harmony default export */ var lodash_keysIn = (keysIn); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/assignIn.js | |
/** Used for built-in method references. */ | |
var assignIn_objectProto = Object.prototype; | |
/** Built-in value references. */ | |
var assignIn_propertyIsEnumerable = assignIn_objectProto.propertyIsEnumerable; | |
/** Detect if properties shadowing those on `Object.prototype` are non-enumerable. */ | |
var nonEnumShadows = !assignIn_propertyIsEnumerable.call({ 'valueOf': 1 }, 'valueOf'); | |
/** | |
* This method is like `_.assign` except that it iterates over own and | |
* inherited source properties. | |
* | |
* **Note:** This method mutates `object`. | |
* | |
* @static | |
* @memberOf _ | |
* @since 4.0.0 | |
* @alias extend | |
* @category Object | |
* @param {Object} object The destination object. | |
* @param {...Object} [sources] The source objects. | |
* @returns {Object} Returns `object`. | |
* @see _.assign | |
* @example | |
* | |
* function Foo() { | |
* this.b = 2; | |
* } | |
* | |
* function Bar() { | |
* this.d = 4; | |
* } | |
* | |
* Foo.prototype.c = 3; | |
* Bar.prototype.e = 5; | |
* | |
* _.assignIn({ 'a': 1 }, new Foo, new Bar); | |
* // => { 'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5 } | |
*/ | |
var assignIn = _createAssigner(function(object, source) { | |
if (nonEnumShadows || _isPrototype(source) || lodash_isArrayLike(source)) { | |
_copyObject(source, lodash_keysIn(source), object); | |
return; | |
} | |
for (var key in source) { | |
_assignValue(object, key, source[key]); | |
} | |
}); | |
/* harmony default export */ var lodash_assignIn = (assignIn); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/extend.js | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/observablemixin.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module utils/observablemixin | |
*/ | |
const attributesSymbol = Symbol( 'attributes' ); | |
const boundObservablesSymbol = Symbol( 'boundObservables' ); | |
const boundAttributesSymbol = Symbol( 'boundAttributes' ); | |
/** | |
* Mixin that injects the "observable attributes" and data binding functionality. | |
* Used mainly in the {@link module:ui/model~Model} class. | |
* | |
* @mixin ObservableMixin | |
* @mixes module:utils/emittermixin~EmitterMixin | |
* @implements module:utils/observablemixin~Observable | |
*/ | |
const ObservableMixin = { | |
/** | |
* Creates and sets the value of an observable attribute of this object. Such an attribute becomes a part | |
* of the state and is be observable. | |
* | |
* It accepts also a single object literal containing key/value pairs with attributes to be set. | |
* | |
* This method throws the observable-set-cannot-override error if the observable instance already | |
* have a property with a given attribute name. This prevents from mistakenly overriding existing | |
* properties and methods, but means that `foo.set( 'bar', 1 )` may be slightly slower than `foo.bar = 1`. | |
* | |
* @method #set | |
* @param {String} name The attributes name. | |
* @param {*} value The attributes value. | |
*/ | |
set( name, value ) { | |
// If the first parameter is an Object, iterate over its properties. | |
if ( lodash_isObject( name ) ) { | |
Object.keys( name ).forEach( attr => { | |
this.set( attr, name[ attr ] ); | |
}, this ); | |
return; | |
} | |
initObservable( this ); | |
const attributes = this[ attributesSymbol ]; | |
if ( ( name in this ) && !attributes.has( name ) ) { | |
/** | |
* Cannot override an existing property. | |
* | |
* This error is thrown when trying to {@link ~Observable#set set} an attribute with | |
* a name of an already existing property. For example: | |
* | |
* let observable = new Model(); | |
* observable.property = 1; | |
* observable.set( 'property', 2 ); // throws | |
* | |
* observable.set( 'attr', 1 ); | |
* observable.set( 'attr', 2 ); // ok, because this is an existing attribute. | |
* | |
* @error observable-set-cannot-override | |
*/ | |
throw new CKEditorError( 'observable-set-cannot-override: Cannot override an existing property.' ); | |
} | |
Object.defineProperty( this, name, { | |
enumerable: true, | |
configurable: true, | |
get() { | |
return attributes.get( name ); | |
}, | |
set( value ) { | |
const oldValue = attributes.get( name ); | |
// Allow undefined as an initial value like A.define( 'x', undefined ) (#132). | |
// Note: When attributes map has no such own property, then its value is undefined. | |
if ( oldValue !== value || !attributes.has( name ) ) { | |
attributes.set( name, value ); | |
this.fire( 'change:' + name, name, value, oldValue ); | |
} | |
} | |
} ); | |
this[ name ] = value; | |
}, | |
/** | |
* Binds observable attributes to another objects implementing {@link ~ObservableMixin} | |
* interface (like {@link module:ui/model~Model}). | |
* | |
* Once bound, the observable will immediately share the current state of attributes | |
* of the observable it is bound to and react to the changes to these attributes | |
* in the future. | |
* | |
* **Note**: To release the binding use {@link module:utils/observablemixin~ObservableMixin#unbind}. | |
* | |
* A.bind( 'a' ).to( B ); | |
* A.bind( 'a' ).to( B, 'b' ); | |
* A.bind( 'a', 'b' ).to( B, 'c', 'd' ); | |
* A.bind( 'a' ).to( B, 'b', C, 'd', ( b, d ) => b + d ); | |
* | |
* @method #bind | |
* @param {...String} bindAttrs Observable attributes that will be bound to another observable(s). | |
* @returns {module:utils/observablemixin~BindChain} | |
*/ | |
bind( ...bindAttrs ) { | |
if ( !bindAttrs.length || !isStringArray( bindAttrs ) ) { | |
/** | |
* All attributes must be strings. | |
* | |
* @error observable-bind-wrong-attrs | |
*/ | |
throw new CKEditorError( 'observable-bind-wrong-attrs: All attributes must be strings.' ); | |
} | |
if ( ( new Set( bindAttrs ) ).size !== bindAttrs.length ) { | |
/** | |
* Attributes must be unique. | |
* | |
* @error observable-bind-duplicate-attrs | |
*/ | |
throw new CKEditorError( 'observable-bind-duplicate-attrs: Attributes must be unique.' ); | |
} | |
initObservable( this ); | |
const boundAttributes = this[ boundAttributesSymbol ]; | |
bindAttrs.forEach( attrName => { | |
if ( boundAttributes.has( attrName ) ) { | |
/** | |
* Cannot bind the same attribute more that once. | |
* | |
* @error observable-bind-rebind | |
*/ | |
throw new CKEditorError( 'observable-bind-rebind: Cannot bind the same attribute more that once.' ); | |
} | |
} ); | |
const bindings = new Map(); | |
/** | |
* @typedef Binding | |
* @type Object | |
* @property {Array} attr Attribute which is bound. | |
* @property {Array} to Array of observable–attribute components of the binding (`{ observable: ..., attr: .. }`). | |
* @property {Array} callback A function which processes `to` components. | |
*/ | |
bindAttrs.forEach( a => { | |
const binding = { attr: a, to: [] }; | |
boundAttributes.set( a, binding ); | |
bindings.set( a, binding ); | |
} ); | |
/** | |
* @typedef BindChain | |
* @type Object | |
* @property {Function} to See {@link ~ObservableMixin#_bindTo}. | |
* @property {module:utils/observablemixin~Observable} _observable The observable which initializes the binding. | |
* @property {Array} _bindAttrs Array of `_observable` attributes to be bound. | |
* @property {Array} _to Array of `to()` observable–attributes (`{ observable: toObservable, attrs: ...toAttrs }`). | |
* @property {Map} _bindings Stores bindings to be kept in | |
* {@link ~ObservableMixin#_boundAttributes}/{@link ~ObservableMixin#_boundObservables} | |
* initiated in this binding chain. | |
*/ | |
return { | |
to: bindTo, | |
_observable: this, | |
_bindAttrs: bindAttrs, | |
_to: [], | |
_bindings: bindings | |
}; | |
}, | |
/** | |
* Removes the binding created with {@link ~ObservableMixin#bind}. | |
* | |
* A.unbind( 'a' ); | |
* A.unbind(); | |
* | |
* @method #unbind | |
* @param {...String} [unbindAttrs] Observable attributes to be unbound. All the bindings will | |
* be released if no attributes provided. | |
*/ | |
unbind( ...unbindAttrs ) { | |
// Nothing to do here if not inited yet. | |
if ( !( attributesSymbol in this ) ) { | |
return; | |
} | |
const boundAttributes = this[ boundAttributesSymbol ]; | |
const boundObservables = this[ boundObservablesSymbol ]; | |
if ( unbindAttrs.length ) { | |
if ( !isStringArray( unbindAttrs ) ) { | |
/** | |
* Attributes must be strings. | |
* | |
* @error observable-unbind-wrong-attrs | |
*/ | |
throw new CKEditorError( 'observable-unbind-wrong-attrs: Attributes must be strings.' ); | |
} | |
unbindAttrs.forEach( attrName => { | |
const binding = boundAttributes.get( attrName ); | |
let toObservable, toAttr, toAttrs, toAttrBindings; | |
binding.to.forEach( to => { | |
// TODO: ES6 destructuring. | |
toObservable = to[ 0 ]; | |
toAttr = to[ 1 ]; | |
toAttrs = boundObservables.get( toObservable ); | |
toAttrBindings = toAttrs[ toAttr ]; | |
toAttrBindings.delete( binding ); | |
if ( !toAttrBindings.size ) { | |
delete toAttrs[ toAttr ]; | |
} | |
if ( !Object.keys( toAttrs ).length ) { | |
boundObservables.delete( toObservable ); | |
this.stopListening( toObservable, 'change' ); | |
} | |
} ); | |
boundAttributes.delete( attrName ); | |
} ); | |
} else { | |
boundObservables.forEach( ( bindings, boundObservable ) => { | |
this.stopListening( boundObservable, 'change' ); | |
} ); | |
boundObservables.clear(); | |
boundAttributes.clear(); | |
} | |
}, | |
/** | |
* Turns the given methods of this object into event-based ones. This means that the new method will fire an event | |
* (named after the method) and the original action will be plugged as a listener to that event. | |
* | |
* This is a very simplified method decoration. Itself it doesn't change the behavior of a method (expect adding the event), | |
* but it allows to modify it later on by listening to the method's event. | |
* | |
* For example, in order to cancel the method execution one can stop the event: | |
* | |
* class Foo { | |
* constructor() { | |
* this.decorate( 'method' ); | |
* } | |
* | |
* method() { | |
* console.log( 'called!' ); | |
* } | |
* } | |
* | |
* const foo = new Foo(); | |
* foo.on( 'method', ( evt ) => { | |
* evt.stop(); | |
* }, { priority: 'high' } ); | |
* | |
* foo.method(); // Nothing is logged. | |
* | |
* | |
* Note: we used a high priority listener here to execute this callback before the one which | |
* calls the orignal method (which used the default priority). | |
* | |
* It's also possible to change the return value: | |
* | |
* foo.on( 'method', ( evt ) => { | |
* evt.return = 'Foo!'; | |
* } ); | |
* | |
* foo.method(); // -> 'Foo' | |
* | |
* Finally, it's possible to access and modify the parameters: | |
* | |
* method( a, b ) { | |
* console.log( `${ a }, ${ b }` ); | |
* } | |
* | |
* // ... | |
* | |
* foo.on( 'method', ( evt, args ) => { | |
* args[ 0 ] = 3; | |
* | |
* console.log( args[ 1 ] ); // -> 2 | |
* }, { priority: 'high' } ); | |
* | |
* foo.method( 1, 2 ); // -> '3, 2' | |
* | |
* @method #decorate | |
* @param {String} methodName Name of the method to decorate. | |
*/ | |
decorate( methodName ) { | |
const originalMethod = this[ methodName ]; | |
if ( !originalMethod ) { | |
/** | |
* Cannot decorate an undefined method. | |
* | |
* @error observablemixin-cannot-decorate-undefined | |
* @param {Object} object The object which method should be decorated. | |
* @param {String} methodName Name of the method which does not exist. | |
*/ | |
throw new CKEditorError( | |
'observablemixin-cannot-decorate-undefined: Cannot decorate an undefined method.', | |
{ object: this, methodName } | |
); | |
} | |
this.on( methodName, ( evt, args ) => { | |
evt.return = originalMethod.apply( this, args ); | |
} ); | |
this[ methodName ] = function( ...args ) { | |
return this.fire( methodName, args ); | |
}; | |
} | |
/** | |
* @private | |
* @member ~ObservableMixin#_boundAttributes | |
*/ | |
/** | |
* @private | |
* @member ~ObservableMixin#_boundObservables | |
*/ | |
/** | |
* @private | |
* @member ~ObservableMixin#_bindTo | |
*/ | |
}; | |
/* harmony default export */ var observablemixin = (ObservableMixin); | |
// Init symbol properties needed to for the observable mechanism to work. | |
// | |
// @private | |
// @param {module:utils/observablemixin~ObservableMixin} observable | |
function initObservable( observable ) { | |
// Do nothing if already inited. | |
if ( attributesSymbol in observable ) { | |
return; | |
} | |
// The internal hash containing the observable's state. | |
// | |
// @private | |
// @type {Map} | |
Object.defineProperty( observable, attributesSymbol, { | |
value: new Map() | |
} ); | |
// Map containing bindings to external observables. It shares the binding objects | |
// (`{ observable: A, attr: 'a', to: ... }`) with {@link module:utils/observablemixin~ObservableMixin#_boundAttributes} and | |
// it is used to observe external observables to update own attributes accordingly. | |
// See {@link module:utils/observablemixin~ObservableMixin#bind}. | |
// | |
// A.bind( 'a', 'b', 'c' ).to( B, 'x', 'y', 'x' ); | |
// console.log( A._boundObservables ); | |
// | |
// Map( { | |
// B: { | |
// x: Set( [ | |
// { observable: A, attr: 'a', to: [ [ B, 'x' ] ] }, | |
// { observable: A, attr: 'c', to: [ [ B, 'x' ] ] } | |
// ] ), | |
// y: Set( [ | |
// { observable: A, attr: 'b', to: [ [ B, 'y' ] ] }, | |
// ] ) | |
// } | |
// } ) | |
// | |
// A.bind( 'd' ).to( B, 'z' ).to( C, 'w' ).as( callback ); | |
// console.log( A._boundObservables ); | |
// | |
// Map( { | |
// B: { | |
// x: Set( [ | |
// { observable: A, attr: 'a', to: [ [ B, 'x' ] ] }, | |
// { observable: A, attr: 'c', to: [ [ B, 'x' ] ] } | |
// ] ), | |
// y: Set( [ | |
// { observable: A, attr: 'b', to: [ [ B, 'y' ] ] }, | |
// ] ), | |
// z: Set( [ | |
// { observable: A, attr: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback } | |
// ] ) | |
// }, | |
// C: { | |
// w: Set( [ | |
// { observable: A, attr: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback } | |
// ] ) | |
// } | |
// } ) | |
// | |
// @private | |
// @type {Map} | |
Object.defineProperty( observable, boundObservablesSymbol, { | |
value: new Map() | |
} ); | |
// Object that stores which attributes of this observable are bound and how. It shares | |
// the binding objects (`{ observable: A, attr: 'a', to: ... }`) with {@link utils.ObservableMixin#_boundObservables}. | |
// This data structure is a reverse of {@link utils.ObservableMixin#_boundObservables} and it is helpful for | |
// {@link utils.ObservableMixin#unbind}. | |
// | |
// See {@link utils.ObservableMixin#bind}. | |
// | |
// A.bind( 'a', 'b', 'c' ).to( B, 'x', 'y', 'x' ); | |
// console.log( A._boundAttributes ); | |
// | |
// Map( { | |
// a: { observable: A, attr: 'a', to: [ [ B, 'x' ] ] }, | |
// b: { observable: A, attr: 'b', to: [ [ B, 'y' ] ] }, | |
// c: { observable: A, attr: 'c', to: [ [ B, 'x' ] ] } | |
// } ) | |
// | |
// A.bind( 'd' ).to( B, 'z' ).to( C, 'w' ).as( callback ); | |
// console.log( A._boundAttributes ); | |
// | |
// Map( { | |
// a: { observable: A, attr: 'a', to: [ [ B, 'x' ] ] }, | |
// b: { observable: A, attr: 'b', to: [ [ B, 'y' ] ] }, | |
// c: { observable: A, attr: 'c', to: [ [ B, 'x' ] ] }, | |
// d: { observable: A, attr: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback } | |
// } ) | |
// | |
// @private | |
// @type {Map} | |
Object.defineProperty( observable, boundAttributesSymbol, { | |
value: new Map() | |
} ); | |
} | |
// A chaining for {@link module:utils/observablemixin~ObservableMixin#bind} providing `.to()` interface. | |
// | |
// @private | |
// @param {...[Observable|String|Function]} args Arguments of the `.to( args )` binding. | |
function bindTo( ...args ) { | |
const parsedArgs = parseBindToArgs( ...args ); | |
const bindingsKeys = Array.from( this._bindings.keys() ); | |
const numberOfBindings = bindingsKeys.length; | |
// Eliminate A.bind( 'x' ).to( B, C ) | |
if ( !parsedArgs.callback && parsedArgs.to.length > 1 ) { | |
/** | |
* Binding multiple observables only possible with callback. | |
* | |
* @error observable-bind-no-callback | |
*/ | |
throw new CKEditorError( 'observable-bind-to-no-callback: Binding multiple observables only possible with callback.' ); | |
} | |
// Eliminate A.bind( 'x', 'y' ).to( B, callback ) | |
if ( numberOfBindings > 1 && parsedArgs.callback ) { | |
/** | |
* Cannot bind multiple attributes and use a callback in one binding. | |
* | |
* @error observable-bind-to-extra-callback | |
*/ | |
throw new CKEditorError( 'observable-bind-to-extra-callback: Cannot bind multiple attributes and use a callback in one binding.' ); | |
} | |
parsedArgs.to.forEach( to => { | |
// Eliminate A.bind( 'x', 'y' ).to( B, 'a' ) | |
if ( to.attrs.length && to.attrs.length !== numberOfBindings ) { | |
/** | |
* The number of attributes must match. | |
* | |
* @error observable-bind-to-attrs-length | |
*/ | |
throw new CKEditorError( 'observable-bind-to-attrs-length: The number of attributes must match.' ); | |
} | |
// When no to.attrs specified, observing source attributes instead i.e. | |
// A.bind( 'x', 'y' ).to( B ) -> Observe B.x and B.y | |
if ( !to.attrs.length ) { | |
to.attrs = this._bindAttrs; | |
} | |
} ); | |
this._to = parsedArgs.to; | |
// Fill {@link BindChain#_bindings} with callback. When the callback is set there's only one binding. | |
if ( parsedArgs.callback ) { | |
this._bindings.get( bindingsKeys[ 0 ] ).callback = parsedArgs.callback; | |
} | |
attachBindToListeners( this._observable, this._to ); | |
// Update observable._boundAttributes and observable._boundObservables. | |
updateBindToBound( this ); | |
// Set initial values of bound attributes. | |
this._bindAttrs.forEach( attrName => { | |
updateBoundObservableAttr( this._observable, attrName ); | |
} ); | |
} | |
// Check if all entries of the array are of `String` type. | |
// | |
// @private | |
// @param {Array} arr An array to be checked. | |
// @returns {Boolean} | |
function isStringArray( arr ) { | |
return arr.every( a => typeof a == 'string' ); | |
} | |
// Parses and validates {@link Observable#bind}`.to( args )` arguments and returns | |
// an object with a parsed structure. For example | |
// | |
// A.bind( 'x' ).to( B, 'a', C, 'b', call ); | |
// | |
// becomes | |
// | |
// { | |
// to: [ | |
// { observable: B, attrs: [ 'a' ] }, | |
// { observable: C, attrs: [ 'b' ] }, | |
// ], | |
// callback: call | |
// } | |
// | |
// @private | |
// @param {...*} args Arguments of {@link Observable#bind}`.to( args )`. | |
// @returns {Object} | |
function parseBindToArgs( ...args ) { | |
// Eliminate A.bind( 'x' ).to() | |
if ( !args.length ) { | |
/** | |
* Invalid argument syntax in `to()`. | |
* | |
* @error observable-bind-to-parse-error | |
*/ | |
throw new CKEditorError( 'observable-bind-to-parse-error: Invalid argument syntax in `to()`.' ); | |
} | |
const parsed = { to: [] }; | |
let lastObservable; | |
if ( typeof args[ args.length - 1 ] == 'function' ) { | |
parsed.callback = args.pop(); | |
} | |
args.forEach( a => { | |
if ( typeof a == 'string' ) { | |
lastObservable.attrs.push( a ); | |
} else if ( typeof a == 'object' ) { | |
lastObservable = { observable: a, attrs: [] }; | |
parsed.to.push( lastObservable ); | |
} else { | |
throw new CKEditorError( 'observable-bind-to-parse-error: Invalid argument syntax in `to()`.' ); | |
} | |
} ); | |
return parsed; | |
} | |
// Synchronizes {@link module:utils/observablemixin#_boundObservables} with {@link Binding}. | |
// | |
// @private | |
// @param {Binding} binding A binding to store in {@link Observable#_boundObservables}. | |
// @param {Observable} toObservable A observable, which is a new component of `binding`. | |
// @param {String} toAttrName A name of `toObservable`'s attribute, a new component of the `binding`. | |
function updateBoundObservables( observable, binding, toObservable, toAttrName ) { | |
const boundObservables = observable[ boundObservablesSymbol ]; | |
const bindingsToObservable = boundObservables.get( toObservable ); | |
const bindings = bindingsToObservable || {}; | |
if ( !bindings[ toAttrName ] ) { | |
bindings[ toAttrName ] = new Set(); | |
} | |
// Pass the binding to a corresponding Set in `observable._boundObservables`. | |
bindings[ toAttrName ].add( binding ); | |
if ( !bindingsToObservable ) { | |
boundObservables.set( toObservable, bindings ); | |
} | |
} | |
// Synchronizes {@link Observable#_boundAttributes} and {@link Observable#_boundObservables} | |
// with {@link BindChain}. | |
// | |
// Assuming the following binding being created | |
// | |
// A.bind( 'a', 'b' ).to( B, 'x', 'y' ); | |
// | |
// the following bindings were initialized by {@link Observable#bind} in {@link BindChain#_bindings}: | |
// | |
// { | |
// a: { observable: A, attr: 'a', to: [] }, | |
// b: { observable: A, attr: 'b', to: [] }, | |
// } | |
// | |
// Iterate over all bindings in this chain and fill their `to` properties with | |
// corresponding to( ... ) arguments (components of the binding), so | |
// | |
// { | |
// a: { observable: A, attr: 'a', to: [ B, 'x' ] }, | |
// b: { observable: A, attr: 'b', to: [ B, 'y' ] }, | |
// } | |
// | |
// Then update the structure of {@link Observable#_boundObservables} with updated | |
// binding, so it becomes: | |
// | |
// Map( { | |
// B: { | |
// x: Set( [ | |
// { observable: A, attr: 'a', to: [ [ B, 'x' ] ] } | |
// ] ), | |
// y: Set( [ | |
// { observable: A, attr: 'b', to: [ [ B, 'y' ] ] }, | |
// ] ) | |
// } | |
// } ) | |
// | |
// @private | |
// @param {BindChain} chain The binding initialized by {@link Observable#bind}. | |
function updateBindToBound( chain ) { | |
let toAttr; | |
chain._bindings.forEach( ( binding, attrName ) => { | |
// Note: For a binding without a callback, this will run only once | |
// like in A.bind( 'x', 'y' ).to( B, 'a', 'b' ) | |
// TODO: ES6 destructuring. | |
chain._to.forEach( to => { | |
toAttr = to.attrs[ binding.callback ? 0 : chain._bindAttrs.indexOf( attrName ) ]; | |
binding.to.push( [ to.observable, toAttr ] ); | |
updateBoundObservables( chain._observable, binding, to.observable, toAttr ); | |
} ); | |
} ); | |
} | |
// Updates an attribute of a {@link Observable} with a value | |
// determined by an entry in {@link Observable#_boundAttributes}. | |
// | |
// @private | |
// @param {Observable} observable A observable which attribute is to be updated. | |
// @param {String} attrName An attribute to be updated. | |
function updateBoundObservableAttr( observable, attrName ) { | |
const boundAttributes = observable[ boundAttributesSymbol ]; | |
const binding = boundAttributes.get( attrName ); | |
let attrValue; | |
// When a binding with callback is created like | |
// | |
// A.bind( 'a' ).to( B, 'b', C, 'c', callback ); | |
// | |
// collect B.b and C.c, then pass them to callback to set A.a. | |
if ( binding.callback ) { | |
attrValue = binding.callback.apply( observable, binding.to.map( to => to[ 0 ][ to[ 1 ] ] ) ); | |
} else { | |
attrValue = binding.to[ 0 ]; | |
attrValue = attrValue[ 0 ][ attrValue[ 1 ] ]; | |
} | |
if ( observable.hasOwnProperty( attrName ) ) { | |
observable[ attrName ] = attrValue; | |
} else { | |
observable.set( attrName, attrValue ); | |
} | |
} | |
// Starts listening to changes in {@link BindChain._to} observables to update | |
// {@link BindChain._observable} {@link BindChain._bindAttrs}. Also sets the | |
// initial state of {@link BindChain._observable}. | |
// | |
// @private | |
// @param {BindChain} chain The chain initialized by {@link Observable#bind}. | |
function attachBindToListeners( observable, toBindings ) { | |
toBindings.forEach( to => { | |
const boundObservables = observable[ boundObservablesSymbol ]; | |
let bindings; | |
// If there's already a chain between the observables (`observable` listens to | |
// `to.observable`), there's no need to create another `change` event listener. | |
if ( !boundObservables.get( to.observable ) ) { | |
observable.listenTo( to.observable, 'change', ( evt, attrName ) => { | |
bindings = boundObservables.get( to.observable )[ attrName ]; | |
// Note: to.observable will fire for any attribute change, react | |
// to changes of attributes which are bound only. | |
if ( bindings ) { | |
bindings.forEach( binding => { | |
updateBoundObservableAttr( observable, binding.attr ); | |
} ); | |
} | |
} ); | |
} | |
} ); | |
} | |
lodash_assignIn( ObservableMixin, emittermixin ); | |
/** | |
* Fired when an attribute changed value. | |
* | |
* observable.set( 'prop', 1 ); | |
* | |
* observable.on( 'change:prop', ( evt, propertyName, newValue, oldValue ) => { | |
* console.log( `${ propertyName } has changed from ${ oldValue } to ${ newValue }` ); | |
* } ) | |
* | |
* observable.prop = 2; // -> 'prop has changed from 1 to 2' | |
* | |
* @event module:utils/observablemixin~ObservableMixin#change:{attribute} | |
* @param {String} name The attribute name. | |
* @param {*} value The new attribute value. | |
* @param {*} oldValue The previous attribute value. | |
*/ | |
/** | |
* Interface representing classes which mix in {@link module:utils/observablemixin~ObservableMixin}. | |
* | |
* @interface Observable | |
*/ | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/objecttomap.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module utils/objecttomap | |
*/ | |
/** | |
* Transforms object to map. | |
* | |
* const map = objectToMap( { 'foo': 1, 'bar': 2 } ); | |
* map.get( 'foo' ); // 1 | |
* | |
* @param {Object} obj Object to transform. | |
* @returns {Map} Map created from object. | |
*/ | |
function objectToMap( obj ) { | |
const map = new Map(); | |
for ( const key in obj ) { | |
map.set( key, obj[ key ] ); | |
} | |
return map; | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/tomap.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module utils/tomap | |
*/ | |
/** | |
* Transforms object or iterable to map. Iterable needs to be in the format acceptable by the `Map` constructor. | |
* | |
* map = toMap( { 'foo': 1, 'bar': 2 } ); | |
* map = toMap( [ [ 'foo', 1 ], [ 'bar', 2 ] ] ); | |
* map = toMap( anotherMap ); | |
* | |
* @param {Object|Iterable} data Object or iterable to transform. | |
* @returns {Map} Map created from data. | |
*/ | |
function toMap( data ) { | |
if ( lodash_isPlainObject( data ) ) { | |
return objectToMap( data ); | |
} else { | |
return new Map( data ); | |
} | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/node.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/model/node | |
*/ | |
/** | |
* Model node. Most basic structure of model tree. | |
* | |
* This is an abstract class that is a base for other classes representing different nodes in model. | |
* | |
* **Note:** If a node is detached from the model tree, you can manipulate it using it's API. | |
* However, it is **very important** that nodes already attached to model tree should be only changed through | |
* {@link module:engine/model/document~Document#batch Batch API}. | |
* | |
* Changes done by `Node` methods, like {@link module:engine/model/element~Element#insertChildren insertChildren} or | |
* {@link module:engine/model/node~Node#setAttribute setAttribute} | |
* do not generate {@link module:engine/model/operation/operation~Operation operations} | |
* which are essential for correct editor work if you modify nodes in {@link module:engine/model/document~Document document} root. | |
* | |
* The flow of working on `Node` (and classes that inherits from it) is as such: | |
* 1. You can create a `Node` instance, modify it using it's API. | |
* 2. Add `Node` to the model using `Batch` API. | |
* 3. Change `Node` that was already added to the model using `Batch` API. | |
* | |
* Similarly, you cannot use `Batch` API on a node that has not been added to the model tree, with the exception | |
* of {@link module:engine/model/batch~Batch#insert inserting} that node to the model tree. | |
* | |
* Be aware that using {@link module:engine/model/batch~Batch#remove remove from Batch API} does not allow to use `Node` API because | |
* the information about `Node` is still kept in model document. | |
* | |
* In case of {@link module:engine/model/element~Element element node}, adding and removing children also counts as changing a node and | |
* follows same rules. | |
*/ | |
class node_Node { | |
/** | |
* Creates a model node. | |
* | |
* This is an abstract class, so this constructor should not be used directly. | |
* | |
* @abstract | |
* @param {Object} [attrs] Node's attributes. See {@link module:utils/tomap~toMap} for a list of accepted values. | |
*/ | |
constructor( attrs ) { | |
/** | |
* Parent of this node. It could be {@link module:engine/model/element~Element} | |
* or {@link module:engine/model/documentfragment~DocumentFragment}. | |
* Equals to `null` if the node has no parent. | |
* | |
* @readonly | |
* @member {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment|null} | |
*/ | |
this.parent = null; | |
/** | |
* Attributes set on this node. | |
* | |
* @private | |
* @member {Map} module:engine/model/node~Node#_attrs | |
*/ | |
this._attrs = toMap( attrs ); | |
} | |
/** | |
* Index of this node in it's parent or `null` if the node has no parent. | |
* | |
* Accessing this property throws an error if this node's parent element does not contain it. | |
* This means that model tree got broken. | |
* | |
* @readonly | |
* @type {Number|null} | |
*/ | |
get index() { | |
let pos; | |
if ( !this.parent ) { | |
return null; | |
} | |
if ( ( pos = this.parent.getChildIndex( this ) ) === null ) { | |
throw new CKEditorError( 'model-node-not-found-in-parent: The node\'s parent does not contain this node.' ); | |
} | |
return pos; | |
} | |
/** | |
* Offset at which this node starts in it's parent. It is equal to the sum of {@link #offsetSize offsetSize} | |
* of all it's previous siblings. Equals to `null` if node has no parent. | |
* | |
* Accessing this property throws an error if this node's parent element does not contain it. | |
* This means that model tree got broken. | |
* | |
* @readonly | |
* @type {Number|Null} | |
*/ | |
get startOffset() { | |
let pos; | |
if ( !this.parent ) { | |
return null; | |
} | |
if ( ( pos = this.parent.getChildStartOffset( this ) ) === null ) { | |
throw new CKEditorError( 'model-node-not-found-in-parent: The node\'s parent does not contain this node.' ); | |
} | |
return pos; | |
} | |
/** | |
* Offset size of this node. Represents how much "offset space" is occupied by the node in it's parent. | |
* It is important for {@link module:engine/model/position~Position position}. When node has `offsetSize` greater than `1`, position | |
* can be placed between that node start and end. `offsetSize` greater than `1` is for nodes that represents more | |
* than one entity, i.e. {@link module:engine/model/text~Text text node}. | |
* | |
* @readonly | |
* @type {Number} | |
*/ | |
get offsetSize() { | |
return 1; | |
} | |
/** | |
* Offset at which this node ends in it's parent. It is equal to the sum of this node's | |
* {@link module:engine/model/node~Node#startOffset start offset} and {@link #offsetSize offset size}. | |
* Equals to `null` if the node has no parent. | |
* | |
* @readonly | |
* @type {Number|null} | |
*/ | |
get endOffset() { | |
if ( !this.parent ) { | |
return null; | |
} | |
return this.startOffset + this.offsetSize; | |
} | |
/** | |
* Node's next sibling or `null` if the node is a last child of it's parent or if the node has no parent. | |
* | |
* @readonly | |
* @type {module:engine/model/node~Node|null} | |
*/ | |
get nextSibling() { | |
const index = this.index; | |
return ( index !== null && this.parent.getChild( index + 1 ) ) || null; | |
} | |
/** | |
* Node's previous sibling or `null` if the node is a first child of it's parent or if the node has no parent. | |
* | |
* @readonly | |
* @type {module:engine/model/node~Node|null} | |
*/ | |
get previousSibling() { | |
const index = this.index; | |
return ( index !== null && this.parent.getChild( index - 1 ) ) || null; | |
} | |
/** | |
* The top-most ancestor of the node. If node has no parent it is the root itself. If the node is a part | |
* of {@link module:engine/model/documentfragment~DocumentFragment}, it's `root` is equal to that `DocumentFragment`. | |
* | |
* @readonly | |
* @type {module:engine/model/node~Node|module:engine/model/documentfragment~DocumentFragment} | |
*/ | |
get root() { | |
let root = this; // eslint-disable-line consistent-this | |
while ( root.parent ) { | |
root = root.parent; | |
} | |
return root; | |
} | |
/** | |
* {@link module:engine/model/document~Document Document} that owns this node or `null` if the node has no parent or is inside | |
* a {@link module:engine/model/documentfragment~DocumentFragment DocumentFragment}. | |
* | |
* @readonly | |
* @type {module:engine/model/document~Document|null} | |
*/ | |
get document() { | |
// This is a top element of a sub-tree. | |
if ( this.root == this ) { | |
return null; | |
} | |
// Root may be `DocumentFragment` which does not have document property. | |
return this.root.document || null; | |
} | |
/** | |
* Creates a copy of this node, that is a node with exactly same attributes, and returns it. | |
* | |
* @returns {module:engine/model/node~Node} Node with same attributes as this node. | |
*/ | |
clone() { | |
return new node_Node( this._attrs ); | |
} | |
/** | |
* Gets path to the node. The path is an array containing starting offsets of consecutive ancestors of this node, | |
* beginning from {@link module:engine/model/node~Node#root root}, down to this node's starting offset. The path can be used to | |
* create {@link module:engine/model/position~Position Position} instance. | |
* | |
* const abc = new Text( 'abc' ); | |
* const foo = new Text( 'foo' ); | |
* const h1 = new Element( 'h1', null, new Text( 'header' ) ); | |
* const p = new Element( 'p', null, [ abc, foo ] ); | |
* const div = new Element( 'div', null, [ h1, p ] ); | |
* foo.getPath(); // Returns [ 1, 3 ]. `foo` is in `p` which is in `div`. `p` starts at offset 1, while `foo` at 3. | |
* h1.getPath(); // Returns [ 0 ]. | |
* div.getPath(); // Returns []. | |
* | |
* @returns {Array.<Number>} The path. | |
*/ | |
getPath() { | |
const path = []; | |
let node = this; // eslint-disable-line consistent-this | |
while ( node.parent ) { | |
path.unshift( node.startOffset ); | |
node = node.parent; | |
} | |
return path; | |
} | |
/** | |
* Returns ancestors array of this node. | |
* | |
* @param {Object} options Options object. | |
* @param {Boolean} [options.includeSelf=false] When set to `true` this node will be also included in parent's array. | |
* @param {Boolean} [options.parentFirst=false] When set to `true`, array will be sorted from node's parent to root element, | |
* otherwise root element will be the first item in the array. | |
* @returns {Array} Array with ancestors. | |
*/ | |
getAncestors( options = { includeSelf: false, parentFirst: false } ) { | |
const ancestors = []; | |
let parent = options.includeSelf ? this : this.parent; | |
while ( parent ) { | |
ancestors[ options.parentFirst ? 'push' : 'unshift' ]( parent ); | |
parent = parent.parent; | |
} | |
return ancestors; | |
} | |
/** | |
* Returns a {@link module:engine/model/element~Element} or {@link module:engine/model/documentfragment~DocumentFragment} | |
* which is a common ancestor of both nodes. | |
* | |
* @param {module:engine/model/node~Node} node The second node. | |
* @param {Object} options Options object. | |
* @param {Boolean} [options.includeSelf=false] When set to `true` both nodes will be considered "ancestors" too. | |
* Which means that if e.g. node A is inside B, then their common ancestor will be B. | |
* @returns {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment|null} | |
*/ | |
getCommonAncestor( node, options = {} ) { | |
const ancestorsA = this.getAncestors( options ); | |
const ancestorsB = node.getAncestors( options ); | |
let i = 0; | |
while ( ancestorsA[ i ] == ancestorsB[ i ] && ancestorsA[ i ] ) { | |
i++; | |
} | |
return i === 0 ? null : ancestorsA[ i - 1 ]; | |
} | |
/** | |
* Removes this node from it's parent. | |
*/ | |
remove() { | |
this.parent.removeChildren( this.index ); | |
} | |
/** | |
* Checks if the node has an attribute with given key. | |
* | |
* @param {String} key Key of attribute to check. | |
* @returns {Boolean} `true` if attribute with given key is set on node, `false` otherwise. | |
*/ | |
hasAttribute( key ) { | |
return this._attrs.has( key ); | |
} | |
/** | |
* Gets an attribute value for given key or `undefined` if that attribute is not set on node. | |
* | |
* @param {String} key Key of attribute to look for. | |
* @returns {*} Attribute value or `undefined`. | |
*/ | |
getAttribute( key ) { | |
return this._attrs.get( key ); | |
} | |
/** | |
* Returns iterator that iterates over this node's attributes. | |
* | |
* Attributes are returned as arrays containing two items. First one is attribute key and second is attribute value. | |
* This format is accepted by native `Map` object and also can be passed in `Node` constructor. | |
* | |
* @returns {Iterable.<*>} | |
*/ | |
getAttributes() { | |
return this._attrs.entries(); | |
} | |
/** | |
* Returns iterator that iterates over this node's attribute keys. | |
* | |
* @returns {Iterator.<String>} | |
*/ | |
getAttributeKeys() { | |
return this._attrs.keys(); | |
} | |
/** | |
* Sets attribute on the node. If attribute with the same key already is set, it's value is overwritten. | |
* | |
* @param {String} key Key of attribute to set. | |
* @param {*} value Attribute value. | |
*/ | |
setAttribute( key, value ) { | |
this._attrs.set( key, value ); | |
} | |
/** | |
* Removes all attributes from the node and sets given attributes. | |
* | |
* @param {Object} [attrs] Attributes to set. See {@link module:utils/tomap~toMap} for a list of accepted values. | |
*/ | |
setAttributesTo( attrs ) { | |
this._attrs = toMap( attrs ); | |
} | |
/** | |
* Removes an attribute with given key from the node. | |
* | |
* @param {String} key Key of attribute to remove. | |
* @returns {Boolean} `true` if the attribute was set on the element, `false` otherwise. | |
*/ | |
removeAttribute( key ) { | |
return this._attrs.delete( key ); | |
} | |
/** | |
* Removes all attributes from the node. | |
*/ | |
clearAttributes() { | |
this._attrs.clear(); | |
} | |
/** | |
* Converts `Node` to plain object and returns it. | |
* | |
* @returns {Object} `Node` converted to plain object. | |
*/ | |
toJSON() { | |
const json = {}; | |
if ( this._attrs.size ) { | |
json.attributes = [ ...this._attrs ]; | |
} | |
return json; | |
} | |
/** | |
* Checks whether given model tree object is of given type. | |
* | |
* This method is useful when processing model tree objects that are of unknown type. For example, a function | |
* may return {@link module:engine/model/documentfragment~DocumentFragment} or {@link module:engine/model/node~Node} | |
* that can be either text node or element. This method can be used to check what kind of object is returned. | |
* | |
* obj.is( 'node' ); // true for any node, false for document fragment | |
* obj.is( 'documentFragment' ); // true for document fragment, false for any node | |
* obj.is( 'element' ); // true for any element, false for text node or document fragment | |
* obj.is( 'element', 'paragraph' ); // true only for element which name is 'paragraph' | |
* obj.is( 'paragraph' ); // shortcut for obj.is( 'element', 'paragraph' ) | |
* obj.is( 'text' ); // true for text node, false for element and document fragment | |
* obj.is( 'textProxy' ); // true for text proxy object | |
* | |
* @method #is | |
* @param {'element'|'rootElement'|'text'|'textProxy'|'documentFragment'} type | |
* @returns {Boolean} | |
*/ | |
} | |
/** | |
* The node's parent does not contain this node. | |
* | |
* This error may be thrown from corrupted trees. | |
* | |
* @error model-node-not-found-in-parent | |
*/ | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/text.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/model/text | |
*/ | |
/** | |
* Model text node. Type of {@link module:engine/model/node~Node node} that contains {@link module:engine/model/text~Text#data text data}. | |
* | |
* **Important:** see {@link module:engine/model/node~Node} to read about restrictions using `Text` and `Node` API. | |
* | |
* **Note:** keep in mind that `Text` instances might indirectly got removed from model tree when model is changed. | |
* This happens when {@link module:engine/model/writer~writer model writer} is used to change model and the text node is merged with | |
* another text node. Then, both text nodes are removed and a new text node is inserted into the model. Because of | |
* this behavior, keeping references to `Text` is not recommended. Instead, consider creating | |
* {@link module:engine/model/liveposition~LivePosition live position} placed before the text node. | |
*/ | |
class text_Text extends node_Node { | |
/** | |
* Creates a text node. | |
* | |
* @param {String} data Node's text. | |
* @param {Object} [attrs] Node's attributes. See {@link module:utils/tomap~toMap} for a list of accepted values. | |
*/ | |
constructor( data, attrs ) { | |
super( attrs ); | |
/** | |
* Text data contained in this text node. | |
* | |
* @type {String} | |
*/ | |
this.data = data || ''; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
get offsetSize() { | |
return this.data.length; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
is( type ) { | |
return type == 'text'; | |
} | |
/** | |
* Creates a copy of this text node and returns it. Created text node has same text data and attributes as original text node. | |
*/ | |
clone() { | |
return new text_Text( this.data, this.getAttributes() ); | |
} | |
/** | |
* Converts `Text` instance to plain object and returns it. | |
* | |
* @returns {Object} `Text` instance converted to plain object. | |
*/ | |
toJSON() { | |
const json = super.toJSON(); | |
json.data = this.data; | |
return json; | |
} | |
/** | |
* Creates a `Text` instance from given plain object (i.e. parsed JSON string). | |
* | |
* @param {Object} json Plain object to be converted to `Text`. | |
* @returns {module:engine/model/text~Text} `Text` instance created using given plain object. | |
*/ | |
static fromJSON( json ) { | |
return new text_Text( json.data, json.attributes ); | |
} | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/textproxy.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/model/textproxy | |
*/ | |
/** | |
* `TextProxy` represents a part of {@link module:engine/model/text~Text text node}. | |
* | |
* Since {@link module:engine/model/position~Position positions} can be placed between characters of a text node, | |
* {@link module:engine/model/range~Range ranges} may contain only parts of text nodes. When {@link module:engine/model/range~Range#getItems | |
* getting items} | |
* contained in such range, we need to represent a part of that text node, since returning the whole text node would be incorrect. | |
* `TextProxy` solves this issue. | |
* | |
* `TextProxy` has an API similar to {@link module:engine/model/text~Text Text} and allows to do most of the common tasks performed | |
* on model nodes. | |
* | |
* **Note:** Some `TextProxy` instances may represent whole text node, not just a part of it. | |
* See {@link module:engine/model/textproxy~TextProxy#isPartial}. | |
* | |
* **Note:** `TextProxy` is not an instance of {@link module:engine/model/node~Node node}. Keep this in mind when using it as a | |
* parameter of methods. | |
* | |
* **Note:** `TextProxy` is a readonly interface. If you want to perform changes on model data represented by a `TextProxy` | |
* use {@link module:engine/model/writer~writer model writer API}. | |
* | |
* **Note:** `TextProxy` instances are created on the fly, basing on the current state of model. Because of this, it is | |
* highly unrecommended to store references to `TextProxy` instances. `TextProxy` instances are not refreshed when | |
* model changes, so they might get invalidated. Instead, consider creating {@link module:engine/model/liveposition~LivePosition live | |
* position}. | |
* | |
* `TextProxy` instances are created by {@link module:engine/model/treewalker~TreeWalker model tree walker}. You should not need to create | |
* an instance of this class by your own. | |
*/ | |
class textproxy_TextProxy { | |
/** | |
* Creates a text proxy. | |
* | |
* @protected | |
* @param {module:engine/model/text~Text} textNode Text node which part is represented by this text proxy. | |
* @param {Number} offsetInText Offset in {@link module:engine/model/textproxy~TextProxy#textNode text node} from which the text proxy | |
* starts. | |
* @param {Number} length Text proxy length, that is how many text node's characters, starting from `offsetInText` it represents. | |
* @constructor | |
*/ | |
constructor( textNode, offsetInText, length ) { | |
/** | |
* Text node which part is represented by this text proxy. | |
* | |
* @readonly | |
* @member {module:engine/model/text~Text} | |
*/ | |
this.textNode = textNode; | |
if ( offsetInText < 0 || offsetInText > textNode.offsetSize ) { | |
/** | |
* Given `offsetInText` value is incorrect. | |
* | |
* @error model-textproxy-wrong-offsetintext | |
*/ | |
throw new CKEditorError( 'model-textproxy-wrong-offsetintext: Given offsetInText value is incorrect.' ); | |
} | |
if ( length < 0 || offsetInText + length > textNode.offsetSize ) { | |
/** | |
* Given `length` value is incorrect. | |
* | |
* @error model-textproxy-wrong-length | |
*/ | |
throw new CKEditorError( 'model-textproxy-wrong-length: Given length value is incorrect.' ); | |
} | |
/** | |
* Text data represented by this text proxy. | |
* | |
* @readonly | |
* @member {String} | |
*/ | |
this.data = textNode.data.substring( offsetInText, offsetInText + length ); | |
/** | |
* Offset in {@link module:engine/model/textproxy~TextProxy#textNode text node} from which the text proxy starts. | |
* | |
* @readonly | |
* @member {Number} | |
*/ | |
this.offsetInText = offsetInText; | |
} | |
/** | |
* Offset at which this text proxy starts in it's parent. | |
* | |
* @see module:engine/model/node~Node#startOffset | |
* @readonly | |
* @type {Number} | |
*/ | |
get startOffset() { | |
return this.textNode.startOffset !== null ? this.textNode.startOffset + this.offsetInText : null; | |
} | |
/** | |
* Offset size of this text proxy. Equal to the number of characters represented by the text proxy. | |
* | |
* @see module:engine/model/node~Node#offsetSize | |
* @readonly | |
* @type {Number} | |
*/ | |
get offsetSize() { | |
return this.data.length; | |
} | |
/** | |
* Offset at which this text proxy ends in it's parent. | |
* | |
* @see module:engine/model/node~Node#endOffset | |
* @readonly | |
* @type {Number} | |
*/ | |
get endOffset() { | |
return this.startOffset !== null ? this.startOffset + this.offsetSize : null; | |
} | |
/** | |
* Flag indicating whether `TextProxy` instance covers only part of the original {@link module:engine/model/text~Text text node} | |
* (`true`) or the whole text node (`false`). | |
* | |
* This is `false` when text proxy starts at the very beginning of {@link module:engine/model/textproxy~TextProxy#textNode textNode} | |
* ({@link module:engine/model/textproxy~TextProxy#offsetInText offsetInText} equals `0`) and text proxy sizes is equal to | |
* text node size. | |
* | |
* @readonly | |
* @type {Boolean} | |
*/ | |
get isPartial() { | |
return this.offsetSize !== this.textNode.offsetSize; | |
} | |
/** | |
* Parent of this text proxy, which is same as parent of text node represented by this text proxy. | |
* | |
* @readonly | |
* @type {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment|null} | |
*/ | |
get parent() { | |
return this.textNode.parent; | |
} | |
/** | |
* Root of this text proxy, which is same as root of text node represented by this text proxy. | |
* | |
* @readonly | |
* @type {module:engine/model/node~Node|module:engine/model/documentfragment~DocumentFragment} | |
*/ | |
get root() { | |
return this.textNode.root; | |
} | |
/** | |
* {@link module:engine/model/document~Document Document} that owns text node represented by this text proxy or `null` if the text node | |
* has no parent or is inside a {@link module:engine/model/documentfragment~DocumentFragment DocumentFragment}. | |
* | |
* @readonly | |
* @type {module:engine/model/document~Document|null} | |
*/ | |
get document() { | |
return this.textNode.document; | |
} | |
/** | |
* Checks whether given model tree object is of given type. | |
* | |
* Read more in {@link module:engine/model/node~Node#is}. | |
* | |
* @param {String} type | |
* @returns {Boolean} | |
*/ | |
is( type ) { | |
return type == 'textProxy'; | |
} | |
/** | |
* Gets path to this text proxy. | |
* | |
* @see module:engine/model/node~Node#getPath | |
* @returns {Array.<Number>} | |
*/ | |
getPath() { | |
const path = this.textNode.getPath(); | |
if ( path.length > 0 ) { | |
path[ path.length - 1 ] += this.offsetInText; | |
} | |
return path; | |
} | |
/** | |
* Returns ancestors array of this text proxy. | |
* | |
* @param {Object} options Options object. | |
* @param {Boolean} [options.includeSelf=false] When set to `true` this text proxy will be also included in parent's array. | |
* @param {Boolean} [options.parentFirst=false] When set to `true`, array will be sorted from text proxy parent to root element, | |
* otherwise root element will be the first item in the array. | |
* @returns {Array} Array with ancestors. | |
*/ | |
getAncestors( options = { includeSelf: false, parentFirst: false } ) { | |
const ancestors = []; | |
let parent = options.includeSelf ? this : this.parent; | |
while ( parent ) { | |
ancestors[ options.parentFirst ? 'push' : 'unshift' ]( parent ); | |
parent = parent.parent; | |
} | |
return ancestors; | |
} | |
/** | |
* Checks if this text proxy has an attribute for given key. | |
* | |
* @param {String} key Key of attribute to check. | |
* @returns {Boolean} `true` if attribute with given key is set on text proxy, `false` otherwise. | |
*/ | |
hasAttribute( key ) { | |
return this.textNode.hasAttribute( key ); | |
} | |
/** | |
* Gets an attribute value for given key or `undefined` if that attribute is not set on text proxy. | |
* | |
* @param {String} key Key of attribute to look for. | |
* @returns {*} Attribute value or `undefined`. | |
*/ | |
getAttribute( key ) { | |
return this.textNode.getAttribute( key ); | |
} | |
/** | |
* Returns iterator that iterates over this node's attributes. Attributes are returned as arrays containing two | |
* items. First one is attribute key and second is attribute value. | |
* | |
* This format is accepted by native `Map` object and also can be passed in `Node` constructor. | |
* | |
* @returns {Iterable.<*>} | |
*/ | |
getAttributes() { | |
return this.textNode.getAttributes(); | |
} | |
/** | |
* Returns iterator that iterates over this node's attribute keys. | |
* | |
* @returns {Iterator.<String>} | |
*/ | |
getAttributeKeys() { | |
return this.textNode.getAttributeKeys(); | |
} | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/nodelist.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/model/nodelist | |
*/ | |
/** | |
* Provides an interface to operate on a list of {@link module:engine/model/node~Node nodes}. `NodeList` is used internally | |
* in classes like {@link module:engine/model/element~Element Element} | |
* or {@link module:engine/model/documentfragment~DocumentFragment DocumentFragment}. | |
*/ | |
class nodelist_NodeList { | |
/** | |
* Creates an empty node list. | |
* | |
* @param {Iterable.<module:engine/model/node~Node>} nodes Nodes contained in this node list. | |
*/ | |
constructor( nodes ) { | |
/** | |
* Nodes contained in this node list. | |
* | |
* @private | |
* @member {Array.<module:engine/model/node~Node>} | |
*/ | |
this._nodes = []; | |
if ( nodes ) { | |
this.insertNodes( 0, nodes ); | |
} | |
} | |
/** | |
* Returns an iterator that iterates over all nodes contained inside this node list. | |
* | |
* @returns {Iterator.<module:engine/model/node~Node>} | |
*/ | |
[ Symbol.iterator ]() { | |
return this._nodes[ Symbol.iterator ](); | |
} | |
/** | |
* Number of nodes contained inside this node list. | |
* | |
* @readonly | |
* @type {Number} | |
*/ | |
get length() { | |
return this._nodes.length; | |
} | |
/** | |
* Sum of {@link module:engine/model/node~Node#offsetSize offset sizes} of all nodes contained inside this node list. | |
* | |
* @readonly | |
* @type {Number} | |
*/ | |
get maxOffset() { | |
return this._nodes.reduce( ( sum, node ) => sum + node.offsetSize, 0 ); | |
} | |
/** | |
* Gets the node at the given index. Returns `null` if incorrect index was passed. | |
* | |
* @param {Number} index Index of node. | |
* @returns {module:engine/model/node~Node|null} Node at given index. | |
*/ | |
getNode( index ) { | |
return this._nodes[ index ] || null; | |
} | |
/** | |
* Returns an index of the given node. Returns `null` if given node is not inside this node list. | |
* | |
* @param {module:engine/model/node~Node} node Child node to look for. | |
* @returns {Number|null} Child node's index. | |
*/ | |
getNodeIndex( node ) { | |
const index = this._nodes.indexOf( node ); | |
return index == -1 ? null : index; | |
} | |
/** | |
* Returns the starting offset of given node. Starting offset is equal to the sum of | |
* {module:engine/model/node~Node#offsetSize offset sizes} of all nodes that are before this node in this node list. | |
* | |
* @param {module:engine/model/node~Node} node Node to look for. | |
* @returns {Number|null} Node's starting offset. | |
*/ | |
getNodeStartOffset( node ) { | |
const index = this.getNodeIndex( node ); | |
return index === null ? null : this._nodes.slice( 0, index ).reduce( ( sum, node ) => sum + node.offsetSize, 0 ); | |
} | |
/** | |
* Converts index to offset in node list. | |
* | |
* Returns starting offset of a node that is at given index. Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} | |
* `model-nodelist-index-out-of-bounds` if given index is less than `0` or more than {@link #length}. | |
* | |
* @param {Number} index Node's index. | |
* @returns {Number} Node's starting offset. | |
*/ | |
indexToOffset( index ) { | |
if ( index == this._nodes.length ) { | |
return this.maxOffset; | |
} | |
const node = this._nodes[ index ]; | |
if ( !node ) { | |
/** | |
* Given index cannot be found in the node list. | |
* | |
* @error nodelist-index-out-of-bounds | |
*/ | |
throw new CKEditorError( 'model-nodelist-index-out-of-bounds: Given index cannot be found in the node list.' ); | |
} | |
return this.getNodeStartOffset( node ); | |
} | |
/** | |
* Converts offset in node list to index. | |
* | |
* Returns index of a node that occupies given offset. Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} | |
* `model-nodelist-offset-out-of-bounds` if given offset is less than `0` or more than {@link #maxOffset}. | |
* | |
* @param {Number} offset Offset to look for. | |
* @returns {Number} Index of a node that occupies given offset. | |
*/ | |
offsetToIndex( offset ) { | |
let totalOffset = 0; | |
for ( const node of this._nodes ) { | |
if ( offset >= totalOffset && offset < totalOffset + node.offsetSize ) { | |
return this.getNodeIndex( node ); | |
} | |
totalOffset += node.offsetSize; | |
} | |
if ( totalOffset != offset ) { | |
/** | |
* Given offset cannot be found in the node list. | |
* | |
* @error nodelist-offset-out-of-bounds | |
*/ | |
throw new CKEditorError( 'model-nodelist-offset-out-of-bounds: Given offset cannot be found in the node list.' ); | |
} | |
return this.length; | |
} | |
/** | |
* Inserts given nodes at given index. | |
* | |
* @param {Number} index Index at which nodes should be inserted. | |
* @param {Iterable.<module:engine/model/node~Node>} nodes Nodes to be inserted. | |
*/ | |
insertNodes( index, nodes ) { | |
// Validation. | |
for ( const node of nodes ) { | |
if ( !( node instanceof node_Node ) ) { | |
/** | |
* Trying to insert an object which is not a Node instance. | |
* | |
* @error nodelist-insertNodes-not-node | |
*/ | |
throw new CKEditorError( 'model-nodelist-insertNodes-not-node: Trying to insert an object which is not a Node instance.' ); | |
} | |
} | |
this._nodes.splice( index, 0, ...nodes ); | |
} | |
/** | |
* Removes one or more nodes starting at the given index. | |
* | |
* @param {Number} indexStart Index of the first node to remove. | |
* @param {Number} [howMany=1] Number of nodes to remove. | |
* @returns {Array.<module:engine/model/node~Node>} Array containing removed nodes. | |
*/ | |
removeNodes( indexStart, howMany = 1 ) { | |
return this._nodes.splice( indexStart, howMany ); | |
} | |
/** | |
* Converts `NodeList` instance to an array containing nodes that were inserted in the node list. Nodes | |
* are also converted to their plain object representation. | |
* | |
* @returns {Array.<module:engine/model/node~Node>} `NodeList` instance converted to `Array`. | |
*/ | |
toJSON() { | |
return this._nodes.map( node => node.toJSON() ); | |
} | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/isiterable.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module utils/isiterable | |
*/ | |
/** | |
* Checks if value implements iterator interface. | |
* | |
* @param {*} value The value to check. | |
* @returns {Boolean} True if value implements iterator interface. | |
*/ | |
function isIterable( value ) { | |
return !!( value && value[ Symbol.iterator ] ); | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/element.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/model/element | |
*/ | |
/** | |
* Model element. Type of {@link module:engine/model/node~Node node} that has a {@link module:engine/model/element~Element#name name} and | |
* {@link module:engine/model/element~Element#getChildren child nodes}. | |
* | |
* **Important**: see {@link module:engine/model/node~Node} to read about restrictions using `Element` and `Node` API. | |
*/ | |
class element_Element extends node_Node { | |
/** | |
* Creates a model element. | |
* | |
* @param {String} name Element's name. | |
* @param {Object} [attrs] Element's attributes. See {@link module:utils/tomap~toMap} for a list of accepted values. | |
* @param {module:engine/model/node~Node|Iterable.<module:engine/model/node~Node>} [children] | |
* One or more nodes to be inserted as children of created element. | |
*/ | |
constructor( name, attrs, children ) { | |
super( attrs ); | |
/** | |
* Element name. | |
* | |
* @member {String} module:engine/model/element~Element#name | |
*/ | |
this.name = name; | |
/** | |
* List of children nodes. | |
* | |
* @private | |
* @member {module:engine/model/nodelist~NodeList} module:engine/model/element~Element#_children | |
*/ | |
this._children = new nodelist_NodeList(); | |
if ( children ) { | |
this.insertChildren( 0, children ); | |
} | |
} | |
/** | |
* Number of this element's children. | |
* | |
* @readonly | |
* @type {Number} | |
*/ | |
get childCount() { | |
return this._children.length; | |
} | |
/** | |
* Sum of {module:engine/model/node~Node#offsetSize offset sizes} of all of this element's children. | |
* | |
* @readonly | |
* @type {Number} | |
*/ | |
get maxOffset() { | |
return this._children.maxOffset; | |
} | |
/** | |
* Is `true` if there are no nodes inside this element, `false` otherwise. | |
* | |
* @readonly | |
* @type {Boolean} | |
*/ | |
get isEmpty() { | |
return this.childCount === 0; | |
} | |
/** | |
* Checks whether given model tree object is of given type. | |
* | |
* obj.name; // 'listItem' | |
* obj instanceof Element; // true | |
* | |
* obj.is( 'element' ); // true | |
* obj.is( 'listItem' ); // true | |
* obj.is( 'element', 'listItem' ); // true | |
* obj.is( 'text' ); // false | |
* obj.is( 'element', 'image' ); // false | |
* | |
* Read more in {@link module:engine/model/node~Node#is}. | |
* | |
* @param {String} type Type to check when `name` parameter is present. | |
* Otherwise, it acts like the `name` parameter. | |
* @param {String} [name] Element name. | |
* @returns {Boolean} | |
*/ | |
is( type, name = null ) { | |
if ( !name ) { | |
return type == 'element' || type == this.name; | |
} else { | |
return type == 'element' && name == this.name; | |
} | |
} | |
/** | |
* Gets the child at the given index. | |
* | |
* @param {Number} index Index of child. | |
* @returns {module:engine/model/node~Node} Child node. | |
*/ | |
getChild( index ) { | |
return this._children.getNode( index ); | |
} | |
/** | |
* Returns an iterator that iterates over all of this element's children. | |
* | |
* @returns {Iterable.<module:engine/model/node~Node>} | |
*/ | |
getChildren() { | |
return this._children[ Symbol.iterator ](); | |
} | |
/** | |
* Returns an index of the given child node. Returns `null` if given node is not a child of this element. | |
* | |
* @param {module:engine/model/node~Node} node Child node to look for. | |
* @returns {Number} Child node's index in this element. | |
*/ | |
getChildIndex( node ) { | |
return this._children.getNodeIndex( node ); | |
} | |
/** | |
* Returns the starting offset of given child. Starting offset is equal to the sum of | |
* {module:engine/model/node~Node#offsetSize offset sizes} of all node's siblings that are before it. Returns `null` if | |
* given node is not a child of this element. | |
* | |
* @param {module:engine/model/node~Node} node Child node to look for. | |
* @returns {Number} Child node's starting offset. | |
*/ | |
getChildStartOffset( node ) { | |
return this._children.getNodeStartOffset( node ); | |
} | |
/** | |
* Creates a copy of this element and returns it. Created element has the same name and attributes as the original element. | |
* If clone is deep, the original element's children are also cloned. If not, then empty element is removed. | |
* | |
* @param {Boolean} [deep=false] If set to `true` clones element and all its children recursively. When set to `false`, | |
* element will be cloned without any child. | |
*/ | |
clone( deep = false ) { | |
const children = deep ? Array.from( this._children ).map( node => node.clone( true ) ) : null; | |
return new element_Element( this.name, this.getAttributes(), children ); | |
} | |
/** | |
* Returns index of a node that occupies given offset. If given offset is too low, returns `0`. If given offset is | |
* too high, returns {@link module:engine/model/element~Element#getChildIndex index after last child}. | |
* | |
* const textNode = new Text( 'foo' ); | |
* const pElement = new Element( 'p' ); | |
* const divElement = new Element( [ textNode, pElement ] ); | |
* divElement.offsetToIndex( -1 ); // Returns 0, because offset is too low. | |
* divElement.offsetToIndex( 0 ); // Returns 0, because offset 0 is taken by `textNode` which is at index 0. | |
* divElement.offsetToIndex( 1 ); // Returns 0, because `textNode` has `offsetSize` equal to 3, so it occupies offset 1 too. | |
* divElement.offsetToIndex( 2 ); // Returns 0. | |
* divElement.offsetToIndex( 3 ); // Returns 1. | |
* divElement.offsetToIndex( 4 ); // Returns 2. There are no nodes at offset 4, so last available index is returned. | |
* | |
* @param {Number} offset Offset to look for. | |
* @returns {Number} | |
*/ | |
offsetToIndex( offset ) { | |
return this._children.offsetToIndex( offset ); | |
} | |
/** | |
* {@link module:engine/model/element~Element#insertChildren Inserts} one or more nodes at the end of this element. | |
* | |
* @param {module:engine/model/node~Node|Iterable.<module:engine/model/node~Node>} nodes Nodes to be inserted. | |
*/ | |
appendChildren( nodes ) { | |
this.insertChildren( this.childCount, nodes ); | |
} | |
/** | |
* Inserts one or more nodes at the given index and sets {@link module:engine/model/node~Node#parent parent} of these nodes | |
* to this element. | |
* | |
* @param {Number} index Index at which nodes should be inserted. | |
* @param {module:engine/model/node~Node|Iterable.<module:engine/model/node~Node>} nodes Nodes to be inserted. | |
*/ | |
insertChildren( index, nodes ) { | |
nodes = normalize( nodes ); | |
for ( const node of nodes ) { | |
// If node that is being added to this element is already inside another element, first remove it from the old parent. | |
if ( node.parent !== null ) { | |
node.remove(); | |
} | |
node.parent = this; | |
} | |
this._children.insertNodes( index, nodes ); | |
} | |
/** | |
* Removes one or more nodes starting at the given index and sets | |
* {@link module:engine/model/node~Node#parent parent} of these nodes to `null`. | |
* | |
* @param {Number} index Index of the first node to remove. | |
* @param {Number} [howMany=1] Number of nodes to remove. | |
* @returns {Array.<module:engine/model/node~Node>} Array containing removed nodes. | |
*/ | |
removeChildren( index, howMany = 1 ) { | |
const nodes = this._children.removeNodes( index, howMany ); | |
for ( const node of nodes ) { | |
node.parent = null; | |
} | |
return nodes; | |
} | |
/** | |
* Returns a descendant node by its path relative to this element. | |
* | |
* // <this>a<b>c</b></this> | |
* this.getNodeByPath( [ 0 ] ); // -> "a" | |
* this.getNodeByPath( [ 1 ] ); // -> <b> | |
* this.getNodeByPath( [ 1, 0 ] ); // -> "c" | |
* | |
* @param {Array.<Number>} relativePath Path of the node to find, relative to this element. | |
* @returns {module:engine/model/node~Node} | |
*/ | |
getNodeByPath( relativePath ) { | |
let node = this; // eslint-disable-line consistent-this | |
for ( const index of relativePath ) { | |
node = node.getChild( node.offsetToIndex( index ) ); | |
} | |
return node; | |
} | |
/** | |
* Converts `Element` instance to plain object and returns it. Takes care of converting all of this element's children. | |
* | |
* @returns {Object} `Element` instance converted to plain object. | |
*/ | |
toJSON() { | |
const json = super.toJSON(); | |
json.name = this.name; | |
if ( this._children.length > 0 ) { | |
json.children = []; | |
for ( const node of this._children ) { | |
json.children.push( node.toJSON() ); | |
} | |
} | |
return json; | |
} | |
/** | |
* Creates an `Element` instance from given plain object (i.e. parsed JSON string). | |
* Converts `Element` children to proper nodes. | |
* | |
* @param {Object} json Plain object to be converted to `Element`. | |
* @returns {module:engine/model/element~Element} `Element` instance created using given plain object. | |
*/ | |
static fromJSON( json ) { | |
let children = null; | |
if ( json.children ) { | |
children = []; | |
for ( const child of json.children ) { | |
if ( child.name ) { | |
// If child has name property, it is an Element. | |
children.push( element_Element.fromJSON( child ) ); | |
} else { | |
// Otherwise, it is a Text node. | |
children.push( text_Text.fromJSON( child ) ); | |
} | |
} | |
} | |
return new element_Element( json.name, json.attributes, children ); | |
} | |
} | |
// Converts strings to Text and non-iterables to arrays. | |
// | |
// @param {String|module:engine/model/node~Node|Iterable.<String|module:engine/model/node~Node>} | |
// @return {Iterable.<module:engine/model/node~Node>} | |
function normalize( nodes ) { | |
// Separate condition because string is iterable. | |
if ( typeof nodes == 'string' ) { | |
return [ new text_Text( nodes ) ]; | |
} | |
if ( !isIterable( nodes ) ) { | |
nodes = [ nodes ]; | |
} | |
// Array.from to enable .map() on non-arrays. | |
return Array.from( nodes ) | |
.map( node => { | |
return typeof node == 'string' ? new text_Text( node ) : node; | |
} ); | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/treewalker.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/model/treewalker | |
*/ | |
/** | |
* Position iterator class. It allows to iterate forward and backward over the document. | |
*/ | |
class treewalker_TreeWalker { | |
/** | |
* Creates a range iterator. All parameters are optional, but you have to specify either `boundaries` or `startPosition`. | |
* | |
* @constructor | |
* @param {Object} [options={}] Object with configuration. | |
* @param {'forward'|'backward'} [options.direction='forward'] Walking direction. | |
* @param {module:engine/model/range~Range} [options.boundaries=null] Range to define boundaries of the iterator. | |
* @param {module:engine/model/position~Position} [options.startPosition] Starting position. | |
* @param {Boolean} [options.singleCharacters=false] Flag indicating whether all consecutive characters with the same attributes | |
* should be returned one by one as multiple {@link module:engine/model/textproxy~TextProxy} (`true`) objects or as one | |
* {@link module:engine/model/textproxy~TextProxy} (`false`). | |
* @param {Boolean} [options.shallow=false] Flag indicating whether iterator should enter elements or not. If the | |
* iterator is shallow child nodes of any iterated node will not be returned along with `elementEnd` tag. | |
* @param {Boolean} [options.ignoreElementEnd=false] Flag indicating whether iterator should ignore `elementEnd` | |
* tags. If the option is true walker will not return a parent node of start position. If this option is `true` | |
* each {@link module:engine/model/element~Element} will be returned once, while if the option is `false` they might be returned | |
* twice: for `'elementStart'` and `'elementEnd'`. | |
*/ | |
constructor( options = {} ) { | |
if ( !options.boundaries && !options.startPosition ) { | |
/** | |
* Neither boundaries nor starting position of a `TreeWalker` have been defined. | |
* | |
* @error model-tree-walker-no-start-position | |
*/ | |
throw new CKEditorError( 'model-tree-walker-no-start-position: Neither boundaries nor starting position have been defined.' ); | |
} | |
const direction = options.direction || 'forward'; | |
if ( direction != 'forward' && direction != 'backward' ) { | |
throw new CKEditorError( | |
'model-tree-walker-unknown-direction: Only `backward` and `forward` direction allowed.', | |
{ direction } | |
); | |
} | |
/** | |
* Walking direction. Defaults `'forward'`. | |
* | |
* @readonly | |
* @member {'backward'|'forward'} module:engine/model/treewalker~TreeWalker#direction | |
*/ | |
this.direction = direction; | |
/** | |
* Iterator boundaries. | |
* | |
* When the iterator is walking `'forward'` on the end of boundary or is walking `'backward'` | |
* on the start of boundary, then `{ done: true }` is returned. | |
* | |
* If boundaries are not defined they are set before first and after last child of the root node. | |
* | |
* @readonly | |
* @member {module:engine/model/range~Range} module:engine/model/treewalker~TreeWalker#boundaries | |
*/ | |
this.boundaries = options.boundaries || null; | |
/** | |
* Iterator position. This is always static position, even if the initial position was a | |
* {@link module:engine/model/liveposition~LivePosition live position}. If start position is not defined then position depends | |
* on {@link #direction}. If direction is `'forward'` position starts form the beginning, when direction | |
* is `'backward'` position starts from the end. | |
* | |
* @readonly | |
* @member {module:engine/model/position~Position} module:engine/model/treewalker~TreeWalker#position | |
*/ | |
if ( options.startPosition ) { | |
this.position = position_Position.createFromPosition( options.startPosition ); | |
} else { | |
this.position = position_Position.createFromPosition( this.boundaries[ this.direction == 'backward' ? 'end' : 'start' ] ); | |
} | |
/** | |
* Flag indicating whether all consecutive characters with the same attributes should be | |
* returned as one {@link module:engine/model/textproxy~TextProxy} (`true`) or one by one (`false`). | |
* | |
* @readonly | |
* @member {Boolean} module:engine/model/treewalker~TreeWalker#singleCharacters | |
*/ | |
this.singleCharacters = !!options.singleCharacters; | |
/** | |
* Flag indicating whether iterator should enter elements or not. If the iterator is shallow child nodes of any | |
* iterated node will not be returned along with `elementEnd` tag. | |
* | |
* @readonly | |
* @member {Boolean} module:engine/model/treewalker~TreeWalker#shallow | |
*/ | |
this.shallow = !!options.shallow; | |
/** | |
* Flag indicating whether iterator should ignore `elementEnd` tags. If the option is true walker will not | |
* return a parent node of the start position. If this option is `true` each {@link module:engine/model/element~Element} will | |
* be returned once, while if the option is `false` they might be returned twice: | |
* for `'elementStart'` and `'elementEnd'`. | |
* | |
* @readonly | |
* @member {Boolean} module:engine/model/treewalker~TreeWalker#ignoreElementEnd | |
*/ | |
this.ignoreElementEnd = !!options.ignoreElementEnd; | |
/** | |
* Start boundary cached for optimization purposes. | |
* | |
* @private | |
* @member {module:engine/model/element~Element} module:engine/model/treewalker~TreeWalker#_boundaryStartParent | |
*/ | |
this._boundaryStartParent = this.boundaries ? this.boundaries.start.parent : null; | |
/** | |
* End boundary cached for optimization purposes. | |
* | |
* @private | |
* @member {module:engine/model/element~Element} module:engine/model/treewalker~TreeWalker#_boundaryEndParent | |
*/ | |
this._boundaryEndParent = this.boundaries ? this.boundaries.end.parent : null; | |
/** | |
* Parent of the most recently visited node. Cached for optimization purposes. | |
* | |
* @private | |
* @member {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} | |
* module:engine/model/treewalker~TreeWalker#_visitedParent | |
*/ | |
this._visitedParent = this.position.parent; | |
} | |
/** | |
* Iterator interface. | |
*/ | |
[ Symbol.iterator ]() { | |
return this; | |
} | |
/** | |
* Moves {@link #position} in the {@link #direction} skipping values as long as the callback function returns `true`. | |
* | |
* For example: | |
* | |
* walker.skip( value => value.type == 'text' ); // <paragraph>[]foo</paragraph> -> <paragraph>foo[]</paragraph> | |
* walker.skip( () => true ); // Move the position to the end: <paragraph>[]foo</paragraph> -> <paragraph>foo</paragraph>[] | |
* walker.skip( () => false ); // Do not move the position. | |
* | |
* @param {Function} skip Callback function. Gets {@link module:engine/model/treewalker~TreeWalkerValue} and should | |
* return `true` if the value should be skipped or `false` if not. | |
*/ | |
skip( skip ) { | |
let done, value, prevPosition, prevVisitedParent; | |
do { | |
prevPosition = this.position; | |
prevVisitedParent = this._visitedParent; | |
( { done, value } = this.next() ); | |
} while ( !done && skip( value ) ); | |
if ( !done ) { | |
this.position = prevPosition; | |
this._visitedParent = prevVisitedParent; | |
} | |
} | |
/** | |
* Iterator interface method. | |
* Detects walking direction and makes step forward or backward. | |
* | |
* @returns {Object} Object implementing iterator interface, returning information about taken step. | |
*/ | |
next() { | |
if ( this.direction == 'forward' ) { | |
return this._next(); | |
} else { | |
return this._previous(); | |
} | |
} | |
/** | |
* Makes a step forward in model. Moves the {@link #position} to the next position and returns the encountered value. | |
* | |
* @private | |
* @returns {Object} | |
* @returns {Boolean} return.done True if iterator is done. | |
* @returns {module:engine/model/treewalker~TreeWalkerValue} return.value Information about taken step. | |
*/ | |
_next() { | |
const previousPosition = this.position; | |
const position = position_Position.createFromPosition( this.position ); | |
const parent = this._visitedParent; | |
// We are at the end of the root. | |
if ( parent.parent === null && position.offset === parent.maxOffset ) { | |
return { done: true }; | |
} | |
// We reached the walker boundary. | |
if ( parent === this._boundaryEndParent && position.offset == this.boundaries.end.offset ) { | |
return { done: true }; | |
} | |
const node = position.textNode ? position.textNode : position.nodeAfter; | |
if ( node instanceof element_Element ) { | |
if ( !this.shallow ) { | |
// Manual operations on path internals for optimization purposes. Here and in the rest of the method. | |
position.path.push( 0 ); | |
this._visitedParent = node; | |
} else { | |
position.offset++; | |
} | |
this.position = position; | |
return formatReturnValue( 'elementStart', node, previousPosition, position, 1 ); | |
} else if ( node instanceof text_Text ) { | |
let charactersCount; | |
if ( this.singleCharacters ) { | |
charactersCount = 1; | |
} else { | |
let offset = node.endOffset; | |
if ( this._boundaryEndParent == parent && this.boundaries.end.offset < offset ) { | |
offset = this.boundaries.end.offset; | |
} | |
charactersCount = offset - position.offset; | |
} | |
const offsetInTextNode = position.offset - node.startOffset; | |
const item = new textproxy_TextProxy( node, offsetInTextNode, charactersCount ); | |
position.offset += charactersCount; | |
this.position = position; | |
return formatReturnValue( 'text', item, previousPosition, position, charactersCount ); | |
} else { | |
// `node` is not set, we reached the end of current `parent`. | |
position.path.pop(); | |
position.offset++; | |
this.position = position; | |
this._visitedParent = parent.parent; | |
if ( this.ignoreElementEnd ) { | |
return this._next(); | |
} else { | |
return formatReturnValue( 'elementEnd', parent, previousPosition, position ); | |
} | |
} | |
} | |
/** | |
* Makes a step backward in model. Moves the {@link #position} to the previous position and returns the encountered value. | |
* | |
* @private | |
* @returns {Object} | |
* @returns {Boolean} return.done True if iterator is done. | |
* @returns {module:engine/model/treewalker~TreeWalkerValue} return.value Information about taken step. | |
*/ | |
_previous() { | |
const previousPosition = this.position; | |
const position = position_Position.createFromPosition( this.position ); | |
const parent = this._visitedParent; | |
// We are at the beginning of the root. | |
if ( parent.parent === null && position.offset === 0 ) { | |
return { done: true }; | |
} | |
// We reached the walker boundary. | |
if ( parent == this._boundaryStartParent && position.offset == this.boundaries.start.offset ) { | |
return { done: true }; | |
} | |
// Get node just before current position | |
const node = position.textNode ? position.textNode : position.nodeBefore; | |
if ( node instanceof element_Element ) { | |
position.offset--; | |
if ( !this.shallow ) { | |
position.path.push( node.maxOffset ); | |
this.position = position; | |
this._visitedParent = node; | |
if ( this.ignoreElementEnd ) { | |
return this._previous(); | |
} else { | |
return formatReturnValue( 'elementEnd', node, previousPosition, position ); | |
} | |
} else { | |
this.position = position; | |
return formatReturnValue( 'elementStart', node, previousPosition, position, 1 ); | |
} | |
} else if ( node instanceof text_Text ) { | |
let charactersCount; | |
if ( this.singleCharacters ) { | |
charactersCount = 1; | |
} else { | |
let offset = node.startOffset; | |
if ( this._boundaryStartParent == parent && this.boundaries.start.offset > offset ) { | |
offset = this.boundaries.start.offset; | |
} | |
charactersCount = position.offset - offset; | |
} | |
const offsetInTextNode = position.offset - node.startOffset; | |
const item = new textproxy_TextProxy( node, offsetInTextNode - charactersCount, charactersCount ); | |
position.offset -= charactersCount; | |
this.position = position; | |
return formatReturnValue( 'text', item, previousPosition, position, charactersCount ); | |
} else { | |
// `node` is not set, we reached the beginning of current `parent`. | |
position.path.pop(); | |
this.position = position; | |
this._visitedParent = parent.parent; | |
return formatReturnValue( 'elementStart', parent, previousPosition, position, 1 ); | |
} | |
} | |
} | |
function formatReturnValue( type, item, previousPosition, nextPosition, length ) { | |
return { | |
done: false, | |
value: { | |
type, | |
item, | |
previousPosition, | |
nextPosition, | |
length | |
} | |
}; | |
} | |
/** | |
* Type of the step made by {@link module:engine/model/treewalker~TreeWalker}. | |
* Possible values: `'elementStart'` if walker is at the beginning of a node, `'elementEnd'` if walker is at the end of node, | |
* `'character'` if walker traversed over a character, or `'text'` if walker traversed over multiple characters (available in | |
* character merging mode, see {@link module:engine/model/treewalker~TreeWalker#constructor}). | |
* | |
* @typedef {'elementStart'|'elementEnd'|'character'|'text'} module:engine/model/treewalker~TreeWalkerValueType | |
*/ | |
/** | |
* Object returned by {@link module:engine/model/treewalker~TreeWalker} when traversing tree model. | |
* | |
* @typedef {Object} module:engine/model/treewalker~TreeWalkerValue | |
* @property {module:engine/model/treewalker~TreeWalkerValueType} type | |
* @property {module:engine/model/item~Item} item Item between old and new positions of {@link module:engine/model/treewalker~TreeWalker}. | |
* @property {module:engine/model/position~Position} previousPosition Previous position of the iterator. | |
* * Forward iteration: For `'elementEnd'` it is the last position inside the element. For all other types it is the | |
* position before the item. Note that it is more efficient to use this position then calculate the position before | |
* the node using {@link module:engine/model/position~Position.createBefore}. It is also more efficient to get the | |
* position after node by shifting `previousPosition` by `length`, using {@link module:engine/model/position~Position#getShiftedBy}, | |
* then calculate the position using {@link module:engine/model/position~Position.createAfter}. | |
* * Backward iteration: For `'elementStart'` it is the first position inside the element. For all other types it is | |
* the position after item. | |
* @property {module:engine/model/position~Position} nextPosition Next position of the iterator. | |
* * Forward iteration: For `'elementStart'` it is the first position inside the element. For all other types it is | |
* the position after the item. | |
* * Backward iteration: For `'elementEnd'` it is last position inside element. For all other types it is the position | |
* before the item. | |
* @property {Number} [length] Length of the item. For `'elementStart'` and `'character'` it is 1. For `'text'` it is | |
* the length of the text. For `'elementEnd'` it is undefined. | |
*/ | |
/** | |
* Tree walking directions. | |
* | |
* @typedef {'forward'|'backward'} module:engine/view/treewalker~TreeWalkerDirection | |
*/ | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/last.js | |
/** | |
* Gets the last element of `array`. | |
* | |
* @static | |
* @memberOf _ | |
* @since 0.1.0 | |
* @category Array | |
* @param {Array} array The array to query. | |
* @returns {*} Returns the last element of `array`. | |
* @example | |
* | |
* _.last([1, 2, 3]); | |
* // => 3 | |
*/ | |
function last_last(array) { | |
var length = array ? array.length : 0; | |
return length ? array[length - 1] : undefined; | |
} | |
/* harmony default export */ var lodash_last = (last_last); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/comparearrays.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module utils/comparearrays | |
*/ | |
/** | |
* Compares how given arrays relate to each other. One array can be: same as another array, prefix of another array | |
* or completely different. If arrays are different, first index at which they differ is returned. Otherwise, | |
* a flag specifying the relation is returned. Flags are negative numbers, so whenever a number >= 0 is returned | |
* it means that arrays differ. | |
* | |
* compareArrays( [ 0, 2 ], [ 0, 2 ] ); // 'same' | |
* compareArrays( [ 0, 2 ], [ 0, 2, 1 ] ); // 'prefix' | |
* compareArrays( [ 0, 2 ], [ 0 ] ); // 'extension' | |
* compareArrays( [ 0, 2 ], [ 1, 2 ] ); // 0 | |
* compareArrays( [ 0, 2 ], [ 0, 1 ] ); // 1 | |
* | |
* @param {Array} a Array that is compared. | |
* @param {Array} b Array to compare with. | |
* @returns {module:utils/comparearrays~ArrayRelation} How array `a` is related to `b`. | |
*/ | |
function compareArrays( a, b ) { | |
const minLen = Math.min( a.length, b.length ); | |
for ( let i = 0; i < minLen; i++ ) { | |
if ( a[ i ] != b[ i ] ) { | |
// The arrays are different. | |
return i; | |
} | |
} | |
// Both arrays were same at all points. | |
if ( a.length == b.length ) { | |
// If their length is also same, they are the same. | |
return 'same'; | |
} else if ( a.length < b.length ) { | |
// Compared array is shorter so it is a prefix of the other array. | |
return 'prefix'; | |
} else { | |
// Compared array is longer so it is an extension of the other array. | |
return 'extension'; | |
} | |
} | |
/** | |
* @typedef {'extension'|'same'|'prefix'} module:utils/comparearrays~ArrayRelation | |
*/ | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/position.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/model/position | |
*/ | |
/** | |
* Represents a position in the model tree. | |
* | |
* **Note:** Position is based on offsets, not indexes. This means that position in element containing two text nodes | |
* with data `foo` and `bar`, position between them has offset `3`, not `1`. | |
* See {@link module:engine/model/position~Position#path} for more. | |
* | |
* Since position in a model is represented by a {@link module:engine/model/position~Position#root position root} and | |
* {@link module:engine/model/position~Position#path position path} it is possible to create positions placed in non-existing elements. | |
* This requirement is important for {@link module:engine/model/operation/transform~transform operational transformation}. | |
* | |
* Also, {@link module:engine/model/operation/operation~Operation operations} | |
* kept in {@link module:engine/model/document~Document#history document history} | |
* are storing positions (and ranges) which were correct when those operations were applied, but may not be correct | |
* after document got changed. | |
* | |
* When changes are applied to model, it may also happen that {@link module:engine/model/position~Position#parent position parent} | |
* will change even if position path has not changed. Keep in mind, that if a position leads to non-existing element, | |
* {@link module:engine/model/position~Position#parent} and some other properties and methods will throw errors. | |
* | |
* In most cases, position with wrong path is caused by an error in code, but it is sometimes needed, as described above. | |
*/ | |
class position_Position { | |
/** | |
* Creates a position. | |
* | |
* @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} root Root of the position. | |
* @param {Array.<Number>} path Position path. See {@link module:engine/model/position~Position#path}. | |
*/ | |
constructor( root, path ) { | |
if ( !root.is( 'element' ) && !root.is( 'documentFragment' ) ) { | |
/** | |
* Position root is invalid. | |
* | |
* Positions can only be anchored in elements or document fragments. | |
* | |
* @error model-position-root-invalid | |
*/ | |
throw new CKEditorError( 'model-position-root-invalid: Position root invalid.' ); | |
} | |
if ( !( path instanceof Array ) || path.length === 0 ) { | |
/** | |
* Position path must be an array with at least one item. | |
* | |
* @error model-position-path-incorrect | |
* @param path | |
*/ | |
throw new CKEditorError( 'model-position-path-incorrect: Position path must be an array with at least one item.', { path } ); | |
} | |
// Normalize the root and path (if element was passed). | |
path = root.getPath().concat( path ); | |
root = root.root; | |
/** | |
* Root of the position path. | |
* | |
* @readonly | |
* @member {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} | |
* module:engine/model/position~Position#root | |
*/ | |
this.root = root; | |
/** | |
* Position of the node in the tree. **Path contains offsets, not indexes.** | |
* | |
* Position can be placed before, after or in a {@link module:engine/model/node~Node node} if that node has | |
* {@link module:engine/model/node~Node#offsetSize} greater than `1`. Items in position path are | |
* {@link module:engine/model/node~Node#startOffset starting offsets} of position ancestors, starting from direct root children, | |
* down to the position offset in it's parent. | |
* | |
* ROOT | |
* |- P before: [ 0 ] after: [ 1 ] | |
* |- UL before: [ 1 ] after: [ 2 ] | |
* |- LI before: [ 1, 0 ] after: [ 1, 1 ] | |
* | |- foo before: [ 1, 0, 0 ] after: [ 1, 0, 3 ] | |
* |- LI before: [ 1, 1 ] after: [ 1, 2 ] | |
* |- bar before: [ 1, 1, 0 ] after: [ 1, 1, 3 ] | |
* | |
* `foo` and `bar` are representing {@link module:engine/model/text~Text text nodes}. Since text nodes has offset size | |
* greater than `1` you can place position offset between their start and end: | |
* | |
* ROOT | |
* |- P | |
* |- UL | |
* |- LI | |
* | |- f^o|o ^ has path: [ 1, 0, 1 ] | has path: [ 1, 0, 2 ] | |
* |- LI | |
* |- b^a|r ^ has path: [ 1, 1, 1 ] | has path: [ 1, 1, 2 ] | |
* | |
* @member {Array.<Number>} module:engine/model/position~Position#path | |
*/ | |
this.path = path; | |
} | |
/** | |
* Offset at which this position is located in its {@link module:engine/model/position~Position#parent parent}. It is equal | |
* to the last item in position {@link module:engine/model/position~Position#path path}. | |
* | |
* @type {Number} | |
*/ | |
get offset() { | |
return lodash_last( this.path ); | |
} | |
/** | |
* @param {Number} newOffset | |
*/ | |
set offset( newOffset ) { | |
this.path[ this.path.length - 1 ] = newOffset; | |
} | |
/** | |
* Parent element of this position. | |
* | |
* Keep in mind that `parent` value is calculated when the property is accessed. | |
* If {@link module:engine/model/position~Position#path position path} | |
* leads to a non-existing element, `parent` property will throw error. | |
* | |
* Also it is a good idea to cache `parent` property if it is used frequently in an algorithm (i.e. in a long loop). | |
* | |
* @readonly | |
* @type {module:engine/model/element~Element} | |
*/ | |
get parent() { | |
let parent = this.root; | |
for ( let i = 0; i < this.path.length - 1; i++ ) { | |
parent = parent.getChild( parent.offsetToIndex( this.path[ i ] ) ); | |
} | |
return parent; | |
} | |
/** | |
* Position {@link module:engine/model/position~Position#offset offset} converted to an index in position's parent node. It is | |
* equal to the {@link module:engine/model/node~Node#index index} of a node after this position. If position is placed | |
* in text node, position index is equal to the index of that text node. | |
* | |
* @readonly | |
* @type {Number} | |
*/ | |
get index() { | |
return this.parent.offsetToIndex( this.offset ); | |
} | |
/** | |
* Returns {@link module:engine/model/text~Text text node} instance in which this position is placed or `null` if this | |
* position is not in a text node. | |
* | |
* @readonly | |
* @type {module:engine/model/text~Text|null} | |
*/ | |
get textNode() { | |
const node = this.parent.getChild( this.index ); | |
return ( node instanceof text_Text && node.startOffset < this.offset ) ? node : null; | |
} | |
/** | |
* Node directly after this position or `null` if this position is in text node. | |
* | |
* @readonly | |
* @type {module:engine/model/node~Node|null} | |
*/ | |
get nodeAfter() { | |
return this.textNode === null ? this.parent.getChild( this.index ) : null; | |
} | |
/** | |
* Node directly before this position or `null` if this position is in text node. | |
* | |
* @readonly | |
* @type {Node} | |
*/ | |
get nodeBefore() { | |
return this.textNode === null ? this.parent.getChild( this.index - 1 ) : null; | |
} | |
/** | |
* Is `true` if position is at the beginning of its {@link module:engine/model/position~Position#parent parent}, `false` otherwise. | |
* | |
* @readonly | |
* @type {Boolean} | |
*/ | |
get isAtStart() { | |
return this.offset === 0; | |
} | |
/** | |
* Is `true` if position is at the end of its {@link module:engine/model/position~Position#parent parent}, `false` otherwise. | |
* | |
* @readonly | |
* @type {Boolean} | |
*/ | |
get isAtEnd() { | |
return this.offset == this.parent.maxOffset; | |
} | |
/** | |
* Checks whether this position is before or after given position. | |
* | |
* @param {module:engine/model/position~Position} otherPosition Position to compare with. | |
* @returns {module:engine/model/position~PositionRelation} | |
*/ | |
compareWith( otherPosition ) { | |
if ( this.root != otherPosition.root ) { | |
return 'different'; | |
} | |
const result = compareArrays( this.path, otherPosition.path ); | |
switch ( result ) { | |
case 'same': | |
return 'same'; | |
case 'prefix': | |
return 'before'; | |
case 'extension': | |
return 'after'; | |
default: | |
if ( this.path[ result ] < otherPosition.path[ result ] ) { | |
return 'before'; | |
} else { | |
return 'after'; | |
} | |
} | |
} | |
/** | |
* Gets the farthest position which matches the callback using | |
* {@link module:engine/model/treewalker~TreeWalker TreeWalker}. | |
* | |
* For example: | |
* | |
* getLastMatchingPosition( value => value.type == 'text' ); | |
* // <paragraph>[]foo</paragraph> -> <paragraph>foo[]</paragraph> | |
* | |
* getLastMatchingPosition( value => value.type == 'text', { direction: 'backward' } ); | |
* // <paragraph>foo[]</paragraph> -> <paragraph>[]foo</paragraph> | |
* | |
* getLastMatchingPosition( value => false ); | |
* // Do not move the position. | |
* | |
* @param {Function} skip Callback function. Gets {@link module:engine/model/treewalker~TreeWalkerValue} and should | |
* return `true` if the value should be skipped or `false` if not. | |
* @param {Object} options Object with configuration options. See {@link module:engine/model/treewalker~TreeWalker}. | |
* | |
* @returns {module:engine/model/position~Position} The position after the last item which matches the `skip` callback test. | |
*/ | |
getLastMatchingPosition( skip, options = {} ) { | |
options.startPosition = this; | |
const treeWalker = new treewalker_TreeWalker( options ); | |
treeWalker.skip( skip ); | |
return treeWalker.position; | |
} | |
/** | |
* Returns a path to this position's parent. Parent path is equal to position {@link module:engine/model/position~Position#path path} | |
* but without the last item. | |
* | |
* This method returns the parent path even if the parent does not exists. | |
* | |
* @returns {Array.<Number>} Path to the parent. | |
*/ | |
getParentPath() { | |
return this.path.slice( 0, -1 ); | |
} | |
/** | |
* Returns ancestors array of this position, that is this position's parent and its ancestors. | |
* | |
* @returns {Array.<module:engine/model/item~Item>} Array with ancestors. | |
*/ | |
getAncestors() { | |
if ( this.parent.is( 'documentFragment' ) ) { | |
return [ this.parent ]; | |
} else { | |
return this.parent.getAncestors( { includeSelf: true } ); | |
} | |
} | |
/** | |
* Returns the slice of two position {@link #path paths} which is identical. The {@link #root roots} | |
* of these two paths must be identical. | |
* | |
* @param {module:engine/model/position~Position} position The second position. | |
* @returns {Array.<Number>} The common path. | |
*/ | |
getCommonPath( position ) { | |
if ( this.root != position.root ) { | |
return []; | |
} | |
// We find on which tree-level start and end have the lowest common ancestor | |
const cmp = compareArrays( this.path, position.path ); | |
// If comparison returned string it means that arrays are same. | |
const diffAt = ( typeof cmp == 'string' ) ? Math.min( this.path.length, position.path.length ) : cmp; | |
return this.path.slice( 0, diffAt ); | |
} | |
/** | |
* Returns an {@link module:engine/model/element~Element} or {@link module:engine/model/documentfragment~DocumentFragment} | |
* which is a common ancestor of both positions. The {@link #root roots} of these two positions must be identical. | |
* | |
* @param {module:engine/model/position~Position} position The second position. | |
* @returns {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment|null} | |
*/ | |
getCommonAncestor( position ) { | |
const ancestorsA = this.getAncestors(); | |
const ancestorsB = position.getAncestors(); | |
let i = 0; | |
while ( ancestorsA[ i ] == ancestorsB[ i ] && ancestorsA[ i ] ) { | |
i++; | |
} | |
return i === 0 ? null : ancestorsA[ i - 1 ]; | |
} | |
/** | |
* Returns a new instance of `Position`, that has same {@link #parent parent} but it's offset | |
* is shifted by `shift` value (can be a negative value). | |
* | |
* @param {Number} shift Offset shift. Can be a negative value. | |
* @returns {module:engine/model/position~Position} Shifted position. | |
*/ | |
getShiftedBy( shift ) { | |
const shifted = position_Position.createFromPosition( this ); | |
const offset = shifted.offset + shift; | |
shifted.offset = offset < 0 ? 0 : offset; | |
return shifted; | |
} | |
/** | |
* Checks whether this position is after given position. | |
* | |
* @see module:engine/model/position~Position#isBefore | |
* | |
* @param {module:engine/model/position~Position} otherPosition Position to compare with. | |
* @returns {Boolean} True if this position is after given position. | |
*/ | |
isAfter( otherPosition ) { | |
return this.compareWith( otherPosition ) == 'after'; | |
} | |
/** | |
* Checks whether this position is before given position. | |
* | |
* **Note:** watch out when using negation of the value returned by this method, because the negation will also | |
* be `true` if positions are in different roots and you might not expect this. You should probably use | |
* `a.isAfter( b ) || a.isEqual( b )` or `!a.isBefore( p ) && a.root == b.root` in most scenarios. If your | |
* condition uses multiple `isAfter` and `isBefore` checks, build them so they do not use negated values, i.e.: | |
* | |
* if ( a.isBefore( b ) && c.isAfter( d ) ) { | |
* // do A. | |
* } else { | |
* // do B. | |
* } | |
* | |
* or, if you have only one if-branch: | |
* | |
* if ( !( a.isBefore( b ) && c.isAfter( d ) ) { | |
* // do B. | |
* } | |
* | |
* rather than: | |
* | |
* if ( !a.isBefore( b ) || && !c.isAfter( d ) ) { | |
* // do B. | |
* } else { | |
* // do A. | |
* } | |
* | |
* @param {module:engine/model/position~Position} otherPosition Position to compare with. | |
* @returns {Boolean} True if this position is before given position. | |
*/ | |
isBefore( otherPosition ) { | |
return this.compareWith( otherPosition ) == 'before'; | |
} | |
/** | |
* Checks whether this position is equal to given position. | |
* | |
* @param {module:engine/model/position~Position} otherPosition Position to compare with. | |
* @returns {Boolean} True if positions are same. | |
*/ | |
isEqual( otherPosition ) { | |
return this.compareWith( otherPosition ) == 'same'; | |
} | |
/** | |
* Checks whether this position is touching given position. Positions touch when there are no text nodes | |
* or empty nodes in a range between them. Technically, those positions are not equal but in many cases | |
* they are very similar or even indistinguishable. | |
* | |
* **Note:** this method traverses model document so it can be only used when range is up-to-date with model document. | |
* | |
* @param {module:engine/model/position~Position} otherPosition Position to compare with. | |
* @returns {Boolean} True if positions touch. | |
*/ | |
isTouching( otherPosition ) { | |
let left = null; | |
let right = null; | |
const compare = this.compareWith( otherPosition ); | |
switch ( compare ) { | |
case 'same': | |
return true; | |
case 'before': | |
left = position_Position.createFromPosition( this ); | |
right = position_Position.createFromPosition( otherPosition ); | |
break; | |
case 'after': | |
left = position_Position.createFromPosition( otherPosition ); | |
right = position_Position.createFromPosition( this ); | |
break; | |
default: | |
return false; | |
} | |
// Cached for optimization purposes. | |
let leftParent = left.parent; | |
while ( left.path.length + right.path.length ) { | |
if ( left.isEqual( right ) ) { | |
return true; | |
} | |
if ( left.path.length > right.path.length ) { | |
if ( left.offset !== leftParent.maxOffset ) { | |
return false; | |
} | |
left.path = left.path.slice( 0, -1 ); | |
leftParent = leftParent.parent; | |
left.offset++; | |
} else { | |
if ( right.offset !== 0 ) { | |
return false; | |
} | |
right.path = right.path.slice( 0, -1 ); | |
} | |
} | |
} | |
/** | |
* Returns a copy of this position that is updated by removing `howMany` nodes starting from `deletePosition`. | |
* It may happen that this position is in a removed node. If that is the case, `null` is returned instead. | |
* | |
* @protected | |
* @param {module:engine/model/position~Position} deletePosition Position before the first removed node. | |
* @param {Number} howMany How many nodes are removed. | |
* @returns {module:engine/model/position~Position|null} Transformed position or `null`. | |
*/ | |
_getTransformedByDeletion( deletePosition, howMany ) { | |
const transformed = position_Position.createFromPosition( this ); | |
// This position can't be affected if deletion was in a different root. | |
if ( this.root != deletePosition.root ) { | |
return transformed; | |
} | |
if ( compareArrays( deletePosition.getParentPath(), this.getParentPath() ) == 'same' ) { | |
// If nodes are removed from the node that is pointed by this position... | |
if ( deletePosition.offset < this.offset ) { | |
// And are removed from before an offset of that position... | |
if ( deletePosition.offset + howMany > this.offset ) { | |
// Position is in removed range, it's no longer in the tree. | |
return null; | |
} else { | |
// Decrement the offset accordingly. | |
transformed.offset -= howMany; | |
} | |
} | |
} else if ( compareArrays( deletePosition.getParentPath(), this.getParentPath() ) == 'prefix' ) { | |
// If nodes are removed from a node that is on a path to this position... | |
const i = deletePosition.path.length - 1; | |
if ( deletePosition.offset <= this.path[ i ] ) { | |
// And are removed from before next node of that path... | |
if ( deletePosition.offset + howMany > this.path[ i ] ) { | |
// If the next node of that path is removed return null | |
// because the node containing this position got removed. | |
return null; | |
} else { | |
// Otherwise, decrement index on that path. | |
transformed.path[ i ] -= howMany; | |
} | |
} | |
} | |
return transformed; | |
} | |
/** | |
* Returns a copy of this position that is updated by inserting `howMany` nodes at `insertPosition`. | |
* | |
* @protected | |
* @param {module:engine/model/position~Position} insertPosition Position where nodes are inserted. | |
* @param {Number} howMany How many nodes are inserted. | |
* @param {Boolean} insertBefore Flag indicating whether nodes are inserted before or after `insertPosition`. | |
* This is important only when `insertPosition` and this position are same. If that is the case and the flag is | |
* set to `true`, this position will get transformed. If the flag is set to `false`, it won't. | |
* @returns {module:engine/model/position~Position} Transformed position. | |
*/ | |
_getTransformedByInsertion( insertPosition, howMany, insertBefore ) { | |
const transformed = position_Position.createFromPosition( this ); | |
// This position can't be affected if insertion was in a different root. | |
if ( this.root != insertPosition.root ) { | |
return transformed; | |
} | |
if ( compareArrays( insertPosition.getParentPath(), this.getParentPath() ) == 'same' ) { | |
// If nodes are inserted in the node that is pointed by this position... | |
if ( insertPosition.offset < this.offset || ( insertPosition.offset == this.offset && insertBefore ) ) { | |
// And are inserted before an offset of that position... | |
// "Push" this positions offset. | |
transformed.offset += howMany; | |
} | |
} else if ( compareArrays( insertPosition.getParentPath(), this.getParentPath() ) == 'prefix' ) { | |
// If nodes are inserted in a node that is on a path to this position... | |
const i = insertPosition.path.length - 1; | |
if ( insertPosition.offset <= this.path[ i ] ) { | |
// And are inserted before next node of that path... | |
// "Push" the index on that path. | |
transformed.path[ i ] += howMany; | |
} | |
} | |
return transformed; | |
} | |
/** | |
* Returns a copy of this position that is updated by moving `howMany` nodes from `sourcePosition` to `targetPosition`. | |
* | |
* @protected | |
* @param {module:engine/model/position~Position} sourcePosition Position before the first element to move. | |
* @param {module:engine/model/position~Position} targetPosition Position where moved elements will be inserted. | |
* @param {Number} howMany How many consecutive nodes to move, starting from `sourcePosition`. | |
* @param {Boolean} insertBefore Flag indicating whether moved nodes are pasted before or after `insertPosition`. | |
* This is important only when `targetPosition` and this position are same. If that is the case and the flag is | |
* set to `true`, this position will get transformed by range insertion. If the flag is set to `false`, it won't. | |
* @param {Boolean} [sticky] Flag indicating whether this position "sticks" to range, that is if it should be moved | |
* with the moved range if it is equal to one of range's boundaries. | |
* @returns {module:engine/model/position~Position} Transformed position. | |
*/ | |
_getTransformedByMove( sourcePosition, targetPosition, howMany, insertBefore, sticky ) { | |
// Moving a range removes nodes from their original position. We acknowledge this by proper transformation. | |
let transformed = this._getTransformedByDeletion( sourcePosition, howMany ); | |
// Then we update target position, as it could be affected by nodes removal too. | |
targetPosition = targetPosition._getTransformedByDeletion( sourcePosition, howMany ); | |
if ( transformed === null || ( sticky && transformed.isEqual( sourcePosition ) ) ) { | |
// This position is inside moved range (or sticks to it). | |
// In this case, we calculate a combination of this position, move source position and target position. | |
transformed = this._getCombined( sourcePosition, targetPosition ); | |
} else { | |
// This position is not inside a removed range. | |
// In next step, we simply reflect inserting `howMany` nodes, which might further affect the position. | |
transformed = transformed._getTransformedByInsertion( targetPosition, howMany, insertBefore ); | |
} | |
return transformed; | |
} | |
/** | |
* Returns a new position that is a combination of this position and given positions. | |
* | |
* The combined position is a copy of this position transformed by moving a range starting at `source` position | |
* to the `target` position. It is expected that this position is inside the moved range. | |
* | |
* Example: | |
* | |
* let original = new Position( root, [ 2, 3, 1 ] ); | |
* let source = new Position( root, [ 2, 2 ] ); | |
* let target = new Position( otherRoot, [ 1, 1, 3 ] ); | |
* original._getCombined( source, target ); // path is [ 1, 1, 4, 1 ], root is `otherRoot` | |
* | |
* Explanation: | |
* | |
* We have a position `[ 2, 3, 1 ]` and move some nodes from `[ 2, 2 ]` to `[ 1, 1, 3 ]`. The original position | |
* was inside moved nodes and now should point to the new place. The moved nodes will be after | |
* positions `[ 1, 1, 3 ]`, `[ 1, 1, 4 ]`, `[ 1, 1, 5 ]`. Since our position was in the second moved node, | |
* the transformed position will be in a sub-tree of a node at `[ 1, 1, 4 ]`. Looking at original path, we | |
* took care of `[ 2, 3 ]` part of it. Now we have to add the rest of the original path to the transformed path. | |
* Finally, the transformed position will point to `[ 1, 1, 4, 1 ]`. | |
* | |
* @protected | |
* @param {module:engine/model/position~Position} source Beginning of the moved range. | |
* @param {module:engine/model/position~Position} target Position where the range is moved. | |
* @returns {module:engine/model/position~Position} Combined position. | |
*/ | |
_getCombined( source, target ) { | |
const i = source.path.length - 1; | |
// The first part of a path to combined position is a path to the place where nodes were moved. | |
const combined = position_Position.createFromPosition( target ); | |
// Then we have to update the rest of the path. | |
// Fix the offset because this position might be after `from` position and we have to reflect that. | |
combined.offset = combined.offset + this.path[ i ] - source.offset; | |
// Then, add the rest of the path. | |
// If this position is at the same level as `from` position nothing will get added. | |
combined.path = combined.path.concat( this.path.slice( i + 1 ) ); | |
return combined; | |
} | |
/** | |
* Creates position at the given location. The location can be specified as: | |
* | |
* * a {@link module:engine/model/position~Position position}, | |
* * parent element and offset (offset defaults to `0`), | |
* * parent element and `'end'` (sets position at the end of that element), | |
* * {@link module:engine/model/item~Item model item} and `'before'` or `'after'` (sets position before or after given model item). | |
* | |
* This method is a shortcut to other constructors such as: | |
* | |
* * {@link module:engine/model/position~Position.createBefore}, | |
* * {@link module:engine/model/position~Position.createAfter}, | |
* * {@link module:engine/model/position~Position.createFromParentAndOffset}, | |
* * {@link module:engine/model/position~Position.createFromPosition}. | |
* | |
* @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition | |
* @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when | |
* first parameter is a {@link module:engine/model/item~Item model item}. | |
*/ | |
static createAt( itemOrPosition, offset ) { | |
if ( itemOrPosition instanceof position_Position ) { | |
return this.createFromPosition( itemOrPosition ); | |
} else { | |
const node = itemOrPosition; | |
if ( offset == 'end' ) { | |
offset = node.maxOffset; | |
} else if ( offset == 'before' ) { | |
return this.createBefore( node ); | |
} else if ( offset == 'after' ) { | |
return this.createAfter( node ); | |
} else if ( !offset ) { | |
offset = 0; | |
} | |
return this.createFromParentAndOffset( node, offset ); | |
} | |
} | |
/** | |
* Creates a new position, after given {@link module:engine/model/item~Item model item}. | |
* | |
* @param {module:engine/model/item~Item} item Item after which the position should be placed. | |
* @returns {module:engine/model/position~Position} | |
*/ | |
static createAfter( item ) { | |
if ( !item.parent ) { | |
/** | |
* You can not make a position after a root element. | |
* | |
* @error model-position-after-root | |
* @param {module:engine/model/item~Item} root | |
*/ | |
throw new CKEditorError( 'model-position-after-root: You cannot make a position after root.', { root: item } ); | |
} | |
return this.createFromParentAndOffset( item.parent, item.endOffset ); | |
} | |
/** | |
* Creates a new position, before the given {@link module:engine/model/item~Item model item}. | |
* | |
* @param {module:engine/model/item~Item} item Item before which the position should be placed. | |
* @returns {module:engine/model/position~Position} | |
*/ | |
static createBefore( item ) { | |
if ( !item.parent ) { | |
/** | |
* You can not make a position before a root element. | |
* | |
* @error model-position-before-root | |
* @param {module:engine/model/item~Item} root | |
*/ | |
throw new CKEditorError( 'model-position-before-root: You cannot make a position before root.', { root: item } ); | |
} | |
return this.createFromParentAndOffset( item.parent, item.startOffset ); | |
} | |
/** | |
* Creates a new position from the parent element and an offset in that element. | |
* | |
* @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} parent Position's parent. | |
* @param {Number} offset Position's offset. | |
* @returns {module:engine/model/position~Position} | |
*/ | |
static createFromParentAndOffset( parent, offset ) { | |
if ( !parent.is( 'element' ) && !parent.is( 'documentFragment' ) ) { | |
/** | |
* Position parent have to be a model element or model document fragment. | |
* | |
* @error model-position-parent-incorrect | |
*/ | |
throw new CKEditorError( 'model-position-parent-incorrect: Position parent have to be a element or document fragment.' ); | |
} | |
const path = parent.getPath(); | |
path.push( offset ); | |
return new this( parent.root, path ); | |
} | |
/** | |
* Creates a new position, which is equal to passed position. | |
* | |
* @param {module:engine/model/position~Position} position Position to be cloned. | |
* @returns {module:engine/model/position~Position} | |
*/ | |
static createFromPosition( position ) { | |
return new this( position.root, position.path.slice() ); | |
} | |
/** | |
* Creates a `Position` instance from given plain object (i.e. parsed JSON string). | |
* | |
* @param {Object} json Plain object to be converted to `Position`. | |
* @returns {module:engine/model/position~Position} `Position` instance created using given plain object. | |
*/ | |
static fromJSON( json, doc ) { | |
if ( json.root === '$graveyard' ) { | |
return new position_Position( doc.graveyard, json.path ); | |
} | |
if ( !doc.hasRoot( json.root ) ) { | |
/** | |
* Cannot create position for document. Root with specified name does not exist. | |
* | |
* @error model-position-fromjson-no-root | |
* @param {String} rootName | |
*/ | |
throw new CKEditorError( | |
'model-position-fromjson-no-root: Cannot create position for document. Root with specified name does not exist.', | |
{ rootName: json.root } | |
); | |
} | |
return new position_Position( doc.getRoot( json.root ), json.path ); | |
} | |
} | |
/** | |
* A flag indicating whether this position is `'before'` or `'after'` or `'same'` as given position. | |
* If positions are in different roots `'different'` flag is returned. | |
* | |
* @typedef {String} module:engine/model/position~PositionRelation | |
*/ | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/range.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/model/range | |
*/ | |
/** | |
* Range class. Range is iterable. | |
*/ | |
class range_Range { | |
/** | |
* Creates a range spanning from `start` position to `end` position. | |
* | |
* **Note:** Constructor creates it's own {@link module:engine/model/position~Position Position} instances basing on passed values. | |
* | |
* @param {module:engine/model/position~Position} start Start position. | |
* @param {module:engine/model/position~Position} [end] End position. If not set, range will be collapsed at `start` position. | |
*/ | |
constructor( start, end = null ) { | |
/** | |
* Start position. | |
* | |
* @readonly | |
* @member {module:engine/model/position~Position} | |
*/ | |
this.start = position_Position.createFromPosition( start ); | |
/** | |
* End position. | |
* | |
* @readonly | |
* @member {module:engine/model/position~Position} | |
*/ | |
this.end = end ? position_Position.createFromPosition( end ) : position_Position.createFromPosition( start ); | |
} | |
/** | |
* Returns an iterator that iterates over all {@link module:engine/model/item~Item items} that are in this range and returns | |
* them together with additional information like length or {@link module:engine/model/position~Position positions}, | |
* grouped as {@link module:engine/model/treewalker~TreeWalkerValue}. | |
* It iterates over all {@link module:engine/model/textproxy~TextProxy text contents} that are inside the range | |
* and all the {@link module:engine/model/element~Element}s that are entered into when iterating over this range. | |
* | |
* This iterator uses {@link module:engine/model/treewalker~TreeWalker} with `boundaries` set to this range | |
* and `ignoreElementEnd` option set to `true`. | |
* | |
* @returns {Iterable.<module:engine/model/treewalker~TreeWalkerValue>} | |
*/ | |
* [ Symbol.iterator ]() { | |
yield* new treewalker_TreeWalker( { boundaries: this, ignoreElementEnd: true } ); | |
} | |
/** | |
* Returns whether the range is collapsed, that is if {@link #start} and | |
* {@link #end} positions are equal. | |
* | |
* @type {Boolean} | |
*/ | |
get isCollapsed() { | |
return this.start.isEqual( this.end ); | |
} | |
/** | |
* Returns whether this range is flat, that is if {@link #start} position and | |
* {@link #end} position are in the same {@link module:engine/model/position~Position#parent}. | |
* | |
* @type {Boolean} | |
*/ | |
get isFlat() { | |
return this.start.parent === this.end.parent; | |
} | |
/** | |
* Range root element. | |
* | |
* @type {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} | |
*/ | |
get root() { | |
return this.start.root; | |
} | |
/** | |
* Checks whether this range contains given {@link module:engine/model/position~Position position}. | |
* | |
* @param {module:engine/model/position~Position} position Position to check. | |
* @returns {Boolean} `true` if given {@link module:engine/model/position~Position position} is contained | |
* in this range,`false` otherwise. | |
*/ | |
containsPosition( position ) { | |
return position.isAfter( this.start ) && position.isBefore( this.end ); | |
} | |
/** | |
* Checks whether this range contains given {@link ~Range range}. | |
* | |
* @param {module:engine/model/range~Range} otherRange Range to check. | |
* @param {Boolean} [loose=false] Whether the check is loose or strict. If the check is strict (`false`), compared range cannot | |
* start or end at the same position as this range boundaries. If the check is loose (`true`), compared range can start, end or | |
* even be equal to this range. Note that collapsed ranges are always compared in strict mode. | |
* @returns {Boolean} `true` if given {@link ~Range range} boundaries are contained by this range, `false` otherwise. | |
*/ | |
containsRange( otherRange, loose = false ) { | |
if ( otherRange.isCollapsed ) { | |
loose = false; | |
} | |
const containsStart = this.containsPosition( otherRange.start ) || ( loose && this.start.isEqual( otherRange.start ) ); | |
const containsEnd = this.containsPosition( otherRange.end ) || ( loose && this.end.isEqual( otherRange.end ) ); | |
return containsStart && containsEnd; | |
} | |
/** | |
* Checks whether given {@link module:engine/model/item~Item} is inside this range. | |
* | |
* @param {module:engine/model/item~Item} item Model item to check. | |
*/ | |
containsItem( item ) { | |
const pos = position_Position.createBefore( item ); | |
return this.containsPosition( pos ) || this.start.isEqual( pos ); | |
} | |
/** | |
* Two ranges are equal if their {@link #start} and {@link #end} positions are equal. | |
* | |
* @param {module:engine/model/range~Range} otherRange Range to compare with. | |
* @returns {Boolean} `true` if ranges are equal, `false` otherwise. | |
*/ | |
isEqual( otherRange ) { | |
return this.start.isEqual( otherRange.start ) && this.end.isEqual( otherRange.end ); | |
} | |
/** | |
* Checks and returns whether this range intersects with given range. | |
* | |
* @param {module:engine/model/range~Range} otherRange Range to compare with. | |
* @returns {Boolean} `true` if ranges intersect, `false` otherwise. | |
*/ | |
isIntersecting( otherRange ) { | |
return this.start.isBefore( otherRange.end ) && this.end.isAfter( otherRange.start ); | |
} | |
/** | |
* Computes which part(s) of this {@link ~Range range} is not a part of given {@link ~Range range}. | |
* Returned array contains zero, one or two {@link ~Range ranges}. | |
* | |
* Examples: | |
* | |
* let range = new Range( new Position( root, [ 2, 7 ] ), new Position( root, [ 4, 0, 1 ] ) ); | |
* let otherRange = new Range( new Position( root, [ 1 ] ), new Position( root, [ 5 ] ) ); | |
* let transformed = range.getDifference( otherRange ); | |
* // transformed array has no ranges because `otherRange` contains `range` | |
* | |
* otherRange = new Range( new Position( root, [ 1 ] ), new Position( root, [ 3 ] ) ); | |
* transformed = range.getDifference( otherRange ); | |
* // transformed array has one range: from [ 3 ] to [ 4, 0, 1 ] | |
* | |
* otherRange = new Range( new Position( root, [ 3 ] ), new Position( root, [ 4 ] ) ); | |
* transformed = range.getDifference( otherRange ); | |
* // transformed array has two ranges: from [ 2, 7 ] to [ 3 ] and from [ 4 ] to [ 4, 0, 1 ] | |
* | |
* @param {module:engine/model/range~Range} otherRange Range to differentiate against. | |
* @returns {Array.<module:engine/model/range~Range>} The difference between ranges. | |
*/ | |
getDifference( otherRange ) { | |
const ranges = []; | |
if ( this.isIntersecting( otherRange ) ) { | |
// Ranges intersect. | |
if ( this.containsPosition( otherRange.start ) ) { | |
// Given range start is inside this range. This means that we have to | |
// add shrunken range - from the start to the middle of this range. | |
ranges.push( new range_Range( this.start, otherRange.start ) ); | |
} | |
if ( this.containsPosition( otherRange.end ) ) { | |
// Given range end is inside this range. This means that we have to | |
// add shrunken range - from the middle of this range to the end. | |
ranges.push( new range_Range( otherRange.end, this.end ) ); | |
} | |
} else { | |
// Ranges do not intersect, return the original range. | |
ranges.push( range_Range.createFromRange( this ) ); | |
} | |
return ranges; | |
} | |
/** | |
* Returns an intersection of this {@link ~Range range} and given {@link ~Range range}. | |
* Intersection is a common part of both of those ranges. If ranges has no common part, returns `null`. | |
* | |
* Examples: | |
* | |
* let range = new Range( new Position( root, [ 2, 7 ] ), new Position( root, [ 4, 0, 1 ] ) ); | |
* let otherRange = new Range( new Position( root, [ 1 ] ), new Position( root, [ 2 ] ) ); | |
* let transformed = range.getIntersection( otherRange ); // null - ranges have no common part | |
* | |
* otherRange = new Range( new Position( root, [ 3 ] ), new Position( root, [ 5 ] ) ); | |
* transformed = range.getIntersection( otherRange ); // range from [ 3 ] to [ 4, 0, 1 ] | |
* | |
* @param {module:engine/model/range~Range} otherRange Range to check for intersection. | |
* @returns {module:engine/model/range~Range|null} A common part of given ranges or `null` if ranges have no common part. | |
*/ | |
getIntersection( otherRange ) { | |
if ( this.isIntersecting( otherRange ) ) { | |
// Ranges intersect, so a common range will be returned. | |
// At most, it will be same as this range. | |
let commonRangeStart = this.start; | |
let commonRangeEnd = this.end; | |
if ( this.containsPosition( otherRange.start ) ) { | |
// Given range start is inside this range. This means thaNt we have to | |
// shrink common range to the given range start. | |
commonRangeStart = otherRange.start; | |
} | |
if ( this.containsPosition( otherRange.end ) ) { | |
// Given range end is inside this range. This means that we have to | |
// shrink common range to the given range end. | |
commonRangeEnd = otherRange.end; | |
} | |
return new range_Range( commonRangeStart, commonRangeEnd ); | |
} | |
// Ranges do not intersect, so they do not have common part. | |
return null; | |
} | |
/** | |
* Computes and returns the smallest set of {@link #isFlat flat} ranges, that covers this range in whole. | |
* | |
* See an example of a model structure (`[` and `]` are range boundaries): | |
* | |
* root root | |
* |- element DIV DIV P2 P3 DIV | |
* | |- element H H P1 f o o b a r H P4 | |
* | | |- "fir[st" fir[st lorem se]cond ipsum | |
* | |- element P1 | |
* | | |- "lorem" || | |
* |- element P2 || | |
* | |- "foo" VV | |
* |- element P3 | |
* | |- "bar" root | |
* |- element DIV DIV [P2 P3] DIV | |
* | |- element H H [P1] f o o b a r H P4 | |
* | | |- "se]cond" fir[st] lorem [se]cond ipsum | |
* | |- element P4 | |
* | | |- "ipsum" | |
* | |
* As it can be seen, letters contained in the range are: `stloremfoobarse`, spread across different parents. | |
* We are looking for minimal set of flat ranges that contains the same nodes. | |
* | |
* Minimal flat ranges for above range `( [ 0, 0, 3 ], [ 3, 0, 2 ] )` will be: | |
* | |
* ( [ 0, 0, 3 ], [ 0, 0, 5 ] ) = "st" | |
* ( [ 0, 1 ], [ 0, 2 ] ) = element P1 ("lorem") | |
* ( [ 1 ], [ 3 ] ) = element P2, element P3 ("foobar") | |
* ( [ 3, 0, 0 ], [ 3, 0, 2 ] ) = "se" | |
* | |
* **Note:** if an {@link module:engine/model/element~Element element} is not wholly contained in this range, it won't be returned | |
* in any of the returned flat ranges. See in the example how `H` elements at the beginning and at the end of the range | |
* were omitted. Only their parts that were wholly in the range were returned. | |
* | |
* **Note:** this method is not returning flat ranges that contain no nodes. | |
* | |
* @returns {Array.<module:engine/model/range~Range>} Array of flat ranges covering this range. | |
*/ | |
getMinimalFlatRanges() { | |
const ranges = []; | |
const diffAt = this.start.getCommonPath( this.end ).length; | |
const pos = position_Position.createFromPosition( this.start ); | |
let posParent = pos.parent; | |
// Go up. | |
while ( pos.path.length > diffAt + 1 ) { | |
const howMany = posParent.maxOffset - pos.offset; | |
if ( howMany !== 0 ) { | |
ranges.push( new range_Range( pos, pos.getShiftedBy( howMany ) ) ); | |
} | |
pos.path = pos.path.slice( 0, -1 ); | |
pos.offset++; | |
posParent = posParent.parent; | |
} | |
// Go down. | |
while ( pos.path.length <= this.end.path.length ) { | |
const offset = this.end.path[ pos.path.length - 1 ]; | |
const howMany = offset - pos.offset; | |
if ( howMany !== 0 ) { | |
ranges.push( new range_Range( pos, pos.getShiftedBy( howMany ) ) ); | |
} | |
pos.offset = offset; | |
pos.path.push( 0 ); | |
} | |
return ranges; | |
} | |
/** | |
* Creates a {@link module:engine/model/treewalker~TreeWalker TreeWalker} instance with this range as a boundary. | |
* | |
* @param {Object} options Object with configuration options. See {@link module:engine/model/treewalker~TreeWalker}. | |
* @param {module:engine/model/position~Position} [options.startPosition] | |
* @param {Boolean} [options.singleCharacters=false] | |
* @param {Boolean} [options.shallow=false] | |
* @param {Boolean} [options.ignoreElementEnd=false] | |
*/ | |
getWalker( options = {} ) { | |
options.boundaries = this; | |
return new treewalker_TreeWalker( options ); | |
} | |
/** | |
* Returns an iterator that iterates over all {@link module:engine/model/item~Item items} that are in this range and returns | |
* them. | |
* | |
* This method uses {@link module:engine/model/treewalker~TreeWalker} with `boundaries` set to this range and `ignoreElementEnd` option | |
* set to `true`. However it returns only {@link module:engine/model/item~Item model items}, | |
* not {@link module:engine/model/treewalker~TreeWalkerValue}. | |
* | |
* You may specify additional options for the tree walker. See {@link module:engine/model/treewalker~TreeWalker} for | |
* a full list of available options. | |
* | |
* @method getItems | |
* @param {Object} options Object with configuration options. See {@link module:engine/model/treewalker~TreeWalker}. | |
* @returns {Iterable.<module:engine/model/item~Item>} | |
*/ | |
* getItems( options = {} ) { | |
options.boundaries = this; | |
options.ignoreElementEnd = true; | |
const treeWalker = new treewalker_TreeWalker( options ); | |
for ( const value of treeWalker ) { | |
yield value.item; | |
} | |
} | |
/** | |
* Returns an iterator that iterates over all {@link module:engine/model/position~Position positions} that are boundaries or | |
* contained in this range. | |
* | |
* This method uses {@link module:engine/model/treewalker~TreeWalker} with `boundaries` set to this range. However it returns only | |
* {@link module:engine/model/position~Position positions}, not {@link module:engine/model/treewalker~TreeWalkerValue}. | |
* | |
* You may specify additional options for the tree walker. See {@link module:engine/model/treewalker~TreeWalker} for | |
* a full list of available options. | |
* | |
* @param {Object} options Object with configuration options. See {@link module:engine/model/treewalker~TreeWalker}. | |
* @returns {Iterable.<module:engine/model/position~Position>} | |
*/ | |
* getPositions( options = {} ) { | |
options.boundaries = this; | |
const treeWalker = new treewalker_TreeWalker( options ); | |
yield treeWalker.position; | |
for ( const value of treeWalker ) { | |
yield value.nextPosition; | |
} | |
} | |
/** | |
* Returns a range that is a result of transforming this range by given `delta`. | |
* | |
* **Note:** transformation may break one range into multiple ranges (e.g. when a part of the range is | |
* moved to a different part of document tree). For this reason, an array is returned by this method and it | |
* may contain one or more `Range` instances. | |
* | |
* @param {module:engine/model/delta/delta~Delta} delta Delta to transform range by. | |
* @returns {Array.<module:engine/model/range~Range>} Range which is the result of transformation. | |
*/ | |
getTransformedByDelta( delta ) { | |
const ranges = [ range_Range.createFromRange( this ) ]; | |
// Operation types that a range can be transformed by. | |
const supportedTypes = new Set( [ 'insert', 'move', 'remove', 'reinsert' ] ); | |
for ( const operation of delta.operations ) { | |
if ( supportedTypes.has( operation.type ) ) { | |
for ( let i = 0; i < ranges.length; i++ ) { | |
const result = ranges[ i ]._getTransformedByDocumentChange( | |
operation.type, | |
delta.type, | |
operation.targetPosition || operation.position, | |
operation.howMany || operation.nodes.maxOffset, | |
operation.sourcePosition | |
); | |
ranges.splice( i, 1, ...result ); | |
i += result.length - 1; | |
} | |
} | |
} | |
return ranges; | |
} | |
/** | |
* Returns a range that is a result of transforming this range by multiple `deltas`. | |
* | |
* **Note:** transformation may break one range into multiple ranges (e.g. when a part of the range is | |
* moved to a different part of document tree). For this reason, an array is returned by this method and it | |
* may contain one or more `Range` instances. | |
* | |
* @param {Iterable.<module:engine/model/delta/delta~Delta>} deltas Deltas to transform the range by. | |
* @returns {Array.<module:engine/model/range~Range>} Range which is the result of transformation. | |
*/ | |
getTransformedByDeltas( deltas ) { | |
const ranges = [ range_Range.createFromRange( this ) ]; | |
for ( const delta of deltas ) { | |
for ( let i = 0; i < ranges.length; i++ ) { | |
const result = ranges[ i ].getTransformedByDelta( delta ); | |
ranges.splice( i, 1, ...result ); | |
i += result.length - 1; | |
} | |
} | |
// It may happen that a range is split into two, and then the part of second "piece" is moved into first | |
// "piece". In this case we will have incorrect third range, which should not be included in the result -- | |
// because it is already included in the first "piece". In this loop we are looking for all such ranges that | |
// are inside other ranges and we simply remove them. | |
for ( let i = 0; i < ranges.length; i++ ) { | |
const range = ranges[ i ]; | |
for ( let j = i + 1; j < ranges.length; j++ ) { | |
const next = ranges[ j ]; | |
if ( range.containsRange( next ) || next.containsRange( range ) || range.isEqual( next ) ) { | |
ranges.splice( j, 1 ); | |
} | |
} | |
} | |
return ranges; | |
} | |
/** | |
* Returns an {@link module:engine/model/element~Element} or {@link module:engine/model/documentfragment~DocumentFragment} | |
* which is a common ancestor of the range's both ends (in which the entire range is contained). | |
* | |
* @returns {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment|null} | |
*/ | |
getCommonAncestor() { | |
return this.start.getCommonAncestor( this.end ); | |
} | |
/** | |
* Returns a range that is a result of transforming this range by a change in the model document. | |
* | |
* @protected | |
* @param {'insert'|'move'|'remove'|'reinsert'} type Change type. | |
* @param {String} deltaType Type of delta that introduced the change. | |
* @param {module:engine/model/position~Position} targetPosition Position before the first changed node. | |
* @param {Number} howMany How many nodes has been changed. | |
* @param {module:engine/model/position~Position} sourcePosition Source position of changes. | |
* @returns {Array.<module:engine/model/range~Range>} | |
*/ | |
_getTransformedByDocumentChange( type, deltaType, targetPosition, howMany, sourcePosition ) { | |
if ( type == 'insert' ) { | |
return this._getTransformedByInsertion( targetPosition, howMany, false, false ); | |
} else { | |
const sourceRange = range_Range.createFromPositionAndShift( sourcePosition, howMany ); | |
// Edge case for merge delta. | |
if ( | |
deltaType == 'merge' && | |
this.isCollapsed && | |
( this.start.isEqual( sourceRange.start ) || this.start.isEqual( sourceRange.end ) ) | |
) { | |
// Collapsed range is in merged element, at the beginning or at the end of it. | |
// Without fix, the range would end up in the graveyard, together with removed element. | |
// <p>foo</p><p>[]bar</p> -> <p>foobar</p><p>[]</p> -> <p>foobar</p> -> <p>foo[]bar</p> | |
// <p>foo</p><p>bar[]</p> -> <p>foobar</p><p>[]</p> -> <p>foobar</p> -> <p>foobar[]</p> | |
// | |
// In most cases, `sourceRange.start.offset` for merge delta's move operation would be 0, | |
// so this formula might look overcomplicated. | |
// However in some scenarios, after operational transformation, move operation might not | |
// in fact start from 0 and we need to properly count new offset. | |
// https://github.com/ckeditor/ckeditor5-engine/pull/1133#issuecomment-329080668. | |
const offset = this.start.offset - sourceRange.start.offset; | |
return [ new range_Range( targetPosition.getShiftedBy( offset ) ) ]; | |
} | |
// | |
// Edge case for split delta. | |
// | |
if ( deltaType == 'split' && this.isCollapsed && this.end.isEqual( sourceRange.end ) ) { | |
// Collapsed range is at the end of split element. | |
// Without fix, the range would end up at the end of split (old) element instead of at the end of new element. | |
// That would happen because this range is not technically inside moved range. Last step below shows the fix. | |
// <p>foobar[]</p> -> <p>foobar[]</p><p></p> -> <p>foo[]</p><p>bar</p> -> <p>foo</p><p>bar[]</p> | |
return [ new range_Range( targetPosition.getShiftedBy( howMany ) ) ]; | |
} | |
// | |
// Other edge cases: | |
// | |
// In all examples `[]` is `this` and `{}` is `sourceRange`, while `^` is move target position. | |
// | |
// Example: | |
// <p>xx</p>^<w>{<p>a[b</p>}</w><p>c]d</p> --> <p>xx</p><p>a[b</p><w></w><p>c]d</p> | |
// ^<p>xx</p><w>{<p>a[b</p>}</w><p>c]d</p> --> <p>a[b</p><p>xx</p><w></w><p>c]d</p> // Note <p>xx</p> inclusion. | |
// <w>{<p>a[b</p>}</w>^<p>c]d</p> --> <w></w><p>a[b</p><p>c]d</p> | |
if ( | |
( sourceRange.containsPosition( this.start ) || sourceRange.start.isEqual( this.start ) ) && | |
this.containsPosition( sourceRange.end ) && | |
this.end.isAfter( targetPosition ) | |
) { | |
const start = this.start._getCombined( | |
sourcePosition, | |
targetPosition._getTransformedByDeletion( sourcePosition, howMany ) | |
); | |
const end = this.end._getTransformedByMove( sourcePosition, targetPosition, howMany, false, false ); | |
return [ new range_Range( start, end ) ]; | |
} | |
// Example: | |
// <p>c[d</p><w>{<p>a]b</p>}</w>^<p>xx</p> --> <p>c[d</p><w></w><p>a]b</p><p>xx</p> | |
// <p>c[d</p><w>{<p>a]b</p>}</w><p>xx</p>^ --> <p>c[d</p><w></w><p>xx</p><p>a]b</p> // Note <p>xx</p> inclusion. | |
// <p>c[d</p>^<w>{<p>a]b</p>}</w> --> <p>c[d</p><p>a]b</p><w></w> | |
if ( | |
( sourceRange.containsPosition( this.end ) || sourceRange.end.isEqual( this.end ) ) && | |
this.containsPosition( sourceRange.start ) && | |
this.start.isBefore( targetPosition ) | |
) { | |
const start = this.start._getTransformedByMove( | |
sourcePosition, | |
targetPosition, | |
howMany, | |
true, | |
false | |
); | |
const end = this.end._getCombined( | |
sourcePosition, | |
targetPosition._getTransformedByDeletion( sourcePosition, howMany ) | |
); | |
return [ new range_Range( start, end ) ]; | |
} | |
return this._getTransformedByMove( sourcePosition, targetPosition, howMany ); | |
} | |
} | |
/** | |
* Returns an array containing one or two {@link ~Range ranges} that are a result of transforming this | |
* {@link ~Range range} by inserting `howMany` nodes at `insertPosition`. Two {@link ~Range ranges} are | |
* returned if the insertion was inside this {@link ~Range range} and `spread` is set to `true`. | |
* | |
* Examples: | |
* | |
* let range = new Range( new Position( root, [ 2, 7 ] ), new Position( root, [ 4, 0, 1 ] ) ); | |
* let transformed = range._getTransformedByInsertion( new Position( root, [ 1 ] ), 2 ); | |
* // transformed array has one range from [ 4, 7 ] to [ 6, 0, 1 ] | |
* | |
* transformed = range._getTransformedByInsertion( new Position( root, [ 4, 0, 0 ] ), 4 ); | |
* // transformed array has one range from [ 2, 7 ] to [ 4, 0, 5 ] | |
* | |
* transformed = range._getTransformedByInsertion( new Position( root, [ 3, 2 ] ), 4 ); | |
* // transformed array has one range, which is equal to original range | |
* | |
* transformed = range._getTransformedByInsertion( new Position( root, [ 3, 2 ] ), 4, true ); | |
* // transformed array has two ranges: from [ 2, 7 ] to [ 3, 2 ] and from [ 3, 6 ] to [ 4, 0, 1 ] | |
* | |
* transformed = range._getTransformedByInsertion( new Position( root, [ 4, 0, 1 ] ), 4, false, false ); | |
* // transformed array has one range which is equal to original range because insertion is after the range boundary | |
* | |
* transformed = range._getTransformedByInsertion( new Position( root, [ 4, 0, 1 ] ), 4, false, true ); | |
* // transformed array has one range: from [ 2, 7 ] to [ 4, 0, 5 ] because range was expanded | |
* | |
* @protected | |
* @param {module:engine/model/position~Position} insertPosition Position where nodes are inserted. | |
* @param {Number} howMany How many nodes are inserted. | |
* @param {Boolean} [spread] Flag indicating whether this {~Range range} should be spread if insertion | |
* was inside the range. Defaults to `false`. | |
* @param {Boolean} [isSticky] Flag indicating whether insertion should expand a range if it is in a place of | |
* range boundary. Defaults to `false`. | |
* @returns {Array.<module:engine/model/range~Range>} Result of the transformation. | |
*/ | |
_getTransformedByInsertion( insertPosition, howMany, spread = false, isSticky = false ) { | |
if ( spread && this.containsPosition( insertPosition ) ) { | |
// Range has to be spread. The first part is from original start to the spread point. | |
// The other part is from spread point to the original end, but transformed by | |
// insertion to reflect insertion changes. | |
return [ | |
new range_Range( this.start, insertPosition ), | |
new range_Range( | |
insertPosition._getTransformedByInsertion( insertPosition, howMany, true ), | |
this.end._getTransformedByInsertion( insertPosition, howMany, this.isCollapsed ) | |
) | |
]; | |
} else { | |
const range = range_Range.createFromRange( this ); | |
const insertBeforeStart = !isSticky; | |
const insertBeforeEnd = range.isCollapsed ? true : isSticky; | |
range.start = range.start._getTransformedByInsertion( insertPosition, howMany, insertBeforeStart ); | |
range.end = range.end._getTransformedByInsertion( insertPosition, howMany, insertBeforeEnd ); | |
return [ range ]; | |
} | |
} | |
/** | |
* Returns an array containing {@link ~Range ranges} that are a result of transforming this | |
* {@link ~Range range} by moving `howMany` nodes from `sourcePosition` to `targetPosition`. | |
* | |
* @protected | |
* @param {module:engine/model/position~Position} sourcePosition Position from which nodes are moved. | |
* @param {module:engine/model/position~Position} targetPosition Position to where nodes are moved. | |
* @param {Number} howMany How many nodes are moved. | |
* @returns {Array.<module:engine/model/range~Range>} Result of the transformation. | |
*/ | |
_getTransformedByMove( sourcePosition, targetPosition, howMany ) { | |
if ( this.isCollapsed ) { | |
const newPos = this.start._getTransformedByMove( sourcePosition, targetPosition, howMany, true, false ); | |
return [ new range_Range( newPos ) ]; | |
} | |
let result; | |
const moveRange = new range_Range( sourcePosition, sourcePosition.getShiftedBy( howMany ) ); | |
const differenceSet = this.getDifference( moveRange ); | |
let difference = null; | |
const common = this.getIntersection( moveRange ); | |
if ( differenceSet.length == 1 ) { | |
// `moveRange` and this range may intersect. | |
difference = new range_Range( | |
differenceSet[ 0 ].start._getTransformedByDeletion( sourcePosition, howMany ), | |
differenceSet[ 0 ].end._getTransformedByDeletion( sourcePosition, howMany ) | |
); | |
} else if ( differenceSet.length == 2 ) { | |
// `moveRange` is inside this range. | |
difference = new range_Range( | |
this.start, | |
this.end._getTransformedByDeletion( sourcePosition, howMany ) | |
); | |
} // else, `moveRange` contains this range. | |
const insertPosition = targetPosition._getTransformedByDeletion( sourcePosition, howMany ); | |
if ( difference ) { | |
result = difference._getTransformedByInsertion( insertPosition, howMany, common !== null ); | |
} else { | |
result = []; | |
} | |
if ( common ) { | |
result.push( new range_Range( | |
common.start._getCombined( moveRange.start, insertPosition ), | |
common.end._getCombined( moveRange.start, insertPosition ) | |
) ); | |
} | |
return result; | |
} | |
/** | |
* Creates a new range, spreading from specified {@link module:engine/model/position~Position position} to a position moved by | |
* given `shift`. If `shift` is a negative value, shifted position is treated as the beginning of the range. | |
* | |
* @param {module:engine/model/position~Position} position Beginning of the range. | |
* @param {Number} shift How long the range should be. | |
* @returns {module:engine/model/range~Range} | |
*/ | |
static createFromPositionAndShift( position, shift ) { | |
const start = position; | |
const end = position.getShiftedBy( shift ); | |
return shift > 0 ? new this( start, end ) : new this( end, start ); | |
} | |
/** | |
* Creates a range from given parents and offsets. | |
* | |
* @param {module:engine/model/element~Element} startElement Start position parent element. | |
* @param {Number} startOffset Start position offset. | |
* @param {module:engine/model/element~Element} endElement End position parent element. | |
* @param {Number} endOffset End position offset. | |
* @returns {module:engine/model/range~Range} | |
*/ | |
static createFromParentsAndOffsets( startElement, startOffset, endElement, endOffset ) { | |
return new this( | |
position_Position.createFromParentAndOffset( startElement, startOffset ), | |
position_Position.createFromParentAndOffset( endElement, endOffset ) | |
); | |
} | |
/** | |
* Creates a new instance of `Range` which is equal to passed range. | |
* | |
* @param {module:engine/model/range~Range} range Range to clone. | |
* @returns {module:engine/model/range~Range} | |
*/ | |
static createFromRange( range ) { | |
return new this( range.start, range.end ); | |
} | |
/** | |
* Creates a range inside an {@link module:engine/model/element~Element element} which starts before the first child of | |
* that element and ends after the last child of that element. | |
* | |
* @param {module:engine/model/element~Element} element Element which is a parent for the range. | |
* @returns {module:engine/model/range~Range} | |
*/ | |
static createIn( element ) { | |
return this.createFromParentsAndOffsets( element, 0, element, element.maxOffset ); | |
} | |
/** | |
* Creates a range that starts before given {@link module:engine/model/item~Item model item} and ends after it. | |
* | |
* @param {module:engine/model/item~Item} item | |
* @returns {module:engine/model/range~Range} | |
*/ | |
static createOn( item ) { | |
return this.createFromPositionAndShift( position_Position.createBefore( item ), item.offsetSize ); | |
} | |
/** | |
* Creates a collapsed range at given {@link module:engine/model/position~Position position} | |
* or on the given {@link module:engine/model/item~Item item}. | |
* | |
* @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition | |
* @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when | |
* first parameter is a {@link module:engine/model/item~Item model item}. | |
*/ | |
static createCollapsedAt( itemOrPosition, offset ) { | |
const start = position_Position.createAt( itemOrPosition, offset ); | |
const end = position_Position.createFromPosition( start ); | |
return new range_Range( start, end ); | |
} | |
/** | |
* Combines all ranges from the passed array into a one range. At least one range has to be passed. | |
* Passed ranges must not have common parts. | |
* | |
* The first range from the array is a reference range. If other ranges start or end on the exactly same position where | |
* the reference range, they get combined into one range. | |
* | |
* [ ][] [ ][ ][ ][ ][] [ ] // Passed ranges, shown sorted | |
* [ ] // The result of the function if the first range was a reference range. | |
* [ ] // The result of the function if the third-to-seventh range was a reference range. | |
* [ ] // The result of the function if the last range was a reference range. | |
* | |
* @param {Array.<module:engine/model/range~Range>} ranges Ranges to combine. | |
* @returns {module:engine/model/range~Range} Combined range. | |
*/ | |
static createFromRanges( ranges ) { | |
if ( ranges.length === 0 ) { | |
/** | |
* At least one range has to be passed to | |
* {@link module:engine/model/range~Range.createFromRanges `Range.createFromRanges()`}. | |
* | |
* @error range-create-from-ranges-empty-array | |
*/ | |
throw new CKEditorError( 'range-create-from-ranges-empty-array: At least one range has to be passed.' ); | |
} else if ( ranges.length == 1 ) { | |
return this.createFromRange( ranges[ 0 ] ); | |
} | |
// 1. Set the first range in `ranges` array as a reference range. | |
// If we are going to return just a one range, one of the ranges need to be the reference one. | |
// Other ranges will be stuck to that range, if possible. | |
const ref = ranges[ 0 ]; | |
// 2. Sort all the ranges so it's easier to process them. | |
ranges.sort( ( a, b ) => { | |
return a.start.isAfter( b.start ) ? 1 : -1; | |
} ); | |
// 3. Check at which index the reference range is now. | |
const refIndex = ranges.indexOf( ref ); | |
// 4. At this moment we don't need the original range. | |
// We are going to modify the result and we need to return a new instance of Range. | |
// We have to create a copy of the reference range. | |
const result = new this( ref.start, ref.end ); | |
// 5. Ranges should be checked and glued starting from the range that is closest to the reference range. | |
// Since ranges are sorted, start with the range with index that is closest to reference range index. | |
for ( let i = refIndex - 1; i >= 0; i++ ) { | |
if ( ranges[ i ].end.isEqual( result.start ) ) { | |
result.start = position_Position.createFromPosition( ranges[ i ].start ); | |
} else { | |
// If ranges are not starting/ending at the same position there is no point in looking further. | |
break; | |
} | |
} | |
// 6. Ranges should be checked and glued starting from the range that is closest to the reference range. | |
// Since ranges are sorted, start with the range with index that is closest to reference range index. | |
for ( let i = refIndex + 1; i < ranges.length; i++ ) { | |
if ( ranges[ i ].start.isEqual( result.end ) ) { | |
result.end = position_Position.createFromPosition( ranges[ i ].end ); | |
} else { | |
// If ranges are not starting/ending at the same position there is no point in looking further. | |
break; | |
} | |
} | |
return result; | |
} | |
/** | |
* Creates a `Range` instance from given plain object (i.e. parsed JSON string). | |
* | |
* @param {Object} json Plain object to be converted to `Range`. | |
* @param {module:engine/model/document~Document} doc Document object that will be range owner. | |
* @returns {module:engine/model/element~Element} `Range` instance created using given plain object. | |
*/ | |
static fromJSON( json, doc ) { | |
return new this( position_Position.fromJSON( json.start, doc ), position_Position.fromJSON( json.end, doc ) ); | |
} | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_listCacheClear.js | |
/** | |
* Removes all key-value entries from the list cache. | |
* | |
* @private | |
* @name clear | |
* @memberOf ListCache | |
*/ | |
function listCacheClear() { | |
this.__data__ = []; | |
} | |
/* harmony default export */ var _listCacheClear = (listCacheClear); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_assocIndexOf.js | |
/** | |
* Gets the index at which the `key` is found in `array` of key-value pairs. | |
* | |
* @private | |
* @param {Array} array The array to search. | |
* @param {*} key The key to search for. | |
* @returns {number} Returns the index of the matched value, else `-1`. | |
*/ | |
function assocIndexOf(array, key) { | |
var length = array.length; | |
while (length--) { | |
if (lodash_eq(array[length][0], key)) { | |
return length; | |
} | |
} | |
return -1; | |
} | |
/* harmony default export */ var _assocIndexOf = (assocIndexOf); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_listCacheDelete.js | |
/** Used for built-in method references. */ | |
var arrayProto = Array.prototype; | |
/** Built-in value references. */ | |
var splice = arrayProto.splice; | |
/** | |
* Removes `key` and its value from the list cache. | |
* | |
* @private | |
* @name delete | |
* @memberOf ListCache | |
* @param {string} key The key of the value to remove. | |
* @returns {boolean} Returns `true` if the entry was removed, else `false`. | |
*/ | |
function listCacheDelete(key) { | |
var data = this.__data__, | |
index = _assocIndexOf(data, key); | |
if (index < 0) { | |
return false; | |
} | |
var lastIndex = data.length - 1; | |
if (index == lastIndex) { | |
data.pop(); | |
} else { | |
splice.call(data, index, 1); | |
} | |
return true; | |
} | |
/* harmony default export */ var _listCacheDelete = (listCacheDelete); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_listCacheGet.js | |
/** | |
* Gets the list cache value for `key`. | |
* | |
* @private | |
* @name get | |
* @memberOf ListCache | |
* @param {string} key The key of the value to get. | |
* @returns {*} Returns the entry value. | |
*/ | |
function listCacheGet(key) { | |
var data = this.__data__, | |
index = _assocIndexOf(data, key); | |
return index < 0 ? undefined : data[index][1]; | |
} | |
/* harmony default export */ var _listCacheGet = (listCacheGet); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_listCacheHas.js | |
/** | |
* Checks if a list cache value for `key` exists. | |
* | |
* @private | |
* @name has | |
* @memberOf ListCache | |
* @param {string} key The key of the entry to check. | |
* @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. | |
*/ | |
function listCacheHas(key) { | |
return _assocIndexOf(this.__data__, key) > -1; | |
} | |
/* harmony default export */ var _listCacheHas = (listCacheHas); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_listCacheSet.js | |
/** | |
* Sets the list cache `key` to `value`. | |
* | |
* @private | |
* @name set | |
* @memberOf ListCache | |
* @param {string} key The key of the value to set. | |
* @param {*} value The value to set. | |
* @returns {Object} Returns the list cache instance. | |
*/ | |
function listCacheSet(key, value) { | |
var data = this.__data__, | |
index = _assocIndexOf(data, key); | |
if (index < 0) { | |
data.push([key, value]); | |
} else { | |
data[index][1] = value; | |
} | |
return this; | |
} | |
/* harmony default export */ var _listCacheSet = (listCacheSet); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_ListCache.js | |
/** | |
* Creates an list cache object. | |
* | |
* @private | |
* @constructor | |
* @param {Array} [entries] The key-value pairs to cache. | |
*/ | |
function ListCache(entries) { | |
var index = -1, | |
length = entries ? entries.length : 0; | |
this.clear(); | |
while (++index < length) { | |
var entry = entries[index]; | |
this.set(entry[0], entry[1]); | |
} | |
} | |
// Add methods to `ListCache`. | |
ListCache.prototype.clear = _listCacheClear; | |
ListCache.prototype['delete'] = _listCacheDelete; | |
ListCache.prototype.get = _listCacheGet; | |
ListCache.prototype.has = _listCacheHas; | |
ListCache.prototype.set = _listCacheSet; | |
/* harmony default export */ var _ListCache = (ListCache); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_stackClear.js | |
/** | |
* Removes all key-value entries from the stack. | |
* | |
* @private | |
* @name clear | |
* @memberOf Stack | |
*/ | |
function stackClear() { | |
this.__data__ = new _ListCache; | |
} | |
/* harmony default export */ var _stackClear = (stackClear); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_stackDelete.js | |
/** | |
* Removes `key` and its value from the stack. | |
* | |
* @private | |
* @name delete | |
* @memberOf Stack | |
* @param {string} key The key of the value to remove. | |
* @returns {boolean} Returns `true` if the entry was removed, else `false`. | |
*/ | |
function stackDelete(key) { | |
return this.__data__['delete'](key); | |
} | |
/* harmony default export */ var _stackDelete = (stackDelete); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_stackGet.js | |
/** | |
* Gets the stack value for `key`. | |
* | |
* @private | |
* @name get | |
* @memberOf Stack | |
* @param {string} key The key of the value to get. | |
* @returns {*} Returns the entry value. | |
*/ | |
function stackGet(key) { | |
return this.__data__.get(key); | |
} | |
/* harmony default export */ var _stackGet = (stackGet); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_stackHas.js | |
/** | |
* Checks if a stack value for `key` exists. | |
* | |
* @private | |
* @name has | |
* @memberOf Stack | |
* @param {string} key The key of the entry to check. | |
* @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. | |
*/ | |
function stackHas(key) { | |
return this.__data__.has(key); | |
} | |
/* harmony default export */ var _stackHas = (stackHas); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_toSource.js | |
/** Used to resolve the decompiled source of functions. */ | |
var _toSource_funcToString = Function.prototype.toString; | |
/** | |
* Converts `func` to its source code. | |
* | |
* @private | |
* @param {Function} func The function to process. | |
* @returns {string} Returns the source code. | |
*/ | |
function toSource(func) { | |
if (func != null) { | |
try { | |
return _toSource_funcToString.call(func); | |
} catch (e) {} | |
try { | |
return (func + ''); | |
} catch (e) {} | |
} | |
return ''; | |
} | |
/* harmony default export */ var _toSource = (toSource); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/isNative.js | |
/** | |
* Used to match `RegExp` | |
* [syntax characters](http://ecma-international.org/ecma-262/6.0/#sec-patterns). | |
*/ | |
var reRegExpChar = /[\\^$.*+?()[\]{}|]/g; | |
/** Used to detect host constructors (Safari). */ | |
var reIsHostCtor = /^\[object .+?Constructor\]$/; | |
/** Used for built-in method references. */ | |
var isNative_objectProto = Object.prototype; | |
/** Used to resolve the decompiled source of functions. */ | |
var isNative_funcToString = Function.prototype.toString; | |
/** Used to check objects for own properties. */ | |
var isNative_hasOwnProperty = isNative_objectProto.hasOwnProperty; | |
/** Used to detect if a method is native. */ | |
var reIsNative = RegExp('^' + | |
isNative_funcToString.call(isNative_hasOwnProperty).replace(reRegExpChar, '\\$&') | |
.replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$' | |
); | |
/** | |
* Checks if `value` is a native function. | |
* | |
* @static | |
* @memberOf _ | |
* @since 3.0.0 | |
* @category Lang | |
* @param {*} value The value to check. | |
* @returns {boolean} Returns `true` if `value` is a native function, | |
* else `false`. | |
* @example | |
* | |
* _.isNative(Array.prototype.push); | |
* // => true | |
* | |
* _.isNative(_); | |
* // => false | |
*/ | |
function isNative(value) { | |
if (!lodash_isObject(value)) { | |
return false; | |
} | |
var pattern = (lodash_isFunction(value) || _isHostObject(value)) ? reIsNative : reIsHostCtor; | |
return pattern.test(_toSource(value)); | |
} | |
/* harmony default export */ var lodash_isNative = (isNative); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_getNative.js | |
/** | |
* Gets the native function at `key` of `object`. | |
* | |
* @private | |
* @param {Object} object The object to query. | |
* @param {string} key The key of the method to get. | |
* @returns {*} Returns the function if it's native, else `undefined`. | |
*/ | |
function getNative(object, key) { | |
var value = object[key]; | |
return lodash_isNative(value) ? value : undefined; | |
} | |
/* harmony default export */ var _getNative = (getNative); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_nativeCreate.js | |
/* Built-in method references that are verified to be native. */ | |
var nativeCreate = _getNative(Object, 'create'); | |
/* harmony default export */ var _nativeCreate = (nativeCreate); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_hashClear.js | |
/** | |
* Removes all key-value entries from the hash. | |
* | |
* @private | |
* @name clear | |
* @memberOf Hash | |
*/ | |
function hashClear() { | |
this.__data__ = _nativeCreate ? _nativeCreate(null) : {}; | |
} | |
/* harmony default export */ var _hashClear = (hashClear); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_hashDelete.js | |
/** | |
* Removes `key` and its value from the hash. | |
* | |
* @private | |
* @name delete | |
* @memberOf Hash | |
* @param {Object} hash The hash to modify. | |
* @param {string} key The key of the value to remove. | |
* @returns {boolean} Returns `true` if the entry was removed, else `false`. | |
*/ | |
function hashDelete(key) { | |
return this.has(key) && delete this.__data__[key]; | |
} | |
/* harmony default export */ var _hashDelete = (hashDelete); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_hashGet.js | |
/** Used to stand-in for `undefined` hash values. */ | |
var HASH_UNDEFINED = '__lodash_hash_undefined__'; | |
/** Used for built-in method references. */ | |
var _hashGet_objectProto = Object.prototype; | |
/** Used to check objects for own properties. */ | |
var _hashGet_hasOwnProperty = _hashGet_objectProto.hasOwnProperty; | |
/** | |
* Gets the hash value for `key`. | |
* | |
* @private | |
* @name get | |
* @memberOf Hash | |
* @param {string} key The key of the value to get. | |
* @returns {*} Returns the entry value. | |
*/ | |
function hashGet(key) { | |
var data = this.__data__; | |
if (_nativeCreate) { | |
var result = data[key]; | |
return result === HASH_UNDEFINED ? undefined : result; | |
} | |
return _hashGet_hasOwnProperty.call(data, key) ? data[key] : undefined; | |
} | |
/* harmony default export */ var _hashGet = (hashGet); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_hashHas.js | |
/** Used for built-in method references. */ | |
var _hashHas_objectProto = Object.prototype; | |
/** Used to check objects for own properties. */ | |
var _hashHas_hasOwnProperty = _hashHas_objectProto.hasOwnProperty; | |
/** | |
* Checks if a hash value for `key` exists. | |
* | |
* @private | |
* @name has | |
* @memberOf Hash | |
* @param {string} key The key of the entry to check. | |
* @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. | |
*/ | |
function hashHas(key) { | |
var data = this.__data__; | |
return _nativeCreate ? data[key] !== undefined : _hashHas_hasOwnProperty.call(data, key); | |
} | |
/* harmony default export */ var _hashHas = (hashHas); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_hashSet.js | |
/** Used to stand-in for `undefined` hash values. */ | |
var _hashSet_HASH_UNDEFINED = '__lodash_hash_undefined__'; | |
/** | |
* Sets the hash `key` to `value`. | |
* | |
* @private | |
* @name set | |
* @memberOf Hash | |
* @param {string} key The key of the value to set. | |
* @param {*} value The value to set. | |
* @returns {Object} Returns the hash instance. | |
*/ | |
function hashSet(key, value) { | |
var data = this.__data__; | |
data[key] = (_nativeCreate && value === undefined) ? _hashSet_HASH_UNDEFINED : value; | |
return this; | |
} | |
/* harmony default export */ var _hashSet = (hashSet); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_Hash.js | |
/** | |
* Creates a hash object. | |
* | |
* @private | |
* @constructor | |
* @param {Array} [entries] The key-value pairs to cache. | |
*/ | |
function Hash(entries) { | |
var index = -1, | |
length = entries ? entries.length : 0; | |
this.clear(); | |
while (++index < length) { | |
var entry = entries[index]; | |
this.set(entry[0], entry[1]); | |
} | |
} | |
// Add methods to `Hash`. | |
Hash.prototype.clear = _hashClear; | |
Hash.prototype['delete'] = _hashDelete; | |
Hash.prototype.get = _hashGet; | |
Hash.prototype.has = _hashHas; | |
Hash.prototype.set = _hashSet; | |
/* harmony default export */ var _Hash = (Hash); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_Map.js | |
/* Built-in method references that are verified to be native. */ | |
var _Map_Map = _getNative(_root["a" /* default */], 'Map'); | |
/* harmony default export */ var _Map = (_Map_Map); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_mapCacheClear.js | |
/** | |
* Removes all key-value entries from the map. | |
* | |
* @private | |
* @name clear | |
* @memberOf MapCache | |
*/ | |
function mapCacheClear() { | |
this.__data__ = { | |
'hash': new _Hash, | |
'map': new (_Map || _ListCache), | |
'string': new _Hash | |
}; | |
} | |
/* harmony default export */ var _mapCacheClear = (mapCacheClear); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_isKeyable.js | |
/** | |
* Checks if `value` is suitable for use as unique object key. | |
* | |
* @private | |
* @param {*} value The value to check. | |
* @returns {boolean} Returns `true` if `value` is suitable, else `false`. | |
*/ | |
function isKeyable(value) { | |
var type = typeof value; | |
return (type == 'string' || type == 'number' || type == 'symbol' || type == 'boolean') | |
? (value !== '__proto__') | |
: (value === null); | |
} | |
/* harmony default export */ var _isKeyable = (isKeyable); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_getMapData.js | |
/** | |
* Gets the data for `map`. | |
* | |
* @private | |
* @param {Object} map The map to query. | |
* @param {string} key The reference key. | |
* @returns {*} Returns the map data. | |
*/ | |
function getMapData(map, key) { | |
var data = map.__data__; | |
return _isKeyable(key) | |
? data[typeof key == 'string' ? 'string' : 'hash'] | |
: data.map; | |
} | |
/* harmony default export */ var _getMapData = (getMapData); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_mapCacheDelete.js | |
/** | |
* Removes `key` and its value from the map. | |
* | |
* @private | |
* @name delete | |
* @memberOf MapCache | |
* @param {string} key The key of the value to remove. | |
* @returns {boolean} Returns `true` if the entry was removed, else `false`. | |
*/ | |
function mapCacheDelete(key) { | |
return _getMapData(this, key)['delete'](key); | |
} | |
/* harmony default export */ var _mapCacheDelete = (mapCacheDelete); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_mapCacheGet.js | |
/** | |
* Gets the map value for `key`. | |
* | |
* @private | |
* @name get | |
* @memberOf MapCache | |
* @param {string} key The key of the value to get. | |
* @returns {*} Returns the entry value. | |
*/ | |
function mapCacheGet(key) { | |
return _getMapData(this, key).get(key); | |
} | |
/* harmony default export */ var _mapCacheGet = (mapCacheGet); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_mapCacheHas.js | |
/** | |
* Checks if a map value for `key` exists. | |
* | |
* @private | |
* @name has | |
* @memberOf MapCache | |
* @param {string} key The key of the entry to check. | |
* @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. | |
*/ | |
function mapCacheHas(key) { | |
return _getMapData(this, key).has(key); | |
} | |
/* harmony default export */ var _mapCacheHas = (mapCacheHas); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_mapCacheSet.js | |
/** | |
* Sets the map `key` to `value`. | |
* | |
* @private | |
* @name set | |
* @memberOf MapCache | |
* @param {string} key The key of the value to set. | |
* @param {*} value The value to set. | |
* @returns {Object} Returns the map cache instance. | |
*/ | |
function mapCacheSet(key, value) { | |
_getMapData(this, key).set(key, value); | |
return this; | |
} | |
/* harmony default export */ var _mapCacheSet = (mapCacheSet); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_MapCache.js | |
/** | |
* Creates a map cache object to store key-value pairs. | |
* | |
* @private | |
* @constructor | |
* @param {Array} [entries] The key-value pairs to cache. | |
*/ | |
function MapCache(entries) { | |
var index = -1, | |
length = entries ? entries.length : 0; | |
this.clear(); | |
while (++index < length) { | |
var entry = entries[index]; | |
this.set(entry[0], entry[1]); | |
} | |
} | |
// Add methods to `MapCache`. | |
MapCache.prototype.clear = _mapCacheClear; | |
MapCache.prototype['delete'] = _mapCacheDelete; | |
MapCache.prototype.get = _mapCacheGet; | |
MapCache.prototype.has = _mapCacheHas; | |
MapCache.prototype.set = _mapCacheSet; | |
/* harmony default export */ var _MapCache = (MapCache); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_stackSet.js | |
/** Used as the size to enable large array optimizations. */ | |
var LARGE_ARRAY_SIZE = 200; | |
/** | |
* Sets the stack `key` to `value`. | |
* | |
* @private | |
* @name set | |
* @memberOf Stack | |
* @param {string} key The key of the value to set. | |
* @param {*} value The value to set. | |
* @returns {Object} Returns the stack cache instance. | |
*/ | |
function stackSet(key, value) { | |
var cache = this.__data__; | |
if (cache instanceof _ListCache && cache.__data__.length == LARGE_ARRAY_SIZE) { | |
cache = this.__data__ = new _MapCache(cache.__data__); | |
} | |
cache.set(key, value); | |
return this; | |
} | |
/* harmony default export */ var _stackSet = (stackSet); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_Stack.js | |
/** | |
* Creates a stack cache object to store key-value pairs. | |
* | |
* @private | |
* @constructor | |
* @param {Array} [entries] The key-value pairs to cache. | |
*/ | |
function Stack(entries) { | |
this.__data__ = new _ListCache(entries); | |
} | |
// Add methods to `Stack`. | |
Stack.prototype.clear = _stackClear; | |
Stack.prototype['delete'] = _stackDelete; | |
Stack.prototype.get = _stackGet; | |
Stack.prototype.has = _stackHas; | |
Stack.prototype.set = _stackSet; | |
/* harmony default export */ var _Stack = (Stack); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_arrayEach.js | |
/** | |
* A specialized version of `_.forEach` for arrays without support for | |
* iteratee shorthands. | |
* | |
* @private | |
* @param {Array} array The array to iterate over. | |
* @param {Function} iteratee The function invoked per iteration. | |
* @returns {Array} Returns `array`. | |
*/ | |
function arrayEach(array, iteratee) { | |
var index = -1, | |
length = array.length; | |
while (++index < length) { | |
if (iteratee(array[index], index, array) === false) { | |
break; | |
} | |
} | |
return array; | |
} | |
/* harmony default export */ var _arrayEach = (arrayEach); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseHas.js | |
/** Used for built-in method references. */ | |
var _baseHas_objectProto = Object.prototype; | |
/** Used to check objects for own properties. */ | |
var _baseHas_hasOwnProperty = _baseHas_objectProto.hasOwnProperty; | |
/** | |
* The base implementation of `_.has` without support for deep paths. | |
* | |
* @private | |
* @param {Object} object The object to query. | |
* @param {Array|string} key The key to check. | |
* @returns {boolean} Returns `true` if `key` exists, else `false`. | |
*/ | |
function baseHas(object, key) { | |
// Avoid a bug in IE 10-11 where objects with a [[Prototype]] of `null`, | |
// that are composed entirely of index properties, return `false` for | |
// `hasOwnProperty` checks of them. | |
return _baseHas_hasOwnProperty.call(object, key) || | |
(typeof object == 'object' && key in object && _getPrototype(object) === null); | |
} | |
/* harmony default export */ var _baseHas = (baseHas); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseKeys.js | |
/* Built-in method references for those with the same name as other `lodash` methods. */ | |
var nativeKeys = Object.keys; | |
/** | |
* The base implementation of `_.keys` which doesn't skip the constructor | |
* property of prototypes or treat sparse arrays as dense. | |
* | |
* @private | |
* @param {Object} object The object to query. | |
* @returns {Array} Returns the array of property names. | |
*/ | |
function baseKeys(object) { | |
return nativeKeys(Object(object)); | |
} | |
/* harmony default export */ var _baseKeys = (baseKeys); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/keys.js | |
/** | |
* Creates an array of the own enumerable property names of `object`. | |
* | |
* **Note:** Non-object values are coerced to objects. See the | |
* [ES spec](http://ecma-international.org/ecma-262/6.0/#sec-object.keys) | |
* for more details. | |
* | |
* @static | |
* @since 0.1.0 | |
* @memberOf _ | |
* @category Object | |
* @param {Object} object The object to query. | |
* @returns {Array} Returns the array of property names. | |
* @example | |
* | |
* function Foo() { | |
* this.a = 1; | |
* this.b = 2; | |
* } | |
* | |
* Foo.prototype.c = 3; | |
* | |
* _.keys(new Foo); | |
* // => ['a', 'b'] (iteration order is not guaranteed) | |
* | |
* _.keys('hi'); | |
* // => ['0', '1'] | |
*/ | |
function keys_keys(object) { | |
var isProto = _isPrototype(object); | |
if (!(isProto || lodash_isArrayLike(object))) { | |
return _baseKeys(object); | |
} | |
var indexes = _indexKeys(object), | |
skipIndexes = !!indexes, | |
result = indexes || [], | |
length = result.length; | |
for (var key in object) { | |
if (_baseHas(object, key) && | |
!(skipIndexes && (key == 'length' || _isIndex(key, length))) && | |
!(isProto && key == 'constructor')) { | |
result.push(key); | |
} | |
} | |
return result; | |
} | |
/* harmony default export */ var lodash_keys = (keys_keys); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseAssign.js | |
/** | |
* The base implementation of `_.assign` without support for multiple sources | |
* or `customizer` functions. | |
* | |
* @private | |
* @param {Object} object The destination object. | |
* @param {Object} source The source object. | |
* @returns {Object} Returns `object`. | |
*/ | |
function baseAssign(object, source) { | |
return object && _copyObject(source, lodash_keys(source), object); | |
} | |
/* harmony default export */ var _baseAssign = (baseAssign); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_cloneBuffer.js | |
/** | |
* Creates a clone of `buffer`. | |
* | |
* @private | |
* @param {Buffer} buffer The buffer to clone. | |
* @param {boolean} [isDeep] Specify a deep clone. | |
* @returns {Buffer} Returns the cloned buffer. | |
*/ | |
function cloneBuffer(buffer, isDeep) { | |
if (isDeep) { | |
return buffer.slice(); | |
} | |
var result = new buffer.constructor(buffer.length); | |
buffer.copy(result); | |
return result; | |
} | |
/* harmony default export */ var _cloneBuffer = (cloneBuffer); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_copyArray.js | |
/** | |
* Copies the values of `source` to `array`. | |
* | |
* @private | |
* @param {Array} source The array to copy values from. | |
* @param {Array} [array=[]] The array to copy values to. | |
* @returns {Array} Returns `array`. | |
*/ | |
function copyArray(source, array) { | |
var index = -1, | |
length = source.length; | |
array || (array = Array(length)); | |
while (++index < length) { | |
array[index] = source[index]; | |
} | |
return array; | |
} | |
/* harmony default export */ var _copyArray = (copyArray); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_getSymbols.js | |
/** Built-in value references. */ | |
var getOwnPropertySymbols = Object.getOwnPropertySymbols; | |
/** | |
* Creates an array of the own enumerable symbol properties of `object`. | |
* | |
* @private | |
* @param {Object} object The object to query. | |
* @returns {Array} Returns the array of symbols. | |
*/ | |
function getSymbols(object) { | |
// Coerce `object` to an object to avoid non-object errors in V8. | |
// See https://bugs.chromium.org/p/v8/issues/detail?id=3443 for more details. | |
return getOwnPropertySymbols(Object(object)); | |
} | |
// Fallback for IE < 11. | |
if (!getOwnPropertySymbols) { | |
getSymbols = function() { | |
return []; | |
}; | |
} | |
/* harmony default export */ var _getSymbols = (getSymbols); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_copySymbols.js | |
/** | |
* Copies own symbol properties of `source` to `object`. | |
* | |
* @private | |
* @param {Object} source The object to copy symbols from. | |
* @param {Object} [object={}] The object to copy symbols to. | |
* @returns {Object} Returns `object`. | |
*/ | |
function copySymbols(source, object) { | |
return _copyObject(source, _getSymbols(source), object); | |
} | |
/* harmony default export */ var _copySymbols = (copySymbols); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_arrayPush.js | |
/** | |
* Appends the elements of `values` to `array`. | |
* | |
* @private | |
* @param {Array} array The array to modify. | |
* @param {Array} values The values to append. | |
* @returns {Array} Returns `array`. | |
*/ | |
function arrayPush(array, values) { | |
var index = -1, | |
length = values.length, | |
offset = array.length; | |
while (++index < length) { | |
array[offset + index] = values[index]; | |
} | |
return array; | |
} | |
/* harmony default export */ var _arrayPush = (arrayPush); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseGetAllKeys.js | |
/** | |
* The base implementation of `getAllKeys` and `getAllKeysIn` which uses | |
* `keysFunc` and `symbolsFunc` to get the enumerable property names and | |
* symbols of `object`. | |
* | |
* @private | |
* @param {Object} object The object to query. | |
* @param {Function} keysFunc The function to get the keys of `object`. | |
* @param {Function} symbolsFunc The function to get the symbols of `object`. | |
* @returns {Array} Returns the array of property names and symbols. | |
*/ | |
function baseGetAllKeys(object, keysFunc, symbolsFunc) { | |
var result = keysFunc(object); | |
return lodash_isArray(object) ? result : _arrayPush(result, symbolsFunc(object)); | |
} | |
/* harmony default export */ var _baseGetAllKeys = (baseGetAllKeys); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_getAllKeys.js | |
/** | |
* Creates an array of own enumerable property names and symbols of `object`. | |
* | |
* @private | |
* @param {Object} object The object to query. | |
* @returns {Array} Returns the array of property names and symbols. | |
*/ | |
function getAllKeys(object) { | |
return _baseGetAllKeys(object, lodash_keys, _getSymbols); | |
} | |
/* harmony default export */ var _getAllKeys = (getAllKeys); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_DataView.js | |
/* Built-in method references that are verified to be native. */ | |
var DataView = _getNative(_root["a" /* default */], 'DataView'); | |
/* harmony default export */ var _DataView = (DataView); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_Promise.js | |
/* Built-in method references that are verified to be native. */ | |
var _Promise_Promise = _getNative(_root["a" /* default */], 'Promise'); | |
/* harmony default export */ var _Promise = (_Promise_Promise); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_Set.js | |
/* Built-in method references that are verified to be native. */ | |
var _Set_Set = _getNative(_root["a" /* default */], 'Set'); | |
/* harmony default export */ var _Set = (_Set_Set); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_WeakMap.js | |
/* Built-in method references that are verified to be native. */ | |
var _WeakMap_WeakMap = _getNative(_root["a" /* default */], 'WeakMap'); | |
/* harmony default export */ var _WeakMap = (_WeakMap_WeakMap); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_getTag.js | |
/** `Object#toString` result references. */ | |
var mapTag = '[object Map]', | |
_getTag_objectTag = '[object Object]', | |
promiseTag = '[object Promise]', | |
setTag = '[object Set]', | |
weakMapTag = '[object WeakMap]'; | |
var dataViewTag = '[object DataView]'; | |
/** Used for built-in method references. */ | |
var _getTag_objectProto = Object.prototype; | |
/** | |
* Used to resolve the | |
* [`toStringTag`](http://ecma-international.org/ecma-262/6.0/#sec-object.prototype.tostring) | |
* of values. | |
*/ | |
var _getTag_objectToString = _getTag_objectProto.toString; | |
/** Used to detect maps, sets, and weakmaps. */ | |
var dataViewCtorString = _toSource(_DataView), | |
mapCtorString = _toSource(_Map), | |
promiseCtorString = _toSource(_Promise), | |
setCtorString = _toSource(_Set), | |
weakMapCtorString = _toSource(_WeakMap); | |
/** | |
* Gets the `toStringTag` of `value`. | |
* | |
* @private | |
* @param {*} value The value to query. | |
* @returns {string} Returns the `toStringTag`. | |
*/ | |
function getTag(value) { | |
return _getTag_objectToString.call(value); | |
} | |
// Fallback for data views, maps, sets, and weak maps in IE 11, | |
// for data views in Edge, and promises in Node.js. | |
if ((_DataView && getTag(new _DataView(new ArrayBuffer(1))) != dataViewTag) || | |
(_Map && getTag(new _Map) != mapTag) || | |
(_Promise && getTag(_Promise.resolve()) != promiseTag) || | |
(_Set && getTag(new _Set) != setTag) || | |
(_WeakMap && getTag(new _WeakMap) != weakMapTag)) { | |
getTag = function(value) { | |
var result = _getTag_objectToString.call(value), | |
Ctor = result == _getTag_objectTag ? value.constructor : undefined, | |
ctorString = Ctor ? _toSource(Ctor) : undefined; | |
if (ctorString) { | |
switch (ctorString) { | |
case dataViewCtorString: return dataViewTag; | |
case mapCtorString: return mapTag; | |
case promiseCtorString: return promiseTag; | |
case setCtorString: return setTag; | |
case weakMapCtorString: return weakMapTag; | |
} | |
} | |
return result; | |
}; | |
} | |
/* harmony default export */ var _getTag = (getTag); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_initCloneArray.js | |
/** Used for built-in method references. */ | |
var _initCloneArray_objectProto = Object.prototype; | |
/** Used to check objects for own properties. */ | |
var _initCloneArray_hasOwnProperty = _initCloneArray_objectProto.hasOwnProperty; | |
/** | |
* Initializes an array clone. | |
* | |
* @private | |
* @param {Array} array The array to clone. | |
* @returns {Array} Returns the initialized clone. | |
*/ | |
function initCloneArray(array) { | |
var length = array.length, | |
result = array.constructor(length); | |
// Add properties assigned by `RegExp#exec`. | |
if (length && typeof array[0] == 'string' && _initCloneArray_hasOwnProperty.call(array, 'index')) { | |
result.index = array.index; | |
result.input = array.input; | |
} | |
return result; | |
} | |
/* harmony default export */ var _initCloneArray = (initCloneArray); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_Uint8Array.js | |
/** Built-in value references. */ | |
var _Uint8Array_Uint8Array = _root["a" /* default */].Uint8Array; | |
/* harmony default export */ var _Uint8Array = (_Uint8Array_Uint8Array); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_cloneArrayBuffer.js | |
/** | |
* Creates a clone of `arrayBuffer`. | |
* | |
* @private | |
* @param {ArrayBuffer} arrayBuffer The array buffer to clone. | |
* @returns {ArrayBuffer} Returns the cloned array buffer. | |
*/ | |
function cloneArrayBuffer(arrayBuffer) { | |
var result = new arrayBuffer.constructor(arrayBuffer.byteLength); | |
new _Uint8Array(result).set(new _Uint8Array(arrayBuffer)); | |
return result; | |
} | |
/* harmony default export */ var _cloneArrayBuffer = (cloneArrayBuffer); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_cloneDataView.js | |
/** | |
* Creates a clone of `dataView`. | |
* | |
* @private | |
* @param {Object} dataView The data view to clone. | |
* @param {boolean} [isDeep] Specify a deep clone. | |
* @returns {Object} Returns the cloned data view. | |
*/ | |
function cloneDataView(dataView, isDeep) { | |
var buffer = isDeep ? _cloneArrayBuffer(dataView.buffer) : dataView.buffer; | |
return new dataView.constructor(buffer, dataView.byteOffset, dataView.byteLength); | |
} | |
/* harmony default export */ var _cloneDataView = (cloneDataView); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_addMapEntry.js | |
/** | |
* Adds the key-value `pair` to `map`. | |
* | |
* @private | |
* @param {Object} map The map to modify. | |
* @param {Array} pair The key-value pair to add. | |
* @returns {Object} Returns `map`. | |
*/ | |
function addMapEntry(map, pair) { | |
// Don't return `Map#set` because it doesn't return the map instance in IE 11. | |
map.set(pair[0], pair[1]); | |
return map; | |
} | |
/* harmony default export */ var _addMapEntry = (addMapEntry); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_arrayReduce.js | |
/** | |
* A specialized version of `_.reduce` for arrays without support for | |
* iteratee shorthands. | |
* | |
* @private | |
* @param {Array} array The array to iterate over. | |
* @param {Function} iteratee The function invoked per iteration. | |
* @param {*} [accumulator] The initial value. | |
* @param {boolean} [initAccum] Specify using the first element of `array` as | |
* the initial value. | |
* @returns {*} Returns the accumulated value. | |
*/ | |
function arrayReduce(array, iteratee, accumulator, initAccum) { | |
var index = -1, | |
length = array.length; | |
if (initAccum && length) { | |
accumulator = array[++index]; | |
} | |
while (++index < length) { | |
accumulator = iteratee(accumulator, array[index], index, array); | |
} | |
return accumulator; | |
} | |
/* harmony default export */ var _arrayReduce = (arrayReduce); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_mapToArray.js | |
/** | |
* Converts `map` to its key-value pairs. | |
* | |
* @private | |
* @param {Object} map The map to convert. | |
* @returns {Array} Returns the key-value pairs. | |
*/ | |
function mapToArray(map) { | |
var index = -1, | |
result = Array(map.size); | |
map.forEach(function(value, key) { | |
result[++index] = [key, value]; | |
}); | |
return result; | |
} | |
/* harmony default export */ var _mapToArray = (mapToArray); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_cloneMap.js | |
/** | |
* Creates a clone of `map`. | |
* | |
* @private | |
* @param {Object} map The map to clone. | |
* @param {Function} cloneFunc The function to clone values. | |
* @param {boolean} [isDeep] Specify a deep clone. | |
* @returns {Object} Returns the cloned map. | |
*/ | |
function cloneMap(map, isDeep, cloneFunc) { | |
var array = isDeep ? cloneFunc(_mapToArray(map), true) : _mapToArray(map); | |
return _arrayReduce(array, _addMapEntry, new map.constructor); | |
} | |
/* harmony default export */ var _cloneMap = (cloneMap); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_cloneRegExp.js | |
/** Used to match `RegExp` flags from their coerced string values. */ | |
var reFlags = /\w*$/; | |
/** | |
* Creates a clone of `regexp`. | |
* | |
* @private | |
* @param {Object} regexp The regexp to clone. | |
* @returns {Object} Returns the cloned regexp. | |
*/ | |
function cloneRegExp(regexp) { | |
var result = new regexp.constructor(regexp.source, reFlags.exec(regexp)); | |
result.lastIndex = regexp.lastIndex; | |
return result; | |
} | |
/* harmony default export */ var _cloneRegExp = (cloneRegExp); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_addSetEntry.js | |
/** | |
* Adds `value` to `set`. | |
* | |
* @private | |
* @param {Object} set The set to modify. | |
* @param {*} value The value to add. | |
* @returns {Object} Returns `set`. | |
*/ | |
function addSetEntry(set, value) { | |
set.add(value); | |
return set; | |
} | |
/* harmony default export */ var _addSetEntry = (addSetEntry); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_setToArray.js | |
/** | |
* Converts `set` to an array of its values. | |
* | |
* @private | |
* @param {Object} set The set to convert. | |
* @returns {Array} Returns the values. | |
*/ | |
function setToArray(set) { | |
var index = -1, | |
result = Array(set.size); | |
set.forEach(function(value) { | |
result[++index] = value; | |
}); | |
return result; | |
} | |
/* harmony default export */ var _setToArray = (setToArray); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_cloneSet.js | |
/** | |
* Creates a clone of `set`. | |
* | |
* @private | |
* @param {Object} set The set to clone. | |
* @param {Function} cloneFunc The function to clone values. | |
* @param {boolean} [isDeep] Specify a deep clone. | |
* @returns {Object} Returns the cloned set. | |
*/ | |
function cloneSet(set, isDeep, cloneFunc) { | |
var array = isDeep ? cloneFunc(_setToArray(set), true) : _setToArray(set); | |
return _arrayReduce(array, _addSetEntry, new set.constructor); | |
} | |
/* harmony default export */ var _cloneSet = (cloneSet); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_Symbol.js | |
/** Built-in value references. */ | |
var _Symbol_Symbol = _root["a" /* default */].Symbol; | |
/* harmony default export */ var _Symbol = (_Symbol_Symbol); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_cloneSymbol.js | |
/** Used to convert symbols to primitives and strings. */ | |
var symbolProto = _Symbol ? _Symbol.prototype : undefined, | |
symbolValueOf = symbolProto ? symbolProto.valueOf : undefined; | |
/** | |
* Creates a clone of the `symbol` object. | |
* | |
* @private | |
* @param {Object} symbol The symbol object to clone. | |
* @returns {Object} Returns the cloned symbol object. | |
*/ | |
function cloneSymbol(symbol) { | |
return symbolValueOf ? Object(symbolValueOf.call(symbol)) : {}; | |
} | |
/* harmony default export */ var _cloneSymbol = (cloneSymbol); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_cloneTypedArray.js | |
/** | |
* Creates a clone of `typedArray`. | |
* | |
* @private | |
* @param {Object} typedArray The typed array to clone. | |
* @param {boolean} [isDeep] Specify a deep clone. | |
* @returns {Object} Returns the cloned typed array. | |
*/ | |
function cloneTypedArray(typedArray, isDeep) { | |
var buffer = isDeep ? _cloneArrayBuffer(typedArray.buffer) : typedArray.buffer; | |
return new typedArray.constructor(buffer, typedArray.byteOffset, typedArray.length); | |
} | |
/* harmony default export */ var _cloneTypedArray = (cloneTypedArray); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_initCloneByTag.js | |
/** `Object#toString` result references. */ | |
var boolTag = '[object Boolean]', | |
dateTag = '[object Date]', | |
_initCloneByTag_mapTag = '[object Map]', | |
numberTag = '[object Number]', | |
regexpTag = '[object RegExp]', | |
_initCloneByTag_setTag = '[object Set]', | |
_initCloneByTag_stringTag = '[object String]', | |
_initCloneByTag_symbolTag = '[object Symbol]'; | |
var arrayBufferTag = '[object ArrayBuffer]', | |
_initCloneByTag_dataViewTag = '[object DataView]', | |
float32Tag = '[object Float32Array]', | |
float64Tag = '[object Float64Array]', | |
int8Tag = '[object Int8Array]', | |
int16Tag = '[object Int16Array]', | |
int32Tag = '[object Int32Array]', | |
uint8Tag = '[object Uint8Array]', | |
uint8ClampedTag = '[object Uint8ClampedArray]', | |
uint16Tag = '[object Uint16Array]', | |
uint32Tag = '[object Uint32Array]'; | |
/** | |
* Initializes an object clone based on its `toStringTag`. | |
* | |
* **Note:** This function only supports cloning values with tags of | |
* `Boolean`, `Date`, `Error`, `Number`, `RegExp`, or `String`. | |
* | |
* @private | |
* @param {Object} object The object to clone. | |
* @param {string} tag The `toStringTag` of the object to clone. | |
* @param {Function} cloneFunc The function to clone values. | |
* @param {boolean} [isDeep] Specify a deep clone. | |
* @returns {Object} Returns the initialized clone. | |
*/ | |
function initCloneByTag(object, tag, cloneFunc, isDeep) { | |
var Ctor = object.constructor; | |
switch (tag) { | |
case arrayBufferTag: | |
return _cloneArrayBuffer(object); | |
case boolTag: | |
case dateTag: | |
return new Ctor(+object); | |
case _initCloneByTag_dataViewTag: | |
return _cloneDataView(object, isDeep); | |
case float32Tag: case float64Tag: | |
case int8Tag: case int16Tag: case int32Tag: | |
case uint8Tag: case uint8ClampedTag: case uint16Tag: case uint32Tag: | |
return _cloneTypedArray(object, isDeep); | |
case _initCloneByTag_mapTag: | |
return _cloneMap(object, isDeep, cloneFunc); | |
case numberTag: | |
case _initCloneByTag_stringTag: | |
return new Ctor(object); | |
case regexpTag: | |
return _cloneRegExp(object); | |
case _initCloneByTag_setTag: | |
return _cloneSet(object, isDeep, cloneFunc); | |
case _initCloneByTag_symbolTag: | |
return _cloneSymbol(object); | |
} | |
} | |
/* harmony default export */ var _initCloneByTag = (initCloneByTag); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseCreate.js | |
/** Built-in value references. */ | |
var objectCreate = Object.create; | |
/** | |
* The base implementation of `_.create` without support for assigning | |
* properties to the created object. | |
* | |
* @private | |
* @param {Object} prototype The object to inherit from. | |
* @returns {Object} Returns the new object. | |
*/ | |
function baseCreate(proto) { | |
return lodash_isObject(proto) ? objectCreate(proto) : {}; | |
} | |
/* harmony default export */ var _baseCreate = (baseCreate); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_initCloneObject.js | |
/** | |
* Initializes an object clone. | |
* | |
* @private | |
* @param {Object} object The object to clone. | |
* @returns {Object} Returns the initialized clone. | |
*/ | |
function initCloneObject(object) { | |
return (typeof object.constructor == 'function' && !_isPrototype(object)) | |
? _baseCreate(_getPrototype(object)) | |
: {}; | |
} | |
/* harmony default export */ var _initCloneObject = (initCloneObject); | |
// EXTERNAL MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/isBuffer.js | |
var isBuffer = __webpack_require__(7); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseClone.js | |
/** `Object#toString` result references. */ | |
var _baseClone_argsTag = '[object Arguments]', | |
arrayTag = '[object Array]', | |
_baseClone_boolTag = '[object Boolean]', | |
_baseClone_dateTag = '[object Date]', | |
errorTag = '[object Error]', | |
_baseClone_funcTag = '[object Function]', | |
_baseClone_genTag = '[object GeneratorFunction]', | |
_baseClone_mapTag = '[object Map]', | |
_baseClone_numberTag = '[object Number]', | |
_baseClone_objectTag = '[object Object]', | |
_baseClone_regexpTag = '[object RegExp]', | |
_baseClone_setTag = '[object Set]', | |
_baseClone_stringTag = '[object String]', | |
_baseClone_symbolTag = '[object Symbol]', | |
_baseClone_weakMapTag = '[object WeakMap]'; | |
var _baseClone_arrayBufferTag = '[object ArrayBuffer]', | |
_baseClone_dataViewTag = '[object DataView]', | |
_baseClone_float32Tag = '[object Float32Array]', | |
_baseClone_float64Tag = '[object Float64Array]', | |
_baseClone_int8Tag = '[object Int8Array]', | |
_baseClone_int16Tag = '[object Int16Array]', | |
_baseClone_int32Tag = '[object Int32Array]', | |
_baseClone_uint8Tag = '[object Uint8Array]', | |
_baseClone_uint8ClampedTag = '[object Uint8ClampedArray]', | |
_baseClone_uint16Tag = '[object Uint16Array]', | |
_baseClone_uint32Tag = '[object Uint32Array]'; | |
/** Used to identify `toStringTag` values supported by `_.clone`. */ | |
var cloneableTags = {}; | |
cloneableTags[_baseClone_argsTag] = cloneableTags[arrayTag] = | |
cloneableTags[_baseClone_arrayBufferTag] = cloneableTags[_baseClone_dataViewTag] = | |
cloneableTags[_baseClone_boolTag] = cloneableTags[_baseClone_dateTag] = | |
cloneableTags[_baseClone_float32Tag] = cloneableTags[_baseClone_float64Tag] = | |
cloneableTags[_baseClone_int8Tag] = cloneableTags[_baseClone_int16Tag] = | |
cloneableTags[_baseClone_int32Tag] = cloneableTags[_baseClone_mapTag] = | |
cloneableTags[_baseClone_numberTag] = cloneableTags[_baseClone_objectTag] = | |
cloneableTags[_baseClone_regexpTag] = cloneableTags[_baseClone_setTag] = | |
cloneableTags[_baseClone_stringTag] = cloneableTags[_baseClone_symbolTag] = | |
cloneableTags[_baseClone_uint8Tag] = cloneableTags[_baseClone_uint8ClampedTag] = | |
cloneableTags[_baseClone_uint16Tag] = cloneableTags[_baseClone_uint32Tag] = true; | |
cloneableTags[errorTag] = cloneableTags[_baseClone_funcTag] = | |
cloneableTags[_baseClone_weakMapTag] = false; | |
/** | |
* The base implementation of `_.clone` and `_.cloneDeep` which tracks | |
* traversed objects. | |
* | |
* @private | |
* @param {*} value The value to clone. | |
* @param {boolean} [isDeep] Specify a deep clone. | |
* @param {boolean} [isFull] Specify a clone including symbols. | |
* @param {Function} [customizer] The function to customize cloning. | |
* @param {string} [key] The key of `value`. | |
* @param {Object} [object] The parent object of `value`. | |
* @param {Object} [stack] Tracks traversed objects and their clone counterparts. | |
* @returns {*} Returns the cloned value. | |
*/ | |
function baseClone(value, isDeep, isFull, customizer, key, object, stack) { | |
var result; | |
if (customizer) { | |
result = object ? customizer(value, key, object, stack) : customizer(value); | |
} | |
if (result !== undefined) { | |
return result; | |
} | |
if (!lodash_isObject(value)) { | |
return value; | |
} | |
var isArr = lodash_isArray(value); | |
if (isArr) { | |
result = _initCloneArray(value); | |
if (!isDeep) { | |
return _copyArray(value, result); | |
} | |
} else { | |
var tag = _getTag(value), | |
isFunc = tag == _baseClone_funcTag || tag == _baseClone_genTag; | |
if (Object(isBuffer["a" /* default */])(value)) { | |
return _cloneBuffer(value, isDeep); | |
} | |
if (tag == _baseClone_objectTag || tag == _baseClone_argsTag || (isFunc && !object)) { | |
if (_isHostObject(value)) { | |
return object ? value : {}; | |
} | |
result = _initCloneObject(isFunc ? {} : value); | |
if (!isDeep) { | |
return _copySymbols(value, _baseAssign(result, value)); | |
} | |
} else { | |
if (!cloneableTags[tag]) { | |
return object ? value : {}; | |
} | |
result = _initCloneByTag(value, tag, baseClone, isDeep); | |
} | |
} | |
// Check for circular references and return its corresponding clone. | |
stack || (stack = new _Stack); | |
var stacked = stack.get(value); | |
if (stacked) { | |
return stacked; | |
} | |
stack.set(value, result); | |
if (!isArr) { | |
var props = isFull ? _getAllKeys(value) : lodash_keys(value); | |
} | |
// Recursively populate clone (susceptible to call stack limits). | |
_arrayEach(props || value, function(subValue, key) { | |
if (props) { | |
key = subValue; | |
subValue = value[key]; | |
} | |
_assignValue(result, key, baseClone(subValue, isDeep, isFull, customizer, key, value, stack)); | |
}); | |
return result; | |
} | |
/* harmony default export */ var _baseClone = (baseClone); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/clone.js | |
/** | |
* Creates a shallow clone of `value`. | |
* | |
* **Note:** This method is loosely based on the | |
* [structured clone algorithm](https://mdn.io/Structured_clone_algorithm) | |
* and supports cloning arrays, array buffers, booleans, date objects, maps, | |
* numbers, `Object` objects, regexes, sets, strings, symbols, and typed | |
* arrays. The own enumerable properties of `arguments` objects are cloned | |
* as plain objects. An empty object is returned for uncloneable values such | |
* as error objects, functions, DOM nodes, and WeakMaps. | |
* | |
* @static | |
* @memberOf _ | |
* @since 0.1.0 | |
* @category Lang | |
* @param {*} value The value to clone. | |
* @returns {*} Returns the cloned value. | |
* @see _.cloneDeep | |
* @example | |
* | |
* var objects = [{ 'a': 1 }, { 'b': 2 }]; | |
* | |
* var shallow = _.clone(objects); | |
* console.log(shallow[0] === objects[0]); | |
* // => true | |
*/ | |
function clone_clone(value) { | |
return _baseClone(value, false, true); | |
} | |
/* harmony default export */ var lodash_clone = (clone_clone); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/view/node.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/view/node | |
*/ | |
/** | |
* Abstract tree view node class. | |
* | |
* @abstract | |
*/ | |
class view_node_Node { | |
/** | |
* Creates a tree view node. | |
* | |
* This is an abstract class, so this constructor should not be used directly. | |
*/ | |
constructor() { | |
/** | |
* Parent element. Null by default. Set by {@link module:engine/view/element~Element#insertChildren}. | |
* | |
* @readonly | |
* @member {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment|null} | |
*/ | |
this.parent = null; | |
} | |
/** | |
* Index of the node in the parent element or null if the node has no parent. | |
* | |
* Accessing this property throws an error if this node's parent element does not contain it. | |
* This means that view tree got broken. | |
* | |
* @readonly | |
* @type {Number|null} | |
*/ | |
get index() { | |
let pos; | |
if ( !this.parent ) { | |
return null; | |
} | |
// No parent or child doesn't exist in parent's children. | |
if ( ( pos = this.parent.getChildIndex( this ) ) == -1 ) { | |
/** | |
* The node's parent does not contain this node. It means that the document tree is corrupted. | |
* | |
* @error view-node-not-found-in-parent | |
*/ | |
throw new CKEditorError( 'view-node-not-found-in-parent: The node\'s parent does not contain this node.' ); | |
} | |
return pos; | |
} | |
/** | |
* Node's next sibling, or `null` if it is the last child. | |
* | |
* @readonly | |
* @type {module:engine/view/node~Node|null} | |
*/ | |
get nextSibling() { | |
const index = this.index; | |
return ( index !== null && this.parent.getChild( index + 1 ) ) || null; | |
} | |
/** | |
* Node's previous sibling, or `null` if it is the first child. | |
* | |
* @readonly | |
* @type {module:engine/view/node~Node|null} | |
*/ | |
get previousSibling() { | |
const index = this.index; | |
return ( index !== null && this.parent.getChild( index - 1 ) ) || null; | |
} | |
/** | |
* Top-most ancestor of the node. If the node has no parent it is the root itself. | |
* | |
* @readonly | |
* @type {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment} | |
*/ | |
get root() { | |
let root = this; // eslint-disable-line consistent-this | |
while ( root.parent ) { | |
root = root.parent; | |
} | |
return root; | |
} | |
/** | |
* {@link module:engine/view/document~Document View document} that owns this node, or `null` if the node is inside | |
* {@link module:engine/view/documentfragment~DocumentFragment document fragment}. | |
* | |
* @readonly | |
* @type {module:engine/view/document~Document|null} | |
*/ | |
get document() { | |
// Parent might be Node, null or DocumentFragment. | |
if ( this.parent instanceof view_node_Node ) { | |
return this.parent.document; | |
} else { | |
return null; | |
} | |
} | |
/** | |
* Returns ancestors array of this node. | |
* | |
* @param {Object} options Options object. | |
* @param {Boolean} [options.includeSelf=false] When set to `true` this node will be also included in parent's array. | |
* @param {Boolean} [options.parentFirst=false] When set to `true`, array will be sorted from node's parent to root element, | |
* otherwise root element will be the first item in the array. | |
* @returns {Array} Array with ancestors. | |
*/ | |
getAncestors( options = { includeSelf: false, parentFirst: false } ) { | |
const ancestors = []; | |
let parent = options.includeSelf ? this : this.parent; | |
while ( parent ) { | |
ancestors[ options.parentFirst ? 'push' : 'unshift' ]( parent ); | |
parent = parent.parent; | |
} | |
return ancestors; | |
} | |
/** | |
* Returns a {@link module:engine/view/element~Element} or {@link module:engine/view/documentfragment~DocumentFragment} | |
* which is a common ancestor of both nodes. | |
* | |
* @param {module:engine/view/node~Node} node The second node. | |
* @param {Object} options Options object. | |
* @param {Boolean} [options.includeSelf=false] When set to `true` both nodes will be considered "ancestors" too. | |
* Which means that if e.g. node A is inside B, then their common ancestor will be B. | |
* @returns {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment|null} | |
*/ | |
getCommonAncestor( node, options = {} ) { | |
const ancestorsA = this.getAncestors( options ); | |
const ancestorsB = node.getAncestors( options ); | |
let i = 0; | |
while ( ancestorsA[ i ] == ancestorsB[ i ] && ancestorsA[ i ] ) { | |
i++; | |
} | |
return i === 0 ? null : ancestorsA[ i - 1 ]; | |
} | |
/** | |
* Removes node from parent. | |
*/ | |
remove() { | |
this.parent.removeChildren( this.index ); | |
} | |
/** | |
* @param {module:engine/view/document~ChangeType} type Type of the change. | |
* @param {module:engine/view/node~Node} node Changed node. | |
* @fires change | |
*/ | |
_fireChange( type, node ) { | |
this.fire( 'change:' + type, node ); | |
if ( this.parent ) { | |
this.parent._fireChange( type, node ); | |
} | |
} | |
/** | |
* Custom toJSON method to solve child-parent circular dependencies. | |
* | |
* @returns {Object} Clone of this object with the parent property removed. | |
*/ | |
toJSON() { | |
const json = lodash_clone( this ); | |
// Due to circular references we need to remove parent reference. | |
delete json.parent; | |
return json; | |
} | |
/** | |
* Clones this node. | |
* | |
* @method #clone | |
* @returns {module:engine/view/node~Node} Clone of this node. | |
*/ | |
/** | |
* Checks if provided node is similar to this node. | |
* | |
* @method #isSimilar | |
* @returns {Boolean} True if nodes are similar. | |
*/ | |
/** | |
* Checks whether given view tree object is of given type. | |
* | |
* This method is useful when processing view tree objects that are of unknown type. For example, a function | |
* may return {@link module:engine/view/documentfragment~DocumentFragment} or {@link module:engine/view/node~Node} | |
* that can be either text node or element. This method can be used to check what kind of object is returned. | |
* | |
* obj.is( 'node' ); // true for any node, false for document fragment | |
* obj.is( 'documentFragment' ); // true for document fragment, false for any node | |
* obj.is( 'element' ); // true for any element, false for text node or document fragment | |
* obj.is( 'element', 'p' ); // true only for element which name is 'p' | |
* obj.is( 'p' ); // shortcut for obj.is( 'element', 'p' ) | |
* obj.is( 'text' ); // true for text node, false for element and document fragment | |
* | |
* @method #is | |
* @param {'element'|'containerElement'|'attributeElement'|'emptyElement'|'uiElement'| | |
* 'rootElement'|'documentFragment'|'text'|'textProxy'} type | |
* @returns {Boolean} | |
*/ | |
} | |
/** | |
* Fired when list of {@link module:engine/view/element~Element elements} children changes. | |
* | |
* Change event is bubbled – it is fired on all ancestors. | |
* | |
* @event change:children | |
* @param {module:engine/view/node~Node} changedNode | |
*/ | |
/** | |
* Fired when list of {@link module:engine/view/element~Element elements} attributes changes. | |
* | |
* Change event is bubbled – it is fired on all ancestors. | |
* | |
* @event change:attributes | |
* @param {module:engine/view/node~Node} changedNode | |
*/ | |
/** | |
* Fired when {@link module:engine/view/text~Text text nodes} data changes. | |
* | |
* Change event is bubbled – it is fired on all ancestors. | |
* | |
* @event change:text | |
* @param {module:engine/view/node~Node} changedNode | |
*/ | |
/** | |
* @event change | |
*/ | |
mix( view_node_Node, emittermixin ); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/view/text.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/view/text | |
*/ | |
/** | |
* Tree view text node. | |
* | |
* @extends module:engine/view/node~Node | |
*/ | |
class view_text_Text extends view_node_Node { | |
/** | |
* Creates a tree view text node. | |
* | |
* @param {String} data Text. | |
*/ | |
constructor( data ) { | |
super(); | |
/** | |
* The text content. | |
* | |
* Setting the data fires the {@link module:engine/view/node~Node#event:change:text change event}. | |
* | |
* @private | |
* @member {String} module:engine/view/text~Text#_data | |
*/ | |
this._data = data; | |
} | |
/** | |
* Clones this node. | |
* | |
* @returns {module:engine/view/text~Text} Text node that is a clone of this node. | |
*/ | |
clone() { | |
return new view_text_Text( this.data ); | |
} | |
/** | |
* @inheritDoc | |
*/ | |
is( type ) { | |
return type == 'text'; | |
} | |
/** | |
* The text content. | |
* | |
* Setting the data fires the {@link module:engine/view/node~Node#event:change:text change event}. | |
*/ | |
get data() { | |
return this._data; | |
} | |
set data( data ) { | |
this._fireChange( 'text', this ); | |
this._data = data; | |
} | |
/** | |
* Checks if this text node is similar to other text node. | |
* Both nodes should have the same data to be considered as similar. | |
* | |
* @param {module:engine/view/text~Text} otherNode Node to check if it is same as this node. | |
* @returns {Boolean} | |
*/ | |
isSimilar( otherNode ) { | |
if ( !( otherNode instanceof view_text_Text ) ) { | |
return false; | |
} | |
return this === otherNode || this.data === otherNode.data; | |
} | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/view/matcher.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/view/matcher | |
*/ | |
/** | |
* View matcher class. | |
* Instance of this class can be used to find {@link module:engine/view/element~Element elements} that match given pattern. | |
*/ | |
class Matcher { | |
/** | |
* Creates new instance of Matcher. | |
* | |
* @param {String|RegExp|Object} [pattern] Match patterns. See {@link module:engine/view/matcher~Matcher#add add method} for | |
* more information. | |
*/ | |
constructor( ...pattern ) { | |
this._patterns = []; | |
this.add( ...pattern ); | |
} | |
/** | |
* Adds pattern or patterns to matcher instance. | |
* | |
* Example patterns matching element's name: | |
* | |
* // String. | |
* matcher.add( 'div' ); | |
* matcher.add( { name: 'div' } ); | |
* | |
* // Regular expression. | |
* matcher.add( /^\w/ ); | |
* matcher.add( { name: /^\w/ } ); | |
* | |
* Example pattern matching element's attributes: | |
* | |
* matcher.add( { | |
* attribute: { | |
* title: 'foobar', | |
* foo: /^\w+/ | |
* } | |
* } ); | |
* | |
* Example patterns matching element's classes: | |
* | |
* // Single class. | |
* matcher.add( { | |
* class: 'foobar' | |
* } ); | |
* | |
* // Single class using regular expression. | |
* matcher.add( { | |
* class: /foo.../ | |
* } ); | |
* | |
* // Multiple classes to match. | |
* matcher.add( { | |
* class: [ 'baz', 'bar', /foo.../ ] | |
* } ): | |
* | |
* Example pattern matching element's styles: | |
* | |
* matcher.add( { | |
* style: { | |
* position: 'absolute', | |
* color: /^\w*blue$/ | |
* } | |
* } ); | |
* | |
* Example function pattern: | |
* | |
* matcher.add( ( element ) => { | |
* // Result of this function will be included in `match` | |
* // property of the object returned from matcher.match() call. | |
* if ( element.name === 'div' && element.childCount > 0 ) { | |
* return { name: true }; | |
* } | |
* | |
* return null; | |
* } ); | |
* | |
* Multiple patterns can be added in one call: | |
* | |
* matcher.add( 'div', { class: 'foobar' } ); | |
* | |
* @param {Object|String|RegExp|Function} pattern Object describing pattern details. If string or regular expression | |
* is provided it will be used to match element's name. Pattern can be also provided in a form | |
* of a function - then this function will be called with each {@link module:engine/view/element~Element element} as a parameter. | |
* Function's return value will be stored under `match` key of the object returned from | |
* {@link module:engine/view/matcher~Matcher#match match} or {@link module:engine/view/matcher~Matcher#matchAll matchAll} methods. | |
* @param {String|RegExp} [pattern.name] Name or regular expression to match element's name. | |
* @param {Object} [pattern.attribute] Object with key-value pairs representing attributes to match. Each object key | |
* represents attribute name. Value under that key can be either a string or a regular expression and it will be | |
* used to match attribute value. | |
* @param {String|RegExp|Array} [pattern.class] Class name or array of class names to match. Each name can be | |
* provided in a form of string or regular expression. | |
* @param {Object} [pattern.style] Object with key-value pairs representing styles to match. Each object key | |
* represents style name. Value under that key can be either a string or a regular expression and it will be used | |
* to match style value. | |
*/ | |
add( ...pattern ) { | |
for ( let item of pattern ) { | |
// String or RegExp pattern is used as element's name. | |
if ( typeof item == 'string' || item instanceof RegExp ) { | |
item = { name: item }; | |
} | |
// Single class name/RegExp can be provided. | |
if ( item.class && ( typeof item.class == 'string' || item.class instanceof RegExp ) ) { | |
item.class = [ item.class ]; | |
} | |
this._patterns.push( item ); | |
} | |
} | |
/** | |
* Matches elements for currently stored patterns. Returns match information about first found | |
* {@link module:engine/view/element~Element element}, otherwise returns `null`. | |
* | |
* Example of returned object: | |
* | |
* { | |
* element: <instance of found element>, | |
* pattern: <pattern used to match found element>, | |
* match: { | |
* name: true, | |
* attribute: [ 'title', 'href' ], | |
* class: [ 'foo' ], | |
* style: [ 'color', 'position' ] | |
* } | |
* } | |
* | |
* @see module:engine/view/matcher~Matcher#add | |
* @see module:engine/view/matcher~Matcher#matchAll | |
* @param {...module:engine/view/element~Element} element View element to match against stored patterns. | |
* @returns {Object|null} result | |
* @returns {module:engine/view/element~Element} result.element Matched view element. | |
* @returns {Object|String|RegExp|Function} result.pattern Pattern that was used to find matched element. | |
* @returns {Object} result.match Object representing matched element parts. | |
* @returns {Boolean} [result.match.name] True if name of the element was matched. | |
* @returns {Array} [result.match.attribute] Array with matched attribute names. | |
* @returns {Array} [result.match.class] Array with matched class names. | |
* @returns {Array} [result.match.style] Array with matched style names. | |
*/ | |
match( ...element ) { | |
for ( const singleElement of element ) { | |
for ( const pattern of this._patterns ) { | |
const match = isElementMatching( singleElement, pattern ); | |
if ( match ) { | |
return { | |
element: singleElement, | |
pattern, | |
match | |
}; | |
} | |
} | |
} | |
return null; | |
} | |
/** | |
* Matches elements for currently stored patterns. Returns array of match information with all found | |
* {@link module:engine/view/element~Element elements}. If no element is found - returns `null`. | |
* | |
* @see module:engine/view/matcher~Matcher#add | |
* @see module:engine/view/matcher~Matcher#match | |
* @param {...module:engine/view/element~Element} element View element to match against stored patterns. | |
* @returns {Array.<Object>|null} Array with match information about found elements or `null`. For more information | |
* see {@link module:engine/view/matcher~Matcher#match match method} description. | |
*/ | |
matchAll( ...element ) { | |
const results = []; | |
for ( const singleElement of element ) { | |
for ( const pattern of this._patterns ) { | |
const match = isElementMatching( singleElement, pattern ); | |
if ( match ) { | |
results.push( { | |
element: singleElement, | |
pattern, | |
match | |
} ); | |
} | |
} | |
} | |
return results.length > 0 ? results : null; | |
} | |
/** | |
* Returns the name of the element to match if there is exactly one pattern added to the matcher instance | |
* and it matches element name defined by `string` (not `RegExp`). Otherwise, returns `null`. | |
* | |
* @returns {String|null} Element name trying to match. | |
*/ | |
getElementName() { | |
if ( this._patterns.length !== 1 ) { | |
return null; | |
} | |
const pattern = this._patterns[ 0 ]; | |
const name = pattern.name; | |
return ( typeof pattern != 'function' && name && !( name instanceof RegExp ) ) ? name : null; | |
} | |
} | |
// Returns match information if {@link module:engine/view/element~Element element} is matching provided pattern. | |
// If element cannot be matched to provided pattern - returns `null`. | |
// | |
// @param {module:engine/view/element~Element} element | |
// @param {Object|String|RegExp|Function} pattern | |
// @returns {Object|null} Returns object with match information or null if element is not matching. | |
function isElementMatching( element, pattern ) { | |
// If pattern is provided as function - return result of that function; | |
if ( typeof pattern == 'function' ) { | |
return pattern( element ); | |
} | |
const match = {}; | |
// Check element's name. | |
if ( pattern.name ) { | |
match.name = matchName( pattern.name, element.name ); | |
if ( !match.name ) { | |
return null; | |
} | |
} | |
// Check element's attributes. | |
if ( pattern.attribute ) { | |
match.attribute = matchAttributes( pattern.attribute, element ); | |
if ( !match.attribute ) { | |
return null; | |
} | |
} | |
// Check element's classes. | |
if ( pattern.class ) { | |
match.class = matchClasses( pattern.class, element ); | |
if ( !match.class ) { | |
return false; | |
} | |
} | |
// Check element's styles. | |
if ( pattern.style ) { | |
match.style = matchStyles( pattern.style, element ); | |
if ( !match.style ) { | |
return false; | |
} | |
} | |
return match; | |
} | |
// Checks if name can be matched by provided pattern. | |
// | |
// @param {String|RegExp} pattern | |
// @param {String} name | |
// @returns {Boolean} Returns `true` if name can be matched, `false` otherwise. | |
function matchName( pattern, name ) { | |
// If pattern is provided as RegExp - test against this regexp. | |
if ( pattern instanceof RegExp ) { | |
return pattern.test( name ); | |
} | |
return pattern === name; | |
} | |
// Checks if attributes of provided element can be matched against provided patterns. | |
// | |
// @param {Object} patterns Object with information about attributes to match. Each key of the object will be | |
// used as attribute name. Value of each key can be a string or regular expression to match against attribute value. | |
// @param {module:engine/view/element~Element} element Element which attributes will be tested. | |
// @returns {Array|null} Returns array with matched attribute names or `null` if no attributes were matched. | |
function matchAttributes( patterns, element ) { | |
const match = []; | |
for ( const name in patterns ) { | |
const pattern = patterns[ name ]; | |
if ( element.hasAttribute( name ) ) { | |
const attribute = element.getAttribute( name ); | |
if ( pattern instanceof RegExp ) { | |
if ( pattern.test( attribute ) ) { | |
match.push( name ); | |
} else { | |
return null; | |
} | |
} else if ( attribute === pattern ) { | |
match.push( name ); | |
} else { | |
return null; | |
} | |
} else { | |
return null; | |
} | |
} | |
return match; | |
} | |
// Checks if classes of provided element can be matched against provided patterns. | |
// | |
// @param {Array.<String|RegExp>} patterns Array of strings or regular expressions to match against element's classes. | |
// @param {module:engine/view/element~Element} element Element which classes will be tested. | |
// @returns {Array|null} Returns array with matched class names or `null` if no classes were matched. | |
function matchClasses( patterns, element ) { | |
const match = []; | |
for ( const pattern of patterns ) { | |
if ( pattern instanceof RegExp ) { | |
const classes = element.getClassNames(); | |
for ( const name of classes ) { | |
if ( pattern.test( name ) ) { | |
match.push( name ); | |
} | |
} | |
if ( match.length === 0 ) { | |
return null; | |
} | |
} else if ( element.hasClass( pattern ) ) { | |
match.push( pattern ); | |
} else { | |
return null; | |
} | |
} | |
return match; | |
} | |
// Checks if styles of provided element can be matched against provided patterns. | |
// | |
// @param {Object} patterns Object with information about styles to match. Each key of the object will be | |
// used as style name. Value of each key can be a string or regular expression to match against style value. | |
// @param {module:engine/view/element~Element} element Element which styles will be tested. | |
// @returns {Array|null} Returns array with matched style names or `null` if no styles were matched. | |
function matchStyles( patterns, element ) { | |
const match = []; | |
for ( const name in patterns ) { | |
const pattern = patterns[ name ]; | |
if ( element.hasStyle( name ) ) { | |
const style = element.getStyle( name ); | |
if ( pattern instanceof RegExp ) { | |
if ( pattern.test( style ) ) { | |
match.push( name ); | |
} else { | |
return null; | |
} | |
} else if ( style === pattern ) { | |
match.push( name ); | |
} else { | |
return null; | |
} | |
} else { | |
return null; | |
} | |
} | |
return match; | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/view/element.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/view/element | |
*/ | |
/** | |
* View element. | |
* | |
* Editing engine does not define fixed HTML DTD. This is why the type of the {@link module:engine/view/element~Element} need to | |
* be defined by the feature developer. Creating an element you should use {@link module:engine/view/containerelement~ContainerElement} | |
* class, {@link module:engine/view/attributeelement~AttributeElement} class or {@link module:engine/view/emptyelement~EmptyElement} class. | |
* | |
* Note that for view elements which are not created from model, like elements from mutations, paste or | |
* {@link module:engine/controller/datacontroller~DataController#set data.set} it is not possible to define the type of the element, so | |
* these will be instances of the {@link module:engine/view/element~Element}. | |
* | |
* @extends module:engine/view/node~Node | |
*/ | |
class view_element_Element extends view_node_Node { | |
/** | |
* Creates a view element. | |
* | |
* Attributes can be passed in various formats: | |
* | |
* new Element( 'div', { 'class': 'editor', 'contentEditable': 'true' } ); // object | |
* new Element( 'div', [ [ 'class', 'editor' ], [ 'contentEditable', 'true' ] ] ); // map-like iterator | |
* new Element( 'div', mapOfAttributes ); // map | |
* | |
* @param {String} name Node name. | |
* @param {Object|Iterable} [attrs] Collection of attributes. | |
* @param {module:engine/view/node~Node|Iterable.<module:engine/view/node~Node>} [children] | |
* List of nodes to be inserted into created element. | |
*/ | |
constructor( name, attrs, children ) { | |
super(); | |
/** | |
* Name of the element. | |
* | |
* @readonly | |
* @member {String} | |
*/ | |
this.name = name; | |
/** | |
* Map of attributes, where attributes names are keys and attributes values are values. | |
* | |
* @protected | |
* @member {Map} #_attrs | |
*/ | |
if ( lodash_isPlainObject( attrs ) ) { | |
this._attrs = objectToMap( attrs ); | |
} else { | |
this._attrs = new Map( attrs ); | |
} | |
/** | |
* Array of child nodes. | |
* | |
* @protected | |
* @member {Array.<module:engine/view/node~Node>} | |
*/ | |
this._children = []; | |
if ( children ) { | |
this.insertChildren( 0, children ); | |
} | |
/** | |
* Set of classes associated with element instance. | |
* | |
* @protected | |
* @member {Set} | |
*/ | |
this._classes = new Set(); | |
if ( this._attrs.has( 'class' ) ) { | |
// Remove class attribute and handle it by class set. | |
const classString = this._attrs.get( 'class' ); | |
parseClasses( this._classes, classString ); | |
this._attrs.delete( 'class' ); | |
} | |
/** | |
* Map of styles. | |
* | |
* @protected | |
* @member {Set} module:engine/view/element~Element#_styles | |
*/ | |
this._styles = new Map(); | |
if ( this._attrs.has( 'style' ) ) { | |
// Remove style attribute and handle it by styles map. | |
parseInlineStyles( this._styles, this._attrs.get( 'style' ) ); | |
this._attrs.delete( 'style' ); | |
} | |
/** | |
* Map of custom properties. | |
* Custom properties can be added to element instance, will be cloned but not rendered into DOM. | |
* | |
* @protected | |
* @memeber {Map} | |
*/ | |
this._customProperties = new Map(); | |
} | |
/** | |
* Number of element's children. | |
* | |
* @readonly | |
* @type {Number} | |
*/ | |
get childCount() { | |
return this._children.length; | |
} | |
/** | |
* Is `true` if there are no nodes inside this element, `false` otherwise. | |
* | |
* @readonly | |
* @type {Boolean} | |
*/ | |
get isEmpty() { | |
return this._children.length === 0; | |
} | |
/** | |
* Checks whether given view tree object is of given type. | |
* | |
* Read more in {@link module:engine/view/node~Node#is}. | |
* | |
* @param {String} type | |
* @param {String} [name] Element name. | |
* @returns {Boolean} | |
*/ | |
is( type, name = null ) { | |
if ( !name ) { | |
return type == 'element' || type == this.name; | |
} else { | |
return type == 'element' && name == this.name; | |
} | |
} | |
/** | |
* Clones provided element. | |
* | |
* @param {Boolean} [deep=false] If set to `true` clones element and all its children recursively. When set to `false`, | |
* element will be cloned without any children. | |
* @returns {module:engine/view/element~Element} Clone of this element. | |
*/ | |
clone( deep = false ) { | |
const childrenClone = []; | |
if ( deep ) { | |
for ( const child of this.getChildren() ) { | |
childrenClone.push( child.clone( deep ) ); | |
} | |
} | |
// ContainerElement and AttributeElement should be also cloned properly. | |
const cloned = new this.constructor( this.name, this._attrs, childrenClone ); | |
// Classes and styles are cloned separately - this solution is faster than adding them back to attributes and | |
// parse once again in constructor. | |
cloned._classes = new Set( this._classes ); | |
cloned._styles = new Map( this._styles ); | |
// Clone custom properties. | |
cloned._customProperties = new Map( this._customProperties ); | |
// Clone filler offset method. | |
// We can't define this method in a prototype because it's behavior which | |
// is changed by e.g. toWidget() function from ckeditor5-widget. Perhaps this should be one of custom props. | |
cloned.getFillerOffset = this.getFillerOffset; | |
return cloned; | |
} | |
/** | |
* {@link module:engine/view/element~Element#insertChildren Insert} a child node or a list of child nodes at the end of this node | |
* and sets the parent of these nodes to this element. | |
* | |
* @fires module:engine/view/node~Node#change | |
* @param {module:engine/view/node~Node|Iterable.<module:engine/view/node~Node>} nodes Node or the list of nodes to be inserted. | |
* @returns {Number} Number of appended nodes. | |
*/ | |
appendChildren( nodes ) { | |
return this.insertChildren( this.childCount, nodes ); | |
} | |
/** | |
* Gets child at the given index. | |
* | |
* @param {Number} index Index of child. | |
* @returns {module:engine/view/node~Node} Child node. | |
*/ | |
getChild( index ) { | |
return this._children[ index ]; | |
} | |
/** | |
* Gets index of the given child node. Returns `-1` if child node is not found. | |
* | |
* @param {module:engine/view/node~Node} node Child node. | |
* @returns {Number} Index of the child node. | |
*/ | |
getChildIndex( node ) { | |
return this._children.indexOf( node ); | |
} | |
/** | |
* Gets child nodes iterator. | |
* | |
* @returns {Iterable.<module:engine/view/node~Node>} Child nodes iterator. | |
*/ | |
getChildren() { | |
return this._children[ Symbol.iterator ](); | |
} | |
/** | |
* Returns an iterator that contains the keys for attributes. Order of inserting attributes is not preserved. | |
* | |
* @returns {Iterator.<String>} Keys for attributes. | |
*/ | |
* getAttributeKeys() { | |
if ( this._classes.size > 0 ) { | |
yield 'class'; | |
} | |
if ( this._styles.size > 0 ) { | |
yield 'style'; | |
} | |
// This is not an optimal solution because of https://github.com/ckeditor/ckeditor5-engine/issues/454. | |
// It can be simplified to `yield* this._attrs.keys();`. | |
for ( const key of this._attrs.keys() ) { | |
yield key; | |
} | |
} | |
/** | |
* Returns iterator that iterates over this element's attributes. | |
* | |
* Attributes are returned as arrays containing two items. First one is attribute key and second is attribute value. | |
* This format is accepted by native `Map` object and also can be passed in `Node` constructor. | |
* | |
* @returns {Iterable.<*>} | |
*/ | |
* getAttributes() { | |
yield* this._attrs.entries(); | |
if ( this._classes.size > 0 ) { | |
yield [ 'class', this.getAttribute( 'class' ) ]; | |
} | |
if ( this._styles.size > 0 ) { | |
yield [ 'style', this.getAttribute( 'style' ) ]; | |
} | |
} | |
/** | |
* Gets attribute by key. If attribute is not present - returns undefined. | |
* | |
* @param {String} key Attribute key. | |
* @returns {String|undefined} Attribute value. | |
*/ | |
getAttribute( key ) { | |
if ( key == 'class' ) { | |
if ( this._classes.size > 0 ) { | |
return [ ...this._classes ].join( ' ' ); | |
} | |
return undefined; | |
} | |
if ( key == 'style' ) { | |
if ( this._styles.size > 0 ) { | |
let styleString = ''; | |
for ( const [ property, value ] of this._styles ) { | |
styleString += `${ property }:${ value };`; | |
} | |
return styleString; | |
} | |
return undefined; | |
} | |
return this._attrs.get( key ); | |
} | |
/** | |
* Returns a boolean indicating whether an attribute with the specified key exists in the element. | |
* | |
* @param {String} key Attribute key. | |
* @returns {Boolean} `true` if attribute with the specified key exists in the element, false otherwise. | |
*/ | |
hasAttribute( key ) { | |
if ( key == 'class' ) { | |
return this._classes.size > 0; | |
} | |
if ( key == 'style' ) { | |
return this._styles.size > 0; | |
} | |
return this._attrs.has( key ); | |
} | |
/** | |
* Adds or overwrite attribute with a specified key and value. | |
* | |
* @param {String} key Attribute key. | |
* @param {String} value Attribute value. | |
* @fires module:engine/view/node~Node#change | |
*/ | |
setAttribute( key, value ) { | |
this._fireChange( 'attributes', this ); | |
if ( key == 'class' ) { | |
parseClasses( this._classes, value ); | |
} else if ( key == 'style' ) { | |
parseInlineStyles( this._styles, value ); | |
} else { | |
this._attrs.set( key, value ); | |
} | |
} | |
/** | |
* Inserts a child node or a list of child nodes on the given index and sets the parent of these nodes to | |
* this element. | |
* | |
* @param {Number} index Position where nodes should be inserted. | |
* @param {module:engine/view/node~Node|Iterable.<module:engine/view/node~Node>} nodes Node or the list of nodes to be inserted. | |
* @fires module:engine/view/node~Node#change | |
* @returns {Number} Number of inserted nodes. | |
*/ | |
insertChildren( index, nodes ) { | |
this._fireChange( 'children', this ); | |
let count = 0; | |
nodes = element_normalize( nodes ); | |
for ( const node of nodes ) { | |
// If node that is being added to this element is already inside another element, first remove it from the old parent. | |
if ( node.parent !== null ) { | |
node.remove(); | |
} | |
node.parent = this; | |
this._children.splice( index, 0, node ); | |
index++; | |
count++; | |
} | |
return count; | |
} | |
/** | |
* Removes attribute from the element. | |
* | |
* @param {String} key Attribute key. | |
* @returns {Boolean} Returns true if an attribute existed and has been removed. | |
* @fires module:engine/view/node~Node#change | |
*/ | |
removeAttribute( key ) { | |
this._fireChange( 'attributes', this ); | |
// Remove class attribute. | |
if ( key == 'class' ) { | |
if ( this._classes.size > 0 ) { | |
this._classes.clear(); | |
return true; | |
} | |
return false; | |
} | |
// Remove style attribute. | |
if ( key == 'style' ) { | |
if ( this._styles.size > 0 ) { | |
this._styles.clear(); | |
return true; | |
} | |
return false; | |
} | |
// Remove other attributes. | |
return this._attrs.delete( key ); | |
} | |
/** | |
* Removes number of child nodes starting at the given index and set the parent of these nodes to `null`. | |
* | |
* @param {Number} index Number of the first node to remove. | |
* @param {Number} [howMany=1] Number of nodes to remove. | |
* @returns {Array.<module:engine/view/node~Node>} The array of removed nodes. | |
* @fires module:engine/view/node~Node#change | |
*/ | |
removeChildren( index, howMany = 1 ) { | |
this._fireChange( 'children', this ); | |
for ( let i = index; i < index + howMany; i++ ) { | |
this._children[ i ].parent = null; | |
} | |
return this._children.splice( index, howMany ); | |
} | |
/** | |
* Checks if this element is similar to other element. | |
* Both elements should have the same name and attributes to be considered as similar. Two similar elements | |
* can contain different set of children nodes. | |
* | |
* @param {module:engine/view/element~Element} otherElement | |
* @returns {Boolean} | |
*/ | |
isSimilar( otherElement ) { | |
if ( !( otherElement instanceof view_element_Element ) ) { | |
return false; | |
} | |
// If exactly the same Element is provided - return true immediately. | |
if ( this === otherElement ) { | |
return true; | |
} | |
// Check element name. | |
if ( this.name != otherElement.name ) { | |
return false; | |
} | |
// Check number of attributes, classes and styles. | |
if ( this._attrs.size !== otherElement._attrs.size || this._classes.size !== otherElement._classes.size || | |
this._styles.size !== otherElement._styles.size ) { | |
return false; | |
} | |
// Check if attributes are the same. | |
for ( const [ key, value ] of this._attrs ) { | |
if ( !otherElement._attrs.has( key ) || otherElement._attrs.get( key ) !== value ) { | |
return false; | |
} | |
} | |
// Check if classes are the same. | |
for ( const className of this._classes ) { | |
if ( !otherElement._classes.has( className ) ) { | |
return false; | |
} | |
} | |
// Check if styles are the same. | |
for ( const [ property, value ] of this._styles ) { | |
if ( !otherElement._styles.has( property ) || otherElement._styles.get( property ) !== value ) { | |
return false; | |
} | |
} | |
return true; | |
} | |
/** | |
* Adds specified class. | |
* | |
* element.addClass( 'foo' ); // Adds 'foo' class. | |
* element.addClass( 'foo', 'bar' ); // Adds 'foo' and 'bar' classes. | |
* | |
* @param {...String} className | |
* @fires module:engine/view/node~Node#change | |
*/ | |
addClass( ...className ) { | |
this._fireChange( 'attributes', this ); | |
className.forEach( name => this._classes.add( name ) ); | |
} | |
/** | |
* Removes specified class. | |
* | |
* element.removeClass( 'foo' ); // Removes 'foo' class. | |
* element.removeClass( 'foo', 'bar' ); // Removes both 'foo' and 'bar' classes. | |
* | |
* @param {...String} className | |
* @fires module:engine/view/node~Node#change | |
*/ | |
removeClass( ...className ) { | |
this._fireChange( 'attributes', this ); | |
className.forEach( name => this._classes.delete( name ) ); | |
} | |
/** | |
* Returns true if class is present. | |
* If more then one class is provided - returns true only when all classes are present. | |
* | |
* element.hasClass( 'foo' ); // Returns true if 'foo' class is present. | |
* element.hasClass( 'foo', 'bar' ); // Returns true if 'foo' and 'bar' classes are both present. | |
* | |
* @param {...String} className | |
*/ | |
hasClass( ...className ) { | |
for ( const name of className ) { | |
if ( !this._classes.has( name ) ) { | |
return false; | |
} | |
} | |
return true; | |
} | |
/** | |
* Returns iterator that contains all class names. | |
* | |
* @returns {Iterator.<String>} | |
*/ | |
getClassNames() { | |
return this._classes.keys(); | |
} | |
/** | |
* Adds style to the element. | |
* | |
* element.setStyle( 'color', 'red' ); | |
* element.setStyle( { | |
* color: 'red', | |
* position: 'fixed' | |
* } ); | |
* | |
* @param {String|Object} property Property name or object with key - value pairs. | |
* @param {String} [value] Value to set. This parameter is ignored if object is provided as the first parameter. | |
* @fires module:engine/view/node~Node#change | |
*/ | |
setStyle( property, value ) { | |
this._fireChange( 'attributes', this ); | |
if ( lodash_isPlainObject( property ) ) { | |
const keys = Object.keys( property ); | |
for ( const key of keys ) { | |
this._styles.set( key, property[ key ] ); | |
} | |
} else { | |
this._styles.set( property, value ); | |
} | |
} | |
/** | |
* Returns style value for given property. | |
* Undefined is returned if style does not exist. | |
* | |
* @param {String} property | |
* @returns {String|undefined} | |
*/ | |
getStyle( property ) { | |
return this._styles.get( property ); | |
} | |
/** | |
* Returns iterator that contains all style names. | |
* | |
* @returns {Iterator.<String>} | |
*/ | |
getStyleNames() { | |
return this._styles.keys(); | |
} | |
/** | |
* Returns true if style keys are present. | |
* If more then one style property is provided - returns true only when all properties are present. | |
* | |
* element.hasStyle( 'color' ); // Returns true if 'border-top' style is present. | |
* element.hasStyle( 'color', 'border-top' ); // Returns true if 'color' and 'border-top' styles are both present. | |
* | |
* @param {...String} property | |
*/ | |
hasStyle( ...property ) { | |
for ( const name of property ) { | |
if ( !this._styles.has( name ) ) { | |
return false; | |
} | |
} | |
return true; | |
} | |
/** | |
* Removes specified style. | |
* | |
* element.removeStyle( 'color' ); // Removes 'color' style. | |
* element.removeStyle( 'color', 'border-top' ); // Removes both 'color' and 'border-top' styles. | |
* | |
* @param {...String} property | |
* @fires module:engine/view/node~Node#change | |
*/ | |
removeStyle( ...property ) { | |
this._fireChange( 'attributes', this ); | |
property.forEach( name => this._styles.delete( name ) ); | |
} | |
/** | |
* Returns ancestor element that match specified pattern. | |
* Provided patterns should be compatible with {@link module:engine/view/matcher~Matcher Matcher} as it is used internally. | |
* | |
* @see module:engine/view/matcher~Matcher | |
* @param {Object|String|RegExp|Function} patterns Patterns used to match correct ancestor. | |
* See {@link module:engine/view/matcher~Matcher}. | |
* @returns {module:engine/view/element~Element|null} Found element or `null` if no matching ancestor was found. | |
*/ | |
findAncestor( ...patterns ) { | |
const matcher = new Matcher( ...patterns ); | |
let parent = this.parent; | |
while ( parent ) { | |
if ( matcher.match( parent ) ) { | |
return parent; | |
} | |
parent = parent.parent; | |
} | |
return null; | |
} | |
/** | |
* Sets a custom property. Unlike attributes, custom properties are not rendered to the DOM, | |
* so they can be used to add special data to elements. | |
* | |
* @param {String|Symbol} key | |
* @param {*} value | |
*/ | |
setCustomProperty( key, value ) { | |
this._customProperties.set( key, value ); | |
} | |
/** | |
* Returns the custom property value for the given key. | |
* | |
* @param {String|Symbol} key | |
* @returns {*} | |
*/ | |
getCustomProperty( key ) { | |
return this._customProperties.get( key ); | |
} | |
/** | |
* Removes the custom property stored under the given key. | |
* | |
* @param {String|Symbol} key | |
* @returns {Boolean} Returns true if property was removed. | |
*/ | |
removeCustomProperty( key ) { | |
return this._customProperties.delete( key ); | |
} | |
/** | |
* Returns an iterator which iterates over this element's custom properties. | |
* Iterator provides [key, value] pair for each stored property. | |
* | |
* @returns {Iterable.<*>} | |
*/ | |
* getCustomProperties() { | |
yield* this._customProperties.entries(); | |
} | |
/** | |
* Returns identity string based on element's name, styles, classes and other attributes. | |
* Two elements that {@link #isSimilar are similar} will have same identity string. | |
* It has the following format: | |
* | |
* 'name class="class1,class2" style="style1:value1;style2:value2" attr1="val1" attr2="val2"' | |
* | |
* For example: | |
* | |
* const element = new ViewElement( 'foo' ); | |
* element.setAttribute( 'banana', '10' ); | |
* element.setAttribute( 'apple', '20' ); | |
* element.setStyle( 'color', 'red' ); | |
* element.setStyle( 'border-color', 'white' ); | |
* element.addClass( 'baz' ); | |
* | |
* // returns 'foo class="baz" style="border-color:white;color:red" apple="20" banana="10"' | |
* element.getIdentity(); | |
* | |
* NOTE: Classes, styles and other attributes are sorted alphabetically. | |
* | |
* @returns {String} | |
*/ | |
getIdentity() { | |
const classes = Array.from( this._classes ).sort().join( ',' ); | |
const styles = Array.from( this._styles ).map( i => `${ i[ 0 ] }:${ i[ 1 ] }` ).sort().join( ';' ); | |
const attributes = Array.from( this._attrs ).map( i => `${ i[ 0 ] }="${ i[ 1 ] }"` ).sort().join( ' ' ); | |
return this.name + | |
( classes == '' ? '' : ` class="${ classes }"` ) + | |
( styles == '' ? '' : ` style="${ styles }"` ) + | |
( attributes == '' ? '' : ` ${ attributes }` ); | |
} | |
/** | |
* Returns block {@link module:engine/view/filler filler} offset or `null` if block filler is not needed. | |
* | |
* @abstract | |
* @method module:engine/view/element~Element#getFillerOffset | |
*/ | |
} | |
// Parses inline styles and puts property - value pairs into styles map. | |
// Styles map is cleared before insertion. | |
// | |
// @param {Map.<String, String>} stylesMap Map to insert parsed properties and values. | |
// @param {String} stylesString Styles to parse. | |
function parseInlineStyles( stylesMap, stylesString ) { | |
// `null` if no quote was found in input string or last found quote was a closing quote. See below. | |
let quoteType = null; | |
let propertyNameStart = 0; | |
let propertyValueStart = 0; | |
let propertyName = null; | |
stylesMap.clear(); | |
// Do not set anything if input string is empty. | |
if ( stylesString === '' ) { | |
return; | |
} | |
// Fix inline styles that do not end with `;` so they are compatible with algorithm below. | |
if ( stylesString.charAt( stylesString.length - 1 ) != ';' ) { | |
stylesString = stylesString + ';'; | |
} | |
// Seek the whole string for "special characters". | |
for ( let i = 0; i < stylesString.length; i++ ) { | |
const char = stylesString.charAt( i ); | |
if ( quoteType === null ) { | |
// No quote found yet or last found quote was a closing quote. | |
switch ( char ) { | |
case ':': | |
// Most of time colon means that property name just ended. | |
// Sometimes however `:` is found inside property value (for example in background image url). | |
if ( !propertyName ) { | |
// Treat this as end of property only if property name is not already saved. | |
// Save property name. | |
propertyName = stylesString.substr( propertyNameStart, i - propertyNameStart ); | |
// Save this point as the start of property value. | |
propertyValueStart = i + 1; | |
} | |
break; | |
case '"': | |
case '\'': | |
// Opening quote found (this is an opening quote, because `quoteType` is `null`). | |
quoteType = char; | |
break; | |
// eslint-disable-next-line no-case-declarations | |
case ';': | |
// Property value just ended. | |
// Use previously stored property value start to obtain property value. | |
const propertyValue = stylesString.substr( propertyValueStart, i - propertyValueStart ); | |
if ( propertyName ) { | |
// Save parsed part. | |
stylesMap.set( propertyName.trim(), propertyValue.trim() ); | |
} | |
propertyName = null; | |
// Save this point as property name start. Property name starts immediately after previous property value ends. | |
propertyNameStart = i + 1; | |
break; | |
} | |
} else if ( char === quoteType ) { | |
// If a quote char is found and it is a closing quote, mark this fact by `null`-ing `quoteType`. | |
quoteType = null; | |
} | |
} | |
} | |
// Parses class attribute and puts all classes into classes set. | |
// Classes set s cleared before insertion. | |
// | |
// @param {Set.<String>} classesSet Set to insert parsed classes. | |
// @param {String} classesString String with classes to parse. | |
function parseClasses( classesSet, classesString ) { | |
const classArray = classesString.split( /\s+/ ); | |
classesSet.clear(); | |
classArray.forEach( name => classesSet.add( name ) ); | |
} | |
// Converts strings to Text and non-iterables to arrays. | |
// | |
// @param {String|module:engine/view/node~Node|Iterable.<String|module:engine/view/node~Node>} | |
// @return {Iterable.<module:engine/view/node~Node>} | |
function element_normalize( nodes ) { | |
// Separate condition because string is iterable. | |
if ( typeof nodes == 'string' ) { | |
return [ new view_text_Text( nodes ) ]; | |
} | |
if ( !isIterable( nodes ) ) { | |
nodes = [ nodes ]; | |
} | |
// Array.from to enable .map() on non-arrays. | |
return Array.from( nodes ) | |
.map( node => { | |
return typeof node == 'string' ? new view_text_Text( node ) : node; | |
} ); | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/view/textproxy.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/view/textproxy | |
*/ | |
/** | |
* TextProxy is a wrapper for substring of {@link module:engine/view/text~Text}. Instance of this class is created by | |
* {@link module:engine/view/treewalker~TreeWalker} when only a part of {@link module:engine/view/text~Text} needs to be returned. | |
* | |
* `TextProxy` has an API similar to {@link module:engine/view/text~Text Text} and allows to do most of the common tasks performed | |
* on view nodes. | |
* | |
* **Note:** Some `TextProxy` instances may represent whole text node, not just a part of it. | |
* See {@link module:engine/view/textproxy~TextProxy#isPartial}. | |
* | |
* **Note:** `TextProxy` is a readonly interface. | |
* | |
* **Note:** `TextProxy` instances are created on the fly basing on the current state of parent {@link module:engine/view/text~Text}. | |
* Because of this it is highly unrecommended to store references to `TextProxy instances because they might get | |
* invalidated due to operations on Document. Also TextProxy is not a {@link module:engine/view/node~Node} so it can not be | |
* inserted as a child of {@link module:engine/view/element~Element}. | |
* | |
* `TextProxy` instances are created by {@link module:engine/view/treewalker~TreeWalker view tree walker}. You should not need to create | |
* an instance of this class by your own. | |
*/ | |
class view_textproxy_TextProxy { | |
/** | |
* Creates a text proxy. | |
* | |
* @protected | |
* @param {module:engine/view/text~Text} textNode Text node which part is represented by this text proxy. | |
* @param {Number} offsetInText Offset in {@link module:engine/view/textproxy~TextProxy#textNode text node} | |
* from which the text proxy starts. | |
* @param {Number} length Text proxy length, that is how many text node's characters, starting from `offsetInText` it represents. | |
* @constructor | |
*/ | |
constructor( textNode, offsetInText, length ) { | |
/** | |
* Reference to the {@link module:engine/view/text~Text} element which TextProxy is a substring. | |
* | |
* @readonly | |
* @member {module:engine/view/text~Text} module:engine/view/textproxy~TextProxy#textNode | |
*/ | |
this.textNode = textNode; | |
if ( offsetInText < 0 || offsetInText > textNode.data.length ) { | |
/** | |
* Given offsetInText value is incorrect. | |
* | |
* @error view-textproxy-wrong-offsetintext | |
*/ | |
throw new CKEditorError( 'view-textproxy-wrong-offsetintext: Given offsetInText value is incorrect.' ); | |
} | |
if ( length < 0 || offsetInText + length > textNode.data.length ) { | |
/** | |
* Given length value is incorrect. | |
* | |
* @error view-textproxy-wrong-length | |
*/ | |
throw new CKEditorError( 'view-textproxy-wrong-length: Given length value is incorrect.' ); | |
} | |
/** | |
* Text data represented by this text proxy. | |
* | |
* @readonly | |
* @member {String} module:engine/view/textproxy~TextProxy#data | |
*/ | |
this.data = textNode.data.substring( offsetInText, offsetInText + length ); | |
/** | |
* Offset in the `textNode` where this `TextProxy` instance starts. | |
* | |
* @readonly | |
* @member {Number} module:engine/view/textproxy~TextProxy#offsetInText | |
*/ | |
this.offsetInText = offsetInText; | |
} | |
/** | |
* Flag indicating whether `TextProxy` instance covers only part of the original {@link module:engine/view/text~Text text node} | |
* (`true`) or the whole text node (`false`). | |
* | |
* This is `false` when text proxy starts at the very beginning of {@link module:engine/view/textproxy~TextProxy#textNode textNode} | |
* ({@link module:engine/view/textproxy~TextProxy#offsetInText offsetInText} equals `0`) and text proxy sizes is equal to | |
* text node size. | |
* | |
* @readonly | |
* @type {Boolean} | |
*/ | |
get isPartial() { | |
return this.data.length !== this.textNode.data.length; | |
} | |
/** | |
* Parent of this text proxy, which is same as parent of text node represented by this text proxy. | |
* | |
* @readonly | |
* @type {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment|null} | |
*/ | |
get parent() { | |
return this.textNode.parent; | |
} | |
/** | |
* Root of this text proxy, which is same as root of text node represented by this text proxy. | |
* | |
* @readonly | |
* @type {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment} | |
*/ | |
get root() { | |
return this.textNode.root; | |
} | |
/** | |
* {@link module:engine/view/document~Document View document} that owns this text proxy, or `null` if the text proxy is inside | |
* {@link module:engine/view/documentfragment~DocumentFragment document fragment}. | |
* | |
* @readonly | |
* @type {module:engine/view/document~Document|null} | |
*/ | |
get document() { | |
return this.textNode.document; | |
} | |
/** | |
* Checks whether given view tree object is of given type. | |
* | |
* Read more in {@link module:engine/view/node~Node#is}. | |
* | |
* @param {String} type | |
* @returns {Boolean} | |
*/ | |
is( type ) { | |
return type == 'textProxy'; | |
} | |
/** | |
* Returns ancestors array of this text proxy. | |
* | |
* @param {Object} options Options object. | |
* @param {Boolean} [options.includeSelf=false] When set to `true` {#textNode} will be also included in parent's array. | |
* @param {Boolean} [options.parentFirst=false] When set to `true`, array will be sorted from text proxy parent to | |
* root element, otherwise root element will be the first item in the array. | |
* @returns {Array} Array with ancestors. | |
*/ | |
getAncestors( options = { includeSelf: false, parentFirst: false } ) { | |
const ancestors = []; | |
let parent = options.includeSelf ? this.textNode : this.parent; | |
while ( parent !== null ) { | |
ancestors[ options.parentFirst ? 'push' : 'unshift' ]( parent ); | |
parent = parent.parent; | |
} | |
return ancestors; | |
} | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/view/treewalker.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/view/treewalker | |
*/ | |
/** | |
* Position iterator class. It allows to iterate forward and backward over the document. | |
*/ | |
class view_treewalker_TreeWalker { | |
/** | |
* Creates a range iterator. All parameters are optional, but you have to specify either `boundaries` or `startPosition`. | |
* | |
* @constructor | |
* @param {Object} options Object with configuration. | |
* @param {module:engine/view/range~Range} [options.boundaries=null] Range to define boundaries of the iterator. | |
* @param {module:engine/view/position~Position} [options.startPosition] Starting position. | |
* @param {'forward'|'backward'} [options.direction='forward'] Walking direction. | |
* @param {Boolean} [options.singleCharacters=false] Flag indicating whether all characters from | |
* {@link module:engine/view/text~Text} should be returned as one {@link module:engine/view/text~Text} (`false`) ore one by one as | |
* {@link module:engine/view/textproxy~TextProxy} (`true`). | |
* @param {Boolean} [options.shallow=false] Flag indicating whether iterator should enter elements or not. If the | |
* iterator is shallow child nodes of any iterated node will not be returned along with `elementEnd` tag. | |
* @param {Boolean} [options.ignoreElementEnd=false] Flag indicating whether iterator should ignore `elementEnd` | |
* tags. If the option is true walker will not return a parent node of start position. If this option is `true` | |
* each {@link module:engine/view/element~Element} will be returned once, while if the option is `false` they might be returned | |
* twice: for `'elementStart'` and `'elementEnd'`. | |
*/ | |
constructor( options = {} ) { | |
if ( !options.boundaries && !options.startPosition ) { | |
/** | |
* Neither boundaries nor starting position have been defined. | |
* | |
* @error view-tree-walker-no-start-position | |
*/ | |
throw new CKEditorError( 'view-tree-walker-no-start-position: Neither boundaries nor starting position have been defined.' ); | |
} | |
if ( options.direction && options.direction != 'forward' && options.direction != 'backward' ) { | |
throw new CKEditorError( | |
'view-tree-walker-unknown-direction: Only `backward` and `forward` direction allowed.', | |
{ direction: options.direction } | |
); | |
} | |
/** | |
* Iterator boundaries. | |
* | |
* When the iterator is walking `'forward'` on the end of boundary or is walking `'backward'` | |
* on the start of boundary, then `{ done: true }` is returned. | |
* | |
* If boundaries are not defined they are set before first and after last child of the root node. | |
* | |
* @readonly | |
* @member {module:engine/view/range~Range} module:engine/view/treewalker~TreeWalker#boundaries | |
*/ | |
this.boundaries = options.boundaries || null; | |
/** | |
* Iterator position. If start position is not defined then position depends on {@link #direction}. If direction is | |
* `'forward'` position starts form the beginning, when direction is `'backward'` position starts from the end. | |
* | |
* @readonly | |
* @member {module:engine/view/position~Position} module:engine/view/treewalker~TreeWalker#position | |
*/ | |
if ( options.startPosition ) { | |
this.position = view_position_Position.createFromPosition( options.startPosition ); | |
} else { | |
this.position = view_position_Position.createFromPosition( options.boundaries[ options.direction == 'backward' ? 'end' : 'start' ] ); | |
} | |
/** | |
* Walking direction. Defaults `'forward'`. | |
* | |
* @readonly | |
* @member {'backward'|'forward'} module:engine/view/treewalker~TreeWalker#direction | |
*/ | |
this.direction = options.direction || 'forward'; | |
/** | |
* Flag indicating whether all characters from {@link module:engine/view/text~Text} should be returned as one | |
* {@link module:engine/view/text~Text} or one by one as {@link module:engine/view/textproxy~TextProxy}. | |
* | |
* @readonly | |
* @member {Boolean} module:engine/view/treewalker~TreeWalker#singleCharacters | |
*/ | |
this.singleCharacters = !!options.singleCharacters; | |
/** | |
* Flag indicating whether iterator should enter elements or not. If the iterator is shallow child nodes of any | |
* iterated node will not be returned along with `elementEnd` tag. | |
* | |
* @readonly | |
* @member {Boolean} module:engine/view/treewalker~TreeWalker#shallow | |
*/ | |
this.shallow = !!options.shallow; | |
/** | |
* Flag indicating whether iterator should ignore `elementEnd` tags. If set to `true`, walker will not | |
* return a parent node of the start position. Each {@link module:engine/view/element~Element} will be returned once. | |
* When set to `false` each element might be returned twice: for `'elementStart'` and `'elementEnd'`. | |
* | |
* @readonly | |
* @member {Boolean} module:engine/view/treewalker~TreeWalker#ignoreElementEnd | |
*/ | |
this.ignoreElementEnd = !!options.ignoreElementEnd; | |
/** | |
* Start boundary parent. | |
* | |
* @private | |
* @member {module:engine/view/node~Node} module:engine/view/treewalker~TreeWalker#_boundaryStartParent | |
*/ | |
this._boundaryStartParent = this.boundaries ? this.boundaries.start.parent : null; | |
/** | |
* End boundary parent. | |
* | |
* @private | |
* @member {module:engine/view/node~Node} module:engine/view/treewalker~TreeWalker#_boundaryEndParent | |
*/ | |
this._boundaryEndParent = this.boundaries ? this.boundaries.end.parent : null; | |
} | |
/** | |
* Iterator interface. | |
*/ | |
[ Symbol.iterator ]() { | |
return this; | |
} | |
/** | |
* Moves {@link #position} in the {@link #direction} skipping values as long as the callback function returns `true`. | |
* | |
* For example: | |
* | |
* walker.skip( value => value.type == 'text' ); // <p>{}foo</p> -> <p>foo[]</p> | |
* walker.skip( value => true ); // Move the position to the end: <p>{}foo</p> -> <p>foo</p>[] | |
* walker.skip( value => false ); // Do not move the position. | |
* | |
* @param {Function} skip Callback function. Gets {@link module:engine/view/treewalker~TreeWalkerValue} and should | |
* return `true` if the value should be skipped or `false` if not. | |
*/ | |
skip( skip ) { | |
let done, value, prevPosition; | |
do { | |
prevPosition = this.position; | |
( { done, value } = this.next() ); | |
} while ( !done && skip( value ) ); | |
if ( !done ) { | |
this.position = prevPosition; | |
} | |
} | |
/** | |
* Iterator interface method. | |
* Detects walking direction and makes step forward or backward. | |
* | |
* @returns {Object} Object implementing iterator interface, returning information about taken step. | |
*/ | |
next() { | |
if ( this.direction == 'forward' ) { | |
return this._next(); | |
} else { | |
return this._previous(); | |
} | |
} | |
/** | |
* Makes a step forward in view. Moves the {@link #position} to the next position and returns the encountered value. | |
* | |
* @private | |
* @returns {Object} | |
* @returns {Boolean} return.done `true` if iterator is done, `false` otherwise. | |
* @returns {module:engine/view/treewalker~TreeWalkerValue} return.value Information about taken step. | |
*/ | |
_next() { | |
let position = view_position_Position.createFromPosition( this.position ); | |
const previousPosition = this.position; | |
const parent = position.parent; | |
// We are at the end of the root. | |
if ( parent.parent === null && position.offset === parent.childCount ) { | |
return { done: true }; | |
} | |
// We reached the walker boundary. | |
if ( parent === this._boundaryEndParent && position.offset == this.boundaries.end.offset ) { | |
return { done: true }; | |
} | |
// Get node just after current position. | |
let node; | |
// Text is a specific parent because it contains string instead of child nodes. | |
if ( parent instanceof view_text_Text ) { | |
if ( position.isAtEnd ) { | |
// Prevent returning "elementEnd" for Text node. Skip that value and return the next walker step. | |
this.position = view_position_Position.createAfter( parent ); | |
return this._next(); | |
} | |
node = parent.data[ position.offset ]; | |
} else { | |
node = parent.getChild( position.offset ); | |
} | |
if ( node instanceof view_element_Element ) { | |
if ( !this.shallow ) { | |
position = new view_position_Position( node, 0 ); | |
} else { | |
position.offset++; | |
} | |
this.position = position; | |
return this._formatReturnValue( 'elementStart', node, previousPosition, position, 1 ); | |
} else if ( node instanceof view_text_Text ) { | |
if ( this.singleCharacters ) { | |
position = new view_position_Position( node, 0 ); | |
this.position = position; | |
return this._next(); | |
} else { | |
let charactersCount = node.data.length; | |
let item = node; | |
// If text stick out of walker range, we need to cut it and wrap by TextProxy. | |
if ( node == this._boundaryEndParent ) { | |
charactersCount = this.boundaries.end.offset; | |
item = new view_textproxy_TextProxy( node, 0, charactersCount ); | |
position = view_position_Position.createAfter( item ); | |
} else { | |
// If not just keep moving forward. | |
position.offset++; | |
} | |
this.position = position; | |
return this._formatReturnValue( 'text', item, previousPosition, position, charactersCount ); | |
} | |
} else if ( typeof node == 'string' ) { | |
let textLength; | |
if ( this.singleCharacters ) { | |
textLength = 1; | |
} else { | |
// Check if text stick out of walker range. | |
const endOffset = parent === this._boundaryEndParent ? this.boundaries.end.offset : parent.data.length; | |
textLength = endOffset - position.offset; | |
} | |
const textProxy = new view_textproxy_TextProxy( parent, position.offset, textLength ); | |
position.offset += textLength; | |
this.position = position; | |
return this._formatReturnValue( 'text', textProxy, previousPosition, position, textLength ); | |
} else { | |
// `node` is not set, we reached the end of current `parent`. | |
position = view_position_Position.createAfter( parent ); | |
this.position = position; | |
if ( this.ignoreElementEnd ) { | |
return this._next(); | |
} else { | |
return this._formatReturnValue( 'elementEnd', parent, previousPosition, position ); | |
} | |
} | |
} | |
/** | |
* Makes a step backward in view. Moves the {@link #position} to the previous position and returns the encountered value. | |
* | |
* @private | |
* @returns {Object} | |
* @returns {Boolean} return.done True if iterator is done. | |
* @returns {module:engine/view/treewalker~TreeWalkerValue} return.value Information about taken step. | |
*/ | |
_previous() { | |
let position = view_position_Position.createFromPosition( this.position ); | |
const previousPosition = this.position; | |
const parent = position.parent; | |
// We are at the beginning of the root. | |
if ( parent.parent === null && position.offset === 0 ) { | |
return { done: true }; | |
} | |
// We reached the walker boundary. | |
if ( parent == this._boundaryStartParent && position.offset == this.boundaries.start.offset ) { | |
return { done: true }; | |
} | |
// Get node just before current position. | |
let node; | |
// Text {@link module:engine/view/text~Text} element is a specific parent because contains string instead of child nodes. | |
if ( parent instanceof view_text_Text ) { | |
if ( position.isAtStart ) { | |
// Prevent returning "elementStart" for Text node. Skip that value and return the next walker step. | |
this.position = view_position_Position.createBefore( parent ); | |
return this._previous(); | |
} | |
node = parent.data[ position.offset - 1 ]; | |
} else { | |
node = parent.getChild( position.offset - 1 ); | |
} | |
if ( node instanceof view_element_Element ) { | |
if ( !this.shallow ) { | |
position = new view_position_Position( node, node.childCount ); | |
this.position = position; | |
if ( this.ignoreElementEnd ) { | |
return this._previous(); | |
} else { | |
return this._formatReturnValue( 'elementEnd', node, previousPosition, position ); | |
} | |
} else { | |
position.offset--; | |
this.position = position; | |
return this._formatReturnValue( 'elementStart', node, previousPosition, position, 1 ); | |
} | |
} else if ( node instanceof view_text_Text ) { | |
if ( this.singleCharacters ) { | |
position = new view_position_Position( node, node.data.length ); | |
this.position = position; | |
return this._previous(); | |
} else { | |
let charactersCount = node.data.length; | |
let item = node; | |
// If text stick out of walker range, we need to cut it and wrap by TextProxy. | |
if ( node == this._boundaryStartParent ) { | |
const offset = this.boundaries.start.offset; | |
item = new view_textproxy_TextProxy( node, offset, node.data.length - offset ); | |
charactersCount = item.data.length; | |
position = view_position_Position.createBefore( item ); | |
} else { | |
// If not just keep moving backward. | |
position.offset--; | |
} | |
this.position = position; | |
return this._formatReturnValue( 'text', item, previousPosition, position, charactersCount ); | |
} | |
} else if ( typeof node == 'string' ) { | |
let textLength; | |
if ( !this.singleCharacters ) { | |
// Check if text stick out of walker range. | |
const startOffset = parent === this._boundaryStartParent ? this.boundaries.start.offset : 0; | |
textLength = position.offset - startOffset; | |
} else { | |
textLength = 1; | |
} | |
position.offset -= textLength; | |
const textProxy = new view_textproxy_TextProxy( parent, position.offset, textLength ); | |
this.position = position; | |
return this._formatReturnValue( 'text', textProxy, previousPosition, position, textLength ); | |
} else { | |
// `node` is not set, we reached the beginning of current `parent`. | |
position = view_position_Position.createBefore( parent ); | |
this.position = position; | |
return this._formatReturnValue( 'elementStart', parent, previousPosition, position, 1 ); | |
} | |
} | |
/** | |
* Format returned data and adjust `previousPosition` and `nextPosition` if reach the bound of the {@link module:engine/view/text~Text}. | |
* | |
* @private | |
* @param {module:engine/view/treewalker~TreeWalkerValueType} type Type of step. | |
* @param {module:engine/view/item~Item} item Item between old and new position. | |
* @param {module:engine/view/position~Position} previousPosition Previous position of iterator. | |
* @param {module:engine/view/position~Position} nextPosition Next position of iterator. | |
* @param {Number} [length] Length of the item. | |
* @returns {module:engine/view/treewalker~TreeWalkerValue} | |
*/ | |
_formatReturnValue( type, item, previousPosition, nextPosition, length ) { | |
// Text is a specific parent, because contains string instead of children. | |
// Walker doesn't enter to the Text except situations when walker is iterating over every single character, | |
// or the bound starts/ends inside the Text. So when the position is at the beginning or at the end of the Text | |
// we move it just before or just after Text. | |
if ( item instanceof view_textproxy_TextProxy ) { | |
// Position is at the end of Text. | |
if ( item.offsetInText + item.data.length == item.textNode.data.length ) { | |
if ( this.direction == 'forward' && !( this.boundaries && this.boundaries.end.isEqual( this.position ) ) ) { | |
nextPosition = view_position_Position.createAfter( item.textNode ); | |
// When we change nextPosition of returned value we need also update walker current position. | |
this.position = nextPosition; | |
} else { | |
previousPosition = view_position_Position.createAfter( item.textNode ); | |
} | |
} | |
// Position is at the begining ot the text. | |
if ( item.offsetInText === 0 ) { | |
if ( this.direction == 'backward' && !( this.boundaries && this.boundaries.start.isEqual( this.position ) ) ) { | |
nextPosition = view_position_Position.createBefore( item.textNode ); | |
// When we change nextPosition of returned value we need also update walker current position. | |
this.position = nextPosition; | |
} else { | |
previousPosition = view_position_Position.createBefore( item.textNode ); | |
} | |
} | |
} | |
return { | |
done: false, | |
value: { | |
type, | |
item, | |
previousPosition, | |
nextPosition, | |
length | |
} | |
}; | |
} | |
} | |
/** | |
* Type of the step made by {@link module:engine/view/treewalker~TreeWalker}. | |
* Possible values: `'elementStart'` if walker is at the beginning of a node, `'elementEnd'` if walker is at the end | |
* of node, or `'text'` if walker traversed over single and multiple characters. | |
* For {@link module:engine/view/text~Text} `elementStart` and `elementEnd` is not returned. | |
* | |
* @typedef {String} module:engine/view/treewalker~TreeWalkerValueType | |
*/ | |
/** | |
* Object returned by {@link module:engine/view/treewalker~TreeWalker} when traversing tree view. | |
* | |
* @typedef {Object} module:engine/view/treewalker~TreeWalkerValue | |
* @property {module:engine/view/treewalker~TreeWalkerValueType} type | |
* @property {module:engine/view/item~Item} item Item between old and new positions of {@link module:engine/view/treewalker~TreeWalker}. | |
* @property {module:engine/view/position~Position} previousPosition Previous position of the iterator. | |
* * Forward iteration: For `'elementEnd'` it is the last position inside the element. For all other types it is the | |
* position before the item. Note that it is more efficient to use this position then calculate the position before | |
* the node using {@link module:engine/view/position~Position.createBefore}. | |
* * Backward iteration: For `'elementStart'` it is the first position inside the element. For all other types it is | |
* the position after item. | |
* * If the position is at the beginning or at the end of the {@link module:engine/view/text~Text} it is always moved from the | |
* inside of the Text to its parent just before or just after Text. | |
* @property {module:engine/view/position~Position} nextPosition Next position of the iterator. | |
* * Forward iteration: For `'elementStart'` it is the first position inside the element. For all other types it is | |
* the position after the item. | |
* * Backward iteration: For `'elementEnd'` it is last position inside element. For all other types it is the position | |
* before the item. | |
* * If the position is at the beginning or at the end of the {@link module:engine/view/text~Text} it is always moved from the | |
* inside of the Text to its parent just before or just after Text. | |
* @property {Number} [length] Length of the item. For `'elementStart'` it is 1. For `'text'` it is | |
* the length of the text. For `'elementEnd'` it is undefined. | |
*/ | |
/** | |
* Tree walking directions. | |
* | |
* @typedef {'forward'|'backward'} module:engine/view/treewalker~TreeWalkerDirection | |
*/ | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/view/containerelement.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/view/containerelement | |
*/ | |
/** | |
* Containers are elements which define document structure. They define boundaries for | |
* {@link module:engine/view/attributeelement~AttributeElement attributes}. They are mostly use for block elements like `<p>` or `<div>`. | |
* | |
* Editing engine does not define fixed HTML DTD. This is why the type of the {@link module:engine/view/element~Element} need to | |
* be defined by the feature developer. | |
* | |
* Creating an element you should use `ContainerElement` class or {@link module:engine/view/attributeelement~AttributeElement}. This is | |
* important to define the type of the element because of two reasons: | |
* | |
* Firstly, {@link module:engine/view/domconverter~DomConverter} needs the information what is an editable block to convert elements to | |
* DOM properly. {@link module:engine/view/domconverter~DomConverter} will ensure that `ContainerElement` is editable and it is possible | |
* to put caret inside it, even if the container is empty. | |
* | |
* Secondly, {@link module:engine/view/writer~writer view writer} uses this information. | |
* Nodes {@link module:engine/view/writer~writer.breakAttributes breaking} and {@link module:engine/view/writer~writer.mergeAttributes | |
* merging} | |
* is performed only in a bounds of a container nodes. | |
* | |
* For instance if `<p>` is an container and `<b>` is attribute: | |
* | |
* <p><b>fo^o</b></p> | |
* | |
* {@link module:engine/view/writer~writer.breakAttributes breakAttributes} will create: | |
* | |
* <p><b>fo</b><b>o</b></p> | |
* | |
* There might be a need to mark `<span>` element as a container node, for example in situation when it will be a | |
* container of an inline widget: | |
* | |
* <span color="red">foobar</span> // attribute | |
* <span data-widget>foobar</span> // container | |
* | |
* @extends module:engine/view/element~Element | |
*/ | |
class ContainerElement extends view_element_Element { | |
/** | |
* Creates a container element. | |
* | |
* @see module:engine/view/element~Element | |
*/ | |
constructor( name, attrs, children ) { | |
super( name, attrs, children ); | |
/** | |
* Returns block {@link module:engine/view/filler filler} offset or `null` if block filler is not needed. | |
* | |
* @method #getFillerOffset | |
* @returns {Number|null} Block filler offset or `null` if block filler is not needed. | |
*/ | |
this.getFillerOffset = getFillerOffset; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
is( type, name = null ) { | |
if ( !name ) { | |
return type == 'containerElement' || super.is( type ); | |
} else { | |
return ( type == 'containerElement' && name == this.name ) || super.is( type, name ); | |
} | |
} | |
} | |
// Returns block {@link module:engine/view/filler filler} offset or `null` if block filler is not needed. | |
// | |
// @returns {Number|null} Block filler offset or `null` if block filler is not needed. | |
function getFillerOffset() { | |
for ( const child of this.getChildren() ) { | |
// If there's any non-UI element – don't render the bogus. | |
if ( !child.is( 'uiElement' ) ) { | |
return null; | |
} | |
} | |
// If there are only UI elements – render the bogus at the end of the element. | |
return this.childCount; | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/view/editableelement.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/view/editableelement | |
*/ | |
const documentSymbol = Symbol( 'document' ); | |
/** | |
* Editable element which can be a {@link module:engine/view/rooteditableelement~RootEditableElement root} | |
* or nested editable area in the editor. | |
* | |
* Editable is automatically read-only when its {module:engine/view/document~Document Document} is read-only. | |
* | |
* @extends module:engine/view/containerelement~ContainerElement | |
* @mixes module:utils/observablemixin~ObservableMixin | |
*/ | |
class editableelement_EditableElement extends ContainerElement { | |
/** | |
* Creates an editable element. | |
*/ | |
constructor( name, attrs, children ) { | |
super( name, attrs, children ); | |
/** | |
* Whether the editable is in read-write or read-only mode. | |
* | |
* @observable | |
* @member {Boolean} module:engine/view/editableelement~EditableElement#isReadOnly | |
*/ | |
this.set( 'isReadOnly', false ); | |
/** | |
* Whether the editable is focused. | |
* | |
* This property updates when {@link module:engine/view/document~Document#isFocused document.isFocused} is changed and after each | |
* {@link module:engine/view/document~Document#render render} method call. | |
* | |
* @readonly | |
* @observable | |
* @member {Boolean} module:engine/view/editableelement~EditableElement#isFocused | |
*/ | |
this.set( 'isFocused', false ); | |
/** | |
* The {@link module:engine/view/document~Document} which is an owner of this root. | |
* Can only by set once. | |
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-editableelement-document-already-set` | |
* when document is already set. | |
* | |
* @member {module:engine/view/document~Document} #document | |
*/ | |
} | |
get document() { | |
return this.getCustomProperty( documentSymbol ); | |
} | |
set document( document ) { | |
if ( this.getCustomProperty( documentSymbol ) ) { | |
/** | |
* View document is already set. It can only be set once. | |
* | |
* @error view-editableelement-document-already-set | |
*/ | |
throw new CKEditorError( 'view-editableelement-document-already-set: View document is already set.' ); | |
} | |
this.setCustomProperty( documentSymbol, document ); | |
this.bind( 'isReadOnly' ).to( document ); | |
this.bind( 'isFocused' ).to( | |
document, | |
'isFocused', | |
isFocused => isFocused && document.selection.editableElement == this | |
); | |
// Update focus state before each rendering. Rendering should not change neither the selection nor the value of | |
// document.isFocused property. | |
this.listenTo( document, 'render', () => { | |
this.isFocused = document.isFocused && document.selection.editableElement == this; | |
}, { priority: 'high' } ); | |
} | |
} | |
mix( editableelement_EditableElement, observablemixin ); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/view/position.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/view/position | |
*/ | |
/** | |
* Position in the tree. Position is always located before or after a node. | |
*/ | |
class view_position_Position { | |
/** | |
* Creates a position. | |
* | |
* @param {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment} parent Position parent. | |
* @param {Number} offset Position offset. | |
*/ | |
constructor( parent, offset ) { | |
/** | |
* Position parent. | |
* | |
* @member {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment} | |
* module:engine/view/position~Position#parent | |
*/ | |
this.parent = parent; | |
/** | |
* Position offset. | |
* | |
* @member {Number} module:engine/view/position~Position#offset | |
*/ | |
this.offset = offset; | |
} | |
/** | |
* Node directly after the position. Equals `null` when there is no node after position or position is located | |
* inside text node. | |
* | |
* @readonly | |
* @type {module:engine/view/node~Node|null} | |
*/ | |
get nodeAfter() { | |
if ( this.parent.is( 'text' ) ) { | |
return null; | |
} | |
return this.parent.getChild( this.offset ) || null; | |
} | |
/** | |
* Node directly before the position. Equals `null` when there is no node before position or position is located | |
* inside text node. | |
* | |
* @readonly | |
* @type {module:engine/view/node~Node|null} | |
*/ | |
get nodeBefore() { | |
if ( this.parent.is( 'text' ) ) { | |
return null; | |
} | |
return this.parent.getChild( this.offset - 1 ) || null; | |
} | |
/** | |
* Is `true` if position is at the beginning of its {@link module:engine/view/position~Position#parent parent}, `false` otherwise. | |
* | |
* @readonly | |
* @type {Boolean} | |
*/ | |
get isAtStart() { | |
return this.offset === 0; | |
} | |
/** | |
* Is `true` if position is at the end of its {@link module:engine/view/position~Position#parent parent}, `false` otherwise. | |
* | |
* @readonly | |
* @type {Boolean} | |
*/ | |
get isAtEnd() { | |
const endOffset = this.parent.is( 'text' ) ? this.parent.data.length : this.parent.childCount; | |
return this.offset === endOffset; | |
} | |
/** | |
* Position's root, that is the root of the position's parent element. | |
* | |
* @readonly | |
* @type {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment} | |
*/ | |
get root() { | |
return this.parent.root; | |
} | |
/** | |
* {@link module:engine/view/editableelement~EditableElement EditableElement} instance that contains this position, or `null` if | |
* position is not inside an editable element. | |
* | |
* @type {module:engine/view/editableelement~EditableElement|null} | |
*/ | |
get editableElement() { | |
let editable = this.parent; | |
while ( !( editable instanceof editableelement_EditableElement ) ) { | |
if ( editable.parent ) { | |
editable = editable.parent; | |
} else { | |
return null; | |
} | |
} | |
return editable; | |
} | |
/** | |
* Returns a new instance of Position with offset incremented by `shift` value. | |
* | |
* @param {Number} shift How position offset should get changed. Accepts negative values. | |
* @returns {module:engine/view/position~Position} Shifted position. | |
*/ | |
getShiftedBy( shift ) { | |
const shifted = view_position_Position.createFromPosition( this ); | |
const offset = shifted.offset + shift; | |
shifted.offset = offset < 0 ? 0 : offset; | |
return shifted; | |
} | |
/** | |
* Gets the farthest position which matches the callback using | |
* {@link module:engine/view/treewalker~TreeWalker TreeWalker}. | |
* | |
* For example: | |
* | |
* getLastMatchingPosition( value => value.type == 'text' ); // <p>{}foo</p> -> <p>foo[]</p> | |
* getLastMatchingPosition( value => value.type == 'text', { direction: 'backward' } ); // <p>foo[]</p> -> <p>{}foo</p> | |
* getLastMatchingPosition( value => false ); // Do not move the position. | |
* | |
* @param {Function} skip Callback function. Gets {@link module:engine/view/treewalker~TreeWalkerValue} and should | |
* return `true` if the value should be skipped or `false` if not. | |
* @param {Object} options Object with configuration options. See {@link module:engine/view/treewalker~TreeWalker}. | |
* | |
* @returns {module:engine/view/position~Position} The position after the last item which matches the `skip` callback test. | |
*/ | |
getLastMatchingPosition( skip, options = {} ) { | |
options.startPosition = this; | |
const treeWalker = new view_treewalker_TreeWalker( options ); | |
treeWalker.skip( skip ); | |
return treeWalker.position; | |
} | |
/** | |
* Returns ancestors array of this position, that is this position's parent and it's ancestors. | |
* | |
* @returns {Array} Array with ancestors. | |
*/ | |
getAncestors() { | |
if ( this.parent.is( 'documentFragment' ) ) { | |
return [ this.parent ]; | |
} else { | |
return this.parent.getAncestors( { includeSelf: true } ); | |
} | |
} | |
/** | |
* Returns a {@link module:engine/view/node~Node} or {@link module:engine/view/documentfragment~DocumentFragment} | |
* which is a common ancestor of both positions. | |
* | |
* @param {module:engine/view/position~Position} position | |
* @returns {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment|null} | |
*/ | |
getCommonAncestor( position ) { | |
const ancestorsA = this.getAncestors(); | |
const ancestorsB = position.getAncestors(); | |
let i = 0; | |
while ( ancestorsA[ i ] == ancestorsB[ i ] && ancestorsA[ i ] ) { | |
i++; | |
} | |
return i === 0 ? null : ancestorsA[ i - 1 ]; | |
} | |
/** | |
* Checks whether this position equals given position. | |
* | |
* @param {module:engine/view/position~Position} otherPosition Position to compare with. | |
* @returns {Boolean} True if positions are same. | |
*/ | |
isEqual( otherPosition ) { | |
return ( this.parent == otherPosition.parent && this.offset == otherPosition.offset ); | |
} | |
/** | |
* Checks whether this position is located before given position. When method returns `false` it does not mean that | |
* this position is after give one. Two positions may be located inside separate roots and in that situation this | |
* method will still return `false`. | |
* | |
* @see module:engine/view/position~Position#isAfter | |
* @see module:engine/view/position~Position#compareWith | |
* @param {module:engine/view/position~Position} otherPosition Position to compare with. | |
* @returns {Boolean} Returns `true` if this position is before given position. | |
*/ | |
isBefore( otherPosition ) { | |
return this.compareWith( otherPosition ) == 'before'; | |
} | |
/** | |
* Checks whether this position is located after given position. When method returns `false` it does not mean that | |
* this position is before give one. Two positions may be located inside separate roots and in that situation this | |
* method will still return `false`. | |
* | |
* @see module:engine/view/position~Position#isBefore | |
* @see module:engine/view/position~Position#compareWith | |
* @param {module:engine/view/position~Position} otherPosition Position to compare with. | |
* @returns {Boolean} Returns `true` if this position is after given position. | |
*/ | |
isAfter( otherPosition ) { | |
return this.compareWith( otherPosition ) == 'after'; | |
} | |
/** | |
* Checks whether this position is before, after or in same position that other position. Two positions may be also | |
* different when they are located in separate roots. | |
* | |
* @param {module:engine/view/position~Position} otherPosition Position to compare with. | |
* @returns {module:engine/view/position~PositionRelation} | |
*/ | |
compareWith( otherPosition ) { | |
if ( this.isEqual( otherPosition ) ) { | |
return 'same'; | |
} | |
// If positions have same parent. | |
if ( this.parent === otherPosition.parent ) { | |
return this.offset - otherPosition.offset < 0 ? 'before' : 'after'; | |
} | |
// Get path from root to position's parent element. | |
const path = this.getAncestors(); | |
const otherPath = otherPosition.getAncestors(); | |
// Compare both path arrays to find common ancestor. | |
const result = compareArrays( path, otherPath ); | |
let commonAncestorIndex; | |
switch ( result ) { | |
case 0: | |
// No common ancestors found. | |
return 'different'; | |
case 'prefix': | |
commonAncestorIndex = path.length - 1; | |
break; | |
case 'extension': | |
commonAncestorIndex = otherPath.length - 1; | |
break; | |
default: | |
commonAncestorIndex = result - 1; | |
} | |
// Common ancestor of two positions. | |
const commonAncestor = path[ commonAncestorIndex ]; | |
const nextAncestor1 = path[ commonAncestorIndex + 1 ]; | |
const nextAncestor2 = otherPath[ commonAncestorIndex + 1 ]; | |
// Check if common ancestor is not one of the parents. | |
if ( commonAncestor === this.parent ) { | |
const index = this.offset - nextAncestor2.index; | |
return index <= 0 ? 'before' : 'after'; | |
} else if ( commonAncestor === otherPosition.parent ) { | |
const index = nextAncestor1.index - otherPosition.offset; | |
return index < 0 ? 'before' : 'after'; | |
} | |
const index = nextAncestor1.index - nextAncestor2.index; | |
// Compare indexes of next ancestors inside common one. | |
return index < 0 ? 'before' : 'after'; | |
} | |
/** | |
* Creates position at the given location. The location can be specified as: | |
* | |
* * a {@link module:engine/view/position~Position position}, | |
* * parent element and offset (offset defaults to `0`), | |
* * parent element and `'end'` (sets position at the end of that element), | |
* * {@link module:engine/view/item~Item view item} and `'before'` or `'after'` (sets position before or after given view item). | |
* | |
* This method is a shortcut to other constructors such as: | |
* | |
* * {@link module:engine/view/position~Position.createBefore}, | |
* * {@link module:engine/view/position~Position.createAfter}, | |
* * {@link module:engine/view/position~Position.createFromPosition}. | |
* | |
* @param {module:engine/view/item~Item|module:engine/model/position~Position} itemOrPosition | |
* @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when | |
* first parameter is a {@link module:engine/view/item~Item view item}. | |
*/ | |
static createAt( itemOrPosition, offset ) { | |
if ( itemOrPosition instanceof view_position_Position ) { | |
return this.createFromPosition( itemOrPosition ); | |
} else { | |
const node = itemOrPosition; | |
if ( offset == 'end' ) { | |
offset = node.is( 'text' ) ? node.data.length : node.childCount; | |
} else if ( offset == 'before' ) { | |
return this.createBefore( node ); | |
} else if ( offset == 'after' ) { | |
return this.createAfter( node ); | |
} else if ( !offset ) { | |
offset = 0; | |
} | |
return new view_position_Position( node, offset ); | |
} | |
} | |
/** | |
* Creates a new position after given view item. | |
* | |
* @param {module:engine/view/item~Item} item View item after which the position should be located. | |
* @returns {module:engine/view/position~Position} | |
*/ | |
static createAfter( item ) { | |
// TextProxy is not a instance of Node so we need do handle it in specific way. | |
if ( item.is( 'textProxy' ) ) { | |
return new view_position_Position( item.textNode, item.offsetInText + item.data.length ); | |
} | |
if ( !item.parent ) { | |
/** | |
* You can not make a position after a root. | |
* | |
* @error view-position-after-root | |
* @param {module:engine/view/node~Node} root | |
*/ | |
throw new CKEditorError( 'view-position-after-root: You can not make position after root.', { root: item } ); | |
} | |
return new view_position_Position( item.parent, item.index + 1 ); | |
} | |
/** | |
* Creates a new position before given view item. | |
* | |
* @param {module:engine/view/item~Item} item View item before which the position should be located. | |
* @returns {module:engine/view/position~Position} | |
*/ | |
static createBefore( item ) { | |
// TextProxy is not a instance of Node so we need do handle it in specific way. | |
if ( item.is( 'textProxy' ) ) { | |
return new view_position_Position( item.textNode, item.offsetInText ); | |
} | |
if ( !item.parent ) { | |
/** | |
* You cannot make a position before a root. | |
* | |
* @error view-position-before-root | |
* @param {module:engine/view/node~Node} root | |
*/ | |
throw new CKEditorError( 'view-position-before-root: You can not make position before root.', { root: item } ); | |
} | |
return new view_position_Position( item.parent, item.index ); | |
} | |
/** | |
* Creates and returns a new instance of `Position`, which is equal to the passed position. | |
* | |
* @param {module:engine/view/position~Position} position Position to be cloned. | |
* @returns {module:engine/view/position~Position} | |
*/ | |
static createFromPosition( position ) { | |
return new this( position.parent, position.offset ); | |
} | |
} | |
/** | |
* A flag indicating whether this position is `'before'` or `'after'` or `'same'` as given position. | |
* If positions are in different roots `'different'` flag is returned. | |
* | |
* @typedef {String} module:engine/view/position~PositionRelation | |
*/ | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/view/range.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/view/range | |
*/ | |
/** | |
* Tree view range. | |
*/ | |
class view_range_Range { | |
/** | |
* Creates a range spanning from `start` position to `end` position. | |
* | |
* **Note:** Constructor creates it's own {@link module:engine/view/position~Position} instances basing on passed values. | |
* | |
* @param {module:engine/view/position~Position} start Start position. | |
* @param {module:engine/view/position~Position} [end] End position. If not set, range will be collapsed at `start` position. | |
*/ | |
constructor( start, end = null ) { | |
/** | |
* Start position. | |
* | |
* @member {module:engine/view/position~Position} | |
*/ | |
this.start = view_position_Position.createFromPosition( start ); | |
/** | |
* End position. | |
* | |
* @member {module:engine/view/position~Position} | |
*/ | |
this.end = end ? view_position_Position.createFromPosition( end ) : view_position_Position.createFromPosition( start ); | |
} | |
/** | |
* Returns an iterator that iterates over all {@link module:engine/view/item~Item view items} that are in this range and returns | |
* them together with additional information like length or {@link module:engine/view/position~Position positions}, | |
* grouped as {@link module:engine/view/treewalker~TreeWalkerValue}. | |
* | |
* This iterator uses {@link module:engine/view/treewalker~TreeWalker TreeWalker} with `boundaries` set to this range and | |
* `ignoreElementEnd` option | |
* set to `true`. | |
* | |
* @returns {Iterable.<module:engine/view/treewalker~TreeWalkerValue>} | |
*/ | |
* [ Symbol.iterator ]() { | |
yield* new view_treewalker_TreeWalker( { boundaries: this, ignoreElementEnd: true } ); | |
} | |
/** | |
* Returns whether the range is collapsed, that is it start and end positions are equal. | |
* | |
* @type {Boolean} | |
*/ | |
get isCollapsed() { | |
return this.start.isEqual( this.end ); | |
} | |
/** | |
* Returns whether this range is flat, that is if {@link module:engine/view/range~Range#start start} position and | |
* {@link module:engine/view/range~Range#end end} position are in the same {@link module:engine/view/position~Position#parent parent}. | |
* | |
* @type {Boolean} | |
*/ | |
get isFlat() { | |
return this.start.parent === this.end.parent; | |
} | |
/** | |
* Range root element. | |
* | |
* @type {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment} | |
*/ | |
get root() { | |
return this.start.root; | |
} | |
/** | |
* Creates a maximal range that has the same content as this range but is expanded in both ways (at the beginning | |
* and at the end). | |
* | |
* For example: | |
* | |
* <p>Foo</p><p><b>{Bar}</b></p> -> <p>Foo</p>[<p><b>Bar</b>]</p> | |
* <p><b>foo</b>{bar}<span></span></p> -> <p><b>foo[</b>bar<span></span>]</p> | |
* | |
* Note that in the sample above: | |
* - `<p>` have type of {@link module:engine/view/containerelement~ContainerElement}, | |
* - `<b>` have type of {@link module:engine/view/attributeelement~AttributeElement}, | |
* - `<span>` have type of {@link module:engine/view/uielement~UIElement}. | |
* | |
* @returns {module:engine/view/range~Range} Enlarged range. | |
*/ | |
getEnlarged() { | |
let start = this.start.getLastMatchingPosition( enlargeTrimSkip, { direction: 'backward' } ); | |
let end = this.end.getLastMatchingPosition( enlargeTrimSkip ); | |
// Fix positions, in case if they are in Text node. | |
if ( start.parent.is( 'text' ) && start.isAtStart ) { | |
start = view_position_Position.createBefore( start.parent ); | |
} | |
if ( end.parent.is( 'text' ) && end.isAtEnd ) { | |
end = view_position_Position.createAfter( end.parent ); | |
} | |
return new view_range_Range( start, end ); | |
} | |
/** | |
* Creates a minimum range that has the same content as this range but is trimmed in both ways (at the beginning | |
* and at the end). | |
* | |
* For example: | |
* | |
* <p>Foo</p>[<p><b>Bar</b>]</p> -> <p>Foo</p><p><b>{Bar}</b></p> | |
* <p><b>foo[</b>bar<span></span>]</p> -> <p><b>foo</b>{bar}<span></span></p> | |
* | |
* Note that in the sample above: | |
* - `<p>` have type of {@link module:engine/view/containerelement~ContainerElement}, | |
* - `<b>` have type of {@link module:engine/view/attributeelement~AttributeElement}, | |
* - `<span>` have type of {@link module:engine/view/uielement~UIElement}. | |
* | |
* @returns {module:engine/view/range~Range} Shrink range. | |
*/ | |
getTrimmed() { | |
let start = this.start.getLastMatchingPosition( enlargeTrimSkip ); | |
if ( start.isAfter( this.end ) || start.isEqual( this.end ) ) { | |
return new view_range_Range( start, start ); | |
} | |
let end = this.end.getLastMatchingPosition( enlargeTrimSkip, { direction: 'backward' } ); | |
const nodeAfterStart = start.nodeAfter; | |
const nodeBeforeEnd = end.nodeBefore; | |
// Because TreeWalker prefers positions next to text node, we need to move them manually into these text nodes. | |
if ( nodeAfterStart && nodeAfterStart.is( 'text' ) ) { | |
start = new view_position_Position( nodeAfterStart, 0 ); | |
} | |
if ( nodeBeforeEnd && nodeBeforeEnd.is( 'text' ) ) { | |
end = new view_position_Position( nodeBeforeEnd, nodeBeforeEnd.data.length ); | |
} | |
return new view_range_Range( start, end ); | |
} | |
/** | |
* Two ranges are equal if their start and end positions are equal. | |
* | |
* @param {module:engine/view/range~Range} otherRange Range to compare with. | |
* @returns {Boolean} `true` if ranges are equal, `false` otherwise | |
*/ | |
isEqual( otherRange ) { | |
return this == otherRange || ( this.start.isEqual( otherRange.start ) && this.end.isEqual( otherRange.end ) ); | |
} | |
/** | |
* Checks whether this range contains given {@link module:engine/view/position~Position position}. | |
* | |
* @param {module:engine/view/position~Position} position Position to check. | |
* @returns {Boolean} `true` if given {@link module:engine/view/position~Position position} is contained in this range, | |
* `false` otherwise. | |
*/ | |
containsPosition( position ) { | |
return position.isAfter( this.start ) && position.isBefore( this.end ); | |
} | |
/** | |
* Checks whether this range contains given {@link module:engine/view/range~Range range}. | |
* | |
* @param {module:engine/view/range~Range} otherRange Range to check. | |
* @param {Boolean} [loose=false] Whether the check is loose or strict. If the check is strict (`false`), compared range cannot | |
* start or end at the same position as this range boundaries. If the check is loose (`true`), compared range can start, end or | |
* even be equal to this range. Note that collapsed ranges are always compared in strict mode. | |
* @returns {Boolean} `true` if given {@link module:engine/view/range~Range range} boundaries are contained by this range, `false` | |
* otherwise. | |
*/ | |
containsRange( otherRange, loose = false ) { | |
if ( otherRange.isCollapsed ) { | |
loose = false; | |
} | |
const containsStart = this.containsPosition( otherRange.start ) || ( loose && this.start.isEqual( otherRange.start ) ); | |
const containsEnd = this.containsPosition( otherRange.end ) || ( loose && this.end.isEqual( otherRange.end ) ); | |
return containsStart && containsEnd; | |
} | |
/** | |
* Computes which part(s) of this {@link module:engine/view/range~Range range} is not a part of given | |
* {@link module:engine/view/range~Range range}. | |
* Returned array contains zero, one or two {@link module:engine/view/range~Range ranges}. | |
* | |
* Examples: | |
* | |
* let foo = new Text( 'foo' ); | |
* let img = new ContainerElement( 'img' ); | |
* let bar = new Text( 'bar' ); | |
* let p = new ContainerElement( 'p', null, [ foo, img, bar ] ); | |
* | |
* let range = new Range( new Position( foo, 2 ), new Position( bar, 1 ); // "o", img, "b" are in range. | |
* let otherRange = new Range( new Position( foo, 1 ), new Position( bar, 2 ); "oo", img, "ba" are in range. | |
* let transformed = range.getDifference( otherRange ); | |
* // transformed array has no ranges because `otherRange` contains `range` | |
* | |
* otherRange = new Range( new Position( foo, 1 ), new Position( p, 2 ); // "oo", img are in range. | |
* transformed = range.getDifference( otherRange ); | |
* // transformed array has one range: from ( p, 2 ) to ( bar, 1 ) | |
* | |
* otherRange = new Range( new Position( p, 1 ), new Position( p, 2 ) ); // img is in range. | |
* transformed = range.getDifference( otherRange ); | |
* // transformed array has two ranges: from ( foo, 1 ) to ( p, 1 ) and from ( p, 2 ) to ( bar, 1 ) | |
* | |
* @param {module:engine/view/range~Range} otherRange Range to differentiate against. | |
* @returns {Array.<module:engine/view/range~Range>} The difference between ranges. | |
*/ | |
getDifference( otherRange ) { | |
const ranges = []; | |
if ( this.isIntersecting( otherRange ) ) { | |
// Ranges intersect. | |
if ( this.containsPosition( otherRange.start ) ) { | |
// Given range start is inside this range. This means that we have to | |
// add shrunken range - from the start to the middle of this range. | |
ranges.push( new view_range_Range( this.start, otherRange.start ) ); | |
} | |
if ( this.containsPosition( otherRange.end ) ) { | |
// Given range end is inside this range. This means that we have to | |
// add shrunken range - from the middle of this range to the end. | |
ranges.push( new view_range_Range( otherRange.end, this.end ) ); | |
} | |
} else { | |
// Ranges do not intersect, return the original range. | |
ranges.push( view_range_Range.createFromRange( this ) ); | |
} | |
return ranges; | |
} | |
/** | |
* Returns an intersection of this {@link module:engine/view/range~Range range} and given {@link module:engine/view/range~Range range}. | |
* Intersection is a common part of both of those ranges. If ranges has no common part, returns `null`. | |
* | |
* Examples: | |
* | |
* let foo = new Text( 'foo' ); | |
* let img = new ContainerElement( 'img' ); | |
* let bar = new Text( 'bar' ); | |
* let p = new ContainerElement( 'p', null, [ foo, img, bar ] ); | |
* | |
* let range = new Range( new Position( foo, 2 ), new Position( bar, 1 ); // "o", img, "b" are in range. | |
* let otherRange = new Range( new Position( foo, 1 ), new Position( p, 2 ); // "oo", img are in range. | |
* let transformed = range.getIntersection( otherRange ); // range from ( foo, 1 ) to ( p, 2 ). | |
* | |
* otherRange = new Range( new Position( bar, 1 ), new Position( bar, 3 ); "ar" is in range. | |
* transformed = range.getIntersection( otherRange ); // null - no common part. | |
* | |
* @param {module:engine/view/range~Range} otherRange Range to check for intersection. | |
* @returns {module:engine/view/range~Range|null} A common part of given ranges or `null` if ranges have no common part. | |
*/ | |
getIntersection( otherRange ) { | |
if ( this.isIntersecting( otherRange ) ) { | |
// Ranges intersect, so a common range will be returned. | |
// At most, it will be same as this range. | |
let commonRangeStart = this.start; | |
let commonRangeEnd = this.end; | |
if ( this.containsPosition( otherRange.start ) ) { | |
// Given range start is inside this range. This means thaNt we have to | |
// shrink common range to the given range start. | |
commonRangeStart = otherRange.start; | |
} | |
if ( this.containsPosition( otherRange.end ) ) { | |
// Given range end is inside this range. This means that we have to | |
// shrink common range to the given range end. | |
commonRangeEnd = otherRange.end; | |
} | |
return new view_range_Range( commonRangeStart, commonRangeEnd ); | |
} | |
// Ranges do not intersect, so they do not have common part. | |
return null; | |
} | |
/** | |
* Creates a {@link module:engine/view/treewalker~TreeWalker TreeWalker} instance with this range as a boundary. | |
* | |
* @param {Object} options Object with configuration options. See {@link module:engine/view/treewalker~TreeWalker}. | |
* @param {module:engine/view/position~Position} [options.startPosition] | |
* @param {Boolean} [options.singleCharacters=false] | |
* @param {Boolean} [options.shallow=false] | |
* @param {Boolean} [options.ignoreElementEnd=false] | |
*/ | |
getWalker( options = {} ) { | |
options.boundaries = this; | |
return new view_treewalker_TreeWalker( options ); | |
} | |
/** | |
* Returns a {@link module:engine/view/node~Node} or {@link module:engine/view/documentfragment~DocumentFragment} | |
* which is a common ancestor of range's both ends (in which the entire range is contained). | |
* | |
* @returns {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment|null} | |
*/ | |
getCommonAncestor() { | |
return this.start.getCommonAncestor( this.end ); | |
} | |
/** | |
* Returns an iterator that iterates over all {@link module:engine/view/item~Item view items} that are in this range and returns | |
* them. | |
* | |
* This method uses {@link module:engine/view/treewalker~TreeWalker} with `boundaries` set to this range and `ignoreElementEnd` option | |
* set to `true`. However it returns only {@link module:engine/view/item~Item items}, | |
* not {@link module:engine/view/treewalker~TreeWalkerValue}. | |
* | |
* You may specify additional options for the tree walker. See {@link module:engine/view/treewalker~TreeWalker} for | |
* a full list of available options. | |
* | |
* @param {Object} options Object with configuration options. See {@link module:engine/view/treewalker~TreeWalker}. | |
* @returns {Iterable.<module:engine/view/item~Item>} | |
*/ | |
* getItems( options = {} ) { | |
options.boundaries = this; | |
options.ignoreElementEnd = true; | |
const treeWalker = new view_treewalker_TreeWalker( options ); | |
for ( const value of treeWalker ) { | |
yield value.item; | |
} | |
} | |
/** | |
* Returns an iterator that iterates over all {@link module:engine/view/position~Position positions} that are boundaries or | |
* contained in this range. | |
* | |
* This method uses {@link module:engine/view/treewalker~TreeWalker} with `boundaries` set to this range. However it returns only | |
* {@link module:engine/view/position~Position positions}, not {@link module:engine/view/treewalker~TreeWalkerValue}. | |
* | |
* You may specify additional options for the tree walker. See {@link module:engine/view/treewalker~TreeWalker} for | |
* a full list of available options. | |
* | |
* @param {Object} options Object with configuration options. See {@link module:engine/view/treewalker~TreeWalker}. | |
* @returns {Iterable.<module:engine/view/position~Position>} | |
*/ | |
* getPositions( options = {} ) { | |
options.boundaries = this; | |
const treeWalker = new view_treewalker_TreeWalker( options ); | |
yield treeWalker.position; | |
for ( const value of treeWalker ) { | |
yield value.nextPosition; | |
} | |
} | |
/** | |
* Checks and returns whether this range intersects with given range. | |
* | |
* @param {module:engine/view/range~Range} otherRange Range to compare with. | |
* @returns {Boolean} True if ranges intersect. | |
*/ | |
isIntersecting( otherRange ) { | |
return this.start.isBefore( otherRange.end ) && this.end.isAfter( otherRange.start ); | |
} | |
/** | |
* Creates a range from given parents and offsets. | |
* | |
* @param {module:engine/view/element~Element} startElement Start position parent element. | |
* @param {Number} startOffset Start position offset. | |
* @param {module:engine/view/element~Element} endElement End position parent element. | |
* @param {Number} endOffset End position offset. | |
* @returns {module:engine/view/range~Range} Created range. | |
*/ | |
static createFromParentsAndOffsets( startElement, startOffset, endElement, endOffset ) { | |
return new this( | |
new view_position_Position( startElement, startOffset ), | |
new view_position_Position( endElement, endOffset ) | |
); | |
} | |
/** | |
* Creates and returns a new instance of Range which is equal to passed range. | |
* | |
* @param {module:engine/view/range~Range} range Range to clone. | |
* @returns {module:engine/view/range~Range} | |
*/ | |
static createFromRange( range ) { | |
return new this( range.start, range.end ); | |
} | |
/** | |
* Creates a new range, spreading from specified {@link module:engine/view/position~Position position} to a position moved by | |
* given `shift`. If `shift` is a negative value, shifted position is treated as the beginning of the range. | |
* | |
* @param {module:engine/view/position~Position} position Beginning of the range. | |
* @param {Number} shift How long the range should be. | |
* @returns {module:engine/view/range~Range} | |
*/ | |
static createFromPositionAndShift( position, shift ) { | |
const start = position; | |
const end = position.getShiftedBy( shift ); | |
return shift > 0 ? new this( start, end ) : new this( end, start ); | |
} | |
/** | |
* Creates a range inside an {@link module:engine/view/element~Element element} which starts before the first child of | |
* that element and ends after the last child of that element. | |
* | |
* @param {module:engine/view/element~Element} element Element which is a parent for the range. | |
* @returns {module:engine/view/range~Range} | |
*/ | |
static createIn( element ) { | |
return this.createFromParentsAndOffsets( element, 0, element, element.childCount ); | |
} | |
/** | |
* Creates a range that starts before given {@link module:engine/view/item~Item view item} and ends after it. | |
* | |
* @param {module:engine/view/item~Item} item | |
* @returns {module:engine/view/range~Range} | |
*/ | |
static createOn( item ) { | |
return this.createFromPositionAndShift( view_position_Position.createBefore( item ), 1 ); | |
} | |
/** | |
* Creates a collapsed range at given {@link module:engine/view/position~Position position} | |
* or on the given {@link module:engine/view/item~Item item}. | |
* | |
* @param {module:engine/view/item~Item|module:engine/view/position~Position} itemOrPosition | |
* @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when | |
* first parameter is a {@link module:engine/view/item~Item view item}. | |
*/ | |
static createCollapsedAt( itemOrPosition, offset ) { | |
const start = view_position_Position.createAt( itemOrPosition, offset ); | |
const end = view_position_Position.createFromPosition( start ); | |
return new view_range_Range( start, end ); | |
} | |
} | |
// Function used by getEnlarged and getTrimmed methods. | |
function enlargeTrimSkip( value ) { | |
if ( value.item.is( 'attributeElement' ) || value.item.is( 'uiElement' ) ) { | |
return true; | |
} | |
return false; | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/conversion/mapper.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/conversion/mapper | |
*/ | |
/** | |
* Maps elements and positions between {@link module:engine/view/document~Document view} and {@link module:engine/model/model model}. | |
* | |
* Mapper use bound elements to find corresponding elements and positions, so, to get proper results, | |
* all model elements should be {@link module:engine/conversion/mapper~Mapper#bindElements bound}. | |
* | |
* To map complex model to/from view relations, you may provide custom callbacks for | |
* {@link module:engine/conversion/mapper~Mapper#event:modelToViewPosition modelToViewPosition event} and | |
* {@link module:engine/conversion/mapper~Mapper#event:viewToModelPosition viewToModelPosition event} that are fired whenever | |
* a position mapping request occurs. | |
* Those events are fired by {@link module:engine/conversion/mapper~Mapper#toViewPosition toViewPosition} | |
* and {@link module:engine/conversion/mapper~Mapper#toModelPosition toModelPosition} methods. `Mapper` adds it's own default callbacks | |
* with `'lowest'` priority. To override default `Mapper` mapping, add custom callback with higher priority and | |
* stop the event. | |
*/ | |
class mapper_Mapper { | |
/** | |
* Creates an instance of the mapper. | |
*/ | |
constructor() { | |
/** | |
* Model element to view element mapping. | |
* | |
* @private | |
* @member {WeakMap} | |
*/ | |
this._modelToViewMapping = new WeakMap(); | |
/** | |
* View element to model element mapping. | |
* | |
* @private | |
* @member {WeakMap} | |
*/ | |
this._viewToModelMapping = new WeakMap(); | |
/** | |
* A map containing callbacks between view element names and functions evaluating length of view elements | |
* in model. | |
* | |
* @private | |
* @member {Map} | |
*/ | |
this._viewToModelLengthCallbacks = new Map(); | |
// Default mapper algorithm for mapping model position to view position. | |
this.on( 'modelToViewPosition', ( evt, data ) => { | |
if ( data.viewPosition ) { | |
return; | |
} | |
const viewContainer = this._modelToViewMapping.get( data.modelPosition.parent ); | |
data.viewPosition = this._findPositionIn( viewContainer, data.modelPosition.offset ); | |
}, { priority: 'low' } ); | |
// Default mapper algorithm for mapping view position to model position. | |
this.on( 'viewToModelPosition', ( evt, data ) => { | |
if ( data.modelPosition ) { | |
return; | |
} | |
let viewBlock = data.viewPosition.parent; | |
let modelParent = this._viewToModelMapping.get( viewBlock ); | |
while ( !modelParent ) { | |
viewBlock = viewBlock.parent; | |
modelParent = this._viewToModelMapping.get( viewBlock ); | |
} | |
const modelOffset = this._toModelOffset( data.viewPosition.parent, data.viewPosition.offset, viewBlock ); | |
data.modelPosition = position_Position.createFromParentAndOffset( modelParent, modelOffset ); | |
}, { priority: 'low' } ); | |
} | |
/** | |
* Marks model and view elements as corresponding. Corresponding elements can be retrieved by using | |
* the {@link module:engine/conversion/mapper~Mapper#toModelElement toModelElement} and | |
* {@link module:engine/conversion/mapper~Mapper#toViewElement toViewElement} methods. | |
* The information that elements are bound is also used to translate positions. | |
* | |
* @param {module:engine/model/element~Element} modelElement Model element. | |
* @param {module:engine/view/element~Element} viewElement View element. | |
*/ | |
bindElements( modelElement, viewElement ) { | |
this._modelToViewMapping.set( modelElement, viewElement ); | |
this._viewToModelMapping.set( viewElement, modelElement ); | |
} | |
/** | |
* Unbinds given {@link module:engine/view/element~Element view element} from the map. | |
* | |
* @param {module:engine/view/element~Element} viewElement View element to unbind. | |
*/ | |
unbindViewElement( viewElement ) { | |
const modelElement = this.toModelElement( viewElement ); | |
this._unbindElements( modelElement, viewElement ); | |
} | |
/** | |
* Unbinds given {@link module:engine/model/element~Element model element} from the map. | |
* | |
* @param {module:engine/model/element~Element} modelElement Model element to unbind. | |
*/ | |
unbindModelElement( modelElement ) { | |
const viewElement = this.toViewElement( modelElement ); | |
this._unbindElements( modelElement, viewElement ); | |
} | |
/** | |
* Removes all model to view and view to model bindings. | |
*/ | |
clearBindings() { | |
this._modelToViewMapping = new WeakMap(); | |
this._viewToModelMapping = new WeakMap(); | |
} | |
/** | |
* Gets the corresponding model element. | |
* | |
* **Note:** {@link module:engine/view/uielement~UIElement} does not have corresponding element in model. | |
* | |
* @param {module:engine/view/element~Element} viewElement View element. | |
* @returns {module:engine/model/element~Element|undefined} Corresponding model element or `undefined` if not found. | |
*/ | |
toModelElement( viewElement ) { | |
return this._viewToModelMapping.get( viewElement ); | |
} | |
/** | |
* Gets the corresponding view element. | |
* | |
* @param {module:engine/model/element~Element} modelElement Model element. | |
* @returns {module:engine/view/element~Element|undefined} Corresponding view element or `undefined` if not found. | |
*/ | |
toViewElement( modelElement ) { | |
return this._modelToViewMapping.get( modelElement ); | |
} | |
/** | |
* Gets the corresponding model range. | |
* | |
* @param {module:engine/view/range~Range} viewRange View range. | |
* @returns {module:engine/model/range~Range} Corresponding model range. | |
*/ | |
toModelRange( viewRange ) { | |
return new range_Range( this.toModelPosition( viewRange.start ), this.toModelPosition( viewRange.end ) ); | |
} | |
/** | |
* Gets the corresponding view range. | |
* | |
* @param {module:engine/model/range~Range} modelRange Model range. | |
* @returns {module:engine/view/range~Range} Corresponding view range. | |
*/ | |
toViewRange( modelRange ) { | |
return new view_range_Range( this.toViewPosition( modelRange.start ), this.toViewPosition( modelRange.end ) ); | |
} | |
/** | |
* Gets the corresponding model position. | |
* | |
* @fires viewToModelPosition | |
* @param {module:engine/view/position~Position} viewPosition View position. | |
* @returns {module:engine/model/position~Position} Corresponding model position. | |
*/ | |
toModelPosition( viewPosition ) { | |
const data = { | |
viewPosition, | |
mapper: this | |
}; | |
this.fire( 'viewToModelPosition', data ); | |
return data.modelPosition; | |
} | |
/** | |
* Gets the corresponding view position. | |
* | |
* @fires modelToViewPosition | |
* @param {module:engine/model/position~Position} modelPosition Model position. | |
* @returns {module:engine/view/position~Position} Corresponding view position. | |
*/ | |
toViewPosition( modelPosition ) { | |
const data = { | |
modelPosition, | |
mapper: this | |
}; | |
this.fire( 'modelToViewPosition', data ); | |
return data.viewPosition; | |
} | |
/** | |
* Registers a callback that evaluates the length in the model of a view element with given name. | |
* | |
* The callback is fired with one argument, which is a view element instance. The callback is expected to return | |
* a number representing the length of view element in model. | |
* | |
* // List item in view may contain nested list, which have other list items. In model though, | |
* // the lists are represented by flat structure. Because of those differences, length of list view element | |
* // may be greater than one. In the callback it's checked how many nested list items are in evaluated list item. | |
* | |
* function getViewListItemLength( element ) { | |
* let length = 1; | |
* | |
* for ( let child of element.getChildren() ) { | |
* if ( child.name == 'ul' || child.name == 'ol' ) { | |
* for ( let item of child.getChildren() ) { | |
* length += getViewListItemLength( item ); | |
* } | |
* } | |
* } | |
* | |
* return length; | |
* } | |
* | |
* mapper.registerViewToModelLength( 'li', getViewListItemLength ); | |
* | |
* @param {String} viewElementName Name of view element for which callback is registered. | |
* @param {Function} lengthCallback Function return a length of view element instance in model. | |
*/ | |
registerViewToModelLength( viewElementName, lengthCallback ) { | |
this._viewToModelLengthCallbacks.set( viewElementName, lengthCallback ); | |
} | |
/** | |
* Calculates model offset based on the view position and the block element. | |
* | |
* Example: | |
* | |
* <p>foo<b>ba|r</b></p> // _toModelOffset( b, 2, p ) -> 5 | |
* | |
* Is a sum of: | |
* | |
* <p>foo|<b>bar</b></p> // _toModelOffset( p, 3, p ) -> 3 | |
* <p>foo<b>ba|r</b></p> // _toModelOffset( b, 2, b ) -> 2 | |
* | |
* @private | |
* @param {module:engine/view/element~Element} viewParent Position parent. | |
* @param {Number} viewOffset Position offset. | |
* @param {module:engine/view/element~Element} viewBlock Block used as a base to calculate offset. | |
* @returns {Number} Offset in the model. | |
*/ | |
_toModelOffset( viewParent, viewOffset, viewBlock ) { | |
if ( viewBlock != viewParent ) { | |
// See example. | |
const offsetToParentStart = this._toModelOffset( viewParent.parent, viewParent.index, viewBlock ); | |
const offsetInParent = this._toModelOffset( viewParent, viewOffset, viewParent ); | |
return offsetToParentStart + offsetInParent; | |
} | |
// viewBlock == viewParent, so we need to calculate the offset in the parent element. | |
// If the position is a text it is simple ("ba|r" -> 2). | |
if ( viewParent.is( 'text' ) ) { | |
return viewOffset; | |
} | |
// If the position is in an element we need to sum lengths of siblings ( <b> bar </b> foo | -> 3 + 3 = 6 ). | |
let modelOffset = 0; | |
for ( let i = 0; i < viewOffset; i++ ) { | |
modelOffset += this.getModelLength( viewParent.getChild( i ) ); | |
} | |
return modelOffset; | |
} | |
/** | |
* Removes binding between given elements. | |
* | |
* @private | |
* @param {module:engine/model/element~Element} modelElement Model element to unbind. | |
* @param {module:engine/view/element~Element} viewElement View element to unbind. | |
*/ | |
_unbindElements( modelElement, viewElement ) { | |
this._viewToModelMapping.delete( viewElement ); | |
this._modelToViewMapping.delete( modelElement ); | |
} | |
/** | |
* Gets the length of the view element in the model. | |
* | |
* The length is calculated as follows: | |
* * if {@link #registerViewToModelLength length mapping callback} is provided for given `viewNode` it is used to | |
* evaluate model length (`viewNode` is used as first and only parameter passed to the callback), | |
* * length of a {@link module:engine/view/text~Text text node} is equal to the length of it's | |
* {@link module:engine/view/text~Text#data data}, | |
* * length of a {@link module:engine/view/uielement~UIElement ui element} is equal to 0, | |
* * length of a mapped {@link module:engine/view/element~Element element} is equal to 1, | |
* * length of a not-mapped {@link module:engine/view/element~Element element} is equal to the length of it's children. | |
* | |
* Examples: | |
* | |
* foo -> 3 // Text length is equal to it's data length. | |
* <p>foo</p> -> 1 // Length of an element which is mapped is by default equal to 1. | |
* <b>foo</b> -> 3 // Length of an element which is not mapped is a length of its children. | |
* <div><p>x</p><p>y</p></div> -> 2 // Assuming that <div> is not mapped and <p> are mapped. | |
* | |
* @param {module:engine/view/element~Element} viewNode View node. | |
* @returns {Number} Length of the node in the tree model. | |
*/ | |
getModelLength( viewNode ) { | |
if ( this._viewToModelLengthCallbacks.get( viewNode.name ) ) { | |
const callback = this._viewToModelLengthCallbacks.get( viewNode.name ); | |
return callback( viewNode ); | |
} else if ( this._viewToModelMapping.has( viewNode ) ) { | |
return 1; | |
} else if ( viewNode.is( 'text' ) ) { | |
return viewNode.data.length; | |
} else if ( viewNode.is( 'uiElement' ) ) { | |
return 0; | |
} else { | |
let len = 0; | |
for ( const child of viewNode.getChildren() ) { | |
len += this.getModelLength( child ); | |
} | |
return len; | |
} | |
} | |
/** | |
* Finds the position in the view node (or its children) with the expected model offset. | |
* | |
* Example: | |
* | |
* <p>fo<b>bar</b>bom</p> -> expected offset: 4 | |
* | |
* _findPositionIn( p, 4 ): | |
* <p>|fo<b>bar</b>bom</p> -> expected offset: 4, actual offset: 0 | |
* <p>fo|<b>bar</b>bom</p> -> expected offset: 4, actual offset: 2 | |
* <p>fo<b>bar</b>|bom</p> -> expected offset: 4, actual offset: 5 -> we are too far | |
* | |
* _findPositionIn( b, 4 - ( 5 - 3 ) ): | |
* <p>fo<b>|bar</b>bom</p> -> expected offset: 2, actual offset: 0 | |
* <p>fo<b>bar|</b>bom</p> -> expected offset: 2, actual offset: 3 -> we are too far | |
* | |
* _findPositionIn( bar, 2 - ( 3 - 3 ) ): | |
* We are in the text node so we can simple find the offset. | |
* <p>fo<b>ba|r</b>bom</p> -> expected offset: 2, actual offset: 2 -> position found | |
* | |
* @private | |
* @param {module:engine/view/element~Element} viewParent Tree view element in which we are looking for the position. | |
* @param {Number} expectedOffset Expected offset. | |
* @returns {module:engine/view/position~Position} Found position. | |
*/ | |
_findPositionIn( viewParent, expectedOffset ) { | |
// Last scanned view node. | |
let viewNode; | |
// Length of the last scanned view node. | |
let lastLength = 0; | |
let modelOffset = 0; | |
let viewOffset = 0; | |
// In the text node it is simple: offset in the model equals offset in the text. | |
if ( viewParent.is( 'text' ) ) { | |
return new view_position_Position( viewParent, expectedOffset ); | |
} | |
// In other cases we add lengths of child nodes to find the proper offset. | |
// If it is smaller we add the length. | |
while ( modelOffset < expectedOffset ) { | |
viewNode = viewParent.getChild( viewOffset ); | |
lastLength = this.getModelLength( viewNode ); | |
modelOffset += lastLength; | |
viewOffset++; | |
} | |
// If it equals we found the position. | |
if ( modelOffset == expectedOffset ) { | |
return this._moveViewPositionToTextNode( new view_position_Position( viewParent, viewOffset ) ); | |
} | |
// If it is higher we need to enter last child. | |
else { | |
// ( modelOffset - lastLength ) is the offset to the child we enter, | |
// so we subtract it from the expected offset to fine the offset in the child. | |
return this._findPositionIn( viewNode, expectedOffset - ( modelOffset - lastLength ) ); | |
} | |
} | |
/** | |
* Because we prefer positions in text nodes over positions next to text node moves view position to the text node | |
* if it was next to it. | |
* | |
* <p>[]<b>foo</b></p> -> <p>[]<b>foo</b></p> // do not touch if position is not directly next to text | |
* <p>foo[]<b>foo</b></p> -> <p>foo{}<b>foo</b></p> // move to text node | |
* <p><b>[]foo</b></p> -> <p><b>{}foo</b></p> // move to text node | |
* | |
* @private | |
* @param {module:engine/view/position~Position} viewPosition Position potentially next to text node. | |
* @returns {module:engine/view/position~Position} Position in text node if possible. | |
*/ | |
_moveViewPositionToTextNode( viewPosition ) { | |
// If the position is just after text node, put it at the end of that text node. | |
// If the position is just before text node, put it at the beginning of that text node. | |
const nodeBefore = viewPosition.nodeBefore; | |
const nodeAfter = viewPosition.nodeAfter; | |
if ( nodeBefore instanceof view_text_Text ) { | |
return new view_position_Position( nodeBefore, nodeBefore.data.length ); | |
} else if ( nodeAfter instanceof view_text_Text ) { | |
return new view_position_Position( nodeAfter, 0 ); | |
} | |
// Otherwise, just return the given position. | |
return viewPosition; | |
} | |
/** | |
* Fired for each model-to-view position mapping request. The purpose of this event is to enable custom model-to-view position | |
* mapping. Callbacks added to this event take {@link module:engine/model/position~Position model position} and are expected to | |
* calculate {@link module:engine/view/position~Position view position}. Calculated view position should be added as `viewPosition` | |
* value in `data` object that is passed as one of parameters to the event callback. | |
* | |
* // Assume that "captionedImage" model element is converted to <img> and following <span> elements in view, | |
* // and the model element is bound to <img> element. Force mapping model positions inside "captionedImage" to that | |
* // <span> element. | |
* mapper.on( 'modelToViewPosition', ( evt, data ) => { | |
* const positionParent = modelPosition.parent; | |
* | |
* if ( positionParent.name == 'captionedImage' ) { | |
* const viewImg = data.mapper.toViewElement( positionParent ); | |
* const viewCaption = viewImg.nextSibling; // The <span> element. | |
* | |
* data.viewPosition = new ViewPosition( viewCaption, modelPosition.offset ); | |
* | |
* // Stop the event if other callbacks should not modify calculated value. | |
* evt.stop(); | |
* } | |
* } ); | |
* | |
* **Note:** keep in mind that custom callback provided for this event should use provided `data.modelPosition` only to check | |
* what is before the position (or position's parent). This is important when model-to-view position mapping is used in | |
* remove change conversion. Model after the removed position (that is being mapped) does not correspond to view, so it cannot be used: | |
* | |
* // Incorrect: | |
* const modelElement = data.modelPosition.nodeAfter; | |
* const viewElement = data.mapper.toViewElement( modelElement ); | |
* // ... Do something with `viewElement` and set `data.viewPosition`. | |
* | |
* // Correct: | |
* const prevModelElement = data.modelPosition.nodeBefore; | |
* const prevViewElement = data.mapper.toViewElement( prevModelElement ); | |
* // ... Use `prevViewElement` to find correct `data.viewPosition`. | |
* | |
* **Note:** default mapping callback is provided with `low` priority setting and does not cancel the event, so it is possible to | |
* attach a custom callback after default callback and also use `data.viewPosition` calculated by default callback | |
* (for example to fix it). | |
* | |
* **Note:** default mapping callback will not fire if `data.viewPosition` is already set. | |
* | |
* **Note:** these callbacks are called **very often**. For efficiency reasons, it is advised to use them only when position | |
* mapping between given model and view elements is unsolvable using just elements mapping and default algorithm. Also, | |
* the condition that checks if special case scenario happened should be as simple as possible. | |
* | |
* @event modelToViewPosition | |
* @param {Object} data Data pipeline object that can store and pass data between callbacks. The callback should add | |
* `viewPosition` value to that object with calculated {@link module:engine/view/position~Position view position}. | |
* @param {module:engine/conversion/mapper~Mapper} data.mapper Mapper instance that fired the event. | |
*/ | |
/** | |
* Fired for each view-to-model position mapping request. See {@link module:engine/conversion/mapper~Mapper#event:modelToViewPosition}. | |
* | |
* // See example in `modelToViewPosition` event description. | |
* // This custom mapping will map positions from <span> element next to <img> to the "captionedImage" element. | |
* mapper.on( 'viewToModelPosition', ( evt, data ) => { | |
* const positionParent = viewPosition.parent; | |
* | |
* if ( positionParent.hasClass( 'image-caption' ) ) { | |
* const viewImg = positionParent.previousSibling; | |
* const modelImg = data.mapper.toModelElement( viewImg ); | |
* | |
* data.modelPosition = new ModelPosition( modelImg, viewPosition.offset ); | |
* evt.stop(); | |
* } | |
* } ); | |
* | |
* **Note:** default mapping callback is provided with `low` priority setting and does not cancel the event, so it is possible to | |
* attach a custom callback after default callback and also use `data.modelPosition` calculated by default callback | |
* (for example to fix it). | |
* | |
* **Note:** default mapping callback will not fire if `data.modelPosition` is already set. | |
* | |
* **Note:** these callbacks are called **very often**. For efficiency reasons, it is advised to use them only when position | |
* mapping between given model and view elements is unsolvable using just elements mapping and default algorithm. Also, | |
* the condition that checks if special case scenario happened should be as simple as possible. | |
* | |
* @event viewToModelPosition | |
* @param {Object} data Data pipeline object that can store and pass data between callbacks. The callback should add | |
* `modelPosition` value to that object with calculated {@link module:engine/model/position~Position model position}. | |
* @param {module:engine/conversion/mapper~Mapper} data.mapper Mapper instance that fired the event. | |
*/ | |
} | |
mix( mapper_Mapper, emittermixin ); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/conversion/modelconsumable.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/conversion/modelconsumable | |
*/ | |
/** | |
* Manages a list of consumable values for {@link module:engine/model/item~Item model items}. | |
* | |
* Consumables are various aspects of the model. A model item can be broken down into singular properties that might be | |
* taken into consideration when converting that item. | |
* | |
* `ModelConsumable` is used by {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher} while analyzing changed | |
* parts of {@link module:engine/model/document~Document the document}. The added / changed / removed model items are broken down | |
* into singular properties (the item itself and it's attributes). All those parts are saved in `ModelConsumable`. Then, | |
* during conversion, when given part of model item is converted (i.e. the view element has been inserted into the view, | |
* but without attributes), consumable value is removed from `ModelConsumable`. | |
* | |
* For model items, `ModelConsumable` stores consumable values of one of following types: `insert`, `addAttribute:<attributeKey>`, | |
* `changeAttribute:<attributeKey>`, `removeAttribute:<attributeKey>`. | |
* | |
* In most cases, it is enough to let {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher} | |
* gather consumable values, so there is no need to use | |
* @link module:engine/conversion/modelconsumable~ModelConsumable#add add method} directly. | |
* However, it is important to understand how consumable values can be | |
* {@link module:engine/conversion/modelconsumable~ModelConsumable#consume consumed}. | |
* See {@link module:engine/conversion/model-selection-to-view-converters default model to view converters} for more information. | |
* | |
* Keep in mind, that one conversion event may have multiple callbacks (converters) attached to it. Each of those is | |
* able to convert one or more parts of the model. However, when one of those callbacks actually converts | |
* something, other should not, because they would duplicate the results. Using `ModelConsumable` helps avoiding | |
* this situation, because callbacks should only convert those values, which were not yet consumed from `ModelConsumable`. | |
* | |
* Consuming multiple values in a single callback: | |
* | |
* // Converter for custom `image` element that might have a `caption` element inside which changes | |
* // how the image is displayed in the view: | |
* // | |
* // Model: | |
* // | |
* // [image] | |
* // └─ [caption] | |
* // └─ foo | |
* // | |
* // View: | |
* // | |
* // <figure> | |
* // ├─ <img /> | |
* // └─ <caption> | |
* // └─ foo | |
* modelConversionDispatcher.on( 'insert:image', ( evt, data, consumable, conversionApi ) => { | |
* // First, consume the `image` element. | |
* consumable.consume( data.item, 'insert' ); | |
* | |
* // Just create normal image element for the view. | |
* // Maybe it will be "decorated" later. | |
* const viewImage = new ViewElement( 'img' ); | |
* const insertPosition = conversionApi.mapper.toViewPosition( data.range.start ); | |
* | |
* // Check if the `image` element has children. | |
* if ( data.item.childCount > 0 ) { | |
* const modelCaption = data.item.getChild( 0 ); | |
* | |
* // `modelCaption` insertion change is consumed from consumable values. | |
* // It will not be converted by other converters, but it's children (probably some text) will be. | |
* // Through mapping, converters for text will know where to insert contents of `modelCaption`. | |
* if ( consumable.consume( modelCaption, 'insert' ) ) { | |
* const viewCaption = new ViewElement( 'figcaption' ); | |
* | |
* const viewImageHolder = new ViewElement( 'figure', null, [ viewImage, viewCaption ] ); | |
* | |
* conversionApi.mapper.bindElements( modelCaption, viewCaption ); | |
* conversionApi.mapper.bindElements( data.item, viewImageHolder ); | |
* viewWriter.insert( insertPosition, viewImageHolder ); | |
* } | |
* } else { | |
* conversionApi.mapper.bindElements( data.item, viewImage ); | |
* viewWriter.insert( insertPosition, viewImage ); | |
* } | |
* | |
* evt.stop(); | |
* } ); | |
*/ | |
class modelconsumable_ModelConsumable { | |
/** | |
* Creates an empty consumables list. | |
*/ | |
constructor() { | |
/** | |
* Contains list of consumable values. | |
* | |
* @private | |
* @member {Map} module:engine/conversion/modelconsumable~ModelConsumable#_consumable | |
*/ | |
this._consumable = new Map(); | |
/** | |
* For each {@link module:engine/model/textproxy~TextProxy} added to `ModelConsumable`, this registry holds parent | |
* of that `TextProxy` and start and end indices of that `TextProxy`. This allows identification of `TextProxy` | |
* instances that points to the same part of the model but are different instances. Each distinct `TextProxy` | |
* is given unique `Symbol` which is then registered as consumable. This process is transparent for `ModelConsumable` | |
* API user because whenever `TextProxy` is added, tested, consumed or reverted, internal mechanisms of | |
* `ModelConsumable` translates `TextProxy` to that unique `Symbol`. | |
* | |
* @private | |
* @member {Map} module:engine/conversion/modelconsumable~ModelConsumable#_textProxyRegistry | |
*/ | |
this._textProxyRegistry = new Map(); | |
} | |
/** | |
* Adds a consumable value to the consumables list and links it with given model item. | |
* | |
* modelConsumable.add( modelElement, 'insert' ); // Add `modelElement` insertion change to consumable values. | |
* modelConsumable.add( modelElement, 'addAttribute:bold' ); // Add `bold` attribute insertion on `modelElement` change. | |
* modelConsumable.add( modelElement, 'removeAttribute:bold' ); // Add `bold` attribute removal on `modelElement` change. | |
* modelConsumable.add( modelSelection, 'selection' ); // Add `modelSelection` to consumable values. | |
* modelConsumable.add( modelSelection, 'selectionAttribute:bold' ); // Add `bold` attribute on `modelSelection` to consumables. | |
* modelConsumable.add( modelRange, 'range' ); // Add `modelRange` to consumable values. | |
* | |
* @param {module:engine/model/item~Item|module:engine/model/selection~Selection|module:engine/model/range~Range} item | |
* Model item, range or selection that has the consumable. | |
* @param {String} type Consumable type. | |
*/ | |
add( item, type ) { | |
if ( item instanceof textproxy_TextProxy ) { | |
item = this._getSymbolForTextProxy( item ); | |
} | |
if ( !this._consumable.has( item ) ) { | |
this._consumable.set( item, new Map() ); | |
} | |
this._consumable.get( item ).set( type, true ); | |
} | |
/** | |
* Removes given consumable value from given model item. | |
* | |
* modelConsumable.consume( modelElement, 'insert' ); // Remove `modelElement` insertion change from consumable values. | |
* modelConsumable.consume( modelElement, 'addAttribute:bold' ); // Remove `bold` attribute insertion on `modelElement` change. | |
* modelConsumable.consume( modelElement, 'removeAttribute:bold' ); // Remove `bold` attribute removal on `modelElement` change. | |
* modelConsumable.consume( modelSelection, 'selection' ); // Remove `modelSelection` from consumable values. | |
* modelConsumable.consume( modelSelection, 'selectionAttribute:bold' ); // Remove `bold` on `modelSelection` from consumables. | |
* modelConsumable.consume( modelRange, 'range' ); // Remove 'modelRange' from consumable values. | |
* | |
* @param {module:engine/model/item~Item|module:engine/model/selection~Selection|module:engine/model/range~Range} item | |
* Model item, range or selection from which consumable will be consumed. | |
* @param {String} type Consumable type. | |
* @returns {Boolean} `true` if consumable value was available and was consumed, `false` otherwise. | |
*/ | |
consume( item, type ) { | |
if ( item instanceof textproxy_TextProxy ) { | |
item = this._getSymbolForTextProxy( item ); | |
} | |
if ( this.test( item, type ) ) { | |
this._consumable.get( item ).set( type, false ); | |
return true; | |
} else { | |
return false; | |
} | |
} | |
/** | |
* Tests whether there is a consumable value of given type connected with given model item. | |
* | |
* modelConsumable.test( modelElement, 'insert' ); // Check for `modelElement` insertion change. | |
* modelConsumable.test( modelElement, 'addAttribute:bold' ); // Check for `bold` attribute insertion on `modelElement` change. | |
* modelConsumable.test( modelElement, 'removeAttribute:bold' ); // Check for `bold` attribute removal on `modelElement` change. | |
* modelConsumable.test( modelSelection, 'selection' ); // Check if `modelSelection` is consumable. | |
* modelConsumable.test( modelSelection, 'selectionAttribute:bold' ); // Check if `bold` on `modelSelection` is consumable. | |
* modelConsumable.test( modelRange, 'range' ); // Check if `modelRange` is consumable. | |
* | |
* @param {module:engine/model/item~Item|module:engine/model/selection~Selection|module:engine/model/range~Range} item | |
* Model item, range or selection to be tested. | |
* @param {String} type Consumable type. | |
* @returns {null|Boolean} `null` if such consumable was never added, `false` if the consumable values was | |
* already consumed or `true` if it was added and not consumed yet. | |
*/ | |
test( item, type ) { | |
if ( item instanceof textproxy_TextProxy ) { | |
item = this._getSymbolForTextProxy( item ); | |
} | |
const itemConsumables = this._consumable.get( item ); | |
if ( itemConsumables === undefined ) { | |
return null; | |
} | |
const value = itemConsumables.get( type ); | |
if ( value === undefined ) { | |
return null; | |
} | |
return value; | |
} | |
/** | |
* Reverts consuming of consumable value. | |
* | |
* modelConsumable.revert( modelElement, 'insert' ); // Revert consuming `modelElement` insertion change. | |
* modelConsumable.revert( modelElement, 'addAttribute:bold' ); // Revert consuming `bold` attribute insert from `modelElement`. | |
* modelConsumable.revert( modelElement, 'removeAttribute:bold' ); // Revert consuming `bold` attribute remove from `modelElement`. | |
* modelConsumable.revert( modelSelection, 'selection' ); // Revert consuming `modelSelection`. | |
* modelConsumable.revert( modelSelection, 'selectionAttribute:bold' ); // Revert consuming `bold` from `modelSelection`. | |
* modelConsumable.revert( modelRange, 'range' ); // Revert consuming `modelRange`. | |
* | |
* @param {module:engine/model/item~Item|module:engine/model/selection~Selection|module:engine/model/range~Range} item | |
* Model item, range or selection to be reverted. | |
* @param {String} type Consumable type. | |
* @returns {null|Boolean} `true` if consumable has been reversed, `false` otherwise. `null` if the consumable has | |
* never been added. | |
*/ | |
revert( item, type ) { | |
if ( item instanceof textproxy_TextProxy ) { | |
item = this._getSymbolForTextProxy( item ); | |
} | |
const test = this.test( item, type ); | |
if ( test === false ) { | |
this._consumable.get( item ).set( type, true ); | |
return true; | |
} else if ( test === true ) { | |
return false; | |
} | |
return null; | |
} | |
/** | |
* Gets a unique symbol for passed {@link module:engine/model/textproxy~TextProxy} instance. All `TextProxy` instances that | |
* have same parent, same start index and same end index will get the same symbol. | |
* | |
* Used internally to correctly consume `TextProxy` instances. | |
* | |
* @private | |
* @param {module:engine/model/textproxy~TextProxy} textProxy `TextProxy` instance to get a symbol for. | |
* @returns {Symbol} Symbol representing all equal instances of `TextProxy`. | |
*/ | |
_getSymbolForTextProxy( textProxy ) { | |
let symbol = null; | |
const startMap = this._textProxyRegistry.get( textProxy.startOffset ); | |
if ( startMap ) { | |
const endMap = startMap.get( textProxy.endOffset ); | |
if ( endMap ) { | |
symbol = endMap.get( textProxy.parent ); | |
} | |
} | |
if ( !symbol ) { | |
symbol = this._addSymbolForTextProxy( textProxy.startOffset, textProxy.endOffset, textProxy.parent ); | |
} | |
return symbol; | |
} | |
/** | |
* Adds a symbol for given properties that characterizes a {@link module:engine/model/textproxy~TextProxy} instance. | |
* | |
* Used internally to correctly consume `TextProxy` instances. | |
* | |
* @private | |
* @param {Number} startIndex Text proxy start index in it's parent. | |
* @param {Number} endIndex Text proxy end index in it's parent. | |
* @param {module:engine/model/element~Element} parent Text proxy parent. | |
* @returns {Symbol} Symbol generated for given properties. | |
*/ | |
_addSymbolForTextProxy( start, end, parent ) { | |
const symbol = Symbol( 'textProxySymbol' ); | |
let startMap, endMap; | |
startMap = this._textProxyRegistry.get( start ); | |
if ( !startMap ) { | |
startMap = new Map(); | |
this._textProxyRegistry.set( start, startMap ); | |
} | |
endMap = startMap.get( end ); | |
if ( !endMap ) { | |
endMap = new Map(); | |
startMap.set( end, endMap ); | |
} | |
endMap.set( parent, symbol ); | |
return symbol; | |
} | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/documentfragment.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module module:engine/model/documentfragment | |
*/ | |
/** | |
* DocumentFragment represents a part of model which does not have a common root but it's top-level nodes | |
* can be seen as siblings. In other words, it is a detached part of model tree, without a root. | |
* | |
* DocumentFragment has own {@link module:engine/model/markercollection~MarkerCollection}. Markers from this collection | |
* will be set to the {@link module:engine/model/document~Document#markers document markers} by a | |
* {@link module:engine/model/writer~writer.insert} function. | |
*/ | |
class documentfragment_DocumentFragment { | |
/** | |
* Creates an empty `DocumentFragment`. | |
* | |
* @param {module:engine/model/node~Node|Iterable.<module:engine/model/node~Node>} [children] | |
* Nodes to be contained inside the `DocumentFragment`. | |
*/ | |
constructor( children ) { | |
/** | |
* DocumentFragment static markers map. This is a list of names and {@link module:engine/model/range~Range ranges} | |
* which will be set as Markers to {@link module:engine/model/document~Document#markers document markers collection} | |
* when DocumentFragment will be inserted to the document. | |
* | |
* @member {Map<String, {module:engine/model/range~Range}>} module:engine/model/documentfragment~DocumentFragment#markers | |
*/ | |
this.markers = new Map(); | |
/** | |
* List of nodes contained inside the document fragment. | |
* | |
* @private | |
* @member {module:engine/model/nodelist~NodeList} module:engine/model/documentfragment~DocumentFragment#_children | |
*/ | |
this._children = new nodelist_NodeList(); | |
if ( children ) { | |
this.insertChildren( 0, children ); | |
} | |
} | |
/** | |
* Returns an iterator that iterates over all nodes contained inside this document fragment. | |
* | |
* @returns {Iterator.<module:engine/model/node~Node>} | |
*/ | |
[ Symbol.iterator ]() { | |
return this.getChildren(); | |
} | |
/** | |
* Number of this document fragment's children. | |
* | |
* @readonly | |
* @type {Number} | |
*/ | |
get childCount() { | |
return this._children.length; | |
} | |
/** | |
* Sum of {module:engine/model/node~Node#offsetSize offset sizes} of all of this document fragment's children. | |
* | |
* @readonly | |
* @type {Number} | |
*/ | |
get maxOffset() { | |
return this._children.maxOffset; | |
} | |
/** | |
* Is `true` if there are no nodes inside this document fragment, `false` otherwise. | |
* | |
* @readonly | |
* @type {Boolean} | |
*/ | |
get isEmpty() { | |
return this.childCount === 0; | |
} | |
/** | |
* Artificial root of `DocumentFragment`. Returns itself. Added for compatibility reasons. | |
* | |
* @readonly | |
* @type {module:engine/model/documentfragment~DocumentFragment} | |
*/ | |
get root() { | |
return this; | |
} | |
/** | |
* Artificial parent of `DocumentFragment`. Returns `null`. Added for compatibility reasons. | |
* | |
* @readonly | |
* @type {null} | |
*/ | |
get parent() { | |
return null; | |
} | |
/** | |
* Checks whether given model tree object is of given type. | |
* | |
* Read more in {@link module:engine/model/node~Node#is}. | |
* | |
* @param {String} type | |
* @returns {Boolean} | |
*/ | |
is( type ) { | |
return type == 'documentFragment'; | |
} | |
/** | |
* Gets the child at the given index. Returns `null` if incorrect index was passed. | |
* | |
* @param {Number} index Index of child. | |
* @returns {module:engine/model/node~Node|null} Child node. | |
*/ | |
getChild( index ) { | |
return this._children.getNode( index ); | |
} | |
/** | |
* Returns an iterator that iterates over all of this document fragment's children. | |
* | |
* @returns {Iterable.<module:engine/model/node~Node>} | |
*/ | |
getChildren() { | |
return this._children[ Symbol.iterator ](); | |
} | |
/** | |
* Returns an index of the given child node. Returns `null` if given node is not a child of this document fragment. | |
* | |
* @param {module:engine/model/node~Node} node Child node to look for. | |
* @returns {Number|null} Child node's index. | |
*/ | |
getChildIndex( node ) { | |
return this._children.getNodeIndex( node ); | |
} | |
/** | |
* Returns the starting offset of given child. Starting offset is equal to the sum of | |
* {module:engine/model/node~Node#offsetSize offset sizes} of all node's siblings that are before it. Returns `null` if | |
* given node is not a child of this document fragment. | |
* | |
* @param {module:engine/model/node~Node} node Child node to look for. | |
* @returns {Number|null} Child node's starting offset. | |
*/ | |
getChildStartOffset( node ) { | |
return this._children.getNodeStartOffset( node ); | |
} | |
/** | |
* Returns path to a `DocumentFragment`, which is an empty array. Added for compatibility reasons. | |
* | |
* @returns {Array} | |
*/ | |
getPath() { | |
return []; | |
} | |
/** | |
* Returns a descendant node by its path relative to this element. | |
* | |
* // <this>a<b>c</b></this> | |
* this.getNodeByPath( [ 0 ] ); // -> "a" | |
* this.getNodeByPath( [ 1 ] ); // -> <b> | |
* this.getNodeByPath( [ 1, 0 ] ); // -> "c" | |
* | |
* @param {Array.<Number>} relativePath Path of the node to find, relative to this element. | |
* @returns {module:engine/model/node~Node|module:engine/model/documentfragment~DocumentFragment} | |
*/ | |
getNodeByPath( relativePath ) { | |
let node = this; // eslint-disable-line consistent-this | |
for ( const index of relativePath ) { | |
node = node.getChild( node.offsetToIndex( index ) ); | |
} | |
return node; | |
} | |
/** | |
* Converts offset "position" to index "position". | |
* | |
* Returns index of a node that occupies given offset. If given offset is too low, returns `0`. If given offset is | |
* too high, returns index after last child}. | |
* | |
* const textNode = new Text( 'foo' ); | |
* const pElement = new Element( 'p' ); | |
* const docFrag = new DocumentFragment( [ textNode, pElement ] ); | |
* docFrag.offsetToIndex( -1 ); // Returns 0, because offset is too low. | |
* docFrag.offsetToIndex( 0 ); // Returns 0, because offset 0 is taken by `textNode` which is at index 0. | |
* docFrag.offsetToIndex( 1 ); // Returns 0, because `textNode` has `offsetSize` equal to 3, so it occupies offset 1 too. | |
* docFrag.offsetToIndex( 2 ); // Returns 0. | |
* docFrag.offsetToIndex( 3 ); // Returns 1. | |
* docFrag.offsetToIndex( 4 ); // Returns 2. There are no nodes at offset 4, so last available index is returned. | |
* | |
* @param {Number} offset Offset to look for. | |
* @returns {Number} Index of a node that occupies given offset. | |
*/ | |
offsetToIndex( offset ) { | |
return this._children.offsetToIndex( offset ); | |
} | |
/** | |
* {@link #insertChildren Inserts} one or more nodes at the end of this document fragment. | |
* | |
* @param {module:engine/model/node~Node|Iterable.<module:engine/model/node~Node>} nodes Nodes to be inserted. | |
*/ | |
appendChildren( nodes ) { | |
this.insertChildren( this.childCount, nodes ); | |
} | |
/** | |
* Inserts one or more nodes at the given index and sets {@link module:engine/model/node~Node#parent parent} of these nodes | |
* to this document fragment. | |
* | |
* @param {Number} index Index at which nodes should be inserted. | |
* @param {module:engine/model/node~Node|Iterable.<module:engine/model/node~Node>} nodes Nodes to be inserted. | |
*/ | |
insertChildren( index, nodes ) { | |
nodes = documentfragment_normalize( nodes ); | |
for ( const node of nodes ) { | |
// If node that is being added to this element is already inside another element, first remove it from the old parent. | |
if ( node.parent !== null ) { | |
node.remove(); | |
} | |
node.parent = this; | |
} | |
this._children.insertNodes( index, nodes ); | |
} | |
/** | |
* Removes one or more nodes starting at the given index | |
* and sets {@link module:engine/model/node~Node#parent parent} of these nodes to `null`. | |
* | |
* @param {Number} index Index of the first node to remove. | |
* @param {Number} [howMany=1] Number of nodes to remove. | |
* @returns {Array.<module:engine/model/node~Node>} Array containing removed nodes. | |
*/ | |
removeChildren( index, howMany = 1 ) { | |
const nodes = this._children.removeNodes( index, howMany ); | |
for ( const node of nodes ) { | |
node.parent = null; | |
} | |
return nodes; | |
} | |
/** | |
* Converts `DocumentFragment` instance to plain object and returns it. | |
* Takes care of converting all of this document fragment's children. | |
* | |
* @returns {Object} `DocumentFragment` instance converted to plain object. | |
*/ | |
toJSON() { | |
const json = []; | |
for ( const node of this._children ) { | |
json.push( node.toJSON() ); | |
} | |
return json; | |
} | |
/** | |
* Creates a `DocumentFragment` instance from given plain object (i.e. parsed JSON string). | |
* Converts `DocumentFragment` children to proper nodes. | |
* | |
* @param {Object} json Plain object to be converted to `DocumentFragment`. | |
* @returns {module:engine/model/documentfragment~DocumentFragment} `DocumentFragment` instance created using given plain object. | |
*/ | |
static fromJSON( json ) { | |
const children = []; | |
for ( const child of json ) { | |
if ( child.name ) { | |
// If child has name property, it is an Element. | |
children.push( element_Element.fromJSON( child ) ); | |
} else { | |
// Otherwise, it is a Text node. | |
children.push( text_Text.fromJSON( child ) ); | |
} | |
} | |
return new documentfragment_DocumentFragment( children ); | |
} | |
} | |
// Converts strings to Text and non-iterables to arrays. | |
// | |
// @param {String|module:engine/model/node~Node|Iterable.<String|module:engine/model/node~Node>} | |
// @return {Iterable.<module:engine/model/node~Node>} | |
function documentfragment_normalize( nodes ) { | |
// Separate condition because string is iterable. | |
if ( typeof nodes == 'string' ) { | |
return [ new text_Text( nodes ) ]; | |
} | |
if ( !isIterable( nodes ) ) { | |
nodes = [ nodes ]; | |
} | |
// Array.from to enable .map() on non-arrays. | |
return Array.from( nodes ) | |
.map( node => { | |
return typeof node == 'string' ? new text_Text( node ) : node; | |
} ); | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/conversion/modelconversiondispatcher.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/conversion/modelconversiondispatcher | |
*/ | |
/** | |
* `ModelConversionDispatcher` is a central point of {@link module:engine/model/model model} conversion, which is | |
* a process of reacting to changes in the model and reflecting them by listeners that listen to those changes. | |
* In default application, {@link module:engine/model/model model} is converted to {@link module:engine/view/view view}. This means | |
* that changes in the model are reflected by changing the view (i.e. adding view nodes or changing attributes on view elements). | |
* | |
* During conversion process, `ModelConversionDispatcher` fires data-manipulation events, basing on state of the model and prepares | |
* data for those events. It is important to note that the events are connected with "change actions" like "inserting" | |
* or "removing" so one might say that we are converting "changes". This is in contrary to view to model conversion, | |
* where we convert view nodes (the structure, not "changes" to the view). Note, that because changes are converted | |
* and not the structure itself, there is a need to have a mapping between model and the structure on which changes are | |
* reflected. To map elements during model to view conversion use {@link module:engine/conversion/mapper~Mapper}. | |
* | |
* The main use for this class is to listen to {@link module:engine/model/document~Document#event:change Document change event}, process it | |
* and then fire specific events telling what exactly has changed. For those events, `ModelConversionDispatcher` | |
* creates {@link module:engine/conversion/modelconsumable~ModelConsumable list of consumable values} that should be handled by event | |
* callbacks. Those events are listened to by model-to-view converters which convert changes done in the | |
* {@link module:engine/model/model model} to changes in the {@link module:engine/view/view view}. `ModelConversionController` also checks | |
* the current state of consumables, so it won't fire events for parts of model that were already consumed. This is | |
* especially important in callbacks that consume multiple values. See {@link module:engine/conversion/modelconsumable~ModelConsumable} | |
* for an example of such callback. | |
* | |
* Although the primary usage for this class is the model-to-view conversion, `ModelConversionDispatcher` can be used | |
* to build custom data processing pipelines that converts model to anything that is needed. Existing model structure can | |
* be used to generate events (listening to {@link module:engine/model/document~Document#event:change Document change event} is not | |
* required) | |
* and custom callbacks can be added to the events (these does not have to be limited to changes in the view). | |
* | |
* When providing your own event listeners for `ModelConversionDispatcher` keep in mind that any callback that had | |
* {@link module:engine/conversion/modelconsumable~ModelConsumable#consume consumed} a value from consumable (and did some changes, i.e. to | |
* the view) should also stop the event. This is because whenever a callback is fired it is assumed that there is something | |
* to be consumed. Thanks to that approach, you do not have to test whether there is anything to consume at the beginning | |
* of your listener callback. | |
* | |
* Example of providing a converter for `ModelConversionDispatcher`: | |
* | |
* // We will convert inserting "paragraph" model element into the model. | |
* modelDispatcher.on( 'insert:paragraph', ( evt, data, consumable, conversionApi ) => { | |
* // Remember to consume the part of consumable. | |
* consumable.consume( data.item, 'insert' ); | |
* | |
* // Translate position in model to position in the view. | |
* const viewPosition = conversionApi.mapper.toViewPosition( data.range.start ); | |
* | |
* // Create a P element (note that this converter is for inserting P elements -> 'insert:paragraph'). | |
* const viewElement = new ViewElement( 'p' ); | |
* | |
* // Bind the newly created view element to model element so positions will map accordingly in future. | |
* conversionApi.mapper.bindElements( data.item, viewElement ); | |
* | |
* // Add the newly created view element to the view. | |
* viewWriter.insert( viewPosition, viewElement ); | |
* | |
* // Remember to stop the event propagation if the data.item was consumed. | |
* evt.stop(); | |
* } ); | |
* | |
* Callback that "overrides" other callback: | |
* | |
* // Special converter for `linkHref` attribute added on custom `quote` element. Note, that this | |
* // attribute may be the same as the attribute added by other features (link feature in this case). | |
* // It might be even added by that feature! It makes sense that a part of content that is a quote is linked | |
* // to an external source so it makes sense that link feature works on the custom quote element. | |
* // However, we have to make sure that the attributes added by link feature are correctly converted. | |
* // To block default `linkHref` conversion we have to: | |
* // 1) add this callback with higher priority than link feature callback, | |
* // 2) consume `linkHref` attribute add change. | |
* modelConversionDispatcher.on( 'addAttribute:linkHref:quote', ( evt, data, consumable, conversionApi ) => { | |
* consumable.consume( data.item, 'addAttribute:linkHref' ); | |
* | |
* // Create a button that will represent the `linkHref` attribute. | |
* let viewSourceBtn = new ViewElement( 'a', { | |
* href: data.item.getAttribute( 'linkHref' ), | |
* title: 'source' | |
* } ); | |
* | |
* // Add a class for the button. | |
* viewSourceBtn.addClass( 'source' ); | |
* | |
* // Insert the button using writer API. | |
* // If `addAttribute` event is fired by | |
* // `module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#convertInsert` it is fired | |
* // after `data.item` insert conversion was done. If the event is fired due to attribute insertion coming from | |
* // different source, `data.item` already existed. This means we are safe to get `viewQuote` from mapper. | |
* const viewQuote = conversionApi.mapper.toViewElement( data.item ); | |
* const position = new ViewPosition( viewQuote, viewQuote.childCount ); | |
* viewWriter.insert( position, viewSourceBtn ); | |
* | |
* evt.stop(); | |
* }, { priority: 'high' } ); | |
*/ | |
class modelconversiondispatcher_ModelConversionDispatcher { | |
/** | |
* Creates a `ModelConversionDispatcher` that operates using passed API. | |
* | |
* @param {module:engine/model/document~Document} modelDocument Model document instance bound with this dispatcher. | |
* @param {Object} [conversionApi] Interface passed by dispatcher to the events callbacks. | |
*/ | |
constructor( modelDocument, conversionApi = {} ) { | |
/** | |
* Model document instance bound with this dispatcher. | |
* | |
* @private | |
* @member {module:engine/model/document~Document} | |
*/ | |
this._modelDocument = modelDocument; | |
/** | |
* Interface passed by dispatcher to the events callbacks. | |
* | |
* @member {Object} | |
*/ | |
this.conversionApi = lodash_assignIn( { dispatcher: this }, conversionApi ); | |
} | |
/** | |
* Prepares data and fires a proper event. | |
* | |
* The method is crafted to take use of parameters passed in {@link module:engine/model/document~Document#event:change Document change | |
* event}. | |
* | |
* @see module:engine/model/document~Document#event:change | |
* @fires insert | |
* @fires remove | |
* @fires addAttribute | |
* @fires removeAttribute | |
* @fires changeAttribute | |
* @fires addMarker | |
* @param {String} type Change type. | |
* @param {Object} data Additional information about the change. | |
*/ | |
convertChange( type, data ) { | |
// Do not convert changes if they happen in graveyard. | |
// Graveyard is a special root that has no view / no other representation and changes done in it should not be converted. | |
if ( type !== 'remove' && data.range && data.range.root.rootName == '$graveyard' ) { | |
return; | |
} | |
if ( type == 'remove' && data.sourcePosition.root.rootName == '$graveyard' ) { | |
return; | |
} | |
if ( type == 'rename' && data.element.root.rootName == '$graveyard' ) { | |
return; | |
} | |
// We can safely dispatch changes. | |
if ( type == 'insert' || type == 'reinsert' ) { | |
this.convertInsertion( data.range ); | |
} else if ( type == 'move' ) { | |
this.convertMove( data.sourcePosition, data.range ); | |
} else if ( type == 'remove' ) { | |
this.convertRemove( data.sourcePosition, data.range ); | |
} else if ( type == 'addAttribute' || type == 'removeAttribute' || type == 'changeAttribute' ) { | |
this.convertAttribute( type, data.range, data.key, data.oldValue, data.newValue ); | |
} else if ( type == 'rename' ) { | |
this.convertRename( data.element, data.oldName ); | |
} | |
} | |
/** | |
* Starts conversion of insertion-change on given `range`. | |
* | |
* Analyzes given range and fires insertion-connected events with data based on that range. | |
* | |
* **Note**: This method will fire separate events for node insertion and attributes insertion. All | |
* attributes that are set on inserted nodes are treated like they were added just after node insertion. | |
* | |
* @fires insert | |
* @fires addAttribute | |
* @fires addMarker | |
* @param {module:engine/model/range~Range} range Inserted range. | |
*/ | |
convertInsertion( range ) { | |
// Create a list of things that can be consumed, consisting of nodes and their attributes. | |
const consumable = this._createInsertConsumable( range ); | |
// Fire a separate insert event for each node and text fragment contained in the range. | |
for ( const value of range ) { | |
const item = value.item; | |
const itemRange = range_Range.createFromPositionAndShift( value.previousPosition, value.length ); | |
const data = { | |
item, | |
range: itemRange | |
}; | |
this._testAndFire( 'insert', data, consumable ); | |
// Fire a separate addAttribute event for each attribute that was set on inserted items. | |
// This is important because most attributes converters will listen only to add/change/removeAttribute events. | |
// If we would not add this part, attributes on inserted nodes would not be converted. | |
for ( const key of item.getAttributeKeys() ) { | |
data.attributeKey = key; | |
data.attributeOldValue = null; | |
data.attributeNewValue = item.getAttribute( key ); | |
this._testAndFire( `addAttribute:${ key }`, data, consumable ); | |
} | |
} | |
for ( const marker of this._modelDocument.markers ) { | |
const markerRange = marker.getRange(); | |
const intersection = markerRange.getIntersection( range ); | |
// Check if inserted content is inserted into a marker. | |
if ( intersection && shouldMarkerChangeBeConverted( range.start, marker, this.conversionApi.mapper ) ) { | |
this.convertMarker( 'addMarker', marker.name, intersection ); | |
} | |
} | |
} | |
/** | |
* Starts conversion of move-change of given `range`, that was moved from given `sourcePosition`. | |
* | |
* Fires {@link ~#event:remove remove event} and {@link ~#event:insert insert event} based on passed parameters. | |
* | |
* @fires remove | |
* @fires insert | |
* @param {module:engine/model/position~Position} sourcePosition The original position from which the range was moved. | |
* @param {module:engine/model/range~Range} range The range containing the moved content. | |
*/ | |
convertMove( sourcePosition, range ) { | |
// Move left – convert insertion first (#847). | |
if ( range.start.isBefore( sourcePosition ) ) { | |
this.convertInsertion( range ); | |
const sourcePositionAfterInsertion = | |
sourcePosition._getTransformedByInsertion( range.start, range.end.offset - range.start.offset ); | |
this.convertRemove( sourcePositionAfterInsertion, range ); | |
} else { | |
this.convertRemove( sourcePosition, range ); | |
this.convertInsertion( range ); | |
} | |
} | |
/** | |
* Starts conversion of remove-change of given `range`, that was removed from given `sourcePosition`. | |
* | |
* Fires {@link ~#event:remove remove event} with data based on passed values. | |
* | |
* @fires remove | |
* @param {module:engine/model/position~Position} sourcePosition Position from where the range has been removed. | |
* @param {module:engine/model/range~Range} range Removed range (after remove, in | |
* {@link module:engine/model/document~Document#graveyard graveyard root}). | |
*/ | |
convertRemove( sourcePosition, range ) { | |
const consumable = this._createConsumableForRange( range, 'remove' ); | |
for ( const item of range.getItems( { shallow: true } ) ) { | |
const data = { | |
sourcePosition, | |
item | |
}; | |
this._testAndFire( 'remove', data, consumable ); | |
} | |
} | |
/** | |
* Starts conversion of attribute-change on given `range`. | |
* | |
* Analyzes given attribute change and fires attributes-connected events with data based on passed values. | |
* | |
* @fires addAttribute | |
* @fires removeAttribute | |
* @fires changeAttribute | |
* @param {String} type Change type. Possible values: `addAttribute`, `removeAttribute`, `changeAttribute`. | |
* @param {module:engine/model/range~Range} range Changed range. | |
* @param {String} key Attribute key. | |
* @param {*} oldValue Attribute value before the change or `null` if attribute has not been set. | |
* @param {*} newValue New attribute value or `null` if attribute has been removed. | |
*/ | |
convertAttribute( type, range, key, oldValue, newValue ) { | |
if ( oldValue == newValue ) { | |
// Do not convert if the attribute did not change. | |
return; | |
} | |
// Create a list with attributes to consume. | |
const consumable = this._createConsumableForRange( range, type + ':' + key ); | |
// Create a separate attribute event for each node in the range. | |
for ( const value of range ) { | |
const item = value.item; | |
const itemRange = range_Range.createFromPositionAndShift( value.previousPosition, value.length ); | |
const data = { | |
item, | |
range: itemRange, | |
attributeKey: key, | |
attributeOldValue: oldValue, | |
attributeNewValue: newValue | |
}; | |
this._testAndFire( `${ type }:${ key }`, data, consumable ); | |
} | |
} | |
/** | |
* Starts conversion of rename-change of given `element` that had given `oldName`. | |
* | |
* Fires {@link ~#event:remove remove event} and {@link ~#event:insert insert event} based on passed values. | |
* | |
* @fires remove | |
* @fires insert | |
* @param {module:engine/model/element~Element} element Renamed element. | |
* @param {String} oldName Name of the renamed element before it was renamed. | |
*/ | |
convertRename( element, oldName ) { | |
if ( element.name == oldName ) { | |
// Do not convert if the name did not change. | |
return; | |
} | |
// Create fake element that will be used to fire remove event. The fake element will have the old element name. | |
const fakeElement = element.clone( true ); | |
fakeElement.name = oldName; | |
// Bind fake element with original view element so the view element will be removed. | |
this.conversionApi.mapper.bindElements( | |
fakeElement, | |
this.conversionApi.mapper.toViewElement( element ) | |
); | |
// Create fake document fragment so a range can be created on fake element. | |
const fakeDocumentFragment = new documentfragment_DocumentFragment(); | |
fakeDocumentFragment.appendChildren( fakeElement ); | |
this.convertRemove( position_Position.createBefore( element ), range_Range.createOn( fakeElement ) ); | |
this.convertInsertion( range_Range.createOn( element ) ); | |
} | |
/** | |
* Starts selection conversion. | |
* | |
* Fires events for given {@link module:engine/model/selection~Selection selection} to start selection conversion. | |
* | |
* @fires selection | |
* @fires selectionAttribute | |
* @param {module:engine/model/selection~Selection} selection Selection to convert. | |
*/ | |
convertSelection( selection ) { | |
const markers = Array.from( this._modelDocument.markers.getMarkersAtPosition( selection.getFirstPosition() ) ); | |
const consumable = this._createSelectionConsumable( selection, markers ); | |
this.fire( 'selection', { selection }, consumable, this.conversionApi ); | |
for ( const marker of markers ) { | |
const markerRange = marker.getRange(); | |
if ( !shouldMarkerChangeBeConverted( selection.getFirstPosition(), marker, this.conversionApi.mapper ) ) { | |
continue; | |
} | |
const data = { | |
selection, | |
markerName: marker.name, | |
markerRange | |
}; | |
if ( consumable.test( selection, 'selectionMarker:' + marker.name ) ) { | |
this.fire( 'selectionMarker:' + marker.name, data, consumable, this.conversionApi ); | |
} | |
} | |
for ( const key of selection.getAttributeKeys() ) { | |
const data = { | |
selection, | |
key, | |
value: selection.getAttribute( key ) | |
}; | |
// Do not fire event if the attribute has been consumed. | |
if ( consumable.test( selection, 'selectionAttribute:' + data.key ) ) { | |
this.fire( 'selectionAttribute:' + data.key, data, consumable, this.conversionApi ); | |
} | |
} | |
} | |
/** | |
* Starts marker conversion. | |
* | |
* Fires {@link ~#event:addMarker addMarker} or {@link ~#event:removeMarker removeMarker} events for each item | |
* in marker's range. If range is collapsed single event is dispatched. See events description for more details. | |
* | |
* @fires addMarker | |
* @fires removeMarker | |
* @param {'addMarker'|'removeMarker'} type Change type. | |
* @param {String} name Marker name. | |
* @param {module:engine/model/range~Range} range Marker range. | |
*/ | |
convertMarker( type, name, range ) { | |
// Do not convert if range is in graveyard or not in the document (e.g. in DocumentFragment). | |
if ( !range.root.document || range.root.rootName == '$graveyard' ) { | |
return; | |
} | |
// In markers case, event name == consumable name. | |
const eventName = type + ':' + name; | |
// When range is collapsed - fire single event with collapsed range in consumable. | |
if ( range.isCollapsed ) { | |
const consumable = new modelconsumable_ModelConsumable(); | |
consumable.add( range, eventName ); | |
this.fire( eventName, { | |
markerName: name, | |
markerRange: range | |
}, consumable, this.conversionApi ); | |
return; | |
} | |
// Create consumable for each item in range. | |
const consumable = this._createConsumableForRange( range, eventName ); | |
// Create separate event for each node in the range. | |
for ( const value of range ) { | |
const item = value.item; | |
// Do not fire event for already consumed items. | |
if ( !consumable.test( item, eventName ) ) { | |
continue; | |
} | |
const data = { | |
item, | |
range: range_Range.createFromPositionAndShift( value.previousPosition, value.length ), | |
markerName: name, | |
markerRange: range | |
}; | |
this.fire( eventName, data, consumable, this.conversionApi ); | |
} | |
} | |
/** | |
* Creates {@link module:engine/conversion/modelconsumable~ModelConsumable} with values to consume from given range, assuming that | |
* given range has just been inserted to the model. | |
* | |
* @private | |
* @param {module:engine/model/range~Range} range Inserted range. | |
* @returns {module:engine/conversion/modelconsumable~ModelConsumable} Values to consume. | |
*/ | |
_createInsertConsumable( range ) { | |
const consumable = new modelconsumable_ModelConsumable(); | |
for ( const value of range ) { | |
const item = value.item; | |
consumable.add( item, 'insert' ); | |
for ( const key of item.getAttributeKeys() ) { | |
consumable.add( item, 'addAttribute:' + key ); | |
} | |
} | |
return consumable; | |
} | |
/** | |
* Creates {@link module:engine/conversion/modelconsumable~ModelConsumable} with values of given `type` | |
* for each item from given `range`. | |
* | |
* @private | |
* @param {module:engine/model/range~Range} range Affected range. | |
* @param {String} type Consumable type. | |
* @returns {module:engine/conversion/modelconsumable~ModelConsumable} Values to consume. | |
*/ | |
_createConsumableForRange( range, type ) { | |
const consumable = new modelconsumable_ModelConsumable(); | |
for ( const item of range.getItems() ) { | |
consumable.add( item, type ); | |
} | |
return consumable; | |
} | |
/** | |
* Creates {@link module:engine/conversion/modelconsumable~ModelConsumable} with selection consumable values. | |
* | |
* @private | |
* @param {module:engine/model/selection~Selection} selection Selection to create consumable from. | |
* @param {Iterable.<module:engine/model/markercollection~Marker>} markers Markers which contains selection. | |
* @returns {module:engine/conversion/modelconsumable~ModelConsumable} Values to consume. | |
*/ | |
_createSelectionConsumable( selection, markers ) { | |
const consumable = new modelconsumable_ModelConsumable(); | |
consumable.add( selection, 'selection' ); | |
for ( const marker of markers ) { | |
consumable.add( selection, 'selectionMarker:' + marker.name ); | |
} | |
for ( const key of selection.getAttributeKeys() ) { | |
consumable.add( selection, 'selectionAttribute:' + key ); | |
} | |
return consumable; | |
} | |
/** | |
* Tests passed `consumable` to check whether given event can be fired and if so, fires it. | |
* | |
* @private | |
* @fires insert | |
* @fires remove | |
* @fires addAttribute | |
* @fires removeAttribute | |
* @fires changeAttribute | |
* @param {String} type Event type. | |
* @param {Object} data Event data. | |
* @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume. | |
*/ | |
_testAndFire( type, data, consumable ) { | |
if ( !consumable.test( data.item, type ) ) { | |
// Do not fire event if the item was consumed. | |
return; | |
} | |
const name = data.item.name || '$text'; | |
this.fire( type + ':' + name, data, consumable, this.conversionApi ); | |
} | |
/** | |
* Fired for inserted nodes. | |
* | |
* `insert` is a namespace for a class of events. Names of actually called events follow this pattern: | |
* `insert:<name>`. `name` is either `'$text'` when one or more characters has been inserted or | |
* {@link module:engine/model/element~Element#name name} of inserted element. | |
* | |
* This way listeners can either listen to a general `insert` event or specific event (for example `insert:paragraph`). | |
* | |
* @event insert | |
* @param {Object} data Additional information about the change. | |
* @param {module:engine/model/item~Item} data.item Inserted item. | |
* @param {module:engine/model/range~Range} data.range Range spanning over inserted item. | |
* @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume. | |
* @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor. | |
*/ | |
/** | |
* Fired for removed nodes. | |
* | |
* `remove` is a namespace for a class of events. Names of actually called events follow this pattern: | |
* `remove:<name>`. `name` is either `'$text'` when one or more characters has been removed or the | |
* {@link module:engine/model/element~Element#name name} of removed element. | |
* | |
* This way listeners can either listen to a general `remove` event or specific event (for example `remove:paragraph`). | |
* | |
* @event remove | |
* @param {Object} data Additional information about the change. | |
* @param {module:engine/model/position~Position} data.sourcePosition Position from where the range has been removed. | |
* @param {module:engine/model/range~Range} data.range Removed range (in {@link module:engine/model/document~Document#graveyard | |
* graveyard root}). | |
* @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor. | |
*/ | |
/** | |
* Fired when attribute has been added on a node. | |
* | |
* `addAttribute` is a namespace for a class of events. Names of actually called events follow this pattern: | |
* `addAttribute:<attributeKey>:<name>`. `attributeKey` is the key of added attribute. `name` is either `'$text'` | |
* if attribute was added on one or more characters, or the {@link module:engine/model/element~Element#name name} of | |
* the element on which attribute was added. | |
* | |
* This way listeners can either listen to a general `addAttribute:bold` event or specific event | |
* (for example `addAttribute:link:image`). | |
* | |
* @event addAttribute | |
* @param {Object} data Additional information about the change. | |
* @param {module:engine/model/item~Item} data.item Changed item. | |
* @param {module:engine/model/range~Range} data.range Range spanning over changed item. | |
* @param {String} data.attributeKey Attribute key. | |
* @param {null} data.attributeOldValue Attribute value before the change - always `null`. Kept for the sake of unifying events. | |
* @param {*} data.attributeNewValue New attribute value. | |
* @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume. | |
* @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor. | |
*/ | |
/** | |
* Fired when attribute has been removed from a node. | |
* | |
* `removeAttribute` is a namespace for a class of events. Names of actually called events follow this pattern: | |
* `removeAttribute:<attributeKey>:<name>`. `attributeKey` is the key of removed attribute. `name` is either `'$text'` | |
* if attribute was removed from one or more characters, or the {@link module:engine/model/element~Element#name name} of | |
* the element from which attribute was removed. | |
* | |
* This way listeners can either listen to a general `removeAttribute:bold` event or specific event | |
* (for example `removeAttribute:link:image`). | |
* | |
* @event removeAttribute | |
* @param {Object} data Additional information about the change. | |
* @param {module:engine/model/item~Item} data.item Changed item. | |
* @param {module:engine/model/range~Range} data.range Range spanning over changed item. | |
* @param {String} data.attributeKey Attribute key. | |
* @param {*} data.attributeOldValue Attribute value before it was removed. | |
* @param {null} data.attributeNewValue New attribute value - always `null`. Kept for the sake of unifying events. | |
* @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume. | |
* @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor. | |
*/ | |
/** | |
* Fired when attribute of a node has been changed. | |
* | |
* `changeAttribute` is a namespace for a class of events. Names of actually called events follow this pattern: | |
* `changeAttribute:<attributeKey>:<name>`. `attributeKey` is the key of changed attribute. `name` is either `'$text'` | |
* if attribute was changed on one or more characters, or the {@link module:engine/model/element~Element#name name} of | |
* the element on which attribute was changed. | |
* | |
* This way listeners can either listen to a general `changeAttribute:link` event or specific event | |
* (for example `changeAttribute:link:image`). | |
* | |
* @event changeAttribute | |
* @param {Object} data Additional information about the change. | |
* @param {module:engine/model/item~Item} data.item Changed item. | |
* @param {module:engine/model/range~Range} data.range Range spanning over changed item. | |
* @param {String} data.attributeKey Attribute key. | |
* @param {*} data.attributeOldValue Attribute value before the change. | |
* @param {*} data.attributeNewValue New attribute value. | |
* @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume. | |
* @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor. | |
*/ | |
/** | |
* Fired for {@link module:engine/model/selection~Selection selection} changes. | |
* | |
* @event selection | |
* @param {module:engine/model/selection~Selection} selection `Selection` instance that is converted. | |
* @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume. | |
* @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor. | |
*/ | |
/** | |
* Fired for {@link module:engine/model/selection~Selection selection} attributes changes. | |
* | |
* `selectionAttribute` is a namespace for a class of events. Names of actually called events follow this pattern: | |
* `selectionAttribute:<attributeKey>`. `attributeKey` is the key of selection attribute. This way listen can listen to | |
* certain attribute, i.e. `addAttribute:bold`. | |
* | |
* @event selectionAttribute | |
* @param {Object} data Additional information about the change. | |
* @param {module:engine/model/selection~Selection} data.selection Selection that is converted. | |
* @param {String} data.attributeKey Key of changed attribute. | |
* @param {*} data.attributeValue Value of changed attribute. | |
* @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume. | |
* @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor. | |
*/ | |
/** | |
* Fired when a new marker is added to the model. | |
* * If marker's range is not collapsed, event is fired separately for each item contained in that range. In this | |
* situation, consumable contains all items from that range. | |
* * If marker's range is collapsed, single event is fired. In this situation, consumable contains only the collapsed range. | |
* | |
* `addMarker` is a namespace for a class of events. Names of actually called events follow this pattern: | |
* `addMarker:<markerName>`. By specifying certain marker names, you can make the events even more gradual. For example, | |
* markers can be named `foo:abc`, `foo:bar`, then it is possible to listen to `addMarker:foo` or `addMarker:foo:abc` and | |
* `addMarker:foo:bar` events. | |
* | |
* @event addMarker | |
* @param {Object} data Additional information about the change. | |
* @param {module:engine/model/item~Item} [data.item] Item contained in marker's range. Not present if collapsed range | |
* is being converted. | |
* @param {module:engine/model/range~Range} [data.range] Range spanning over item. Not present if collapsed range | |
* is being converted. | |
* @param {String} data.markerName Name of the marker. | |
* @param {module:engine/model/range~Range} data.markerRange Marker's range spanning on all items. | |
* @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume. When non-collapsed | |
* marker is being converted then consumable contains all items in marker's range. For collapsed markers it contains | |
* only marker's range to consume. | |
* @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor. | |
*/ | |
/** | |
* Fired when marker is removed from the model. | |
* * If marker's range is not collapsed, event is fired separately for each item contained in that range. In this | |
* situation, consumable contains all items from that range. | |
* * If marker's range is collapsed, single event is fired. In this situation, consumable contains only the collapsed range. | |
* | |
* `removeMarker` is a namespace for a class of events. Names of actually called events follow this pattern: | |
* `removeMarker:<markerName>`. By specifying certain marker names, you can make the events even more gradual. For example, | |
* markers can be named `foo:abc`, `foo:bar`, then it is possible to listen to `removeMarker:foo` or `removeMarker:foo:abc` and | |
* `removeMarker:foo:bar` events. | |
* | |
* @event removeMarker | |
* @param {Object} data Additional information about the change. | |
* @param {module:engine/model/item~Item} [data.item] Item contained in marker's range. Not present if collapsed range | |
* is being converted. | |
* @param {module:engine/model/range~Range} [data.range] Range spanning over item. Not present if collapsed range | |
* is being converted. | |
* @param {String} data.markerName Name of the marker. | |
* @param {module:engine/model/range~Range} data.markerRange Whole marker's range. | |
* @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume. When non-collapsed | |
* marker is being converted then consumable contains all items in marker's range. For collapsed markers it contains | |
* only marker's range to consume. | |
* @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor. | |
*/ | |
} | |
mix( modelconversiondispatcher_ModelConversionDispatcher, emittermixin ); | |
// Helper function, checks whether change of `marker` at `modelPosition` should be converted. Marker changes are not | |
// converted if they happen inside an element with custom conversion method. | |
// | |
// @param {module:engine/model/position~Position} modelPosition | |
// @param {module:engine/model/markercollection~Marker} marker | |
// @param {module:engine/conversion/mapper~Mapper} mapper | |
// @returns {Boolean} | |
function shouldMarkerChangeBeConverted( modelPosition, marker, mapper ) { | |
const range = marker.getRange(); | |
const ancestors = Array.from( modelPosition.getAncestors() ); | |
ancestors.shift(); // Remove root element. It cannot be passed to `model.Range#containsItem`. | |
ancestors.reverse(); | |
const hasCustomHandling = ancestors.some( element => { | |
if ( range.containsItem( element ) ) { | |
const viewElement = mapper.toViewElement( element ); | |
return !!viewElement.getCustomProperty( 'addHighlight' ); | |
} | |
} ); | |
return !hasCustomHandling; | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/view/attributeelement.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/view/attributeelement | |
*/ | |
// Default attribute priority. | |
const DEFAULT_PRIORITY = 10; | |
/** | |
* Attributes are elements which define document presentation. They are mostly elements like `<b>` or `<span>`. | |
* Attributes can be broken and merged by the {@link module:engine/view/writer~writer view writer}. | |
* | |
* Editing engine does not define fixed HTML DTD. This is why the type of the {@link module:engine/view/element~Element} need to | |
* be defined by the feature developer. Creating an element you should use {@link module:engine/view/containerelement~ContainerElement} | |
* class or `AttributeElement`. | |
* | |
* @extends module:engine/view/element~Element | |
*/ | |
class AttributeElement extends view_element_Element { | |
/** | |
* Creates a attribute element. | |
* | |
* @see module:engine/view/element~Element | |
*/ | |
constructor( name, attrs, children ) { | |
super( name, attrs, children ); | |
/** | |
* Element priority. Attributes have to have the same priority to be | |
* {@link module:engine/view/element~Element#isSimilar similar}. Setting different priorities on similar | |
* nodes may prevent merging, e.g. two `<abbr>` nodes next each other shouldn't be merged. | |
* | |
* @member {Number} | |
*/ | |
this.priority = DEFAULT_PRIORITY; | |
/** | |
* Returns block {@link module:engine/view/filler filler} offset or `null` if block filler is not needed. | |
* | |
* @method #getFillerOffset | |
* @returns {Number|null} Block filler offset or `null` if block filler is not needed. | |
*/ | |
this.getFillerOffset = attributeelement_getFillerOffset; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
is( type, name = null ) { | |
if ( !name ) { | |
return type == 'attributeElement' || super.is( type ); | |
} else { | |
return ( type == 'attributeElement' && name == this.name ) || super.is( type, name ); | |
} | |
} | |
/** | |
* Clones provided element with priority. | |
* | |
* @param {Boolean} deep If set to `true` clones element and all its children recursively. When set to `false`, | |
* element will be cloned without any children. | |
* @returns {module:engine/view/attributeelement~AttributeElement} Clone of this element. | |
*/ | |
clone( deep ) { | |
const cloned = super.clone( deep ); | |
// Clone priority too. | |
cloned.priority = this.priority; | |
return cloned; | |
} | |
/** | |
* Checks if this element is similar to other element. | |
* Both elements should have the same name, attributes and priority to be considered as similar. | |
* Two similar elements can contain different set of children nodes. | |
* | |
* @param {module:engine/view/element~Element} otherElement | |
* @returns {Boolean} | |
*/ | |
isSimilar( otherElement ) { | |
return super.isSimilar( otherElement ) && this.priority == otherElement.priority; | |
} | |
} | |
/** | |
* Default attribute priority. | |
* | |
* @member {Number} module:engine/view/attributeelement~AttributeElement.DEFAULT_PRIORITY | |
*/ | |
AttributeElement.DEFAULT_PRIORITY = DEFAULT_PRIORITY; | |
// Returns block {@link module:engine/view/filler~Filler filler} offset or `null` if block filler is not needed. | |
// | |
// @returns {Number|null} Block filler offset or `null` if block filler is not needed. | |
function attributeelement_getFillerOffset() { | |
// <b>foo</b> does not need filler. | |
if ( nonUiChildrenCount( this ) ) { | |
return null; | |
} | |
let element = this.parent; | |
// <p><b></b></p> needs filler -> <p><b><br></b></p> | |
while ( element && element.is( 'attributeElement' ) ) { | |
if ( nonUiChildrenCount( element ) > 1 ) { | |
return null; | |
} | |
element = element.parent; | |
} | |
if ( !element || nonUiChildrenCount( element ) > 1 ) { | |
return null; | |
} | |
// Render block filler at the end of element (after all ui elements). | |
return this.childCount; | |
} | |
// Returns total count of children that are not {@link module:engine/view/uielement~UIElement UIElements}. | |
// | |
// @param {module:engine/view/element~Element} element | |
// @return {Number} | |
function nonUiChildrenCount( element ) { | |
return Array.from( element.getChildren() ).filter( element => !element.is( 'uiElement' ) ).length; | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/view/emptyelement.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/view/emptyelement | |
*/ | |
/** | |
* EmptyElement class. It is used to represent elements that cannot contain any child nodes. | |
*/ | |
class emptyelement_EmptyElement extends view_element_Element { | |
/** | |
* Creates new instance of EmptyElement. | |
* | |
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-emptyelement-cannot-add` when third parameter is passed, | |
* to inform that usage of EmptyElement is incorrect (adding child nodes to EmptyElement is forbidden). | |
* | |
* @param {String} name Node name. | |
* @param {Object|Iterable} [attributes] Collection of attributes. | |
*/ | |
constructor( name, attributes, children ) { | |
super( name, attributes, children ); | |
/** | |
* Returns `null` because filler is not needed for EmptyElements. | |
* | |
* @method #getFillerOffset | |
* @returns {null} Always returns null. | |
*/ | |
this.getFillerOffset = emptyelement_getFillerOffset; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
is( type, name = null ) { | |
if ( !name ) { | |
return type == 'emptyElement' || super.is( type ); | |
} else { | |
return ( type == 'emptyElement' && name == this.name ) || super.is( type, name ); | |
} | |
} | |
/** | |
* Overrides {@link module:engine/view/element~Element#insertChildren} method. | |
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-emptyelement-cannot-add` to prevent | |
* adding any child nodes to EmptyElement. | |
*/ | |
insertChildren( index, nodes ) { | |
if ( nodes && ( nodes instanceof view_node_Node || Array.from( nodes ).length > 0 ) ) { | |
/** | |
* Cannot add children to {@link module:engine/view/emptyelement~EmptyElement}. | |
* | |
* @error view-emptyelement-cannot-add | |
*/ | |
throw new CKEditorError( 'view-emptyelement-cannot-add: Cannot add child nodes to EmptyElement instance.' ); | |
} | |
} | |
} | |
// Returns `null` because block filler is not needed for EmptyElements. | |
// | |
// @returns {null} | |
function emptyelement_getFillerOffset() { | |
return null; | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/env.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/* globals navigator:false */ | |
/** | |
* @module utils/env | |
*/ | |
const userAgent = navigator.userAgent.toLowerCase(); | |
/** | |
* A namespace containing environment and browser information. | |
* | |
* @namespace | |
*/ | |
const env = { | |
/** | |
* Indicates that application is running on Macintosh. | |
* | |
* @static | |
* @member {Boolean} module:utils/env~env#mac | |
*/ | |
mac: isMac( userAgent ) | |
}; | |
/* harmony default export */ var src_env = (env); | |
/** | |
* Checks if User Agent represented by the string is running on Macintosh. | |
* | |
* @param {String} userAgent **Lowercase** `navigator.userAgent` string. | |
* @returns {Boolean} Whether User Agent is running on Macintosh or not. | |
*/ | |
function isMac( userAgent ) { | |
return userAgent.indexOf( 'macintosh' ) > -1; | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/keyboard.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* Set of utils related to keyboard support. | |
* | |
* @module utils/keyboard | |
*/ | |
/** | |
* Object with `keyName => keyCode` pairs for a set of known keys. | |
* | |
* Contains: | |
* | |
* * `a-z`, | |
* * `0-9`, | |
* * `f1-f12`, | |
* * `arrow(left|up|right|bottom)`, | |
* * `backspace`, `delete`, `enter`, `esc`, `tab`, | |
* * `ctrl`, `cmd`, `shift`, `alt`. | |
*/ | |
const keyCodes = generateKnownKeyCodes(); | |
/** | |
* Converts a key name or a {@link module:utils/keyboard~KeystrokeInfo keystroke info} into a key code. | |
* | |
* Note: Key names are matched with {@link module:utils/keyboard~keyCodes} in a case-insensitive way. | |
* | |
* @param {String|module:utils/keyboard~KeystrokeInfo} Key name (see {@link module:utils/keyboard~keyCodes}) | |
* or a keystroke data object. | |
* @returns {Number} Key or keystroke code. | |
*/ | |
function getCode( key ) { | |
let keyCode; | |
if ( typeof key == 'string' ) { | |
keyCode = keyCodes[ key.toLowerCase() ]; | |
if ( !keyCode ) { | |
/** | |
* Unknown key name. Only key names contained by the {@link module:utils/keyboard~keyCodes} can be used. | |
* | |
* @errror keyboard-unknown-key | |
* @param {String} key | |
*/ | |
throw new CKEditorError( 'keyboard-unknown-key: Unknown key name.', { key } ); | |
} | |
} else { | |
keyCode = key.keyCode + | |
( key.altKey ? keyCodes.alt : 0 ) + | |
( key.ctrlKey ? keyCodes.ctrl : 0 ) + | |
( key.shiftKey ? keyCodes.shift : 0 ); | |
} | |
return keyCode; | |
} | |
/** | |
* Parses keystroke and returns a keystroke code that will match the code returned by | |
* link {@link module:utils/keyboard.getCode} for a corresponding {@link module:utils/keyboard~KeystrokeInfo keystroke info}. | |
* | |
* The keystroke can be passed in two formats: | |
* | |
* * as a single string – e.g. `ctrl + A`, | |
* * as an array of {@link module:utils/keyboard~keyCodes known key names} and key codes – e.g.: | |
* * `[ 'ctrl', 32 ]` (ctrl + space), | |
* * `[ 'ctrl', 'a' ]` (ctrl + A). | |
* | |
* Note: Key names are matched with {@link module:utils/keyboard~keyCodes} in a case-insensitive way. | |
* | |
* Note: Only keystrokes with a single non-modifier key are supported (e.g. `ctrl+A` is OK, but `ctrl+A+B` is not). | |
* | |
* @param {String|Array.<Number|String>} keystroke Keystroke definition. | |
* @returns {Number} Keystroke code. | |
*/ | |
function parseKeystroke( keystroke ) { | |
if ( typeof keystroke == 'string' ) { | |
keystroke = splitKeystrokeText( keystroke ); | |
} | |
return keystroke | |
.map( key => ( typeof key == 'string' ) ? getCode( key ) : key ) | |
.reduce( ( key, sum ) => sum + key, 0 ); | |
} | |
/** | |
* It translates any keystroke string text like `"CTRL+A"` to an | |
* environment–specific keystroke, i.e. `"⌘A"` on Mac OSX. | |
* | |
* @param {String} keystroke Keystroke text. | |
* @returns {String} Keystroke text specific for the environment. | |
*/ | |
function getEnvKeystrokeText( keystroke ) { | |
const split = splitKeystrokeText( keystroke ); | |
if ( src_env.mac ) { | |
if ( split[ 0 ].toLowerCase() == 'ctrl' ) { | |
return '⌘' + ( split[ 1 ] || '' ); | |
} | |
} | |
return keystroke; | |
} | |
function generateKnownKeyCodes() { | |
const keyCodes = { | |
arrowleft: 37, | |
arrowup: 38, | |
arrowright: 39, | |
arrowdown: 40, | |
backspace: 8, | |
delete: 46, | |
enter: 13, | |
space: 32, | |
esc: 27, | |
tab: 9, | |
// The idea about these numbers is that they do not collide with any real key codes, so we can use them | |
// like bit masks. | |
ctrl: 0x110000, | |
// Has the same code as ctrl, because their behaviour should be unified across the editor. | |
// See http://ckeditor.github.io/editor-recommendations/general-policies#ctrl-vs-cmd | |
cmd: 0x110000, | |
shift: 0x220000, | |
alt: 0x440000 | |
}; | |
// a-z | |
for ( let code = 65; code <= 90; code++ ) { | |
const letter = String.fromCharCode( code ); | |
keyCodes[ letter.toLowerCase() ] = code; | |
} | |
// 0-9 | |
for ( let code = 48; code <= 57; code++ ) { | |
keyCodes[ code - 48 ] = code; | |
} | |
// F1-F12 | |
for ( let code = 112; code <= 123; code++ ) { | |
keyCodes[ 'f' + ( code - 111 ) ] = code; | |
} | |
return keyCodes; | |
} | |
function splitKeystrokeText( keystroke ) { | |
return keystroke.split( /\s*\+\s*/ ); | |
} | |
/** | |
* Information about a keystroke. | |
* | |
* @interface module:utils/keyboard~KeystrokeInfo | |
*/ | |
/** | |
* The [key code](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode). | |
* | |
* @member {Number} module:utils/keyboard~KeystrokeInfo#keyCode | |
*/ | |
/** | |
* Whether the <kbd>Alt</kbd> modifier was pressed. | |
* | |
* @member {Bolean} module:utils/keyboard~KeystrokeInfo#altKey | |
*/ | |
/** | |
* Whether the <kbd>Ctrl</kbd> or <kbd>Cmd</kbd> modifier was pressed. | |
* | |
* @member {Bolean} module:utils/keyboard~KeystrokeInfo#ctrlKey | |
*/ | |
/** | |
* Whether the <kbd>Shift</kbd> modifier was pressed. | |
* | |
* @member {Bolean} module:utils/keyboard~KeystrokeInfo#shiftKey | |
*/ | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/view/uielement.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/view/uielement | |
*/ | |
/** | |
* UIElement class. It is used to represent UI not a content of the document. | |
* This element can't be split and selection can't be placed inside this element. | |
*/ | |
class uielement_UIElement extends view_element_Element { | |
/** | |
* Creates new instance of UIElement. | |
* | |
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-uielement-cannot-add` when third parameter is passed, | |
* to inform that usage of UIElement is incorrect (adding child nodes to UIElement is forbidden). | |
* | |
* @param {String} name Node name. | |
* @param {Object|Iterable} [attributes] Collection of attributes. | |
*/ | |
constructor( name, attributes, children ) { | |
super( name, attributes, children ); | |
/** | |
* Returns `null` because filler is not needed for UIElements. | |
* | |
* @method #getFillerOffset | |
* @returns {null} Always returns null. | |
*/ | |
this.getFillerOffset = uielement_getFillerOffset; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
is( type, name = null ) { | |
if ( !name ) { | |
return type == 'uiElement' || super.is( type ); | |
} else { | |
return ( type == 'uiElement' && name == this.name ) || super.is( type, name ); | |
} | |
} | |
/** | |
* Overrides {@link module:engine/view/element~Element#insertChildren} method. | |
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-uielement-cannot-add` to prevent adding any child nodes | |
* to UIElement. | |
*/ | |
insertChildren( index, nodes ) { | |
if ( nodes && ( nodes instanceof view_node_Node || Array.from( nodes ).length > 0 ) ) { | |
/** | |
* Cannot add children to {@link module:engine/view/uielement~UIElement}. | |
* | |
* @error view-uielement-cannot-add | |
*/ | |
throw new CKEditorError( 'view-uielement-cannot-add: Cannot add child nodes to UIElement instance.' ); | |
} | |
} | |
/** | |
* Renders this {@link module:engine/view/uielement~UIElement} to DOM. This method is called by | |
* {@link module:engine/view/domconverter~DomConverter}. | |
* | |
* @param {Document} domDocument | |
* @return {HTMLElement} | |
*/ | |
render( domDocument ) { | |
const domElement = domDocument.createElement( this.name ); | |
for ( const key of this.getAttributeKeys() ) { | |
domElement.setAttribute( key, this.getAttribute( key ) ); | |
} | |
return domElement; | |
} | |
} | |
/** | |
* This function injects UI element handling to the given {@link module:engine/view/document~Document document}. | |
* | |
* A callback is added to {@link module:engine/view/document~Document#event:keydown document keydown event}. | |
* The callback handles the situation when right arrow key is pressed and selection is collapsed before a UI element. | |
* Without this handler, it would be impossible to "jump over" UI element using right arrow key. | |
* | |
* @param {module:engine/view/document~Document} document Document to which the quirks handling will be injected. | |
*/ | |
function injectUiElementHandling( document ) { | |
document.on( 'keydown', ( evt, data ) => jumpOverUiElement( evt, data, document.domConverter ) ); | |
} | |
// Returns `null` because block filler is not needed for UIElements. | |
// | |
// @returns {null} | |
function uielement_getFillerOffset() { | |
return null; | |
} | |
// Selection cannot be placed in a `UIElement`. Whenever it is placed there, it is moved before it. This | |
// causes a situation when it is impossible to jump over `UIElement` using right arrow key, because the selection | |
// ends up in ui element (in DOM) and is moved back to the left. This handler fixes this situation. | |
function jumpOverUiElement( evt, data, domConverter ) { | |
if ( data.keyCode == keyCodes.arrowright ) { | |
const domSelection = data.domTarget.ownerDocument.defaultView.getSelection(); | |
const domSelectionCollapsed = domSelection.rangeCount == 1 && domSelection.getRangeAt( 0 ).collapsed; | |
// Jump over UI element if selection is collapsed or shift key is pressed. These are the cases when selection would extend. | |
if ( domSelectionCollapsed || data.shiftKey ) { | |
const domParent = domSelection.focusNode; | |
const domOffset = domSelection.focusOffset; | |
const viewPosition = domConverter.domPositionToView( domParent, domOffset ); | |
// In case if dom element is not converted to view or is not mapped or something. Happens for example in some tests. | |
if ( viewPosition === null ) { | |
return; | |
} | |
// Skip all following ui elements. | |
let jumpedOverAnyUiElement = false; | |
const nextViewPosition = viewPosition.getLastMatchingPosition( value => { | |
if ( value.item.is( 'uiElement' ) ) { | |
// Remember that there was at least one ui element. | |
jumpedOverAnyUiElement = true; | |
} | |
// Jump over ui elements, jump over empty attribute elements, move up from inside of attribute element. | |
if ( value.item.is( 'uiElement' ) || value.item.is( 'attributeElement' ) ) { | |
return true; | |
} | |
// Don't jump over text or don't get out of container element. | |
return false; | |
} ); | |
// If anything has been skipped, fix position. | |
// This `if` could be possibly omitted but maybe it is better not to mess with DOM selection if not needed. | |
if ( jumpedOverAnyUiElement ) { | |
const newDomPosition = domConverter.viewPositionToDom( nextViewPosition ); | |
if ( domSelectionCollapsed ) { | |
// Selection was collapsed, so collapse it at further position. | |
domSelection.collapse( newDomPosition.parent, newDomPosition.offset ); | |
} else { | |
// Selection was not collapse, so extend it instead of collapsing. | |
domSelection.extend( newDomPosition.parent, newDomPosition.offset ); | |
} | |
} | |
} | |
} | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/view/documentfragment.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/view/documentfragment | |
*/ | |
/** | |
* DocumentFragment class. | |
*/ | |
class view_documentfragment_DocumentFragment { | |
/** | |
* Creates new DocumentFragment instance. | |
* | |
* @param {module:engine/view/node~Node|Iterable.<module:engine/view/node~Node>} [children] List of nodes to be inserted into | |
* created document fragment. | |
*/ | |
constructor( children ) { | |
/** | |
* Array of child nodes. | |
* | |
* @protected | |
* @member {Array.<module:engine/view/element~Element>} module:engine/view/documentfragment~DocumentFragment#_children | |
*/ | |
this._children = []; | |
if ( children ) { | |
this.insertChildren( 0, children ); | |
} | |
} | |
/** | |
* Iterates over nodes added to this DocumentFragment. | |
*/ | |
[ Symbol.iterator ]() { | |
return this._children[ Symbol.iterator ](); | |
} | |
/** | |
* Number of child nodes in this document fragment. | |
* | |
* @readonly | |
* @type {Number} | |
*/ | |
get childCount() { | |
return this._children.length; | |
} | |
/** | |
* Is `true` if there are no nodes inside this document fragment, `false` otherwise. | |
* | |
* @readonly | |
* @type {Boolean} | |
*/ | |
get isEmpty() { | |
return this.childCount === 0; | |
} | |
/** | |
* Artificial root of `DocumentFragment`. Returns itself. Added for compatibility reasons. | |
* | |
* @readonly | |
* @type {module:engine/model/documentfragment~DocumentFragment} | |
*/ | |
get root() { | |
return this; | |
} | |
/** | |
* Artificial parent of `DocumentFragment`. Returns `null`. Added for compatibility reasons. | |
* | |
* @readonly | |
* @type {null} | |
*/ | |
get parent() { | |
return null; | |
} | |
/** | |
* Checks whether given view tree object is of given type. | |
* | |
* Read more in {@link module:engine/view/node~Node#is}. | |
* | |
* @param {String} type | |
* @returns {Boolean} | |
*/ | |
is( type ) { | |
return type == 'documentFragment'; | |
} | |
/** | |
* {@link module:engine/view/documentfragment~DocumentFragment#insertChildren Insert} a child node or a list of child nodes at the end | |
* and sets the parent of these nodes to this fragment. | |
* | |
* @param {module:engine/view/node~Node|Iterable.<module:engine/view/node~Node>} nodes Node or the list of nodes to be inserted. | |
* @returns {Number} Number of appended nodes. | |
*/ | |
appendChildren( nodes ) { | |
return this.insertChildren( this.childCount, nodes ); | |
} | |
/** | |
* Gets child at the given index. | |
* | |
* @param {Number} index Index of child. | |
* @returns {module:engine/view/node~Node} Child node. | |
*/ | |
getChild( index ) { | |
return this._children[ index ]; | |
} | |
/** | |
* Gets index of the given child node. Returns `-1` if child node is not found. | |
* | |
* @param {module:engine/view/node~Node} node Child node. | |
* @returns {Number} Index of the child node. | |
*/ | |
getChildIndex( node ) { | |
return this._children.indexOf( node ); | |
} | |
/** | |
* Gets child nodes iterator. | |
* | |
* @returns {Iterable.<module:engine/view/node~Node>} Child nodes iterator. | |
*/ | |
getChildren() { | |
return this._children[ Symbol.iterator ](); | |
} | |
/** | |
* Inserts a child node or a list of child nodes on the given index and sets the parent of these nodes to | |
* this fragment. | |
* | |
* @param {Number} index Position where nodes should be inserted. | |
* @param {module:engine/view/node~Node|Iterable.<module:engine/view/node~Node>} nodes Node or list of nodes to be inserted. | |
* @returns {Number} Number of inserted nodes. | |
*/ | |
insertChildren( index, nodes ) { | |
this._fireChange( 'children', this ); | |
let count = 0; | |
nodes = view_documentfragment_normalize( nodes ); | |
for ( const node of nodes ) { | |
// If node that is being added to this element is already inside another element, first remove it from the old parent. | |
if ( node.parent !== null ) { | |
node.remove(); | |
} | |
node.parent = this; | |
this._children.splice( index, 0, node ); | |
index++; | |
count++; | |
} | |
return count; | |
} | |
/** | |
* Removes number of child nodes starting at the given index and set the parent of these nodes to `null`. | |
* | |
* @param {Number} index Number of the first node to remove. | |
* @param {Number} [howMany=1] Number of nodes to remove. | |
* @returns {Array.<module:engine/view/node~Node>} The array of removed nodes. | |
*/ | |
removeChildren( index, howMany = 1 ) { | |
this._fireChange( 'children', this ); | |
for ( let i = index; i < index + howMany; i++ ) { | |
this._children[ i ].parent = null; | |
} | |
return this._children.splice( index, howMany ); | |
} | |
/** | |
* Fires `change` event with given type of the change. | |
* | |
* @private | |
* @param {module:engine/view/document~ChangeType} type Type of the change. | |
* @param {module:engine/view/node~Node} node Changed node. | |
* @fires module:engine/view/node~Node#change | |
*/ | |
_fireChange( type, node ) { | |
this.fire( 'change:' + type, node ); | |
} | |
} | |
mix( view_documentfragment_DocumentFragment, emittermixin ); | |
// Converts strings to Text and non-iterables to arrays. | |
// | |
// @param {String|module:engine/view/node~Node|Iterable.<String|module:engine/view/node~Node>} | |
// @return {Iterable.<module:engine/view/node~Node>} | |
function view_documentfragment_normalize( nodes ) { | |
// Separate condition because string is iterable. | |
if ( typeof nodes == 'string' ) { | |
return [ new view_text_Text( nodes ) ]; | |
} | |
if ( !isIterable( nodes ) ) { | |
nodes = [ nodes ]; | |
} | |
// Array.from to enable .map() on non-arrays. | |
return Array.from( nodes ) | |
.map( node => { | |
return typeof node == 'string' ? new view_text_Text( node ) : node; | |
} ); | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/view/writer.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module module:engine/view/writer | |
*/ | |
/** | |
* Contains functions used for composing view tree. | |
* | |
* @namespace writer | |
*/ | |
const writer = { | |
breakAttributes, | |
breakContainer, | |
mergeAttributes, | |
mergeContainers, | |
insert: writer_insert, | |
remove: writer_remove, | |
clear, | |
move: writer_move, | |
wrap, | |
wrapPosition, | |
unwrap, | |
rename | |
}; | |
/* harmony default export */ var view_writer = (writer); | |
/** | |
* Breaks attribute nodes at provided position or at boundaries of provided range. It breaks attribute elements inside | |
* up to a container element. | |
* | |
* In following examples `<p>` is a container, `<b>` and `<u>` are attribute nodes: | |
* | |
* <p>foo<b><u>bar{}</u></b></p> -> <p>foo<b><u>bar</u></b>[]</p> | |
* <p>foo<b><u>{}bar</u></b></p> -> <p>foo{}<b><u>bar</u></b></p> | |
* <p>foo<b><u>b{}ar</u></b></p> -> <p>foo<b><u>b</u></b>[]<b><u>ar</u></b></p> | |
* <p><b>fo{o</b><u>ba}r</u></p> -> <p><b>fo</b><b>o</b><u>ba</u><u>r</u></b></p> | |
* | |
* **Note:** {@link module:engine/view/documentfragment~DocumentFragment DocumentFragment} is treated like a container. | |
* | |
* **Note:** Difference between {@link module:engine/view/writer~writer.breakAttributes breakAttributes} and | |
* {@link module:engine/view/writer~writer.breakContainer breakContainer} is that `breakAttributes` breaks all | |
* {@link module:engine/view/attributeelement~AttributeElement attribute elements} that are ancestors of given `position`, up to the first | |
* encountered {@link module:engine/view/containerelement~ContainerElement container element}. `breakContainer` assumes that given | |
* `position` | |
* is directly in container element and breaks that container element. | |
* | |
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` | |
* when {@link module:engine/view/range~Range#start start} | |
* and {@link module:engine/view/range~Range#end end} positions of a passed range are not placed inside same parent container. | |
* | |
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-cannot-break-empty-element` | |
* when trying to break attributes | |
* inside {@link module:engine/view/emptyelement~EmptyElement EmptyElement}. | |
* | |
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-cannot-break-ui-element` | |
* when trying to break attributes | |
* inside {@link module:engine/view/uielement~UIElement UIElement}. | |
* | |
* @see module:engine/view/attributeelement~AttributeElement | |
* @see module:engine/view/containerelement~ContainerElement | |
* @see module:engine/view/writer~writer.breakContainer | |
* @function module:engine/view/writer~writer.breakAttributes | |
* @param {module:engine/view/position~Position|module:engine/view/range~Range} positionOrRange Position where to break attribute elements. | |
* @returns {module:engine/view/position~Position|module:engine/view/range~Range} New position or range, after breaking the attribute | |
* elements. | |
*/ | |
function breakAttributes( positionOrRange ) { | |
if ( positionOrRange instanceof view_position_Position ) { | |
return _breakAttributes( positionOrRange ); | |
} else { | |
return _breakAttributesRange( positionOrRange ); | |
} | |
} | |
/** | |
* Breaks {@link module:engine/view/containerelement~ContainerElement container view element} into two, at the given position. Position | |
* has to be directly inside container element and cannot be in root. Does not break if position is at the beginning | |
* or at the end of it's parent element. | |
* | |
* <p>foo^bar</p> -> <p>foo</p><p>bar</p> | |
* <div><p>foo</p>^<p>bar</p></div> -> <div><p>foo</p></div><div><p>bar</p></div> | |
* <p>^foobar</p> -> ^<p>foobar</p> | |
* <p>foobar^</p> -> <p>foobar</p>^ | |
* | |
* **Note:** Difference between {@link module:engine/view/writer~writer.breakAttributes breakAttributes} and | |
* {@link module:engine/view/writer~writer.breakContainer breakContainer} is that `breakAttributes` breaks all | |
* {@link module:engine/view/attributeelement~AttributeElement attribute elements} that are ancestors of given `position`, up to the first | |
* encountered {@link module:engine/view/containerelement~ContainerElement container element}. `breakContainer` assumes that given | |
* `position` | |
* is directly in container element and breaks that container element. | |
* | |
* @see module:engine/view/attributeelement~AttributeElement | |
* @see module:engine/view/containerelement~ContainerElement | |
* @see module:engine/view/writer~writer.breakAttributes | |
* @function module:engine/view/writer~writer.breakContainer | |
* @param {module:engine/view/position~Position} position Position where to break element. | |
* @returns {module:engine/view/position~Position} Position between broken elements. If element has not been broken, the returned position | |
* is placed either before it or after it. | |
*/ | |
function breakContainer( position ) { | |
const element = position.parent; | |
if ( !( element.is( 'containerElement' ) ) ) { | |
/** | |
* Trying to break an element which is not a container element. | |
* | |
* @error view-writer-break-non-container-element | |
*/ | |
throw new CKEditorError( 'view-writer-break-non-container-element: Trying to break an element which is not a container element.' ); | |
} | |
if ( !element.parent ) { | |
/** | |
* Trying to break root element. | |
* | |
* @error view-writer-break-root | |
*/ | |
throw new CKEditorError( 'view-writer-break-root: Trying to break root element.' ); | |
} | |
if ( position.isAtStart ) { | |
return view_position_Position.createBefore( element ); | |
} else if ( !position.isAtEnd ) { | |
const newElement = element.clone( false ); | |
writer_insert( view_position_Position.createAfter( element ), newElement ); | |
const sourceRange = new view_range_Range( position, view_position_Position.createAt( element, 'end' ) ); | |
const targetPosition = new view_position_Position( newElement, 0 ); | |
writer_move( sourceRange, targetPosition ); | |
} | |
return view_position_Position.createAfter( element ); | |
} | |
/** | |
* Merges {@link module:engine/view/attributeelement~AttributeElement attribute elements}. It also merges text nodes if needed. | |
* Only {@link module:engine/view/attributeelement~AttributeElement#isSimilar similar} attribute elements can be merged. | |
* | |
* In following examples `<p>` is a container and `<b>` is an attribute element: | |
* | |
* <p>foo[]bar</p> -> <p>foo{}bar</p> | |
* <p><b>foo</b>[]<b>bar</b></p> -> <p><b>foo{}bar</b></p> | |
* <p><b foo="bar">a</b>[]<b foo="baz">b</b></p> -> <p><b foo="bar">a</b>[]<b foo="baz">b</b></p> | |
* | |
* It will also take care about empty attributes when merging: | |
* | |
* <p><b>[]</b></p> -> <p>[]</p> | |
* <p><b>foo</b><i>[]</i><b>bar</b></p> -> <p><b>foo{}bar</b></p> | |
* | |
* **Note:** Difference between {@link module:engine/view/writer~writer.mergeAttributes mergeAttributes} and | |
* {@link module:engine/view/writer~writer.mergeContainers mergeContainers} is that `mergeAttributes` merges two | |
* {@link module:engine/view/attributeelement~AttributeElement attribute elements} or {@link module:engine/view/text~Text text nodes} | |
* while `mergeContainer` merges two {@link module:engine/view/containerelement~ContainerElement container elements}. | |
* | |
* @see module:engine/view/attributeelement~AttributeElement | |
* @see module:engine/view/containerelement~ContainerElement | |
* @see module:engine/view/writer~writer.mergeContainers | |
* @function module:engine/view/writer~writer.mergeAttributes | |
* @param {module:engine/view/position~Position} position Merge position. | |
* @returns {module:engine/view/position~Position} Position after merge. | |
*/ | |
function mergeAttributes( position ) { | |
const positionOffset = position.offset; | |
const positionParent = position.parent; | |
// When inside text node - nothing to merge. | |
if ( positionParent.is( 'text' ) ) { | |
return position; | |
} | |
// When inside empty attribute - remove it. | |
if ( positionParent.is( 'attributeElement' ) && positionParent.childCount === 0 ) { | |
const parent = positionParent.parent; | |
const offset = positionParent.index; | |
positionParent.remove(); | |
return mergeAttributes( new view_position_Position( parent, offset ) ); | |
} | |
const nodeBefore = positionParent.getChild( positionOffset - 1 ); | |
const nodeAfter = positionParent.getChild( positionOffset ); | |
// Position should be placed between two nodes. | |
if ( !nodeBefore || !nodeAfter ) { | |
return position; | |
} | |
// When position is between two text nodes. | |
if ( nodeBefore.is( 'text' ) && nodeAfter.is( 'text' ) ) { | |
return mergeTextNodes( nodeBefore, nodeAfter ); | |
} | |
// When selection is between two same attribute elements. | |
else if ( nodeBefore.is( 'attributeElement' ) && nodeAfter.is( 'attributeElement' ) && nodeBefore.isSimilar( nodeAfter ) ) { | |
// Move all children nodes from node placed after selection and remove that node. | |
const count = nodeBefore.childCount; | |
nodeBefore.appendChildren( nodeAfter.getChildren() ); | |
nodeAfter.remove(); | |
// New position is located inside the first node, before new nodes. | |
// Call this method recursively to merge again if needed. | |
return mergeAttributes( new view_position_Position( nodeBefore, count ) ); | |
} | |
return position; | |
} | |
/** | |
* Merges two {@link module:engine/view/containerelement~ContainerElement container elements} that are before and after given position. | |
* Precisely, the element after the position is removed and it's contents are moved to element before the position. | |
* | |
* <p>foo</p>^<p>bar</p> -> <p>foo^bar</p> | |
* <div>foo</div>^<p>bar</p> -> <div>foo^bar</div> | |
* | |
* **Note:** Difference between {@link module:engine/view/writer~writer.mergeAttributes mergeAttributes} and | |
* {@link module:engine/view/writer~writer.mergeContainers mergeContainers} is that `mergeAttributes` merges two | |
* {@link module:engine/view/attributeelement~AttributeElement attribute elements} or {@link module:engine/view/text~Text text nodes} | |
* while `mergeContainer` merges two {@link module:engine/view/containerelement~ContainerElement container elements}. | |
* | |
* @see module:engine/view/attributeelement~AttributeElement | |
* @see module:engine/view/containerelement~ContainerElement | |
* @see module:engine/view/writer~writer.mergeAttributes | |
* @function module:engine/view/writer~writer.mergeContainers | |
* @param {module:engine/view/position~Position} position Merge position. | |
* @returns {module:engine/view/position~Position} Position after merge. | |
*/ | |
function mergeContainers( position ) { | |
const prev = position.nodeBefore; | |
const next = position.nodeAfter; | |
if ( !prev || !next || !prev.is( 'containerElement' ) || !next.is( 'containerElement' ) ) { | |
/** | |
* Element before and after given position cannot be merged. | |
* | |
* @error view-writer-merge-containers-invalid-position | |
*/ | |
throw new CKEditorError( 'view-writer-merge-containers-invalid-position: ' + | |
'Element before and after given position cannot be merged.' ); | |
} | |
const lastChild = prev.getChild( prev.childCount - 1 ); | |
const newPosition = lastChild instanceof view_text_Text ? view_position_Position.createAt( lastChild, 'end' ) : view_position_Position.createAt( prev, 'end' ); | |
writer_move( view_range_Range.createIn( next ), view_position_Position.createAt( prev, 'end' ) ); | |
writer_remove( view_range_Range.createOn( next ) ); | |
return newPosition; | |
} | |
/** | |
* Insert node or nodes at specified position. Takes care about breaking attributes before insertion | |
* and merging them afterwards. | |
* | |
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-insert-invalid-node` when nodes to insert | |
* contains instances that are not {@link module:engine/view/text~Text Texts}, | |
* {@link module:engine/view/attributeelement~AttributeElement AttributeElements}, | |
* {@link module:engine/view/containerelement~ContainerElement ContainerElements}, | |
* {@link module:engine/view/emptyelement~EmptyElement EmptyElements} or | |
* {@link module:engine/view/uielement~UIElement UIElements}. | |
* | |
* @function insert | |
* @param {module:engine/view/position~Position} position Insertion position. | |
* @param {module:engine/view/text~Text|module:engine/view/attributeelement~AttributeElement| | |
* module:engine/view/containerelement~ContainerElement|module:engine/view/emptyelement~EmptyElement| | |
* module:engine/view/uielement~UIElement|Iterable.<module:engine/view/text~Text| | |
* module:engine/view/attributeelement~AttributeElement|module:engine/view/containerelement~ContainerElement| | |
* module:engine/view/emptyelement~EmptyElement|module:engine/view/uielement~UIElement>} nodes Node or nodes to insert. | |
* @returns {module:engine/view/range~Range} Range around inserted nodes. | |
*/ | |
function writer_insert( position, nodes ) { | |
nodes = isIterable( nodes ) ? [ ...nodes ] : [ nodes ]; | |
// Check if nodes to insert are instances of AttributeElements, ContainerElements, EmptyElements, UIElements or Text. | |
validateNodesToInsert( nodes ); | |
const container = getParentContainer( position ); | |
if ( !container ) { | |
/** | |
* Position's parent container cannot be found. | |
* | |
* @error view-writer-invalid-position-container | |
*/ | |
throw new CKEditorError( 'view-writer-invalid-position-container' ); | |
} | |
const insertionPosition = _breakAttributes( position, true ); | |
const length = container.insertChildren( insertionPosition.offset, nodes ); | |
const endPosition = insertionPosition.getShiftedBy( length ); | |
const start = mergeAttributes( insertionPosition ); | |
// When no nodes were inserted - return collapsed range. | |
if ( length === 0 ) { | |
return new view_range_Range( start, start ); | |
} else { | |
// If start position was merged - move end position. | |
if ( !start.isEqual( insertionPosition ) ) { | |
endPosition.offset--; | |
} | |
const end = mergeAttributes( endPosition ); | |
return new view_range_Range( start, end ); | |
} | |
} | |
/** | |
* Removes provided range from the container. | |
* | |
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when | |
* {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside | |
* same parent container. | |
* | |
* @function module:engine/view/writer~writer.remove | |
* @param {module:engine/view/range~Range} range Range to remove from container. After removing, it will be updated | |
* to a collapsed range showing the new position. | |
* @returns {module:engine/view/documentfragment~DocumentFragment} Document fragment containing removed nodes. | |
*/ | |
function writer_remove( range ) { | |
validateRangeContainer( range ); | |
// If range is collapsed - nothing to remove. | |
if ( range.isCollapsed ) { | |
return new view_documentfragment_DocumentFragment(); | |
} | |
// Break attributes at range start and end. | |
const { start: breakStart, end: breakEnd } = _breakAttributesRange( range, true ); | |
const parentContainer = breakStart.parent; | |
const count = breakEnd.offset - breakStart.offset; | |
// Remove nodes in range. | |
const removed = parentContainer.removeChildren( breakStart.offset, count ); | |
// Merge after removing. | |
const mergePosition = mergeAttributes( breakStart ); | |
range.start = mergePosition; | |
range.end = view_position_Position.createFromPosition( mergePosition ); | |
// Return removed nodes. | |
return new view_documentfragment_DocumentFragment( removed ); | |
} | |
/** | |
* Removes matching elements from given range. | |
* | |
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when | |
* {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside | |
* same parent container. | |
* | |
* @function module:engine/view/writer~writer.clear | |
* @param {module:engine/view/range~Range} range Range to clear. | |
* @param {module:engine/view/element~Element} element Element to remove. | |
*/ | |
function clear( range, element ) { | |
validateRangeContainer( range ); | |
// Create walker on given range. | |
// We walk backward because when we remove element during walk it modifies range end position. | |
const walker = range.getWalker( { | |
direction: 'backward', | |
ignoreElementEnd: true | |
} ); | |
// Let's walk. | |
for ( const current of walker ) { | |
const item = current.item; | |
let rangeToRemove; | |
// When current item matches to the given element. | |
if ( item.is( 'element' ) && element.isSimilar( item ) ) { | |
// Create range on this element. | |
rangeToRemove = view_range_Range.createOn( item ); | |
// When range starts inside Text or TextProxy element. | |
} else if ( !current.nextPosition.isAfter( range.start ) && ( item.is( 'text' ) || item.is( 'textProxy' ) ) ) { | |
// We need to check if parent of this text matches to given element. | |
const parentElement = item.getAncestors().find( ancestor => { | |
return ancestor.is( 'element' ) && element.isSimilar( ancestor ); | |
} ); | |
// If it is then create range inside this element. | |
if ( parentElement ) { | |
rangeToRemove = view_range_Range.createIn( parentElement ); | |
} | |
} | |
// If we have found element to remove. | |
if ( rangeToRemove ) { | |
// We need to check if element range stick out of the given range and truncate if it is. | |
if ( rangeToRemove.end.isAfter( range.end ) ) { | |
rangeToRemove.end = range.end; | |
} | |
if ( rangeToRemove.start.isBefore( range.start ) ) { | |
rangeToRemove.start = range.start; | |
} | |
// At the end we remove range with found element. | |
writer_remove( rangeToRemove ); | |
} | |
} | |
} | |
/** | |
* Moves nodes from provided range to target position. | |
* | |
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when | |
* {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside | |
* same parent container. | |
* | |
* @function module:engine/view/writer~writer.move | |
* @param {module:engine/view/range~Range} sourceRange Range containing nodes to move. | |
* @param {module:engine/view/position~Position} targetPosition Position to insert. | |
* @returns {module:engine/view/range~Range} Range in target container. Inserted nodes are placed between | |
* {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions. | |
*/ | |
function writer_move( sourceRange, targetPosition ) { | |
let nodes; | |
if ( targetPosition.isAfter( sourceRange.end ) ) { | |
targetPosition = _breakAttributes( targetPosition, true ); | |
const parent = targetPosition.parent; | |
const countBefore = parent.childCount; | |
sourceRange = _breakAttributesRange( sourceRange, true ); | |
nodes = writer_remove( sourceRange ); | |
targetPosition.offset += ( parent.childCount - countBefore ); | |
} else { | |
nodes = writer_remove( sourceRange ); | |
} | |
return writer_insert( targetPosition, nodes ); | |
} | |
/** | |
* Wraps elements within range with provided {@link module:engine/view/attributeelement~AttributeElement AttributeElement}. | |
* | |
* Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-invalid-range-container` | |
* when {@link module:engine/view/range~Range#start} | |
* and {@link module:engine/view/range~Range#end} positions are not placed inside same parent container. | |
* Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-invalid-attribute` when passed attribute element is not | |
* an instance of {module:engine/view/attributeelement~AttributeElement AttributeElement}. | |
* | |
* @function module:engine/view/writer~writer.wrap | |
* @param {module:engine/view/range~Range} range Range to wrap. | |
* @param {module:engine/view/attributeelement~AttributeElement} attribute Attribute element to use as wrapper. | |
*/ | |
function wrap( range, attribute ) { | |
if ( !( attribute instanceof AttributeElement ) ) { | |
throw new CKEditorError( 'view-writer-wrap-invalid-attribute' ); | |
} | |
validateRangeContainer( range ); | |
// If range is collapsed - nothing to wrap. | |
if ( range.isCollapsed ) { | |
return range; | |
} | |
// Range around one element. | |
if ( range.end.isEqual( range.start.getShiftedBy( 1 ) ) ) { | |
const node = range.start.nodeAfter; | |
if ( node instanceof AttributeElement && wrapAttributeElement( attribute, node ) ) { | |
return range; | |
} | |
} | |
// Range is inside single attribute and spans on all children. | |
if ( rangeSpansOnAllChildren( range ) && wrapAttributeElement( attribute, range.start.parent ) ) { | |
const parent = range.start.parent.parent; | |
const index = range.start.parent.index; | |
return view_range_Range.createFromParentsAndOffsets( parent, index, parent, index + 1 ); | |
} | |
// Break attributes at range start and end. | |
const { start: breakStart, end: breakEnd } = _breakAttributesRange( range, true ); | |
const parentContainer = breakStart.parent; | |
// Unwrap children located between break points. | |
const unwrappedRange = unwrapChildren( parentContainer, breakStart.offset, breakEnd.offset, attribute ); | |
// Wrap all children with attribute. | |
const newRange = wrapChildren( parentContainer, unwrappedRange.start.offset, unwrappedRange.end.offset, attribute ); | |
// Merge attributes at the both ends and return a new range. | |
const start = mergeAttributes( newRange.start ); | |
// If start position was merged - move end position back. | |
if ( !start.isEqual( newRange.start ) ) { | |
newRange.end.offset--; | |
} | |
const end = mergeAttributes( newRange.end ); | |
return new view_range_Range( start, end ); | |
} | |
/** | |
* Wraps position with provided attribute. Returns new position after wrapping. This method will also merge newly | |
* added attribute with its siblings whenever possible. | |
* | |
* Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-invalid-attribute` when passed attribute element is not | |
* an instance of {module:engine/view/attributeelement~AttributeElement AttributeElement}. | |
* | |
* @param {module:engine/view/position~Position} position | |
* @param {module:engine/view/attributeelement~AttributeElement} attribute | |
* @returns {module:engine/view/position~Position} New position after wrapping. | |
*/ | |
function wrapPosition( position, attribute ) { | |
if ( !( attribute instanceof AttributeElement ) ) { | |
throw new CKEditorError( 'view-writer-wrap-invalid-attribute' ); | |
} | |
// Return same position when trying to wrap with attribute similar to position parent. | |
if ( attribute.isSimilar( position.parent ) ) { | |
return movePositionToTextNode( view_position_Position.createFromPosition( position ) ); | |
} | |
// When position is inside text node - break it and place new position between two text nodes. | |
if ( position.parent.is( 'text' ) ) { | |
position = breakTextNode( position ); | |
} | |
// Create fake element that will represent position, and will not be merged with other attributes. | |
const fakePosition = new AttributeElement(); | |
fakePosition.priority = Number.POSITIVE_INFINITY; | |
fakePosition.isSimilar = () => false; | |
// Insert fake element in position location. | |
position.parent.insertChildren( position.offset, fakePosition ); | |
// Range around inserted fake attribute element. | |
const wrapRange = new view_range_Range( position, position.getShiftedBy( 1 ) ); | |
// Wrap fake element with attribute (it will also merge if possible). | |
wrap( wrapRange, attribute ); | |
// Remove fake element and place new position there. | |
const newPosition = new view_position_Position( fakePosition.parent, fakePosition.index ); | |
fakePosition.remove(); | |
// If position is placed between text nodes - merge them and return position inside. | |
const nodeBefore = newPosition.nodeBefore; | |
const nodeAfter = newPosition.nodeAfter; | |
if ( nodeBefore instanceof view_text_Text && nodeAfter instanceof view_text_Text ) { | |
return mergeTextNodes( nodeBefore, nodeAfter ); | |
} | |
// If position is next to text node - move position inside. | |
return movePositionToTextNode( newPosition ); | |
} | |
/** | |
* Unwraps nodes within provided range from attribute element. | |
* | |
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when | |
* {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside | |
* same parent container. | |
* | |
* @param {module:engine/view/range~Range} range | |
* @param {module:engine/view/attributeelement~AttributeElement} element | |
*/ | |
function unwrap( range, attribute ) { | |
if ( !( attribute instanceof AttributeElement ) ) { | |
/** | |
* Attribute element need to be instance of attribute element. | |
* | |
* @error view-writer-unwrap-invalid-attribute | |
*/ | |
throw new CKEditorError( 'view-writer-unwrap-invalid-attribute' ); | |
} | |
validateRangeContainer( range ); | |
// If range is collapsed - nothing to unwrap. | |
if ( range.isCollapsed ) { | |
return range; | |
} | |
// Range around one element - check if AttributeElement can be unwrapped partially when it's not similar. | |
// For example: | |
// <b class="foo bar" title="baz"></b> unwrap with: <b class="foo"></p> result: <b class"bar" title="baz"></b> | |
if ( range.end.isEqual( range.start.getShiftedBy( 1 ) ) ) { | |
const node = range.start.nodeAfter; | |
// Unwrap single attribute element. | |
if ( !attribute.isSimilar( node ) && node instanceof AttributeElement && unwrapAttributeElement( attribute, node ) ) { | |
return range; | |
} | |
} | |
// Break attributes at range start and end. | |
const { start: breakStart, end: breakEnd } = _breakAttributesRange( range, true ); | |
const parentContainer = breakStart.parent; | |
// Unwrap children located between break points. | |
const newRange = unwrapChildren( parentContainer, breakStart.offset, breakEnd.offset, attribute ); | |
// Merge attributes at the both ends and return a new range. | |
const start = mergeAttributes( newRange.start ); | |
// If start position was merged - move end position back. | |
if ( !start.isEqual( newRange.start ) ) { | |
newRange.end.offset--; | |
} | |
const end = mergeAttributes( newRange.end ); | |
return new view_range_Range( start, end ); | |
} | |
/** | |
* Renames element by creating a copy of renamed element but with changed name and then moving contents of the | |
* old element to the new one. Keep in mind that this will invalidate all {@link module:engine/view/position~Position positions} which | |
* has renamed element as {@link module:engine/view/position~Position#parent a parent}. | |
* | |
* New element has to be created because `Element#tagName` property in DOM is readonly. | |
* | |
* Since this function creates a new element and removes the given one, the new element is returned to keep reference. | |
* | |
* @param {module:engine/view/containerelement~ContainerElement} viewElement Element to be renamed. | |
* @param {String} newName New name for element. | |
*/ | |
function rename( viewElement, newName ) { | |
const newElement = new ContainerElement( newName, viewElement.getAttributes() ); | |
writer_insert( view_position_Position.createAfter( viewElement ), newElement ); | |
writer_move( view_range_Range.createIn( viewElement ), view_position_Position.createAt( newElement ) ); | |
writer_remove( view_range_Range.createOn( viewElement ) ); | |
return newElement; | |
} | |
/** | |
* Attribute element need to be instance of attribute element. | |
* | |
* @error view-writer-wrap-invalid-attribute | |
*/ | |
// Returns first parent container of specified {@link module:engine/view/position~Position Position}. | |
// Position's parent node is checked as first, then next parents are checked. | |
// Note that {@link module:engine/view/documentfragment~DocumentFragment DocumentFragment} is treated like a container. | |
// | |
// @param {module:engine/view/position~Position} position Position used as a start point to locate parent container. | |
// @returns {module:engine/view/containerelement~ContainerElement|module:engine/view/documentfragment~DocumentFragment|undefined} | |
// Parent container element or `undefined` if container is not found. | |
function getParentContainer( position ) { | |
let parent = position.parent; | |
while ( !isContainerOrFragment( parent ) ) { | |
if ( !parent ) { | |
return undefined; | |
} | |
parent = parent.parent; | |
} | |
return parent; | |
} | |
// Function used by both public breakAttributes (without splitting text nodes) and by other methods (with | |
// splitting text nodes). | |
// | |
// @param {module:engine/view/range~Range} range Range which `start` and `end` positions will be used to break attributes. | |
// @param {Boolean} [forceSplitText = false] If set to `true`, will break text nodes even if they are directly in | |
// container element. This behavior will result in incorrect view state, but is needed by other view writing methods | |
// which then fixes view state. Defaults to `false`. | |
// @returns {module:engine/view/range~Range} New range with located at break positions. | |
function _breakAttributesRange( range, forceSplitText = false ) { | |
const rangeStart = range.start; | |
const rangeEnd = range.end; | |
validateRangeContainer( range ); | |
// Break at the collapsed position. Return new collapsed range. | |
if ( range.isCollapsed ) { | |
const position = _breakAttributes( range.start, forceSplitText ); | |
return new view_range_Range( position, position ); | |
} | |
const breakEnd = _breakAttributes( rangeEnd, forceSplitText ); | |
const count = breakEnd.parent.childCount; | |
const breakStart = _breakAttributes( rangeStart, forceSplitText ); | |
// Calculate new break end offset. | |
breakEnd.offset += breakEnd.parent.childCount - count; | |
return new view_range_Range( breakStart, breakEnd ); | |
} | |
// Function used by public breakAttributes (without splitting text nodes) and by other methods (with | |
// splitting text nodes). | |
// | |
// Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-cannot-break-empty-element` when break position | |
// is placed inside {@link module:engine/view/emptyelement~EmptyElement EmptyElement}. | |
// | |
// Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-cannot-break-ui-element` when break position | |
// is placed inside {@link module:engine/view/uielement~UIElement UIElement}. | |
// | |
// @param {module:engine/view/position~Position} position Position where to break attributes. | |
// @param {Boolean} [forceSplitText = false] If set to `true`, will break text nodes even if they are directly in | |
// container element. This behavior will result in incorrect view state, but is needed by other view writing methods | |
// which then fixes view state. Defaults to `false`. | |
// @returns {module:engine/view/position~Position} New position after breaking the attributes. | |
function _breakAttributes( position, forceSplitText = false ) { | |
const positionOffset = position.offset; | |
const positionParent = position.parent; | |
// If position is placed inside EmptyElement - throw an exception as we cannot break inside. | |
if ( position.parent.is( 'emptyElement' ) ) { | |
/** | |
* Cannot break inside EmptyElement instance. | |
* | |
* @error view-writer-cannot-break-empty-element | |
*/ | |
throw new CKEditorError( 'view-writer-cannot-break-empty-element' ); | |
} | |
// If position is placed inside UIElement - throw an exception as we cannot break inside. | |
if ( position.parent.is( 'uiElement' ) ) { | |
/** | |
* Cannot break inside UIElement instance. | |
* | |
* @error view-writer-cannot-break-ui-element | |
*/ | |
throw new CKEditorError( 'view-writer-cannot-break-ui-element' ); | |
} | |
// There are no attributes to break and text nodes breaking is not forced. | |
if ( !forceSplitText && positionParent.is( 'text' ) && isContainerOrFragment( positionParent.parent ) ) { | |
return view_position_Position.createFromPosition( position ); | |
} | |
// Position's parent is container, so no attributes to break. | |
if ( isContainerOrFragment( positionParent ) ) { | |
return view_position_Position.createFromPosition( position ); | |
} | |
// Break text and start again in new position. | |
if ( positionParent.is( 'text' ) ) { | |
return _breakAttributes( breakTextNode( position ), forceSplitText ); | |
} | |
const length = positionParent.childCount; | |
// <p>foo<b><u>bar{}</u></b></p> | |
// <p>foo<b><u>bar</u>[]</b></p> | |
// <p>foo<b><u>bar</u></b>[]</p> | |
if ( positionOffset == length ) { | |
const newPosition = new view_position_Position( positionParent.parent, positionParent.index + 1 ); | |
return _breakAttributes( newPosition, forceSplitText ); | |
} else | |
// <p>foo<b><u>{}bar</u></b></p> | |
// <p>foo<b>[]<u>bar</u></b></p> | |
// <p>foo{}<b><u>bar</u></b></p> | |
if ( positionOffset === 0 ) { | |
const newPosition = new view_position_Position( positionParent.parent, positionParent.index ); | |
return _breakAttributes( newPosition, forceSplitText ); | |
} | |
// <p>foo<b><u>b{}ar</u></b></p> | |
// <p>foo<b><u>b[]ar</u></b></p> | |
// <p>foo<b><u>b</u>[]<u>ar</u></b></p> | |
// <p>foo<b><u>b</u></b>[]<b><u>ar</u></b></p> | |
else { | |
const offsetAfter = positionParent.index + 1; | |
// Break element. | |
const clonedNode = positionParent.clone(); | |
// Insert cloned node to position's parent node. | |
positionParent.parent.insertChildren( offsetAfter, clonedNode ); | |
// Get nodes to move. | |
const count = positionParent.childCount - positionOffset; | |
const nodesToMove = positionParent.removeChildren( positionOffset, count ); | |
// Move nodes to cloned node. | |
clonedNode.appendChildren( nodesToMove ); | |
// Create new position to work on. | |
const newPosition = new view_position_Position( positionParent.parent, offsetAfter ); | |
return _breakAttributes( newPosition, forceSplitText ); | |
} | |
} | |
// Unwraps children from provided `attribute`. Only children contained in `parent` element between | |
// `startOffset` and `endOffset` will be unwrapped. | |
// | |
// @param {module:engine/view/element~Element} parent | |
// @param {Number} startOffset | |
// @param {Number} endOffset | |
// @param {module:engine/view/element~Element} attribute | |
function unwrapChildren( parent, startOffset, endOffset, attribute ) { | |
let i = startOffset; | |
const unwrapPositions = []; | |
// Iterate over each element between provided offsets inside parent. | |
while ( i < endOffset ) { | |
const child = parent.getChild( i ); | |
// If attributes are the similar, then unwrap. | |
if ( child.isSimilar( attribute ) ) { | |
const unwrapped = child.getChildren(); | |
const count = child.childCount; | |
// Replace wrapper element with its children | |
child.remove(); | |
parent.insertChildren( i, unwrapped ); | |
// Save start and end position of moved items. | |
unwrapPositions.push( | |
new view_position_Position( parent, i ), | |
new view_position_Position( parent, i + count ) | |
); | |
// Skip elements that were unwrapped. Assuming that there won't be another element to unwrap in child | |
// elements. | |
i += count; | |
endOffset += count - 1; | |
} else { | |
// If other nested attribute is found start unwrapping there. | |
if ( child.is( 'attributeElement' ) ) { | |
unwrapChildren( child, 0, child.childCount, attribute ); | |
} | |
i++; | |
} | |
} | |
// Merge at each unwrap. | |
let offsetChange = 0; | |
for ( const position of unwrapPositions ) { | |
position.offset -= offsetChange; | |
// Do not merge with elements outside selected children. | |
if ( position.offset == startOffset || position.offset == endOffset ) { | |
continue; | |
} | |
const newPosition = mergeAttributes( position ); | |
// If nodes were merged - other merge offsets will change. | |
if ( !newPosition.isEqual( position ) ) { | |
offsetChange++; | |
endOffset--; | |
} | |
} | |
return view_range_Range.createFromParentsAndOffsets( parent, startOffset, parent, endOffset ); | |
} | |
// Wraps children with provided `attribute`. Only children contained in `parent` element between | |
// `startOffset` and `endOffset` will be wrapped. | |
// | |
// @param {module:engine/view/element~Element} parent | |
// @param {Number} startOffset | |
// @param {Number} endOffset | |
// @param {module:engine/view/element~Element} attribute | |
function wrapChildren( parent, startOffset, endOffset, attribute ) { | |
let i = startOffset; | |
const wrapPositions = []; | |
while ( i < endOffset ) { | |
const child = parent.getChild( i ); | |
const isText = child.is( 'text' ); | |
const isAttribute = child.is( 'attributeElement' ); | |
const isEmpty = child.is( 'emptyElement' ); | |
const isUI = child.is( 'uiElement' ); | |
// Wrap text, empty elements, ui elements or attributes with higher or equal priority. | |
if ( isText || isEmpty || isUI || ( isAttribute && shouldABeOutsideB( attribute, child ) ) ) { | |
// Clone attribute. | |
const newAttribute = attribute.clone(); | |
// Wrap current node with new attribute; | |
child.remove(); | |
newAttribute.appendChildren( child ); | |
parent.insertChildren( i, newAttribute ); | |
wrapPositions.push( new view_position_Position( parent, i ) ); | |
} | |
// If other nested attribute is found start wrapping there. | |
else if ( isAttribute ) { | |
wrapChildren( child, 0, child.childCount, attribute ); | |
} | |
i++; | |
} | |
// Merge at each wrap. | |
let offsetChange = 0; | |
for ( const position of wrapPositions ) { | |
position.offset -= offsetChange; | |
// Do not merge with elements outside selected children. | |
if ( position.offset == startOffset ) { | |
continue; | |
} | |
const newPosition = mergeAttributes( position ); | |
// If nodes were merged - other merge offsets will change. | |
if ( !newPosition.isEqual( position ) ) { | |
offsetChange++; | |
endOffset--; | |
} | |
} | |
return view_range_Range.createFromParentsAndOffsets( parent, startOffset, parent, endOffset ); | |
} | |
// Checks if first {@link module:engine/view/attributeelement~AttributeElement AttributeElement} provided to the function | |
// can be wrapped otuside second element. It is done by comparing elements' | |
// {@link module:engine/view/attributeelement~AttributeElement#priority priorities}, if both have same priority | |
// {@link module:engine/view/element~Element#getIdentity identities} are compared. | |
// | |
// @param {module:engine/view/attributeelement~AttributeElement} a | |
// @param {module:engine/view/attributeelement~AttributeElement} b | |
// @returns {Boolean} | |
function shouldABeOutsideB( a, b ) { | |
if ( a.priority < b.priority ) { | |
return true; | |
} else if ( a.priority > b.priority ) { | |
return false; | |
} | |
// When priorities are equal and names are different - use identities. | |
return a.getIdentity() < b.getIdentity(); | |
} | |
// Returns new position that is moved to near text node. Returns same position if there is no text node before of after | |
// specified position. | |
// | |
// <p>foo[]</p> -> <p>foo{}</p> | |
// <p>[]foo</p> -> <p>{}foo</p> | |
// | |
// @param {module:engine/view/position~Position} position | |
// @returns {module:engine/view/position~Position} Position located inside text node or same position if there is no text nodes | |
// before or after position location. | |
function movePositionToTextNode( position ) { | |
const nodeBefore = position.nodeBefore; | |
if ( nodeBefore && nodeBefore.is( 'text' ) ) { | |
return new view_position_Position( nodeBefore, nodeBefore.data.length ); | |
} | |
const nodeAfter = position.nodeAfter; | |
if ( nodeAfter && nodeAfter.is( 'text' ) ) { | |
return new view_position_Position( nodeAfter, 0 ); | |
} | |
return position; | |
} | |
// Breaks text node into two text nodes when possible. | |
// | |
// <p>foo{}bar</p> -> <p>foo[]bar</p> | |
// <p>{}foobar</p> -> <p>[]foobar</p> | |
// <p>foobar{}</p> -> <p>foobar[]</p> | |
// | |
// @param {module:engine/view/position~Position} position Position that need to be placed inside text node. | |
// @returns {module:engine/view/position~Position} New position after breaking text node. | |
function breakTextNode( position ) { | |
if ( position.offset == position.parent.data.length ) { | |
return new view_position_Position( position.parent.parent, position.parent.index + 1 ); | |
} | |
if ( position.offset === 0 ) { | |
return new view_position_Position( position.parent.parent, position.parent.index ); | |
} | |
// Get part of the text that need to be moved. | |
const textToMove = position.parent.data.slice( position.offset ); | |
// Leave rest of the text in position's parent. | |
position.parent.data = position.parent.data.slice( 0, position.offset ); | |
// Insert new text node after position's parent text node. | |
position.parent.parent.insertChildren( position.parent.index + 1, new view_text_Text( textToMove ) ); | |
// Return new position between two newly created text nodes. | |
return new view_position_Position( position.parent.parent, position.parent.index + 1 ); | |
} | |
// Merges two text nodes into first node. Removes second node and returns merge position. | |
// | |
// @param {module:engine/view/text~Text} t1 First text node to merge. Data from second text node will be moved at the end of | |
// this text node. | |
// @param {module:engine/view/text~Text} t2 Second text node to merge. This node will be removed after merging. | |
// @returns {module:engine/view/position~Position} Position after merging text nodes. | |
function mergeTextNodes( t1, t2 ) { | |
// Merge text data into first text node and remove second one. | |
const nodeBeforeLength = t1.data.length; | |
t1.data += t2.data; | |
t2.remove(); | |
return new view_position_Position( t1, nodeBeforeLength ); | |
} | |
// Wraps one {@link module:engine/view/attributeelement~AttributeElement AttributeElement} into another by merging them if possible. | |
// When merging is possible - all attributes, styles and classes are moved from wrapper element to element being | |
// wrapped. | |
// | |
// @param {module:engine/view/attributeelement~AttributeElement} wrapper Wrapper AttributeElement. | |
// @param {module:engine/view/attributeelement~AttributeElement} toWrap AttributeElement to wrap using wrapper element. | |
// @returns {Boolean} Returns `true` if elements are merged. | |
function wrapAttributeElement( wrapper, toWrap ) { | |
// Can't merge if name or priority differs. | |
if ( wrapper.name !== toWrap.name || wrapper.priority !== toWrap.priority ) { | |
return false; | |
} | |
// Check if attributes can be merged. | |
for ( const key of wrapper.getAttributeKeys() ) { | |
// Classes and styles should be checked separately. | |
if ( key === 'class' || key === 'style' ) { | |
continue; | |
} | |
// If some attributes are different we cannot wrap. | |
if ( toWrap.hasAttribute( key ) && toWrap.getAttribute( key ) !== wrapper.getAttribute( key ) ) { | |
return false; | |
} | |
} | |
// Check if styles can be merged. | |
for ( const key of wrapper.getStyleNames() ) { | |
if ( toWrap.hasStyle( key ) && toWrap.getStyle( key ) !== wrapper.getStyle( key ) ) { | |
return false; | |
} | |
} | |
// Move all attributes/classes/styles from wrapper to wrapped AttributeElement. | |
for ( const key of wrapper.getAttributeKeys() ) { | |
// Classes and styles should be checked separately. | |
if ( key === 'class' || key === 'style' ) { | |
continue; | |
} | |
// Move only these attributes that are not present - other are similar. | |
if ( !toWrap.hasAttribute( key ) ) { | |
toWrap.setAttribute( key, wrapper.getAttribute( key ) ); | |
} | |
} | |
for ( const key of wrapper.getStyleNames() ) { | |
if ( !toWrap.hasStyle( key ) ) { | |
toWrap.setStyle( key, wrapper.getStyle( key ) ); | |
} | |
} | |
for ( const key of wrapper.getClassNames() ) { | |
if ( !toWrap.hasClass( key ) ) { | |
toWrap.addClass( key ); | |
} | |
} | |
return true; | |
} | |
// Unwraps {@link module:engine/view/attributeelement~AttributeElement AttributeElement} from another by removing corresponding attributes, | |
// classes and styles. All attributes, classes and styles from wrapper should be present inside element being unwrapped. | |
// | |
// @param {module:engine/view/attributeelement~AttributeElement} wrapper Wrapper AttributeElement. | |
// @param {module:engine/view/attributeelement~AttributeElement} toUnwrap AttributeElement to unwrap using wrapper element. | |
// @returns {Boolean} Returns `true` if elements are unwrapped. | |
function unwrapAttributeElement( wrapper, toUnwrap ) { | |
// Can't unwrap if name or priority differs. | |
if ( wrapper.name !== toUnwrap.name || wrapper.priority !== toUnwrap.priority ) { | |
return false; | |
} | |
// Check if AttributeElement has all wrapper attributes. | |
for ( const key of wrapper.getAttributeKeys() ) { | |
// Classes and styles should be checked separately. | |
if ( key === 'class' || key === 'style' ) { | |
continue; | |
} | |
// If some attributes are missing or different we cannot unwrap. | |
if ( !toUnwrap.hasAttribute( key ) || toUnwrap.getAttribute( key ) !== wrapper.getAttribute( key ) ) { | |
return false; | |
} | |
} | |
// Check if AttributeElement has all wrapper classes. | |
if ( !toUnwrap.hasClass( ...wrapper.getClassNames() ) ) { | |
return false; | |
} | |
// Check if AttributeElement has all wrapper styles. | |
for ( const key of wrapper.getStyleNames() ) { | |
// If some styles are missing or different we cannot unwrap. | |
if ( !toUnwrap.hasStyle( key ) || toUnwrap.getStyle( key ) !== wrapper.getStyle( key ) ) { | |
return false; | |
} | |
} | |
// Remove all wrapper's attributes from unwrapped element. | |
for ( const key of wrapper.getAttributeKeys() ) { | |
// Classes and styles should be checked separately. | |
if ( key === 'class' || key === 'style' ) { | |
continue; | |
} | |
toUnwrap.removeAttribute( key ); | |
} | |
// Remove all wrapper's classes from unwrapped element. | |
toUnwrap.removeClass( ...wrapper.getClassNames() ); | |
// Remove all wrapper's styles from unwrapped element. | |
toUnwrap.removeStyle( ...wrapper.getStyleNames() ); | |
return true; | |
} | |
// Returns `true` if range is located in same {@link module:engine/view/attributeelement~AttributeElement AttributeElement} | |
// (`start` and `end` positions are located inside same {@link module:engine/view/attributeelement~AttributeElement AttributeElement}), | |
// starts on 0 offset and ends after last child node. | |
// | |
// @param {module:engine/view/range~Range} Range | |
// @returns {Boolean} | |
function rangeSpansOnAllChildren( range ) { | |
return range.start.parent == range.end.parent && range.start.parent.is( 'attributeElement' ) && | |
range.start.offset === 0 && range.end.offset === range.start.parent.childCount; | |
} | |
// Checks if provided nodes are valid to insert. Checks if each node is an instance of | |
// {@link module:engine/view/text~Text Text} or {@link module:engine/view/attributeelement~AttributeElement AttributeElement}, | |
// {@link module:engine/view/containerelement~ContainerElement ContainerElement}, | |
// {@link module:engine/view/emptyelement~EmptyElement EmptyElement} or | |
// {@link module:engine/view/uielement~UIElement UIElement}. | |
// | |
// Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-insert-invalid-node` when nodes to insert | |
// contains instances that are not {@link module:engine/view/text~Text Texts}, | |
// {@link module:engine/view/emptyelement~EmptyElement EmptyElements}, | |
// {@link module:engine/view/uielement~UIElement UIElements}, | |
// {@link module:engine/view/attributeelement~AttributeElement AttributeElements} or | |
// {@link module:engine/view/containerelement~ContainerElement ContainerElements}. | |
// | |
// @param Iterable.<module:engine/view/text~Text|module:engine/view/attributeelement~AttributeElement | |
// |module:engine/view/containerelement~ContainerElement> nodes | |
function validateNodesToInsert( nodes ) { | |
for ( const node of nodes ) { | |
if ( !validNodesToInsert.some( ( validNode => node instanceof validNode ) ) ) { // eslint-disable-line no-use-before-define | |
/** | |
* Inserted nodes should be valid to insert. of {@link module:engine/view/attributeelement~AttributeElement AttributeElement}, | |
* {@link module:engine/view/containerelement~ContainerElement ContainerElement}, | |
* {@link module:engine/view/emptyelement~EmptyElement EmptyElement}, | |
* {@link module:engine/view/uielement~UIElement UIElement}, {@link module:engine/view/text~Text Text}. | |
* | |
* @error view-writer-insert-invalid-node | |
*/ | |
throw new CKEditorError( 'view-writer-insert-invalid-node' ); | |
} | |
if ( !node.is( 'text' ) ) { | |
validateNodesToInsert( node.getChildren() ); | |
} | |
} | |
} | |
const validNodesToInsert = [ view_text_Text, AttributeElement, ContainerElement, emptyelement_EmptyElement, uielement_UIElement ]; | |
// Checks if node is ContainerElement or DocumentFragment, because in most cases they should be treated the same way. | |
// | |
// @param {module:engine/view/node~Node} node | |
// @returns {Boolean} Returns `true` if node is instance of ContainerElement or DocumentFragment. | |
function isContainerOrFragment( node ) { | |
return node && ( node.is( 'containerElement' ) || node.is( 'documentFragment' ) ); | |
} | |
// Checks if {@link module:engine/view/range~Range#start range start} and {@link module:engine/view/range~Range#end range end} are placed | |
// inside same {@link module:engine/view/containerelement~ContainerElement container element}. | |
// Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when validation fails. | |
// | |
// @param {module:engine/view/range~Range} range | |
function validateRangeContainer( range ) { | |
const startContainer = getParentContainer( range.start ); | |
const endContainer = getParentContainer( range.end ); | |
if ( !startContainer || !endContainer || startContainer !== endContainer ) { | |
/** | |
* Range container is invalid. This can happen if {@link module:engine/view/range~Range#start range start} and | |
* {@link module:engine/view/range~Range#end range end} positions are not placed inside same container or | |
* parent container for these positions cannot be found. | |
* | |
* @error view-writer-invalid-range-container | |
*/ | |
throw new CKEditorError( 'view-writer-invalid-range-container' ); | |
} | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/conversion/model-to-view-converters.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* Contains {@link module:engine/model/model model} to {@link module:engine/view/view view} converters for | |
* {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}. | |
* | |
* @module engine/conversion/model-to-view-converters | |
*/ | |
/** | |
* Function factory, creates a converter that converts node insertion changes from the model to the view. | |
* The view element that will be added to the view depends on passed parameter. If {@link module:engine/view/element~Element} was passed, | |
* it will be cloned and the copy will be inserted. If `Function` is provided, it is passed all the parameters of the | |
* dispatcher's {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:insert insert event}. | |
* It's expected that the function returns a {@link module:engine/view/element~Element}. | |
* The result of the function will be inserted to the view. | |
* | |
* The converter automatically consumes corresponding value from consumables list, stops the event (see | |
* {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}) and bind model and view elements. | |
* | |
* modelDispatcher.on( 'insert:paragraph', insertElement( new ViewElement( 'p' ) ) ); | |
* | |
* modelDispatcher.on( | |
* 'insert:myElem', | |
* insertElement( ( data, consumable, conversionApi ) => { | |
* let myElem = new ViewElement( 'myElem', { myAttr: true }, new ViewText( 'myText' ) ); | |
* | |
* // Do something fancy with myElem using data/consumable/conversionApi ... | |
* | |
* return myElem; | |
* } | |
* ) ); | |
* | |
* @param {module:engine/view/element~Element|Function} elementCreator View element, or function returning a view element, which | |
* will be inserted. | |
* @returns {Function} Insert element event converter. | |
*/ | |
function insertElement( elementCreator ) { | |
return ( evt, data, consumable, conversionApi ) => { | |
const viewElement = ( elementCreator instanceof view_element_Element ) ? | |
elementCreator.clone( true ) : | |
elementCreator( data, consumable, conversionApi ); | |
if ( !viewElement ) { | |
return; | |
} | |
if ( !consumable.consume( data.item, 'insert' ) ) { | |
return; | |
} | |
const viewPosition = conversionApi.mapper.toViewPosition( data.range.start ); | |
conversionApi.mapper.bindElements( data.item, viewElement ); | |
view_writer.insert( viewPosition, viewElement ); | |
}; | |
} | |
/** | |
* Function factory, creates a default model-to-view converter for text insertion changes. | |
* | |
* The converter automatically consumes corresponding value from consumables list and stops the event (see | |
* {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}). | |
* | |
* modelDispatcher.on( 'insert:$text', insertText() ); | |
* | |
* @returns {Function} Insert text event converter. | |
*/ | |
function model_to_view_converters_insertText() { | |
return ( evt, data, consumable, conversionApi ) => { | |
if ( !consumable.consume( data.item, 'insert' ) ) { | |
return; | |
} | |
const viewPosition = conversionApi.mapper.toViewPosition( data.range.start ); | |
const viewText = new view_text_Text( data.item.data ); | |
view_writer.insert( viewPosition, viewText ); | |
}; | |
} | |
/** | |
* Function factory, creates a converter that converts marker adding change to the view ui element. | |
* The view ui element that will be added to the view depends on passed parameter. See {@link ~insertElement}. | |
* In a case of collapsed range element will not wrap range but separate elements will be placed at the beginning | |
* and at the end of the range. | |
* | |
* **Note:** unlike {@link ~insertElement}, the converter does not bind view element to model, because this converter | |
* uses marker as "model source of data". This means that view ui element does not have corresponding model element. | |
* | |
* @param {module:engine/view/uielement~UIElement|Function} elementCreator View ui element, or function returning a view element, which | |
* will be inserted. | |
* @returns {Function} Insert element event converter. | |
*/ | |
function insertUIElement( elementCreator ) { | |
return ( evt, data, consumable, conversionApi ) => { | |
let viewStartElement, viewEndElement; | |
if ( elementCreator instanceof view_element_Element ) { | |
viewStartElement = elementCreator.clone( true ); | |
viewEndElement = elementCreator.clone( true ); | |
} else { | |
data.isOpening = true; | |
viewStartElement = elementCreator( data, consumable, conversionApi ); | |
data.isOpening = false; | |
viewEndElement = elementCreator( data, consumable, conversionApi ); | |
} | |
if ( !viewStartElement || !viewEndElement ) { | |
return; | |
} | |
const markerRange = data.markerRange; | |
const eventName = evt.name; | |
// Marker that is collapsed has consumable build differently that non-collapsed one. | |
// For more information see `addMarker` and `removeMarker` events description. | |
// If marker's range is collapsed - check if it can be consumed. | |
if ( markerRange.isCollapsed && !consumable.consume( markerRange, eventName ) ) { | |
return; | |
} | |
// If marker's range is not collapsed - consume all items inside. | |
for ( const value of markerRange ) { | |
if ( !consumable.consume( value.item, eventName ) ) { | |
return; | |
} | |
} | |
const mapper = conversionApi.mapper; | |
view_writer.insert( mapper.toViewPosition( markerRange.start ), viewStartElement ); | |
if ( !markerRange.isCollapsed ) { | |
view_writer.insert( mapper.toViewPosition( markerRange.end ), viewEndElement ); | |
} | |
}; | |
} | |
/** | |
* Function factory, creates a converter that converts set/change attribute changes from the model to the view. Attributes | |
* from model are converted to the view element attributes in the view. You may provide a custom function to generate a | |
* key-value attribute pair to add/change. If not provided, model attributes will be converted to view elements attributes | |
* on 1-to-1 basis. | |
* | |
* **Note:** Provided attribute creator should always return the same `key` for given attribute from the model. | |
* | |
* The converter automatically consumes corresponding value from consumables list and stops the event (see | |
* {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}). | |
* | |
* modelDispatcher.on( 'addAttribute:customAttr:myElem', setAttribute( ( data ) => { | |
* // Change attribute key from `customAttr` to `class` in view. | |
* const key = 'class'; | |
* let value = data.attributeNewValue; | |
* | |
* // Force attribute value to 'empty' if the model element is empty. | |
* if ( data.item.childCount === 0 ) { | |
* value = 'empty'; | |
* } | |
* | |
* // Return key-value pair. | |
* return { key, value }; | |
* } ) ); | |
* | |
* @param {Function} [attributeCreator] Function returning an object with two properties: `key` and `value`, which | |
* represents attribute key and attribute value to be set on a {@link module:engine/view/element~Element view element}. | |
* The function is passed all the parameters of the | |
* {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:addAttribute} | |
* or {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:changeAttribute} event. | |
* @returns {Function} Set/change attribute converter. | |
*/ | |
function setAttribute( attributeCreator ) { | |
attributeCreator = attributeCreator || ( ( value, key ) => ( { value, key } ) ); | |
return ( evt, data, consumable, conversionApi ) => { | |
if ( !consumable.consume( data.item, eventNameToConsumableType( evt.name ) ) ) { | |
return; | |
} | |
const { key, value } = attributeCreator( data.attributeNewValue, data.attributeKey, data, consumable, conversionApi ); | |
conversionApi.mapper.toViewElement( data.item ).setAttribute( key, value ); | |
}; | |
} | |
/** | |
* Function factory, creates a converter that converts remove attribute changes from the model to the view. Removes attributes | |
* that were converted to the view element attributes in the view. You may provide a custom function to generate a | |
* key-value attribute pair to remove. If not provided, model attributes will be removed from view elements on 1-to-1 basis. | |
* | |
* **Note:** Provided attribute creator should always return the same `key` for given attribute from the model. | |
* | |
* **Note:** You can use the same attribute creator as in {@link module:engine/conversion/model-to-view-converters~setAttribute}. | |
* | |
* The converter automatically consumes corresponding value from consumables list and stops the event (see | |
* {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}). | |
* | |
* modelDispatcher.on( 'removeAttribute:customAttr:myElem', removeAttribute( ( data ) => { | |
* // Change attribute key from `customAttr` to `class` in view. | |
* const key = 'class'; | |
* let value = data.attributeNewValue; | |
* | |
* // Force attribute value to 'empty' if the model element is empty. | |
* if ( data.item.childCount === 0 ) { | |
* value = 'empty'; | |
* } | |
* | |
* // Return key-value pair. | |
* return { key, value }; | |
* } ) ); | |
* | |
* @param {Function} [attributeCreator] Function returning an object with two properties: `key` and `value`, which | |
* represents attribute key and attribute value to be removed from {@link module:engine/view/element~Element view element}. | |
* The function is passed all the parameters of the | |
* {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:addAttribute addAttribute event} | |
* or {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:changeAttribute changeAttribute event}. | |
* @returns {Function} Remove attribute converter. | |
*/ | |
function removeAttribute( attributeCreator ) { | |
attributeCreator = attributeCreator || ( ( value, key ) => ( { key } ) ); | |
return ( evt, data, consumable, conversionApi ) => { | |
if ( !consumable.consume( data.item, eventNameToConsumableType( evt.name ) ) ) { | |
return; | |
} | |
const { key } = attributeCreator( data.attributeOldValue, data.attributeKey, data, consumable, conversionApi ); | |
conversionApi.mapper.toViewElement( data.item ).removeAttribute( key ); | |
}; | |
} | |
/** | |
* Function factory, creates a converter that converts set/change attribute changes from the model to the view. In this case, | |
* model attributes are converted to a view element that will be wrapping view nodes which corresponding model nodes had | |
* the attribute set. This is useful for attributes like `bold`, which may be set on text nodes in model but are | |
* represented as an element in the view: | |
* | |
* [paragraph] MODEL ====> VIEW <p> | |
* |- a {bold: true} |- <b> | |
* |- b {bold: true} | |- ab | |
* |- c |- c | |
* | |
* The wrapping node depends on passed parameter. If {@link module:engine/view/element~Element} was passed, it will be cloned and | |
* the copy will become the wrapping element. If `Function` is provided, it is passed attribute value and then all the parameters of the | |
* {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:addAttribute addAttribute event}. | |
* It's expected that the function returns a {@link module:engine/view/element~Element}. | |
* The result of the function will be the wrapping element. | |
* When provided `Function` does not return element, then will be no conversion. | |
* | |
* The converter automatically consumes corresponding value from consumables list, stops the event (see | |
* {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}). | |
* | |
* modelDispatcher.on( 'addAttribute:bold', wrapItem( new ViewAttributeElement( 'strong' ) ) ); | |
* | |
* @param {module:engine/view/element~Element|Function} elementCreator View element, or function returning a view element, which will | |
* be used for wrapping. | |
* @returns {Function} Set/change attribute converter. | |
*/ | |
function wrapItem( elementCreator ) { | |
return ( evt, data, consumable, conversionApi ) => { | |
const viewElement = ( elementCreator instanceof view_element_Element ) ? | |
elementCreator.clone( true ) : | |
elementCreator( data.attributeNewValue, data, consumable, conversionApi ); | |
if ( !viewElement ) { | |
return; | |
} | |
if ( !consumable.consume( data.item, eventNameToConsumableType( evt.name ) ) ) { | |
return; | |
} | |
let viewRange = conversionApi.mapper.toViewRange( data.range ); | |
// If this is a change event (because old value is not empty) and the creator is a function (so | |
// it may create different view elements basing on attribute value) we have to create | |
// view element basing on old value and unwrap it before wrapping with a newly created view element. | |
if ( data.attributeOldValue !== null && !( elementCreator instanceof view_element_Element ) ) { | |
const oldViewElement = elementCreator( data.attributeOldValue, data, consumable, conversionApi ); | |
viewRange = view_writer.unwrap( viewRange, oldViewElement ); | |
} | |
view_writer.wrap( viewRange, viewElement ); | |
}; | |
} | |
/** | |
* Function factory, creates a converter that converts remove attribute changes from the model to the view. It assumes, that | |
* attributes from model were converted to elements in the view. This converter will unwrap view nodes from corresponding | |
* view element if given attribute was removed. | |
* | |
* The view element type that will be unwrapped depends on passed parameter. | |
* If {@link module:engine/view/element~Element} was passed, it will be used to look for similar element in the view for unwrapping. | |
* If `Function` is provided, it is passed all the parameters of the | |
* {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:addAttribute addAttribute event}. | |
* It's expected that the function returns a {@link module:engine/view/element~Element}. | |
* The result of the function will be used to look for similar element in the view for unwrapping. | |
* | |
* The converter automatically consumes corresponding value from consumables list, stops the event (see | |
* {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}) and bind model and view elements. | |
* | |
* modelDispatcher.on( 'removeAttribute:bold', unwrapItem( new ViewAttributeElement( 'strong' ) ) ); | |
* | |
* @see module:engine/conversion/model-to-view-converters~wrapItem | |
* @param {module:engine/view/element~Element|Function} elementCreator View element, or function returning a view element, which will | |
* be used for unwrapping. | |
* @returns {Function} Remove attribute converter. | |
*/ | |
function unwrapItem( elementCreator ) { | |
return ( evt, data, consumable, conversionApi ) => { | |
const viewElement = ( elementCreator instanceof view_element_Element ) ? | |
elementCreator.clone( true ) : | |
elementCreator( data.attributeOldValue, data, consumable, conversionApi ); | |
if ( !viewElement ) { | |
return; | |
} | |
if ( !consumable.consume( data.item, eventNameToConsumableType( evt.name ) ) ) { | |
return; | |
} | |
const viewRange = conversionApi.mapper.toViewRange( data.range ); | |
view_writer.unwrap( viewRange, viewElement ); | |
}; | |
} | |
/** | |
* Function factory, creates a default model-to-view converter for node remove changes. | |
* | |
* modelDispatcher.on( 'remove', remove() ); | |
* | |
* @returns {Function} Remove event converter. | |
*/ | |
function model_to_view_converters_remove() { | |
return ( evt, data, consumable, conversionApi ) => { | |
if ( !consumable.consume( data.item, 'remove' ) ) { | |
return; | |
} | |
// We cannot map non-existing positions from model to view. Since a range was removed | |
// from the model, we cannot recreate that range and map it to view, because | |
// end of that range is incorrect. | |
// Instead we will use `data.sourcePosition` as this is the last correct model position and | |
// it is a position before the removed item. Then, we will calculate view range to remove "manually". | |
let viewPosition = conversionApi.mapper.toViewPosition( data.sourcePosition ); | |
let viewRange; | |
if ( data.item.is( 'element' ) ) { | |
// Note: in remove conversion we cannot use model-to-view element mapping because `data.item` may be | |
// already mapped to another element (this happens when move change is converted). | |
// In this case however, `viewPosition` is the position before view element that corresponds to removed model element. | |
// | |
// First, fix the position. Traverse the tree forward until the container element is found. The `viewPosition` | |
// may be before a ui element, before attribute element or at the end of text element. | |
viewPosition = viewPosition.getLastMatchingPosition( value => !value.item.is( 'containerElement' ) ); | |
if ( viewPosition.parent.is( 'text' ) && viewPosition.isAtEnd ) { | |
viewPosition = view_position_Position.createAfter( viewPosition.parent ); | |
} | |
viewRange = view_range_Range.createOn( viewPosition.nodeAfter ); | |
} else { | |
// If removed item is a text node, we need to traverse view tree to find the view range to remove. | |
// Range to remove will start `viewPosition` and should contain amount of characters equal to the amount of removed characters. | |
const viewRangeEnd = _shiftViewPositionByCharacters( viewPosition, data.item.offsetSize ); | |
viewRange = new view_range_Range( viewPosition, viewRangeEnd ); | |
} | |
// Trim the range to remove in case some UI elements are on the view range boundaries. | |
view_writer.remove( viewRange.getTrimmed() ); | |
// Unbind this element only if it was moved to graveyard. | |
// The dispatcher#remove event will also be fired if the element was moved to another place (remove+insert are fired). | |
// Let's say that <b> is moved before <a>. The view will be changed like this: | |
// | |
// 1) start: <a></a><b></b> | |
// 2) insert: <b (new)></b><a></a><b></b> | |
// 3) remove: <b (new)></b><a></a> | |
// | |
// If we'll unbind the <b> element in step 3 we'll also lose binding of the <b (new)> element in the view, | |
// because unbindModelElement() cancels both bindings – (model <b> => view <b (new)>) and (view <b (new)> => model <b>). | |
// We can't lose any of these. | |
// | |
// See #847. | |
if ( data.item.root.rootName == '$graveyard' ) { | |
conversionApi.mapper.unbindModelElement( data.item ); | |
} | |
}; | |
} | |
/** | |
* Function factory, creates converter that converts all texts inside marker's range. Converter wraps each text with | |
* {@link module:engine/view/attributeelement~AttributeElement} created from provided descriptor. | |
* See {link module:engine/conversion/model-to-view-converters~highlightDescriptorToAttributeElement}. | |
* | |
* @param {module:engine/conversion/model-to-view-converters~HighlightDescriptor|Function} highlightDescriptor | |
* @return {Function} | |
*/ | |
function highlightText( highlightDescriptor ) { | |
return ( evt, data, consumable, conversionApi ) => { | |
const descriptor = typeof highlightDescriptor == 'function' ? | |
highlightDescriptor( data, consumable, conversionApi ) : | |
highlightDescriptor; | |
const modelItem = data.item; | |
if ( !descriptor || data.markerRange.isCollapsed || !modelItem.is( 'textProxy' ) ) { | |
return; | |
} | |
if ( !consumable.consume( modelItem, evt.name ) ) { | |
return; | |
} | |
if ( !descriptor.id ) { | |
descriptor.id = data.markerName; | |
} | |
const viewElement = createViewElementFromHighlightDescriptor( descriptor ); | |
const viewRange = conversionApi.mapper.toViewRange( data.range ); | |
if ( evt.name.split( ':' )[ 0 ] == 'addMarker' ) { | |
view_writer.wrap( viewRange, viewElement ); | |
} else { | |
view_writer.unwrap( viewRange, viewElement ); | |
} | |
}; | |
} | |
/** | |
* Converter function factory. Creates a function which applies the marker's highlight to all elements inside a marker's range. | |
* The converter checks if an element has the addHighlight and removeHighlight functions stored as | |
* {@link module:engine/view/element~Element#setCustomProperty custom properties} and if so use them to apply the highlight. | |
* In such case converter will consume all element's children, assuming that they were handled by element itself. | |
* If the highlight descriptor will not provide priority, priority `10` will be used as default, to be compliant with | |
* {@link module:engine/conversion/model-to-view-converters~highlightText} method which uses default priority of | |
* {@link module:engine/view/attributeelement~AttributeElement}. | |
* | |
* If the highlight descriptor will not provide `id` property, name of the marker will be used. | |
* When `addHighlight` and `removeHighlight` custom properties are not present, element is not converted | |
* in any special way. This means that converters will proceed to convert element's child nodes. | |
* | |
* @param {module:engine/conversion/model-to-view-converters~HighlightDescriptor|Function} highlightDescriptor | |
* @return {Function} | |
*/ | |
function highlightElement( highlightDescriptor ) { | |
return ( evt, data, consumable, conversionApi ) => { | |
const descriptor = typeof highlightDescriptor == 'function' ? | |
highlightDescriptor( data, consumable, conversionApi ) : | |
highlightDescriptor; | |
const modelItem = data.item; | |
if ( !descriptor || data.markerRange.isCollapsed || !modelItem.is( 'element' ) ) { | |
return; | |
} | |
if ( !consumable.test( data.item, evt.name ) ) { | |
return; | |
} | |
if ( !descriptor.priority ) { | |
descriptor.priority = 10; | |
} | |
if ( !descriptor.id ) { | |
descriptor.id = data.markerName; | |
} | |
const viewElement = conversionApi.mapper.toViewElement( modelItem ); | |
const addMarker = evt.name.split( ':' )[ 0 ] == 'addMarker'; | |
const highlightHandlingMethod = addMarker ? 'addHighlight' : 'removeHighlight'; | |
if ( viewElement && viewElement.getCustomProperty( highlightHandlingMethod ) ) { | |
// Consume element itself. | |
consumable.consume( data.item, evt.name ); | |
// Consume all children nodes. | |
for ( const value of range_Range.createIn( modelItem ) ) { | |
consumable.consume( value.item, evt.name ); | |
} | |
viewElement.getCustomProperty( highlightHandlingMethod )( viewElement, addMarker ? descriptor : descriptor.id ); | |
} | |
}; | |
} | |
/** | |
* Function factory, creates a default model-to-view converter for removing {@link module:engine/view/uielement~UIElement ui element} | |
* basing on marker remove change. | |
* | |
* @param {module:engine/view/uielement~UIElement|Function} elementCreator View ui element, or function returning | |
* a view ui element, which will be used as a pattern when look for element to remove at the marker start position. | |
* @returns {Function} Remove ui element converter. | |
*/ | |
function removeUIElement( elementCreator ) { | |
return ( evt, data, consumable, conversionApi ) => { | |
let viewStartElement, viewEndElement; | |
if ( elementCreator instanceof view_element_Element ) { | |
viewStartElement = elementCreator.clone( true ); | |
viewEndElement = elementCreator.clone( true ); | |
} else { | |
data.isOpening = true; | |
viewStartElement = elementCreator( data, consumable, conversionApi ); | |
data.isOpening = false; | |
viewEndElement = elementCreator( data, consumable, conversionApi ); | |
} | |
if ( !viewStartElement || !viewEndElement ) { | |
return; | |
} | |
const markerRange = data.markerRange; | |
const eventName = evt.name; | |
// If marker's range is collapsed - check if it can be consumed. | |
if ( markerRange.isCollapsed && !consumable.consume( markerRange, eventName ) ) { | |
return; | |
} | |
// Check if all items in the range can be consumed, and consume them. | |
for ( const value of markerRange ) { | |
if ( !consumable.consume( value.item, eventName ) ) { | |
return; | |
} | |
} | |
const viewRange = conversionApi.mapper.toViewRange( markerRange ); | |
// First remove closing element. | |
view_writer.clear( viewRange.getEnlarged(), viewEndElement ); | |
// If closing and opening elements are not the same then remove opening element. | |
if ( !viewStartElement.isSimilar( viewEndElement ) ) { | |
view_writer.clear( viewRange.getEnlarged(), viewStartElement ); | |
} | |
}; | |
} | |
/** | |
* Returns the consumable type that is to be consumed in an event, basing on that event name. | |
* | |
* @param {String} evtName Event name. | |
* @returns {String} Consumable type. | |
*/ | |
function eventNameToConsumableType( evtName ) { | |
const parts = evtName.split( ':' ); | |
return parts[ 0 ] + ':' + parts[ 1 ]; | |
} | |
// Helper function that shifts given view `position` in a way that returned position is after `howMany` characters compared | |
// to the original `position`. | |
// Because in view there might be view ui elements splitting text nodes, we cannot simply use `ViewPosition#getShiftedBy()`. | |
function _shiftViewPositionByCharacters( position, howMany ) { | |
// Create a walker that will walk the view tree starting from given position and walking characters one-by-one. | |
const walker = new view_treewalker_TreeWalker( { startPosition: position, singleCharacters: true } ); | |
// We will count visited characters and return the position after `howMany` characters. | |
let charactersFound = 0; | |
for ( const value of walker ) { | |
if ( value.type == 'text' ) { | |
charactersFound++; | |
if ( charactersFound == howMany ) { | |
return walker.position; | |
} | |
} | |
} | |
} | |
/** | |
* Creates `span` {@link module:engine/view/attributeelement~AttributeElement view attribute element} from information | |
* provided by {@link module:engine/conversion/model-to-view-converters~HighlightDescriptor} object. If priority | |
* is not provided in descriptor - default priority will be used. | |
* | |
* @param {module:engine/conversion/model-to-view-converters~HighlightDescriptor} descriptor | |
* @return {module:engine/conversion/model-to-view-converters~HighlightAttributeElement} | |
*/ | |
function createViewElementFromHighlightDescriptor( descriptor ) { | |
const viewElement = new HighlightAttributeElement( 'span', descriptor.attributes ); | |
if ( descriptor.class ) { | |
const cssClasses = Array.isArray( descriptor.class ) ? descriptor.class : [ descriptor.class ]; | |
viewElement.addClass( ...cssClasses ); | |
} | |
if ( descriptor.priority ) { | |
viewElement.priority = descriptor.priority; | |
} | |
viewElement.setCustomProperty( 'highlightDescriptorId', descriptor.id ); | |
return viewElement; | |
} | |
/** | |
* Special kind of {@link module:engine/view/attributeelement~AttributeElement} that is created and used in | |
* marker-to-highlight conversion. | |
* | |
* The difference between `HighlightAttributeElement` and {@link module:engine/view/attributeelement~AttributeElement} | |
* is {@link module:engine/view/attributeelement~AttributeElement#isSimilar} method. | |
* | |
* For `HighlightAttributeElement` it checks just `highlightDescriptorId` custom property, that is set during marker-to-highlight | |
* conversion basing on {@link module:engine/conversion/model-to-view-converters~HighlightDescriptor} object. | |
* `HighlightAttributeElement`s with same `highlightDescriptorId` property are considered similar. | |
*/ | |
class HighlightAttributeElement extends AttributeElement { | |
isSimilar( otherElement ) { | |
if ( otherElement.is( 'attributeElement' ) ) { | |
return this.getCustomProperty( 'highlightDescriptorId' ) === otherElement.getCustomProperty( 'highlightDescriptorId' ); | |
} | |
return false; | |
} | |
} | |
/** | |
* Object describing how the content highlight should be created in the view. | |
* | |
* Each text node contained in the highlight will be wrapped with `span` element with CSS class(es), attributes and priority | |
* described by this object. | |
* | |
* Each element can handle displaying the highlight separately by providing `addHighlight` and `removeHighlight` custom | |
* properties: | |
* * `HighlightDescriptor` is passed to the `addHighlight` function upon conversion and should be used to apply the highlight to | |
* the element, | |
* * descriptor id is passed to the `removeHighlight` function upon conversion and should be used to remove the highlight of given | |
* id from the element. | |
* | |
* @typedef {Object} module:engine/conversion/model-to-view-converters~HighlightDescriptor | |
* | |
* @property {String|Array.<String>} class CSS class or array of classes to set. If descriptor is used to | |
* create {@link module:engine/view/attributeelement~AttributeElement} over text nodes, those classes will be set | |
* on that {@link module:engine/view/attributeelement~AttributeElement}. If descriptor is applied to an element, | |
* usually those class will be set on that element, however this depends on how the element converts the descriptor. | |
* | |
* @property {String} [id] Descriptor identifier. If not provided, defaults to converted marker's name. | |
* | |
* @property {Number} [priority] Descriptor priority. If not provided, defaults to `10`. If descriptor is used to create | |
* {@link module:engine/view/attributeelement~AttributeElement}, it will be that element's | |
* {@link module:engine/view/attributeelement~AttributeElement#priority}. If descriptor is applied to an element, | |
* the priority will be used to determine which descriptor is more important. | |
* | |
* @property {Object} [attributes] Attributes to set. If descriptor is used to create | |
* {@link module:engine/view/attributeelement~AttributeElement} over text nodes, those attributes will be set on that | |
* {@link module:engine/view/attributeelement~AttributeElement}. If descriptor is applied to an element, usually those | |
* attributes will be set on that element, however this depends on how the element converts the descriptor. | |
*/ | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/conversion/viewconsumable.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/conversion/viewconsumable | |
*/ | |
/** | |
* Class used for handling consumption of view {@link module:engine/view/element~Element elements}, | |
* {@link module:engine/view/text~Text text nodes} and {@link module:engine/view/documentfragment~DocumentFragment document fragments}. | |
* Element's name and its parts (attributes, classes and styles) can be consumed separately. Consuming an element's name | |
* does not consume its attributes, classes and styles. | |
* To add items for consumption use {@link module:engine/conversion/viewconsumable~ViewConsumable#add add method}. | |
* To test items use {@link module:engine/conversion/viewconsumable~ViewConsumable#test test method}. | |
* To consume items use {@link module:engine/conversion/viewconsumable~ViewConsumable#consume consume method}. | |
* To revert already consumed items use {@link module:engine/conversion/viewconsumable~ViewConsumable#revert revert method}. | |
* | |
* viewConsumable.add( element, { name: true } ); // Adds element's name as ready to be consumed. | |
* viewConsumable.add( textNode ); // Adds text node for consumption. | |
* viewConsumable.add( docFragment ); // Adds document fragment for consumption. | |
* viewConsumable.test( element, { name: true } ); // Tests if element's name can be consumed. | |
* viewConsumable.test( textNode ); // Tests if text node can be consumed. | |
* viewConsumable.test( docFragment ); // Tests if document fragment can be consumed. | |
* viewConsumable.consume( element, { name: true } ); // Consume element's name. | |
* viewConsumable.consume( textNode ); // Consume text node. | |
* viewConsumable.consume( docFragment ); // Consume document fragment. | |
* viewConsumable.revert( element, { name: true } ); // Revert already consumed element's name. | |
* viewConsumable.revert( textNode ); // Revert already consumed text node. | |
* viewConsumable.revert( docFragment ); // Revert already consumed document fragment. | |
*/ | |
class ViewConsumable { | |
/** | |
* Creates new ViewConsumable. | |
*/ | |
constructor() { | |
/** | |
* Map of consumable elements. If {@link module:engine/view/element~Element element} is used as a key, | |
* {@link module:engine/conversion/viewconsumable~ViewElementConsumables ViewElementConsumables} instance is stored as value. | |
* For {@link module:engine/view/text~Text text nodes} and | |
* {@link module:engine/view/documentfragment~DocumentFragment document fragments} boolean value is stored as value. | |
* | |
* @protected | |
* @member {Map.<module:engine/conversion/viewconsumable~ViewElementConsumables|Boolean>} | |
*/ | |
this._consumables = new Map(); | |
} | |
/** | |
* Adds {@link module:engine/view/element~Element view element}, {@link module:engine/view/text~Text text node} or | |
* {@link module:engine/view/documentfragment~DocumentFragment document fragment} as ready to be consumed. | |
* | |
* viewConsumable.add( p, { name: true } ); // Adds element's name to consume. | |
* viewConsumable.add( p, { attribute: 'name' } ); // Adds element's attribute. | |
* viewConsumable.add( p, { class: 'foobar' } ); // Adds element's class. | |
* viewConsumable.add( p, { style: 'color' } ); // Adds element's style | |
* viewConsumable.add( p, { attribute: 'name', style: 'color' } ); // Adds attribute and style. | |
* viewConsumable.add( p, { class: [ 'baz', 'bar' ] } ); // Multiple consumables can be provided. | |
* viewConsumable.add( textNode ); // Adds text node to consume. | |
* viewConsumable.add( docFragment ); // Adds document fragment to consume. | |
* | |
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `viewconsumable-invalid-attribute` when `class` or `style` | |
* attribute is provided - it should be handled separately by providing actual style/class. | |
* | |
* viewConsumable.add( p, { attribute: 'style' } ); // This call will throw an exception. | |
* viewConsumable.add( p, { style: 'color' } ); // This is properly handled style. | |
* | |
* @param {module:engine/view/element~Element|module:engine/view/text~Text|module:engine/view/documentfragment~DocumentFragment} element | |
* @param {Object} [consumables] Used only if first parameter is {@link module:engine/view/element~Element view element} instance. | |
* @param {Boolean} consumables.name If set to true element's name will be included. | |
* @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names. | |
* @param {String|Array.<String>} consumables.class Class name or array of class names. | |
* @param {String|Array.<String>} consumables.style Style name or array of style names. | |
*/ | |
add( element, consumables ) { | |
let elementConsumables; | |
// For text nodes and document fragments just mark them as consumable. | |
if ( element.is( 'text' ) || element.is( 'documentFragment' ) ) { | |
this._consumables.set( element, true ); | |
return; | |
} | |
// For elements create new ViewElementConsumables or update already existing one. | |
if ( !this._consumables.has( element ) ) { | |
elementConsumables = new viewconsumable_ViewElementConsumables(); | |
this._consumables.set( element, elementConsumables ); | |
} else { | |
elementConsumables = this._consumables.get( element ); | |
} | |
elementConsumables.add( consumables ); | |
} | |
/** | |
* Tests if {@link module:engine/view/element~Element view element}, {@link module:engine/view/text~Text text node} or | |
* {@link module:engine/view/documentfragment~DocumentFragment document fragment} can be consumed. | |
* It returns `true` when all items included in method's call can be consumed. Returns `false` when | |
* first already consumed item is found and `null` when first non-consumable item is found. | |
* | |
* viewConsumable.test( p, { name: true } ); // Tests element's name. | |
* viewConsumable.test( p, { attribute: 'name' } ); // Tests attribute. | |
* viewConsumable.test( p, { class: 'foobar' } ); // Tests class. | |
* viewConsumable.test( p, { style: 'color' } ); // Tests style. | |
* viewConsumable.test( p, { attribute: 'name', style: 'color' } ); // Tests attribute and style. | |
* viewConsumable.test( p, { class: [ 'baz', 'bar' ] } ); // Multiple consumables can be tested. | |
* viewConsumable.test( textNode ); // Tests text node. | |
* viewConsumable.test( docFragment ); // Tests document fragment. | |
* | |
* Testing classes and styles as attribute will test if all added classes/styles can be consumed. | |
* | |
* viewConsumable.test( p, { attribute: 'class' } ); // Tests if all added classes can be consumed. | |
* viewConsumable.test( p, { attribute: 'style' } ); // Tests if all added styles can be consumed. | |
* | |
* @param {module:engine/view/element~Element|module:engine/view/text~Text|module:engine/view/documentfragment~DocumentFragment} element | |
* @param {Object} [consumables] Used only if first parameter is {@link module:engine/view/element~Element view element} instance. | |
* @param {Boolean} consumables.name If set to true element's name will be included. | |
* @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names. | |
* @param {String|Array.<String>} consumables.class Class name or array of class names. | |
* @param {String|Array.<String>} consumables.style Style name or array of style names. | |
* @returns {Boolean|null} Returns `true` when all items included in method's call can be consumed. Returns `false` | |
* when first already consumed item is found and `null` when first non-consumable item is found. | |
*/ | |
test( element, consumables ) { | |
const elementConsumables = this._consumables.get( element ); | |
if ( elementConsumables === undefined ) { | |
return null; | |
} | |
// For text nodes and document fragments return stored boolean value. | |
if ( element.is( 'text' ) || element.is( 'documentFragment' ) ) { | |
return elementConsumables; | |
} | |
// For elements test consumables object. | |
return elementConsumables.test( consumables ); | |
} | |
/** | |
* Consumes {@link module:engine/view/element~Element view element}, {@link module:engine/view/text~Text text node} or | |
* {@link module:engine/view/documentfragment~DocumentFragment document fragment}. | |
* It returns `true` when all items included in method's call can be consumed, otherwise returns `false`. | |
* | |
* viewConsumable.consume( p, { name: true } ); // Consumes element's name. | |
* viewConsumable.consume( p, { attribute: 'name' } ); // Consumes element's attribute. | |
* viewConsumable.consume( p, { class: 'foobar' } ); // Consumes element's class. | |
* viewConsumable.consume( p, { style: 'color' } ); // Consumes element's style. | |
* viewConsumable.consume( p, { attribute: 'name', style: 'color' } ); // Consumes attribute and style. | |
* viewConsumable.consume( p, { class: [ 'baz', 'bar' ] } ); // Multiple consumables can be consumed. | |
* viewConsumable.consume( textNode ); // Consumes text node. | |
* viewConsumable.consume( docFragment ); // Consumes document fragment. | |
* | |
* Consuming classes and styles as attribute will test if all added classes/styles can be consumed. | |
* | |
* viewConsumable.consume( p, { attribute: 'class' } ); // Consume only if all added classes can be consumed. | |
* viewConsumable.consume( p, { attribute: 'style' } ); // Consume only if all added styles can be consumed. | |
* | |
* @param {module:engine/view/element~Element|module:engine/view/text~Text|module:engine/view/documentfragment~DocumentFragment} element | |
* @param {Object} [consumables] Used only if first parameter is {@link module:engine/view/element~Element view element} instance. | |
* @param {Boolean} consumables.name If set to true element's name will be included. | |
* @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names. | |
* @param {String|Array.<String>} consumables.class Class name or array of class names. | |
* @param {String|Array.<String>} consumables.style Style name or array of style names. | |
* @returns {Boolean} Returns `true` when all items included in method's call can be consumed, | |
* otherwise returns `false`. | |
*/ | |
consume( element, consumables ) { | |
if ( this.test( element, consumables ) ) { | |
if ( element.is( 'text' ) || element.is( 'documentFragment' ) ) { | |
// For text nodes and document fragments set value to false. | |
this._consumables.set( element, false ); | |
} else { | |
// For elements - consume consumables object. | |
this._consumables.get( element ).consume( consumables ); | |
} | |
return true; | |
} | |
return false; | |
} | |
/** | |
* Reverts {@link module:engine/view/element~Element view element}, {@link module:engine/view/text~Text text node} or | |
* {@link module:engine/view/documentfragment~DocumentFragment document fragment} so they can be consumed once again. | |
* Method does not revert items that were never previously added for consumption, even if they are included in | |
* method's call. | |
* | |
* viewConsumable.revert( p, { name: true } ); // Reverts element's name. | |
* viewConsumable.revert( p, { attribute: 'name' } ); // Reverts element's attribute. | |
* viewConsumable.revert( p, { class: 'foobar' } ); // Reverts element's class. | |
* viewConsumable.revert( p, { style: 'color' } ); // Reverts element's style. | |
* viewConsumable.revert( p, { attribute: 'name', style: 'color' } ); // Reverts attribute and style. | |
* viewConsumable.revert( p, { class: [ 'baz', 'bar' ] } ); // Multiple names can be reverted. | |
* viewConsumable.revert( textNode ); // Reverts text node. | |
* viewConsumable.revert( docFragment ); // Reverts document fragment. | |
* | |
* Reverting classes and styles as attribute will revert all classes/styles that were previously added for | |
* consumption. | |
* | |
* viewConsumable.revert( p, { attribute: 'class' } ); // Reverts all classes added for consumption. | |
* viewConsumable.revert( p, { attribute: 'style' } ); // Reverts all styles added for consumption. | |
* | |
* @param {module:engine/view/element~Element|module:engine/view/text~Text|module:engine/view/documentfragment~DocumentFragment} element | |
* @param {Object} [consumables] Used only if first parameter is {@link module:engine/view/element~Element view element} instance. | |
* @param {Boolean} consumables.name If set to true element's name will be included. | |
* @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names. | |
* @param {String|Array.<String>} consumables.class Class name or array of class names. | |
* @param {String|Array.<String>} consumables.style Style name or array of style names. | |
*/ | |
revert( element, consumables ) { | |
const elementConsumables = this._consumables.get( element ); | |
if ( elementConsumables !== undefined ) { | |
if ( element.is( 'text' ) || element.is( 'documentFragment' ) ) { | |
// For text nodes and document fragments - set consumable to true. | |
this._consumables.set( element, true ); | |
} else { | |
// For elements - revert items from consumables object. | |
elementConsumables.revert( consumables ); | |
} | |
} | |
} | |
/** | |
* Creates consumable object from {@link module:engine/view/element~Element view element}. Consumable object will include | |
* element's name and all its attributes, classes and styles. | |
* | |
* @static | |
* @param {module:engine/view/element~Element} element | |
* @returns {Object} consumables | |
*/ | |
static consumablesFromElement( element ) { | |
const consumables = { | |
name: true, | |
attribute: [], | |
class: [], | |
style: [] | |
}; | |
const attributes = element.getAttributeKeys(); | |
for ( const attribute of attributes ) { | |
// Skip classes and styles - will be added separately. | |
if ( attribute == 'style' || attribute == 'class' ) { | |
continue; | |
} | |
consumables.attribute.push( attribute ); | |
} | |
const classes = element.getClassNames(); | |
for ( const className of classes ) { | |
consumables.class.push( className ); | |
} | |
const styles = element.getStyleNames(); | |
for ( const style of styles ) { | |
consumables.style.push( style ); | |
} | |
return consumables; | |
} | |
/** | |
* Creates {@link module:engine/conversion/viewconsumable~ViewConsumable ViewConsumable} instance from | |
* {@link module:engine/view/node~Node node} or {@link module:engine/view/documentfragment~DocumentFragment document fragment}. | |
* Instance will contain all elements, child nodes, attributes, styles and classes added for consumption. | |
* | |
* @static | |
* @param {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment} from View node or document fragment | |
* from which `ViewConsumable` will be created. | |
* @param {module:engine/conversion/viewconsumable~ViewConsumable} [instance] If provided, given `ViewConsumable` instance will be used | |
* to add all consumables. It will be returned instead of a new instance. | |
*/ | |
static createFrom( from, instance ) { | |
if ( !instance ) { | |
instance = new ViewConsumable(); | |
} | |
if ( from.is( 'text' ) ) { | |
instance.add( from ); | |
return instance; | |
} | |
// Add `from` itself, if it is an element. | |
if ( from.is( 'element' ) ) { | |
instance.add( from, ViewConsumable.consumablesFromElement( from ) ); | |
} | |
if ( from.is( 'documentFragment' ) ) { | |
instance.add( from ); | |
} | |
for ( const child of from.getChildren() ) { | |
instance = ViewConsumable.createFrom( child, instance ); | |
} | |
return instance; | |
} | |
} | |
/** | |
* This is a private helper-class for {@link module:engine/conversion/viewconsumable~ViewConsumable}. | |
* It represents and manipulates consumable parts of a single {@link module:engine/view/element~Element}. | |
* | |
* @private | |
*/ | |
class viewconsumable_ViewElementConsumables { | |
/** | |
* Creates ViewElementConsumables instance. | |
*/ | |
constructor() { | |
/** | |
* Flag indicating if name of the element can be consumed. | |
* | |
* @private | |
* @member {Boolean} | |
*/ | |
this._canConsumeName = null; | |
/** | |
* Contains maps of element's consumables: attributes, classes and styles. | |
* | |
* @private | |
* @member {Object} | |
*/ | |
this._consumables = { | |
attribute: new Map(), | |
style: new Map(), | |
class: new Map() | |
}; | |
} | |
/** | |
* Adds consumable parts of the {@link module:engine/view/element~Element view element}. | |
* Element's name itself can be marked to be consumed (when element's name is consumed its attributes, classes and | |
* styles still could be consumed): | |
* | |
* consumables.add( { name: true } ); | |
* | |
* Attributes classes and styles: | |
* | |
* consumables.add( { attribute: 'title', class: 'foo', style: 'color' } ); | |
* consumables.add( { attribute: [ 'title', 'name' ], class: [ 'foo', 'bar' ] ); | |
* | |
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `viewconsumable-invalid-attribute` when `class` or `style` | |
* attribute is provided - it should be handled separately by providing `style` and `class` in consumables object. | |
* | |
* @param {Object} consumables Object describing which parts of the element can be consumed. | |
* @param {Boolean} consumables.name If set to `true` element's name will be added as consumable. | |
* @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names to add as consumable. | |
* @param {String|Array.<String>} consumables.class Class name or array of class names to add as consumable. | |
* @param {String|Array.<String>} consumables.style Style name or array of style names to add as consumable. | |
*/ | |
add( consumables ) { | |
if ( consumables.name ) { | |
this._canConsumeName = true; | |
} | |
for ( const type in this._consumables ) { | |
if ( type in consumables ) { | |
this._add( type, consumables[ type ] ); | |
} | |
} | |
} | |
/** | |
* Tests if parts of the {@link module:engine/view/node~Node view node} can be consumed. | |
* | |
* Element's name can be tested: | |
* | |
* consumables.test( { name: true } ); | |
* | |
* Attributes classes and styles: | |
* | |
* consumables.test( { attribute: 'title', class: 'foo', style: 'color' } ); | |
* consumables.test( { attribute: [ 'title', 'name' ], class: [ 'foo', 'bar' ] ); | |
* | |
* @param {Object} consumables Object describing which parts of the element should be tested. | |
* @param {Boolean} consumables.name If set to `true` element's name will be tested. | |
* @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names to test. | |
* @param {String|Array.<String>} consumables.class Class name or array of class names to test. | |
* @param {String|Array.<String>} consumables.style Style name or array of style names to test. | |
* @returns {Boolean|null} `true` when all tested items can be consumed, `null` when even one of the items | |
* was never marked for consumption and `false` when even one of the items was already consumed. | |
*/ | |
test( consumables ) { | |
// Check if name can be consumed. | |
if ( consumables.name && !this._canConsumeName ) { | |
return this._canConsumeName; | |
} | |
for ( const type in this._consumables ) { | |
if ( type in consumables ) { | |
const value = this._test( type, consumables[ type ] ); | |
if ( value !== true ) { | |
return value; | |
} | |
} | |
} | |
// Return true only if all can be consumed. | |
return true; | |
} | |
/** | |
* Consumes parts of {@link module:engine/view/element~Element view element}. This function does not check if consumable item | |
* is already consumed - it consumes all consumable items provided. | |
* Element's name can be consumed: | |
* | |
* consumables.consume( { name: true } ); | |
* | |
* Attributes classes and styles: | |
* | |
* consumables.consume( { attribute: 'title', class: 'foo', style: 'color' } ); | |
* consumables.consume( { attribute: [ 'title', 'name' ], class: [ 'foo', 'bar' ] ); | |
* | |
* @param {Object} consumables Object describing which parts of the element should be consumed. | |
* @param {Boolean} consumables.name If set to `true` element's name will be consumed. | |
* @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names to consume. | |
* @param {String|Array.<String>} consumables.class Class name or array of class names to consume. | |
* @param {String|Array.<String>} consumables.style Style name or array of style names to consume. | |
*/ | |
consume( consumables ) { | |
if ( consumables.name ) { | |
this._canConsumeName = false; | |
} | |
for ( const type in this._consumables ) { | |
if ( type in consumables ) { | |
this._consume( type, consumables[ type ] ); | |
} | |
} | |
} | |
/** | |
* Revert already consumed parts of {@link module:engine/view/element~Element view Element}, so they can be consumed once again. | |
* Element's name can be reverted: | |
* | |
* consumables.revert( { name: true } ); | |
* | |
* Attributes classes and styles: | |
* | |
* consumables.revert( { attribute: 'title', class: 'foo', style: 'color' } ); | |
* consumables.revert( { attribute: [ 'title', 'name' ], class: [ 'foo', 'bar' ] ); | |
* | |
* @param {Object} consumables Object describing which parts of the element should be reverted. | |
* @param {Boolean} consumables.name If set to `true` element's name will be reverted. | |
* @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names to revert. | |
* @param {String|Array.<String>} consumables.class Class name or array of class names to revert. | |
* @param {String|Array.<String>} consumables.style Style name or array of style names to revert. | |
*/ | |
revert( consumables ) { | |
if ( consumables.name ) { | |
this._canConsumeName = true; | |
} | |
for ( const type in this._consumables ) { | |
if ( type in consumables ) { | |
this._revert( type, consumables[ type ] ); | |
} | |
} | |
} | |
/** | |
* Helper method that adds consumables of a given type: attribute, class or style. | |
* | |
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `viewconsumable-invalid-attribute` when `class` or `style` | |
* type is provided - it should be handled separately by providing actual style/class type. | |
* | |
* @private | |
* @param {String} type Type of the consumable item: `attribute`, `class` or `style`. | |
* @param {String|Array.<String>} item Consumable item or array of items. | |
*/ | |
_add( type, item ) { | |
const items = lodash_isArray( item ) ? item : [ item ]; | |
const consumables = this._consumables[ type ]; | |
for ( const name of items ) { | |
if ( type === 'attribute' && ( name === 'class' || name === 'style' ) ) { | |
/** | |
* Class and style attributes should be handled separately in | |
* {@link module:engine/conversion/viewconsumable~ViewConsumable#add `ViewConsumable#add()`}. | |
* | |
* What you have done is trying to use: | |
* | |
* consumables.add( { attribute: [ 'class', 'style' ] } ); | |
* | |
* While each class and style should be registered separately: | |
* | |
* consumables.add( { class: 'some-class', style: 'font-weight' } ); | |
* | |
* @error viewconsumable-invalid-attribute | |
*/ | |
throw new CKEditorError( 'viewconsumable-invalid-attribute: Classes and styles should be handled separately.' ); | |
} | |
consumables.set( name, true ); | |
} | |
} | |
/** | |
* Helper method that tests consumables of a given type: attribute, class or style. | |
* | |
* @private | |
* @param {String} type Type of the consumable item: `attribute`, `class` or `style`. | |
* @param {String|Array.<String>} item Consumable item or array of items. | |
* @returns {Boolean|null} Returns `true` if all items can be consumed, `null` when one of the items cannot be | |
* consumed and `false` when one of the items is already consumed. | |
*/ | |
_test( type, item ) { | |
const items = lodash_isArray( item ) ? item : [ item ]; | |
const consumables = this._consumables[ type ]; | |
for ( const name of items ) { | |
if ( type === 'attribute' && ( name === 'class' || name === 'style' ) ) { | |
// Check all classes/styles if class/style attribute is tested. | |
const value = this._test( name, [ ...this._consumables[ name ].keys() ] ); | |
if ( value !== true ) { | |
return value; | |
} | |
} else { | |
const value = consumables.get( name ); | |
// Return null if attribute is not found. | |
if ( value === undefined ) { | |
return null; | |
} | |
if ( !value ) { | |
return false; | |
} | |
} | |
} | |
return true; | |
} | |
/** | |
* Helper method that consumes items of a given type: attribute, class or style. | |
* | |
* @private | |
* @param {String} type Type of the consumable item: `attribute`, `class` or `style`. | |
* @param {String|Array.<String>} item Consumable item or array of items. | |
*/ | |
_consume( type, item ) { | |
const items = lodash_isArray( item ) ? item : [ item ]; | |
const consumables = this._consumables[ type ]; | |
for ( const name of items ) { | |
if ( type === 'attribute' && ( name === 'class' || name === 'style' ) ) { | |
// If class or style is provided for consumption - consume them all. | |
this._consume( name, [ ...this._consumables[ name ].keys() ] ); | |
} else { | |
consumables.set( name, false ); | |
} | |
} | |
} | |
/** | |
* Helper method that reverts items of a given type: attribute, class or style. | |
* | |
* @private | |
* @param {String} type Type of the consumable item: `attribute`, `class` or , `style`. | |
* @param {String|Array.<String>} item Consumable item or array of items. | |
*/ | |
_revert( type, item ) { | |
const items = lodash_isArray( item ) ? item : [ item ]; | |
const consumables = this._consumables[ type ]; | |
for ( const name of items ) { | |
if ( type === 'attribute' && ( name === 'class' || name === 'style' ) ) { | |
// If class or style is provided for reverting - revert them all. | |
this._revert( name, [ ...this._consumables[ name ].keys() ] ); | |
} else { | |
const value = consumables.get( name ); | |
if ( value === false ) { | |
consumables.set( name, true ); | |
} | |
} | |
} | |
} | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/writer.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/model/writer | |
*/ | |
/** | |
* Contains functions used for composing model tree, grouped together under "model writer" name. Those functions | |
* are built on top of {@link module:engine/model/node~Node node}, and it's child classes', APIs. | |
* | |
* Model writer API has multiple advantages and it is highly recommended to use it when changing model tree and nodes: | |
* * model writer API {@link module:engine/model/writer~writer.normalizeNodes normalizes inserted nodes}, which means that you can insert | |
* not only {@link module:engine/model/node~Node nodes}, but also `String`s, {@link module:engine/model/textproxy~TextProxy text proxies} | |
* and | |
* {@link module:engine/model/documentfragment~DocumentFragment document fragments}, | |
* * model writer API operates on {@link module:engine/model/position~Position positions}, which means that you have | |
* better control over manipulating model tree as positions operate on offsets rather than indexes, | |
* * model writer API automatically merges {@link module:engine/model/text~Text text nodes} with same attributes, which means | |
* lower memory usage and better efficiency. | |
* | |
* @namespace writer | |
*/ | |
const writer_writer = { | |
insert: model_writer_insert, | |
remove: model_writer_remove, | |
move: model_writer_move, | |
setAttribute: writer_setAttribute, | |
removeAttribute: writer_removeAttribute, | |
normalizeNodes | |
}; | |
/* harmony default export */ var model_writer = (writer_writer); | |
/** | |
* Inserts given nodes at given position. | |
* | |
* @function module:engine/model/writer~writer.insert | |
* @param {module:engine/model/position~Position} position Position at which nodes should be inserted. | |
* @param {module:engine/model/node~NodeSet} nodes Nodes to insert. | |
* @returns {module:engine/model/range~Range} Range spanning over inserted elements. | |
*/ | |
function model_writer_insert( position, nodes ) { | |
nodes = normalizeNodes( nodes ); | |
// We have to count offset before inserting nodes because they can get merged and we would get wrong offsets. | |
const offset = nodes.reduce( ( sum, node ) => sum + node.offsetSize, 0 ); | |
const parent = position.parent; | |
// Insertion might be in a text node, we should split it if that's the case. | |
_splitNodeAtPosition( position ); | |
const index = position.index; | |
// Insert nodes at given index. After splitting we have a proper index and insertion is between nodes, | |
// using basic `Element` API. | |
parent.insertChildren( index, nodes ); | |
// Merge text nodes, if possible. Merging is needed only at points where inserted nodes "touch" "old" nodes. | |
_mergeNodesAtIndex( parent, index + nodes.length ); | |
_mergeNodesAtIndex( parent, index ); | |
return new range_Range( position, position.getShiftedBy( offset ) ); | |
} | |
/** | |
* Removed nodes in given range. Only {@link module:engine/model/range~Range#isFlat flat} ranges are accepted. | |
* | |
* @function module:engine/model/writer~writer.remove | |
* @param {module:engine/model/range~Range} range Range containing nodes to remove. | |
* @returns {Array.<module:engine/model/node~Node>} | |
*/ | |
function model_writer_remove( range ) { | |
if ( !range.isFlat ) { | |
/** | |
* Trying to remove a range which starts and ends in different element. | |
* | |
* @error model-writer-remove-range-not-flat | |
*/ | |
throw new CKEditorError( 'model-writer-remove-range-not-flat: ' + | |
'Trying to remove a range which starts and ends in different element.' ); | |
} | |
const parent = range.start.parent; | |
// Range may be inside text nodes, we have to split them if that's the case. | |
_splitNodeAtPosition( range.start ); | |
_splitNodeAtPosition( range.end ); | |
// Remove the text nodes using basic `Element` API. | |
const removed = parent.removeChildren( range.start.index, range.end.index - range.start.index ); | |
// Merge text nodes, if possible. After some nodes were removed, node before and after removed range will be | |
// touching at the position equal to the removed range beginning. We check merging possibility there. | |
_mergeNodesAtIndex( parent, range.start.index ); | |
return removed; | |
} | |
/** | |
* Moves nodes in given range to given target position. Only {@link module:engine/model/range~Range#isFlat flat} ranges are accepted. | |
* | |
* @param {module:engine/model/range~Range} sourceRange Range containing nodes to move. | |
* @param {module:engine/model/position~Position} targetPosition Position to which nodes should be moved. | |
* @returns {module:engine/model/range~Range} Range containing moved nodes. | |
*/ | |
function model_writer_move( sourceRange, targetPosition ) { | |
if ( !sourceRange.isFlat ) { | |
/** | |
* Trying to move a range which starts and ends in different element. | |
* | |
* @error model-writer-move-range-not-flat | |
*/ | |
throw new CKEditorError( 'model-writer-move-range-not-flat: ' + | |
'Trying to move a range which starts and ends in different element.' ); | |
} | |
const nodes = this.remove( sourceRange ); | |
// We have to fix `targetPosition` because model changed after nodes from `sourceRange` got removed and | |
// that change might have an impact on `targetPosition`. | |
targetPosition = targetPosition._getTransformedByDeletion( sourceRange.start, sourceRange.end.offset - sourceRange.start.offset ); | |
return this.insert( targetPosition, nodes ); | |
} | |
/** | |
* Sets given attribute on nodes in given range. | |
* | |
* @param {module:engine/model/range~Range} range Range containing nodes that should have the attribute set. | |
* @param {String} key Key of attribute to set. | |
* @param {*} value Attribute value. | |
*/ | |
function writer_setAttribute( range, key, value ) { | |
// Range might start or end in text nodes, so we have to split them. | |
_splitNodeAtPosition( range.start ); | |
_splitNodeAtPosition( range.end ); | |
// Iterate over all items in the range. | |
for ( const item of range.getItems() ) { | |
// Iterator will return `TextProxy` instances but we know that those text proxies will | |
// always represent full text nodes (this is guaranteed thanks to splitting we did before). | |
// So, we can operate on those text proxies' text nodes. | |
const node = item.is( 'textProxy' ) ? item.textNode : item; | |
if ( value !== null ) { | |
node.setAttribute( key, value ); | |
} else { | |
node.removeAttribute( key ); | |
} | |
// After attributes changing it may happen that some text nodes can be merged. Try to merge with previous node. | |
_mergeNodesAtIndex( node.parent, node.index ); | |
} | |
// Try to merge last changed node with it's previous sibling (not covered by the loop above). | |
_mergeNodesAtIndex( range.end.parent, range.end.index ); | |
} | |
/** | |
* Removes given attribute from nodes in given range. | |
* | |
* @param {module:engine/model/range~Range} range Range containing nodes that should have the attribute removed. | |
* @param {String} key Key of attribute to remove. | |
*/ | |
function writer_removeAttribute( range, key ) { | |
this.setAttribute( range, key, null ); | |
} | |
/** | |
* Normalizes given object or an array of objects to an array of {@link module:engine/model/node~Node nodes}. See | |
* {@link module:engine/model/node~NodeSet NodeSet} for details on how normalization is performed. | |
* | |
* @param {module:engine/model/node~NodeSet} nodes Objects to normalize. | |
* @returns {Array.<module:engine/model/node~Node>} Normalized nodes. | |
*/ | |
function normalizeNodes( nodes ) { | |
const normalized = []; | |
if ( !( nodes instanceof Array ) ) { | |
nodes = [ nodes ]; | |
} | |
// Convert instances of classes other than Node. | |
for ( let i = 0; i < nodes.length; i++ ) { | |
if ( typeof nodes[ i ] == 'string' ) { | |
normalized.push( new text_Text( nodes[ i ] ) ); | |
} else if ( nodes[ i ] instanceof textproxy_TextProxy ) { | |
normalized.push( new text_Text( nodes[ i ].data, nodes[ i ].getAttributes() ) ); | |
} else if ( nodes[ i ] instanceof documentfragment_DocumentFragment || nodes[ i ] instanceof nodelist_NodeList ) { | |
for ( const child of nodes[ i ] ) { | |
normalized.push( child ); | |
} | |
} else if ( nodes[ i ] instanceof node_Node ) { | |
normalized.push( nodes[ i ] ); | |
} | |
// Skip unrecognized type. | |
} | |
// Merge text nodes. | |
for ( let i = 1; i < normalized.length; i++ ) { | |
const node = normalized[ i ]; | |
const prev = normalized[ i - 1 ]; | |
if ( node instanceof text_Text && prev instanceof text_Text && _haveSameAttributes( node, prev ) ) { | |
// Doing this instead changing prev.data because .data is readonly. | |
normalized.splice( i - 1, 2, new text_Text( prev.data + node.data, prev.getAttributes() ) ); | |
i--; | |
} | |
} | |
return normalized; | |
} | |
/** | |
* Checks if nodes before and after given index in given element are {@link module:engine/model/text~Text text nodes} and | |
* merges them into one node if they have same attributes. | |
* | |
* Merging is done by removing two text nodes and inserting a new text node containing data from both merged text nodes. | |
* | |
* @ignore | |
* @private | |
* @param {module:engine/model/element~Element} element Parent element of nodes to merge. | |
* @param {Number} index Index between nodes to merge. | |
*/ | |
function _mergeNodesAtIndex( element, index ) { | |
const nodeBefore = element.getChild( index - 1 ); | |
const nodeAfter = element.getChild( index ); | |
// Check if both of those nodes are text objects with same attributes. | |
if ( nodeBefore && nodeAfter && nodeBefore.is( 'text' ) && nodeAfter.is( 'text' ) && _haveSameAttributes( nodeBefore, nodeAfter ) ) { | |
// Append text of text node after index to the before one. | |
const mergedNode = new text_Text( nodeBefore.data + nodeAfter.data, nodeBefore.getAttributes() ); | |
// Remove separate text nodes. | |
element.removeChildren( index - 1, 2 ); | |
// Insert merged text node. | |
element.insertChildren( index - 1, mergedNode ); | |
} | |
} | |
/** | |
* Checks if given position is in a text node, and if so, splits the text node in two text nodes, each of them | |
* containing a part of original text node. | |
* | |
* @ignore | |
* @private | |
* @param {module:engine/model/position~Position} position Position at which node should be split. | |
*/ | |
function _splitNodeAtPosition( position ) { | |
const textNode = position.textNode; | |
const element = position.parent; | |
if ( textNode ) { | |
const offsetDiff = position.offset - textNode.startOffset; | |
const index = textNode.index; | |
element.removeChildren( index, 1 ); | |
const firstPart = new text_Text( textNode.data.substr( 0, offsetDiff ), textNode.getAttributes() ); | |
const secondPart = new text_Text( textNode.data.substr( offsetDiff ), textNode.getAttributes() ); | |
element.insertChildren( index, [ firstPart, secondPart ] ); | |
} | |
} | |
/** | |
* Checks whether two given nodes have same attributes. | |
* | |
* @ignore | |
* @private | |
* @param {module:engine/model/node~Node} nodeA Node to check. | |
* @param {module:engine/model/node~Node} nodeB Node to check. | |
* @returns {Boolean} `true` if nodes have same attributes, `false` otherwise. | |
*/ | |
function _haveSameAttributes( nodeA, nodeB ) { | |
const iteratorA = nodeA.getAttributes(); | |
const iteratorB = nodeB.getAttributes(); | |
for ( const attr of iteratorA ) { | |
if ( attr[ 1 ] !== nodeB.getAttribute( attr[ 0 ] ) ) { | |
return false; | |
} | |
iteratorB.next(); | |
} | |
return iteratorB.next().done; | |
} | |
/** | |
* Value that can be normalized to an array of {@link module:engine/model/node~Node nodes}. | |
* | |
* Non-arrays are normalized as follows: | |
* * {@link module:engine/model/node~Node Node} is left as is, | |
* * {@link module:engine/model/textproxy~TextProxy TextProxy} and `String` are normalized to {@link module:engine/model/text~Text Text}, | |
* * {@link module:engine/model/nodelist~NodeList NodeList} is normalized to an array containing all nodes that are in that node list, | |
* * {@link module:engine/model/documentfragment~DocumentFragment DocumentFragment} is normalized to an array containing all of it's | |
* * children. | |
* | |
* Arrays are processed item by item like non-array values and flattened to one array. Normalization always results in | |
* a flat array of {@link module:engine/model/node~Node nodes}. Consecutive text nodes (or items normalized to text nodes) will be | |
* merged if they have same attributes. | |
* | |
* @typedef {module:engine/model/node~Node|module:engine/model/textproxy~TextProxy|String| | |
* module:engine/model/nodelist~NodeList|module:engine/model/documentfragment~DocumentFragment|Iterable} | |
* module:engine/model/node~NodeSet | |
*/ | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/conversion/viewconversiondispatcher.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/conversion/viewconversiondispatcher | |
*/ | |
/** | |
* `ViewConversionDispatcher` is a central point of {@link module:engine/view/view view} conversion, which is a process of | |
* converting given {@link module:engine/view/documentfragment~DocumentFragment view document fragment} or | |
* {@link module:engine/view/element~Element} | |
* into another structure. In default application, {@link module:engine/view/view view} is converted to {@link module:engine/model/model}. | |
* | |
* During conversion process, for all {@link module:engine/view/node~Node view nodes} from the converted view document fragment, | |
* `ViewConversionDispatcher` fires corresponding events. Special callbacks called "converters" should listen to | |
* `ViewConversionDispatcher` for those events. | |
* | |
* Each callback, as a first argument, is passed a special object `data` that has `input` and `output` properties. | |
* `input` property contains {@link module:engine/view/node~Node view node} or | |
* {@link module:engine/view/documentfragment~DocumentFragment view document fragment} | |
* that is converted at the moment and might be handled by the callback. `output` property should be used to save the result | |
* of conversion. Keep in mind that the `data` parameter is customizable and may contain other values - see | |
* {@link ~ViewConversionDispatcher#convert}. It is also shared by reference by all callbacks | |
* listening to given event. **Note**: in view to model conversion - `data` contains `context` property that is an array | |
* of {@link module:engine/model/element~Element model elements}. These are model elements that will be the parent of currently | |
* converted view item. `context` property is used in examples below. | |
* | |
* The second parameter passed to a callback is an instance of {@link module:engine/conversion/viewconsumable~ViewConsumable}. It stores | |
* information about what parts of processed view item are still waiting to be handled. After a piece of view item | |
* was converted, appropriate consumable value should be {@link module:engine/conversion/viewconsumable~ViewConsumable#consume consumed}. | |
* | |
* The third parameter passed to a callback is an instance of {@link ~ViewConversionDispatcher} | |
* which provides additional tools for converters. | |
* | |
* Examples of providing callbacks for `ViewConversionDispatcher`: | |
* | |
* // Converter for paragraphs (<p>). | |
* viewDispatcher.on( 'element:p', ( evt, data, consumable, conversionApi ) => { | |
* const paragraph = new ModelElement( 'paragraph' ); | |
* const schemaQuery = { | |
* name: 'paragraph', | |
* inside: data.context | |
* }; | |
* | |
* if ( conversionApi.schema.check( schemaQuery ) ) { | |
* if ( !consumable.consume( data.input, { name: true } ) ) { | |
* // Before converting this paragraph's children we have to update their context by this paragraph. | |
* data.context.push( paragraph ); | |
* const children = conversionApi.convertChildren( data.input, consumable, data ); | |
* data.context.pop(); | |
* paragraph.appendChildren( children ); | |
* data.output = paragraph; | |
* } | |
* } | |
* } ); | |
* | |
* // Converter for links (<a>). | |
* viewDispatcher.on( 'element:a', ( evt, data, consumable, conversionApi ) => { | |
* if ( consumable.consume( data.input, { name: true, attributes: [ 'href' ] } ) ) { | |
* // <a> element is inline and is represented by an attribute in the model. | |
* // This is why we are not updating `context` property. | |
* data.output = conversionApi.convertChildren( data.input, consumable, data ); | |
* | |
* for ( let item of Range.createFrom( data.output ) ) { | |
* const schemaQuery = { | |
* name: item.name || '$text', | |
* attribute: 'link', | |
* inside: data.context | |
* }; | |
* | |
* if ( conversionApi.schema.check( schemaQuery ) ) { | |
* item.setAttribute( 'link', data.input.getAttribute( 'href' ) ); | |
* } | |
* } | |
* } | |
* } ); | |
* | |
* // Fire conversion. | |
* // Always take care where the converted model structure will be appended to. If this `viewDocumentFragment` | |
* // is going to be appended directly to a '$root' element, use that in `context`. | |
* viewDispatcher.convert( viewDocumentFragment, { context: [ '$root' ] } ); | |
* | |
* Before each conversion process, `ViewConversionDispatcher` fires {@link ~ViewConversionDispatcher#event:viewCleanup} | |
* event which can be used to prepare tree view for conversion. | |
* | |
* @mixes module:utils/emittermixin~EmitterMixin | |
* @fires viewCleanup | |
* @fires element | |
* @fires text | |
* @fires documentFragment | |
*/ | |
class viewconversiondispatcher_ViewConversionDispatcher { | |
/** | |
* Creates a `ViewConversionDispatcher` that operates using passed API. | |
* | |
* @see module:engine/conversion/viewconversiondispatcher~ViewConversionApi | |
* @param {Object} [conversionApi] Additional properties for interface that will be passed to events fired | |
* by `ViewConversionDispatcher`. | |
*/ | |
constructor( conversionApi = {} ) { | |
/** | |
* Interface passed by dispatcher to the events callbacks. | |
* | |
* @member {module:engine/conversion/viewconversiondispatcher~ViewConversionApi} | |
*/ | |
this.conversionApi = lodash_assignIn( {}, conversionApi ); | |
// `convertItem` and `convertChildren` are bound to this `ViewConversionDispatcher` instance and | |
// set on `conversionApi`. This way only a part of `ViewConversionDispatcher` API is exposed. | |
this.conversionApi.convertItem = this._convertItem.bind( this ); | |
this.conversionApi.convertChildren = this._convertChildren.bind( this ); | |
} | |
/** | |
* Starts the conversion process. The entry point for the conversion. | |
* | |
* @fires element | |
* @fires text | |
* @fires documentFragment | |
* @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element} viewItem | |
* Part of the view to be converted. | |
* @param {Object} [additionalData] Additional data to be passed in `data` argument when firing `ViewConversionDispatcher` | |
* events. See also {@link ~ViewConversionDispatcher#event:element element event}. | |
* @returns {module:engine/model/documentfragment~DocumentFragment} Model data that is a result of the conversion process | |
* wrapped in `DocumentFragment`. Converted marker elements will be set as that document fragment's | |
* {@link module:engine/model/documentfragment~DocumentFragment#markers static markers map}. | |
*/ | |
convert( viewItem, additionalData = {} ) { | |
this.fire( 'viewCleanup', viewItem ); | |
const consumable = ViewConsumable.createFrom( viewItem ); | |
let conversionResult = this._convertItem( viewItem, consumable, additionalData ); | |
// We can get a null here if conversion failed (see _convertItem()) | |
// or simply if an item could not be converted (e.g. due to the schema). | |
if ( !conversionResult ) { | |
return new documentfragment_DocumentFragment(); | |
} | |
// When conversion result is not a document fragment we need to wrap it in document fragment. | |
if ( !conversionResult.is( 'documentFragment' ) ) { | |
conversionResult = new documentfragment_DocumentFragment( [ conversionResult ] ); | |
} | |
// Extract temporary markers elements from model and set as static markers collection. | |
conversionResult.markers = extractMarkersFromModelFragment( conversionResult ); | |
return conversionResult; | |
} | |
/** | |
* @private | |
* @see module:engine/conversion/viewconversiondispatcher~ViewConversionApi#convertItem | |
*/ | |
_convertItem( input, consumable, additionalData = {} ) { | |
const data = lodash_assignIn( {}, additionalData, { | |
input, | |
output: null | |
} ); | |
if ( input.is( 'element' ) ) { | |
this.fire( 'element:' + input.name, data, consumable, this.conversionApi ); | |
} else if ( input.is( 'text' ) ) { | |
this.fire( 'text', data, consumable, this.conversionApi ); | |
} else { | |
this.fire( 'documentFragment', data, consumable, this.conversionApi ); | |
} | |
// Handle incorrect `data.output`. | |
if ( data.output && !( data.output instanceof node_Node || data.output instanceof documentfragment_DocumentFragment ) ) { | |
/** | |
* Incorrect conversion result was dropped. | |
* | |
* Item may be converted to either {@link module:engine/model/node~Node model node} or | |
* {@link module:engine/model/documentfragment~DocumentFragment model document fragment}. | |
* | |
* @error view-conversion-dispatcher-incorrect-result | |
*/ | |
src_log.warn( 'view-conversion-dispatcher-incorrect-result: Incorrect conversion result was dropped.', [ input, data.output ] ); | |
return null; | |
} | |
return data.output; | |
} | |
/** | |
* @private | |
* @see module:engine/conversion/viewconversiondispatcher~ViewConversionApi#convertChildren | |
*/ | |
_convertChildren( input, consumable, additionalData = {} ) { | |
// Get all children of view input item. | |
const viewChildren = Array.from( input.getChildren() ); | |
// 1. Map those children to model. | |
// 2. Filter out items that has not been converted or for which conversion returned wrong result (for those warning is logged). | |
// 3. Extract children from document fragments to flatten results. | |
const convertedChildren = viewChildren | |
.map( viewChild => this._convertItem( viewChild, consumable, additionalData ) ) | |
.filter( converted => converted instanceof node_Node || converted instanceof documentfragment_DocumentFragment ) | |
.reduce( ( result, filtered ) => { | |
return result.concat( | |
filtered.is( 'documentFragment' ) ? Array.from( filtered.getChildren() ) : filtered | |
); | |
}, [] ); | |
// Normalize array to model document fragment. | |
return new documentfragment_DocumentFragment( convertedChildren ); | |
} | |
/** | |
* Fired before the first conversion event, at the beginning of view to model conversion process. | |
* | |
* @event viewCleanup | |
* @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element} | |
* viewItem Part of the view to be converted. | |
*/ | |
/** | |
* Fired when {@link module:engine/view/element~Element} is converted. | |
* | |
* `element` is a namespace event for a class of events. Names of actually called events follow this pattern: | |
* `element:<elementName>` where `elementName` is the name of converted element. This way listeners may listen to | |
* all elements conversion or to conversion of specific elements. | |
* | |
* @event element | |
* @param {Object} data Object containing conversion input and a placeholder for conversion output and possibly other | |
* values (see {@link #convert}). | |
* Keep in mind that this object is shared by reference between all callbacks that will be called. | |
* This means that callbacks can add their own values if needed, | |
* and those values will be available in other callbacks. | |
* @param {module:engine/view/element~Element} data.input Converted element. | |
* @param {*} data.output The current state of conversion result. Every change to converted element should | |
* be reflected by setting or modifying this property. | |
* @param {module:engine/model/schema~SchemaPath} data.context The conversion context. | |
* @param {module:engine/conversion/viewconsumable~ViewConsumable} consumable Values to consume. | |
* @param {Object} conversionApi Conversion interface to be used by callback, passed in `ViewConversionDispatcher` constructor. | |
* Besides of properties passed in constructor, it also has `convertItem` and `convertChildren` methods which are references | |
* to {@link #_convertItem} and | |
* {@link ~ViewConversionDispatcher#_convertChildren}. Those methods are needed to convert | |
* the whole view-tree they were exposed in `conversionApi` for callbacks. | |
*/ | |
/** | |
* Fired when {@link module:engine/view/text~Text} is converted. | |
* | |
* @event text | |
* @see #event:element | |
*/ | |
/** | |
* Fired when {@link module:engine/view/documentfragment~DocumentFragment} is converted. | |
* | |
* @event documentFragment | |
* @see #event:element | |
*/ | |
} | |
mix( viewconversiondispatcher_ViewConversionDispatcher, emittermixin ); | |
// Traverses given model item and searches elements which marks marker range. Found element is removed from | |
// DocumentFragment but path of this element is stored in a Map which is then returned. | |
// | |
// @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/node~Node} modelItem Fragment of model. | |
// @returns {Map<String, module:engine/model/range~Range>} List of static markers. | |
function extractMarkersFromModelFragment( modelItem ) { | |
const markerElements = new Set(); | |
const markers = new Map(); | |
// Create ModelTreeWalker. | |
const walker = new treewalker_TreeWalker( { | |
startPosition: position_Position.createAt( modelItem, 0 ), | |
ignoreElementEnd: true | |
} ); | |
// Walk through DocumentFragment and collect marker elements. | |
for ( const value of walker ) { | |
// Check if current element is a marker. | |
if ( value.item.name == '$marker' ) { | |
markerElements.add( value.item ); | |
} | |
} | |
// Walk through collected marker elements store its path and remove its from the DocumentFragment. | |
for ( const markerElement of markerElements ) { | |
const markerName = markerElement.getAttribute( 'data-name' ); | |
const currentPosition = position_Position.createBefore( markerElement ); | |
// When marker of given name is not stored it means that we have found the beginning of the range. | |
if ( !markers.has( markerName ) ) { | |
markers.set( markerName, new range_Range( position_Position.createFromPosition( currentPosition ) ) ); | |
// Otherwise is means that we have found end of the marker range. | |
} else { | |
markers.get( markerName ).end = position_Position.createFromPosition( currentPosition ); | |
} | |
// Remove marker element from DocumentFragment. | |
model_writer_remove( range_Range.createOn( markerElement ) ); | |
} | |
return markers; | |
} | |
/** | |
* Conversion interface that is registered for given {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher} | |
* and is passed as one of parameters when {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher dispatcher} | |
* fires it's events. | |
* | |
* `ViewConversionApi` object is built by {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher} constructor. | |
* The exact list of properties of this object is determined by the object passed to the constructor. | |
* | |
* @interface ViewConversionApi | |
*/ | |
/** | |
* Starts conversion of given item by firing an appropriate event. | |
* | |
* Every fired event is passed (as first parameter) an object with `output` property. Every event may set and/or | |
* modify that property. When all callbacks are done, the final value of `output` property is returned by this method. | |
* The `output` must be either {@link module:engine/model/node~Node model node} or | |
* {@link module:engine/model/documentfragment~DocumentFragment model document fragment} or `null` (as set by default). | |
* | |
* @method #convertItem | |
* @fires module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:element | |
* @fires module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:text | |
* @fires module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:documentFragment | |
* @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element|module:engine/view/text~Text} | |
* input Item to convert. | |
* @param {module:engine/conversion/viewconsumable~ViewConsumable} consumable Values to consume. | |
* @param {Object} [additionalData] Additional data to be passed in `data` argument when firing `ViewConversionDispatcher` | |
* events. See also {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:element element event}. | |
* @returns {module:engine/model/node~Node|module:engine/model/documentfragment~DocumentFragment|null} The result of item conversion, | |
* created and modified by callbacks attached to fired event, or `null` if the conversion result was incorrect. | |
*/ | |
/** | |
* Starts conversion of all children of given item by firing appropriate events for all those children. | |
* | |
* @method #convertChildren | |
* @fires module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:element | |
* @fires module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:text | |
* @fires module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:documentFragment | |
* @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element} | |
* input Item which children will be converted. | |
* @param {module:engine/conversion/viewconsumable~ViewConsumable} consumable Values to consume. | |
* @param {Object} [additionalData] Additional data to be passed in `data` argument when firing `ViewConversionDispatcher` | |
* events. See also {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:element element event}. | |
* @returns {module:engine/model/documentfragment~DocumentFragment} Model document fragment containing results of conversion | |
* of all children of given item. | |
*/ | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/conversion/view-to-model-converters.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* Contains {@link module:engine/view/view view} to {@link module:engine/model/model model} converters for | |
* {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher}. | |
* | |
* @module engine/conversion/view-to-model-converters | |
*/ | |
/** | |
* Function factory, creates a converter that converts {@link module:engine/view/documentfragment~DocumentFragment view document fragment} | |
* or all children of {@link module:engine/view/element~Element} into | |
* {@link module:engine/model/documentfragment~DocumentFragment model document fragment}. | |
* This is the "entry-point" converter for view to model conversion. This converter starts the conversion of all children | |
* of passed view document fragment. Those children {@link module:engine/view/node~Node view nodes} are then handled by other converters. | |
* | |
* This also a "default", last resort converter for all view elements that has not been converted by other converters. | |
* When a view element is being converted to the model but it does not have converter specified, that view element | |
* will be converted to {@link module:engine/model/documentfragment~DocumentFragment model document fragment} and returned. | |
* | |
* @returns {Function} Universal converter for view {@link module:engine/view/documentfragment~DocumentFragment fragments} and | |
* {@link module:engine/view/element~Element elements} that returns | |
* {@link module:engine/model/documentfragment~DocumentFragment model fragment} with children of converted view item. | |
*/ | |
function convertToModelFragment() { | |
return ( evt, data, consumable, conversionApi ) => { | |
// Second argument in `consumable.consume` is discarded for ViewDocumentFragment but is needed for ViewElement. | |
if ( !data.output && consumable.consume( data.input, { name: true } ) ) { | |
const convertedChildren = conversionApi.convertChildren( data.input, consumable, data ); | |
data.output = new documentfragment_DocumentFragment( normalizeNodes( convertedChildren ) ); | |
} | |
}; | |
} | |
/** | |
* Function factory, creates a converter that converts {@link module:engine/view/text~Text} to {@link module:engine/model/text~Text}. | |
* | |
* @returns {Function} {@link module:engine/view/text~Text View text} converter. | |
*/ | |
function convertText() { | |
return ( evt, data, consumable, conversionApi ) => { | |
const schemaQuery = { | |
name: '$text', | |
inside: data.context | |
}; | |
if ( conversionApi.schema.check( schemaQuery ) ) { | |
if ( consumable.consume( data.input ) ) { | |
data.output = new text_Text( data.input.data ); | |
} | |
} | |
}; | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/liveposition.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/model/liveposition | |
*/ | |
/** | |
* `LivePosition` is a type of {@link module:engine/model/position~Position Position} | |
* that updates itself as {@link module:engine/model/document~Document document} | |
* is changed through operations. It may be used as a bookmark. | |
* | |
* **Note:** Contrary to {@link module:engine/model/position~Position}, `LivePosition` works only in roots that are | |
* {@link module:engine/model/rootelement~RootElement}. | |
* If {@link module:engine/model/documentfragment~DocumentFragment} is passed, error will be thrown. | |
* | |
* **Note:** Be very careful when dealing with `LivePosition`. Each `LivePosition` instance bind events that might | |
* have to be unbound. | |
* Use {@link module:engine/model/liveposition~LivePosition#detach} whenever you don't need `LivePosition` anymore. | |
* | |
* @extends module:engine/model/position~Position | |
*/ | |
class liveposition_LivePosition extends position_Position { | |
/** | |
* Creates a live position. | |
* | |
* @see module:engine/model/position~Position | |
* @param {module:engine/model/rootelement~RootElement} root | |
* @param {Array.<Number>} path | |
* @param {module:engine/model/position~PositionStickiness} [stickiness] Defaults to `'sticksToNext'`. | |
* See {@link module:engine/model/liveposition~LivePosition#stickiness}. | |
*/ | |
constructor( root, path, stickiness ) { | |
super( root, path ); | |
if ( !this.root.is( 'rootElement' ) ) { | |
/** | |
* LivePosition's root has to be an instance of RootElement. | |
* | |
* @error liveposition-root-not-rootelement | |
*/ | |
throw new CKEditorError( | |
'model-liveposition-root-not-rootelement: LivePosition\'s root has to be an instance of RootElement.' | |
); | |
} | |
/** | |
* Flag representing `LivePosition` stickiness. `LivePosition` might be sticking to previous node or next node. | |
* Whenever some nodes are inserted at the same position as `LivePosition`, `stickiness` is checked to decide if | |
* LivePosition should be moved. Similar applies when a range of nodes is moved and one of it's boundary | |
* position is same as `LivePosition`. | |
* | |
* Examples: | |
* | |
* Insert: | |
* Position is at | and we insert at the same position, marked as ^: | |
* - | sticks to previous node: `<p>f|^oo</p>` => `<p>f|baroo</p>` | |
* - | sticks to next node: `<p>f^|oo</p>` => `<p>fbar|oo</p>` | |
* | |
* Move: | |
* Position is at | and range [ ] is moved to position ^: | |
* - | sticks to previous node: `<p>f|[oo]</p><p>b^ar</p>` => `<p>f|</p><p>booar</p>` | |
* - | sticks to next node: `<p>f|[oo]</p><p>b^ar</p>` => `<p>f</p><p>b|ooar</p>` | |
* | |
* @member {module:engine/model/position~PositionStickiness} module:engine/model/liveposition~LivePosition#stickiness | |
*/ | |
this.stickiness = stickiness || 'sticksToNext'; | |
bindWithDocument.call( this ); | |
} | |
/** | |
* Unbinds all events previously bound by `LivePosition`. Use it whenever you don't need `LivePosition` instance | |
* anymore (i.e. when leaving scope in which it was declared or before re-assigning variable that was | |
* referring to it). | |
*/ | |
detach() { | |
this.stopListening(); | |
} | |
/** | |
* @static | |
* @method module:engine/model/liveposition~LivePosition.createAfter | |
* @see module:engine/model/position~Position.createAfter | |
* @param {module:engine/model/node~Node} node | |
* @returns {module:engine/model/liveposition~LivePosition} | |
*/ | |
/** | |
* @static | |
* @method module:engine/model/liveposition~LivePosition.createBefore | |
* @see module:engine/model/position~Position.createBefore | |
* @param {module:engine/model/node~Node} node | |
* @returns {module:engine/model/liveposition~LivePosition} | |
*/ | |
/** | |
* @static | |
* @method module:engine/model/liveposition~LivePosition.createFromParentAndOffset | |
* @see module:engine/model/position~Position.createFromParentAndOffset | |
* @param {module:engine/model/element~Element} parent | |
* @param {Number} offset | |
* @returns {module:engine/model/liveposition~LivePosition} | |
*/ | |
/** | |
* @static | |
* @method module:engine/model/liveposition~LivePosition.createFromPosition | |
* @see module:engine/model/position~Position.createFromPosition | |
* @param {module:engine/model/position~Position} position | |
* @returns {module:engine/model/liveposition~LivePosition} | |
*/ | |
/** | |
* Fired when `LivePosition` instance is changed due to changes on {@link module:engine/model/document~Document}. | |
* | |
* @event module:engine/model/liveposition~LivePosition#change | |
* @param {module:engine/model/position~Position} oldPosition Position equal to this live position before it got changed. | |
*/ | |
} | |
/** | |
* Binds this `LivePosition` to the {@link module:engine/model/document~Document document} that owns | |
* this position's {@link module:engine/model/position~Position#root root}. | |
* | |
* @ignore | |
* @private | |
* @method module:engine/model/liveposition~LivePosition.bindWithDocument | |
*/ | |
function bindWithDocument() { | |
// Operation types handled by LivePosition (these are operations that change model tree structure). | |
const supportedTypes = new Set( [ 'insert', 'move', 'remove', 'reinsert' ] ); | |
this.listenTo( | |
this.root.document, | |
'change', | |
( event, type, changes ) => { | |
if ( supportedTypes.has( type ) ) { | |
transform.call( this, type, changes.range, changes.sourcePosition ); | |
} | |
}, | |
{ priority: 'high' } | |
); | |
} | |
/** | |
* Updates this position accordingly to the updates applied to the model. Bases on change events. | |
* | |
* @ignore | |
* @private | |
* @method transform | |
* @param {String} type Type of changes applied to the Tree Model. | |
* @param {module:engine/model/range~Range} range Range containing the result of applied change. | |
* @param {module:engine/model/position~Position} [position] Additional position parameter provided by some change events. | |
*/ | |
function transform( type, range, position ) { | |
/* eslint-disable no-case-declarations */ | |
const howMany = range.end.offset - range.start.offset; | |
let transformed; | |
switch ( type ) { | |
case 'insert': | |
const insertBefore = this.stickiness == 'sticksToNext'; | |
transformed = this._getTransformedByInsertion( range.start, howMany, insertBefore ); | |
break; | |
case 'move': | |
case 'remove': | |
case 'reinsert': | |
const originalRange = range_Range.createFromPositionAndShift( position, howMany ); | |
const gotMoved = originalRange.containsPosition( this ) || | |
( originalRange.start.isEqual( this ) && this.stickiness == 'sticksToNext' ) || | |
( originalRange.end.isEqual( this ) && this.stickiness == 'sticksToPrevious' ); | |
// We can't use ._getTransformedByMove() because we have a different if-condition. | |
if ( gotMoved ) { | |
transformed = this._getCombined( position, range.start ); | |
} else { | |
const insertBefore = this.stickiness == 'sticksToNext'; | |
transformed = this._getTransformedByMove( position, range.start, howMany, insertBefore ); | |
} | |
break; | |
} | |
if ( !this.isEqual( transformed ) ) { | |
const oldPosition = position_Position.createFromPosition( this ); | |
this.path = transformed.path; | |
this.root = transformed.root; | |
this.fire( 'change', oldPosition ); | |
} | |
/* eslint-enable no-case-declarations */ | |
} | |
mix( liveposition_LivePosition, emittermixin ); | |
/** | |
* Enum representing how position is "sticking" with their neighbour nodes. | |
* Possible values: `'sticksToNext'`, `'sticksToPrevious'`. | |
* | |
* @typedef {String} module:engine/model/position~PositionStickiness | |
*/ | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/controller/insertcontent.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/controller/insertcontent | |
*/ | |
/** | |
* Inserts content into the editor (specified selection) as one would expect the paste | |
* functionality to work. | |
* | |
* **Note:** Use {@link module:engine/controller/datacontroller~DataController#insertContent} instead of this function. | |
* This function is only exposed to be reusable in algorithms | |
* which change the {@link module:engine/controller/datacontroller~DataController#insertContent} | |
* method's behavior. | |
* | |
* @param {module:engine/controller/datacontroller~DataController} dataController The data controller in context of which the insertion | |
* should be performed. | |
* @param {module:engine/model/documentfragment~DocumentFragment|module:engine/model/item~Item} content The content to insert. | |
* @param {module:engine/model/selection~Selection} selection Selection into which the content should be inserted. | |
* @param {module:engine/model/batch~Batch} [batch] Batch to which deltas will be added. If not specified, then | |
* changes will be added to a new batch. | |
*/ | |
function insertContent( dataController, content, selection, batch ) { | |
if ( !batch ) { | |
batch = dataController.model.batch(); | |
} | |
if ( !selection.isCollapsed ) { | |
dataController.deleteContent( selection, batch ); | |
} | |
const insertion = new insertcontent_Insertion( dataController, batch, selection.anchor ); | |
let nodesToInsert; | |
if ( content.is( 'documentFragment' ) ) { | |
nodesToInsert = content.getChildren(); | |
} else { | |
nodesToInsert = [ content ]; | |
} | |
insertion.handleNodes( nodesToInsert, { | |
// The set of children being inserted is the only set in this context | |
// so it's the first and last (it's a hack ;)). | |
isFirst: true, | |
isLast: true | |
} ); | |
const newRange = insertion.getSelectionRange(); | |
/* istanbul ignore else */ | |
if ( newRange ) { | |
selection.setRanges( [ newRange ] ); | |
} else { | |
// We are not testing else because it's a safe check for unpredictable edge cases: | |
// an insertion without proper range to select. | |
/** | |
* Cannot determine a proper selection range after insertion. | |
* | |
* @warning insertcontent-no-range | |
*/ | |
src_log.warn( 'insertcontent-no-range: Cannot determine a proper selection range after insertion.' ); | |
} | |
} | |
/** | |
* Utility class for performing content insertion. | |
* | |
* @private | |
*/ | |
class insertcontent_Insertion { | |
constructor( dataController, batch, position ) { | |
/** | |
* The data controller in context of which the insertion should be performed. | |
* | |
* @member {module:engine/controller/datacontroller~DataController} #dataController | |
*/ | |
this.dataController = dataController; | |
/** | |
* Batch to which deltas will be added. | |
* | |
* @member {module:engine/controller/batch~Batch} #batch | |
*/ | |
this.batch = batch; | |
/** | |
* The position at which (or near which) the next node will be inserted. | |
* | |
* @member {module:engine/model/position~Position} #position | |
*/ | |
this.position = position; | |
/** | |
* Elements with which the inserted elements can be merged. | |
* | |
* <p>x^</p><p>y</p> + <p>z</p> (can merge to <p>x</p>) | |
* <p>x</p><p>^y</p> + <p>z</p> (can merge to <p>y</p>) | |
* <p>x^y</p> + <p>z</p> (can merge to <p>xy</p> which will be split during the action, | |
* so both its pieces will be added to this set) | |
* | |
* | |
* @member {Set} #canMergeWith | |
*/ | |
this.canMergeWith = new Set( [ this.position.parent ] ); | |
/** | |
* Schema of the model. | |
* | |
* @member {module:engine/model/schema~Schema} #schema | |
*/ | |
this.schema = dataController.model.schema; | |
} | |
/** | |
* Handles insertion of a set of nodes. | |
* | |
* @param {Iterable.<module:engine/model/node~Node>} nodes Nodes to insert. | |
* @param {Object} parentContext Context in which parent of these nodes was supposed to be inserted. | |
* If the parent context is passed it means that the parent element was stripped (was not allowed). | |
*/ | |
handleNodes( nodes, parentContext ) { | |
nodes = Array.from( nodes ); | |
for ( let i = 0; i < nodes.length; i++ ) { | |
const node = nodes[ i ]; | |
this._handleNode( node, { | |
isFirst: i === 0 && parentContext.isFirst, | |
isLast: ( i === ( nodes.length - 1 ) ) && parentContext.isLast | |
} ); | |
} | |
} | |
/** | |
* Returns range to be selected after insertion. | |
* Returns null if there is no valid range to select after insertion. | |
* | |
* @returns {module:engine/model/range~Range|null} | |
*/ | |
getSelectionRange() { | |
if ( this.nodeToSelect ) { | |
return range_Range.createOn( this.nodeToSelect ); | |
} | |
return this.dataController.model.getNearestSelectionRange( this.position ); | |
} | |
/** | |
* Handles insertion of a single node. | |
* | |
* @param {module:engine/model/node~Node} node | |
* @param {Object} context | |
* @param {Boolean} context.isFirst Whether the given node is the first one in the content to be inserted. | |
* @param {Boolean} context.isLast Whether the given node is the last one in the content to be inserted. | |
*/ | |
_handleNode( node, context ) { | |
// Let's handle object in a special way. | |
// * They should never be merged with other elements. | |
// * If they are not allowed in any of the selection ancestors, they could be either autoparagraphed or totally removed. | |
if ( this._checkIsObject( node ) ) { | |
this._handleObject( node, context ); | |
return; | |
} | |
// Try to find a place for the given node. | |
// Split the position.parent's branch up to a point where the node can be inserted. | |
// If it isn't allowed in the whole branch, then of course don't split anything. | |
const isAllowed = this._checkAndSplitToAllowedPosition( node, context ); | |
if ( !isAllowed ) { | |
this._handleDisallowedNode( node, context ); | |
return; | |
} | |
this._insert( node ); | |
// After the node was inserted we may try to merge it with its siblings. | |
// This should happen only if it was the first and/or last of the nodes (so only with boundary nodes) | |
// and only if the selection was in those elements initially. | |
// | |
// E.g.: | |
// <p>x^</p> + <p>y</p> => <p>x</p><p>y</p> => <p>xy[]</p> | |
// and: | |
// <p>x^y</p> + <p>z</p> => <p>x</p>^<p>y</p> + <p>z</p> => <p>x</p><p>y</p><p>z</p> => <p>xy[]z</p> | |
// but: | |
// <p>x</p><p>^</p><p>z</p> + <p>y</p> => <p>x</p><p>y</p><p>z</p> (no merging) | |
// <p>x</p>[<img>]<p>z</p> + <p>y</p> => <p>x</p><p>y</p><p>z</p> (no merging, note: after running deletetContents | |
// it's exactly the same case as above) | |
this._mergeSiblingsOf( node, context ); | |
} | |
/** | |
* @param {module:engine/model/element~Element} node The object element. | |
* @param {Object} context | |
*/ | |
_handleObject( node, context ) { | |
// Try finding it a place in the tree. | |
if ( this._checkAndSplitToAllowedPosition( node ) ) { | |
this._insert( node ); | |
} | |
// Try autoparagraphing. | |
else { | |
this._tryAutoparagraphing( node, context ); | |
} | |
} | |
/** | |
* @param {module:engine/model/node~Node} node The disallowed node which needs to be handled. | |
* @param {Object} context | |
*/ | |
_handleDisallowedNode( node, context ) { | |
// If the node is an element, try inserting its children (strip the parent). | |
if ( node.is( 'element' ) ) { | |
this.handleNodes( node.getChildren(), context ); | |
} | |
// If the node is a text and bare text is allowed in current position it means that the node | |
// contains disallowed attributes and we have to remove them. | |
else if ( this.schema.check( { name: '$text', inside: this.position } ) ) { | |
this.schema.removeDisallowedAttributes( [ node ], this.position ); | |
this._handleNode( node, context ); | |
} | |
// If text is not allowed, try autoparagraphing. | |
else { | |
this._tryAutoparagraphing( node, context ); | |
} | |
} | |
/** | |
* @param {module:engine/model/node~Node} node The node to insert. | |
*/ | |
_insert( node ) { | |
/* istanbul ignore if */ | |
if ( !this._checkIsAllowed( node, this.position ) ) { | |
// Algorithm's correctness check. We should never end up here but it's good to know that we did. | |
// Note that it would often be a silent issue if we insert node in a place where it's not allowed. | |
src_log.error( | |
'insertcontent-wrong-position: The node cannot be inserted on the given position.', | |
{ node, position: this.position } | |
); | |
return; | |
} | |
const livePos = liveposition_LivePosition.createFromPosition( this.position ); | |
this.batch.insert( this.position, node ); | |
this.position = position_Position.createFromPosition( livePos ); | |
livePos.detach(); | |
// The last inserted object should be selected because we can't put a collapsed selection after it. | |
if ( this._checkIsObject( node ) && !this.schema.check( { name: '$text', inside: this.position } ) ) { | |
this.nodeToSelect = node; | |
} else { | |
this.nodeToSelect = null; | |
} | |
} | |
/** | |
* @param {module:engine/model/node~Node} node The node which could potentially be merged. | |
* @param {Object} context | |
*/ | |
_mergeSiblingsOf( node, context ) { | |
if ( !( node instanceof element_Element ) ) { | |
return; | |
} | |
const mergeLeft = context.isFirst && ( node.previousSibling instanceof element_Element ) && this.canMergeWith.has( node.previousSibling ); | |
const mergeRight = context.isLast && ( node.nextSibling instanceof element_Element ) && this.canMergeWith.has( node.nextSibling ); | |
const mergePosLeft = liveposition_LivePosition.createBefore( node ); | |
const mergePosRight = liveposition_LivePosition.createAfter( node ); | |
if ( mergeLeft ) { | |
const position = liveposition_LivePosition.createFromPosition( this.position ); | |
this.batch.merge( mergePosLeft ); | |
// We need to check and strip disallowed attributes in all nested nodes because after merge | |
// some attributes could end up in a path where are disallowed. | |
const parent = position.nodeBefore; | |
this.schema.removeDisallowedAttributes( parent.getChildren(), position_Position.createAt( parent ), this.batch ); | |
this.position = position_Position.createFromPosition( position ); | |
position.detach(); | |
} | |
if ( mergeRight ) { | |
/* istanbul ignore if */ | |
if ( !this.position.isEqual( mergePosRight ) ) { | |
// Algorithm's correctness check. We should never end up here but it's good to know that we did. | |
// At this point the insertion position should be after the node we'll merge. If it isn't, | |
// it should need to be secured as in the left merge case. | |
src_log.error( 'insertcontent-wrong-position-on-merge: The insertion position should equal the merge position' ); | |
} | |
// Move the position to the previous node, so it isn't moved to the graveyard on merge. | |
// <p>x</p>[]<p>y</p> => <p>x[]</p><p>y</p> | |
this.position = position_Position.createAt( mergePosRight.nodeBefore, 'end' ); | |
// OK: <p>xx[]</p> + <p>yy</p> => <p>xx[]yy</p> (when sticks to previous) | |
// NOK: <p>xx[]</p> + <p>yy</p> => <p>xxyy[]</p> (when sticks to next) | |
const position = new liveposition_LivePosition( this.position.root, this.position.path, 'sticksToPrevious' ); | |
this.batch.merge( mergePosRight ); | |
// We need to check and strip disallowed attributes in all nested nodes because after merge | |
// some attributes could end up in a place where are disallowed. | |
this.schema.removeDisallowedAttributes( position.parent.getChildren(), position, this.batch ); | |
this.position = position_Position.createFromPosition( position ); | |
position.detach(); | |
} | |
mergePosLeft.detach(); | |
mergePosRight.detach(); | |
// When there was no merge we need to check and strip disallowed attributes in all nested nodes of | |
// just inserted node because some attributes could end up in a place where are disallowed. | |
if ( !mergeLeft && !mergeRight ) { | |
this.schema.removeDisallowedAttributes( node.getChildren(), position_Position.createAt( node ), this.batch ); | |
} | |
} | |
/** | |
* Tries wrapping the node in a new paragraph and inserting it this way. | |
* | |
* @param {module:engine/model/node~Node} node The node which needs to be autoparagraphed. | |
* @param {Object} context | |
*/ | |
_tryAutoparagraphing( node, context ) { | |
const paragraph = new element_Element( 'paragraph' ); | |
// Do not autoparagraph if the paragraph won't be allowed there, | |
// cause that would lead to an infinite loop. The paragraph would be rejected in | |
// the next _handleNode() call and we'd be here again. | |
if ( this._getAllowedIn( paragraph, this.position.parent ) ) { | |
// When node is a text and is disallowed by schema it means that contains disallowed attributes | |
// and we need to remove them. | |
if ( node.is( 'text' ) && !this._checkIsAllowed( node, [ paragraph ] ) ) { | |
this.schema.removeDisallowedAttributes( [ node ], [ paragraph ] ); | |
} | |
if ( this._checkIsAllowed( node, [ paragraph ] ) ) { | |
paragraph.appendChildren( node ); | |
this._handleNode( paragraph, context ); | |
} | |
} | |
} | |
/** | |
* @param {module:engine/model/node~Node} node | |
* @returns {Boolean} Whether an allowed position was found. | |
* `false` is returned if the node isn't allowed at any position up in the tree, `true` if was. | |
*/ | |
_checkAndSplitToAllowedPosition( node ) { | |
const allowedIn = this._getAllowedIn( node, this.position.parent ); | |
if ( !allowedIn ) { | |
return false; | |
} | |
while ( allowedIn != this.position.parent ) { | |
// If a parent which we'd need to leave is a limit element, break. | |
if ( this.schema.limits.has( this.position.parent.name ) ) { | |
return false; | |
} | |
if ( this.position.isAtStart ) { | |
const parent = this.position.parent; | |
this.position = position_Position.createBefore( parent ); | |
// Special case – parent is empty (<p>^</p>) so isAtStart == isAtEnd == true. | |
// We can remove the element after moving selection out of it. | |
if ( parent.isEmpty ) { | |
this.batch.remove( parent ); | |
} | |
} else if ( this.position.isAtEnd ) { | |
this.position = position_Position.createAfter( this.position.parent ); | |
} else { | |
const tempPos = position_Position.createAfter( this.position.parent ); | |
this.batch.split( this.position ); | |
this.position = tempPos; | |
this.canMergeWith.add( this.position.nodeAfter ); | |
} | |
} | |
return true; | |
} | |
/** | |
* Gets the element in which the given node is allowed. It checks the passed element and all its ancestors. | |
* | |
* @param {module:engine/model/node~Node} node The node to check. | |
* @param {module:engine/model/element~Element} element The element in which the node's correctness should be checked. | |
* @returns {module:engine/model/element~Element|null} | |
*/ | |
_getAllowedIn( node, element ) { | |
if ( this._checkIsAllowed( node, [ element ] ) ) { | |
return element; | |
} | |
if ( element.parent ) { | |
return this._getAllowedIn( node, element.parent ); | |
} | |
return null; | |
} | |
/** | |
* Check whether the given node is allowed in the specified schema path. | |
* | |
* @param {module:engine/model/node~Node} node | |
* @param {module:engine/model/schema~SchemaPath} path | |
*/ | |
_checkIsAllowed( node, path ) { | |
return this.schema.check( { | |
name: getNodeSchemaName( node ), | |
attributes: Array.from( node.getAttributeKeys() ), | |
inside: path | |
} ); | |
} | |
/** | |
* Checks whether according to the schema this is an object type element. | |
* | |
* @param {module:engine/model/node~Node} node The node to check. | |
*/ | |
_checkIsObject( node ) { | |
return this.schema.objects.has( getNodeSchemaName( node ) ); | |
} | |
} | |
// Gets a name under which we should check this node in the schema. | |
// | |
// @param {module:engine/model/node~Node} node The node. | |
// @returns {String} Node name. | |
function getNodeSchemaName( node ) { | |
return node.is( 'text' ) ? '$text' : node.name; | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/controller/deletecontent.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/controller/deletecontent | |
*/ | |
/** | |
* Deletes content of the selection and merge siblings. The resulting selection is always collapsed. | |
* | |
* @param {module:engine/model/selection~Selection} selection Selection of which the content should be deleted. | |
* @param {module:engine/model/batch~Batch} batch Batch to which the deltas will be added. | |
* @param {Object} [options] | |
* @param {Boolean} [options.leaveUnmerged=false] Whether to merge elements after removing the content of the selection. | |
* | |
* For example `<heading>x[x</heading><paragraph>y]y</paragraph>` will become: | |
* | |
* * `<heading>x^y</heading>` with the option disabled (`leaveUnmerged == false`) | |
* * `<heading>x^</heading><paragraph>y</paragraph>` with enabled (`leaveUnmerged == true`). | |
* | |
* Note: {@link module:engine/model/schema~Schema#objects object} and {@link module:engine/model/schema~Schema#limits limit} | |
* elements will not be merged. | |
* | |
* @param {Boolean} [options.doNotResetEntireContent=false] Whether to skip replacing the entire content with a | |
* paragraph when the entire content was selected. | |
* | |
* For example `<heading>[x</heading><paragraph>y]</paragraph> will become: | |
* | |
* * `<paragraph>^</paragraph>` with the option disabled (`doNotResetEntireContent == false`) | |
* * `<heading>^</heading>` with enabled (`doNotResetEntireContent == true`). | |
*/ | |
function deleteContent( selection, batch, options = {} ) { | |
if ( selection.isCollapsed ) { | |
return; | |
} | |
const schema = batch.document.schema; | |
// 1. Replace the entire content with paragraph. | |
// See: https://github.com/ckeditor/ckeditor5-engine/issues/1012#issuecomment-315017594. | |
if ( !options.doNotResetEntireContent && shouldEntireContentBeReplacedWithParagraph( schema, selection ) ) { | |
replaceEntireContentWithParagraph( batch, selection ); | |
return; | |
} | |
const selRange = selection.getFirstRange(); | |
const startPos = selRange.start; | |
const endPos = liveposition_LivePosition.createFromPosition( selRange.end ); | |
// 2. Remove the content if there is any. | |
if ( !selRange.start.isTouching( selRange.end ) ) { | |
batch.remove( selRange ); | |
} | |
// 3. Merge elements in the right branch to the elements in the left branch. | |
// The only reasonable (in terms of data and selection correctness) case in which we need to do that is: | |
// | |
// <heading type=1>Fo[</heading><paragraph>]ar</paragraph> => <heading type=1>Fo^ar</heading> | |
// | |
// However, the algorithm supports also merging deeper structures (up to the depth of the shallower branch), | |
// as it's hard to imagine what should actually be the default behavior. Usually, specific features will | |
// want to override that behavior anyway. | |
if ( !options.leaveUnmerged ) { | |
mergeBranches( batch, startPos, endPos ); | |
// We need to check and strip disallowed attributes in all nested nodes because after merge | |
// some attributes could end up in a path where are disallowed. | |
// | |
// e.g. bold is disallowed for <H1> | |
// <h1>Fo{o</h1><p>b}a<b>r</b><p> -> <h1>Fo{}a<b>r</b><h1> -> <h1>Fo{}ar<h1>. | |
schema.removeDisallowedAttributes( startPos.parent.getChildren(), startPos, batch ); | |
} | |
selection.setCollapsedAt( startPos ); | |
// 4. Autoparagraphing. | |
// Check if a text is allowed in the new container. If not, try to create a new paragraph (if it's allowed here). | |
if ( deletecontent_shouldAutoparagraph( schema, startPos ) ) { | |
insertParagraph( batch, startPos, selection ); | |
} | |
endPos.detach(); | |
} | |
// This function is a result of reaching the Ballmer's peak for just the right amount of time. | |
// Even I had troubles documenting it after a while and after reading it again I couldn't believe that it really works. | |
function mergeBranches( batch, startPos, endPos ) { | |
const startParent = startPos.parent; | |
const endParent = endPos.parent; | |
// If both positions ended up in the same parent, then there's nothing more to merge: | |
// <$root><p>x[]</p><p>{}y</p></$root> => <$root><p>xy</p>[]{}</$root> | |
if ( startParent == endParent ) { | |
return; | |
} | |
// If one of the positions is a root, then there's nothing more to merge (at least in the current state of implementation). | |
// Theoretically in this case we could unwrap the <p>: <$root>x[]<p>{}y</p></$root>, but we don't need to support it yet | |
// so let's just abort. | |
if ( !startParent.parent || !endParent.parent ) { | |
return; | |
} | |
// Check if operations we'll need to do won't need to cross object or limit boundaries. | |
// E.g., we can't merge endParent into startParent in this case: | |
// <limit><startParent>x[]</startParent></limit><endParent>{}</endParent> | |
if ( !checkCanBeMerged( startPos, endPos ) ) { | |
return; | |
} | |
// Remember next positions to merge. For example: | |
// <a><b>x[]</b></a><c><d>{}y</d></c> | |
// will become: | |
// <a><b>xy</b>[]</a><c>{}</c> | |
startPos = position_Position.createAfter( startParent ); | |
endPos = position_Position.createBefore( endParent ); | |
if ( !endPos.isEqual( startPos ) ) { | |
// In this case, before we merge, we need to move `endParent` to the `startPos`: | |
// <a><b>x[]</b></a><c><d>{}y</d></c> | |
// becomes: | |
// <a><b>x</b>[]<d>y</d></a><c>{}</c> | |
batch.move( endParent, startPos ); | |
} | |
// Merge two siblings: | |
// <a>x</a>[]<b>y</b> -> <a>xy</a> (the usual case) | |
// <a><b>x</b>[]<d>y</d></a><c></c> -> <a><b>xy</b>[]</a><c></c> (this is the "move parent" case shown above) | |
batch.merge( startPos ); | |
// Remove empty end ancestors: | |
// <a>fo[o</a><b><a><c>bar]</c></a></b> | |
// becomes: | |
// <a>fo[]</a><b><a>{}</a></b> | |
// So we can remove <a> and <b>. | |
while ( endPos.parent.isEmpty ) { | |
const parentToRemove = endPos.parent; | |
endPos = position_Position.createBefore( parentToRemove ); | |
batch.remove( parentToRemove ); | |
} | |
// Continue merging next level. | |
mergeBranches( batch, startPos, endPos ); | |
} | |
function deletecontent_shouldAutoparagraph( schema, position ) { | |
const isTextAllowed = schema.check( { name: '$text', inside: position } ); | |
const isParagraphAllowed = schema.check( { name: 'paragraph', inside: position } ); | |
return !isTextAllowed && isParagraphAllowed; | |
} | |
// Check if parents of two positions can be merged by checking if there are no limit/object | |
// boundaries between those two positions. | |
// | |
// E.g. in <bQ><p>x[]</p></bQ><widget><caption>{}</caption></widget> | |
// we'll check <p>, <bQ>, <widget> and <caption>. | |
// Usually, widget and caption are marked as objects/limits in the schema, so in this case merging will be blocked. | |
function checkCanBeMerged( leftPos, rightPos ) { | |
const schema = leftPos.root.document.schema; | |
const rangeToCheck = new range_Range( leftPos, rightPos ); | |
for ( const value of rangeToCheck.getWalker() ) { | |
if ( schema.objects.has( value.item.name ) || schema.limits.has( value.item.name ) ) { | |
return false; | |
} | |
} | |
return true; | |
} | |
function insertParagraph( batch, position, selection ) { | |
const paragraph = new element_Element( 'paragraph' ); | |
batch.insert( position, paragraph ); | |
selection.setCollapsedAt( paragraph ); | |
} | |
function replaceEntireContentWithParagraph( batch, selection ) { | |
const limitElement = batch.document.schema.getLimitElement( selection ); | |
batch.remove( range_Range.createIn( limitElement ) ); | |
insertParagraph( batch, position_Position.createAt( limitElement ), selection ); | |
} | |
// We want to replace the entire content with a paragraph when: | |
// * the entire content is selected, | |
// * selection contains at least two elements, | |
// * whether the paragraph is allowed in schema in the common ancestor. | |
function shouldEntireContentBeReplacedWithParagraph( schema, selection ) { | |
const limitElement = schema.getLimitElement( selection ); | |
if ( !selection.containsEntireContent( limitElement ) ) { | |
return false; | |
} | |
const range = selection.getFirstRange(); | |
if ( range.start.parent == range.end.parent ) { | |
return false; | |
} | |
return schema.check( { name: 'paragraph', inside: limitElement.name } ); | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/unicode.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* Set of utils to handle unicode characters. | |
* | |
* @module utils/unicode | |
*/ | |
/** | |
* Checks whether given `character` is a combining mark. | |
* | |
* @param {String} character Character to check. | |
* @returns {Boolean} | |
*/ | |
function isCombiningMark( character ) { | |
return !!character && character.length == 1 && /[\u0300-\u036f\u1ab0-\u1aff\u1dc0-\u1dff\u20d0-\u20ff\ufe20-\ufe2f]/.test( character ); | |
} | |
/** | |
* Checks whether given `character` is a high half of surrogate pair. | |
* | |
* Using UTF-16 terminology, a surrogate pair denotes UTF-16 character using two UTF-8 characters. The surrogate pair | |
* consist of high surrogate pair character followed by low surrogate pair character. | |
* | |
* @param {String} character Character to check. | |
* @returns {Boolean} | |
*/ | |
function isHighSurrogateHalf( character ) { | |
return !!character && character.length == 1 && /[\ud800-\udbff]/.test( character ); | |
} | |
/** | |
* Checks whether given `character` is a low half of surrogate pair. | |
* | |
* Using UTF-16 terminology, a surrogate pair denotes UTF-16 character using two UTF-8 characters. The surrogate pair | |
* consist of high surrogate pair character followed by low surrogate pair character. | |
* | |
* @param {String} character Character to check. | |
* @returns {Boolean} | |
*/ | |
function isLowSurrogateHalf( character ) { | |
return !!character && character.length == 1 && /[\udc00-\udfff]/.test( character ); | |
} | |
/** | |
* Checks whether given offset in a string is inside a surrogate pair (between two surrogate halves). | |
* | |
* @param {String} string String to check. | |
* @param {Number} offset Offset to check. | |
* @returns {Boolean} | |
*/ | |
function isInsideSurrogatePair( string, offset ) { | |
return isHighSurrogateHalf( string.charAt( offset - 1 ) ) && isLowSurrogateHalf( string.charAt( offset ) ); | |
} | |
/** | |
* Checks whether given offset in a string is between base character and combining mark or between two combining marks. | |
* | |
* @param {String} string String to check. | |
* @param {Number} offset Offset to check. | |
* @returns {Boolean} | |
*/ | |
function isInsideCombinedSymbol( string, offset ) { | |
return isCombiningMark( string.charAt( offset ) ); | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/controller/modifyselection.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/controller/modifyselection | |
*/ | |
/** | |
* Modifies the selection. Currently, the supported modifications are: | |
* | |
* * Extending. The selection focus is moved in the specified `options.direction` with a step specified in `options.unit`. | |
* Possible values for `unit` are: | |
* * `'character'` (default) - moves selection by one user-perceived character. In most cases this means moving by one | |
* character in `String` sense. However, unicode also defines "combing marks". These are special symbols, that combines | |
* with a symbol before it ("base character") to create one user-perceived character. For example, `q̣̇` is a normal | |
* letter `q` with two "combining marks": upper dot (`Ux0307`) and lower dot (`Ux0323`). For most actions, i.e. extending | |
* selection by one position, it is correct to include both "base character" and all of it's "combining marks". That is | |
* why `'character'` value is most natural and common method of modifying selection. | |
* * `'codePoint'` - moves selection by one unicode code point. In contrary to, `'character'` unit, this will insert | |
* selection between "base character" and "combining mark", because "combining marks" have their own unicode code points. | |
* However, for technical reasons, unicode code points with values above `UxFFFF` are represented in native `String` by | |
* two characters, called "surrogate pairs". Halves of "surrogate pairs" have a meaning only when placed next to each other. | |
* For example `𨭎` is represented in `String` by `\uD862\uDF4E`. Both `\uD862` and `\uDF4E` do not have any meaning | |
* outside the pair (are rendered as ? when alone). Position between them would be incorrect. In this case, selection | |
* extension will include whole "surrogate pair". | |
* | |
* **Note:** if you extend a forward selection in a backward direction you will in fact shrink it. | |
* | |
* @param {module:engine/controller/datacontroller~DataController} dataController The data controller in context of which | |
* the selection modification should be performed. | |
* @param {module:engine/model/selection~Selection} selection The selection to modify. | |
* @param {Object} [options] | |
* @param {'forward'|'backward'} [options.direction='forward'] The direction in which the selection should be modified. | |
* @param {'character'|'codePoint'} [options.unit='character'] The unit by which selection should be modified. | |
*/ | |
function modifySelection( dataController, selection, options = {} ) { | |
const schema = dataController.model.schema; | |
const isForward = options.direction != 'backward'; | |
const unit = options.unit ? options.unit : 'character'; | |
const focus = selection.focus; | |
const walker = new treewalker_TreeWalker( { | |
boundaries: getSearchRange( focus, isForward ), | |
singleCharacters: true, | |
direction: isForward ? 'forward' : 'backward' | |
} ); | |
const data = { walker, schema, isForward, unit }; | |
let next; | |
while ( ( next = walker.next() ) ) { | |
if ( next.done ) { | |
return; | |
} | |
const position = tryExtendingTo( data, next.value ); | |
if ( position ) { | |
selection.moveFocusTo( position ); | |
return; | |
} | |
} | |
} | |
// Checks whether the selection can be extended to the the walker's next value (next position). | |
function tryExtendingTo( data, value ) { | |
// If found text, we can certainly put the focus in it. Let's just find a correct position | |
// based on the unit. | |
if ( value.type == 'text' ) { | |
return getCorrectPosition( data.walker, data.unit ); | |
} | |
// Entering an element. | |
if ( value.type == ( data.isForward ? 'elementStart' : 'elementEnd' ) ) { | |
// If it's an object, we can select it now. | |
if ( data.schema.objects.has( value.item.name ) ) { | |
return position_Position.createAt( value.item, data.isForward ? 'after' : 'before' ); | |
} | |
// If text allowed on this position, extend to this place. | |
if ( data.schema.check( { name: '$text', inside: value.nextPosition } ) ) { | |
return value.nextPosition; | |
} | |
} | |
// Leaving an element. | |
else { | |
// If leaving a limit element, stop. | |
if ( data.schema.limits.has( value.item.name ) ) { | |
// NOTE: Fast-forward the walker until the end. | |
data.walker.skip( () => true ); | |
return; | |
} | |
// If text allowed on this position, extend to this place. | |
if ( data.schema.check( { name: '$text', inside: value.nextPosition } ) ) { | |
return value.nextPosition; | |
} | |
} | |
} | |
// Finds a correct position by walking in a text node and checking whether selection can be extended to given position | |
// or should be extended further. | |
function getCorrectPosition( walker, unit ) { | |
const textNode = walker.position.textNode; | |
if ( textNode ) { | |
const data = textNode.data; | |
let offset = walker.position.offset - textNode.startOffset; | |
while ( isInsideSurrogatePair( data, offset ) || ( unit == 'character' && isInsideCombinedSymbol( data, offset ) ) ) { | |
walker.next(); | |
offset = walker.position.offset - textNode.startOffset; | |
} | |
} | |
return walker.position; | |
} | |
function getSearchRange( start, isForward ) { | |
const root = start.root; | |
const searchEnd = position_Position.createAt( root, isForward ? 'end' : 0 ); | |
if ( isForward ) { | |
return new range_Range( start, searchEnd ); | |
} else { | |
return new range_Range( searchEnd, start ); | |
} | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/controller/getselectedcontent.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/controller/getselectedcontent | |
*/ | |
/** | |
* Gets a clone of the selected content. | |
* | |
* For example, for the following selection: | |
* | |
* <p>x</p><quote><p>y</p><h>fir[st</h></quote><p>se]cond</p><p>z</p> | |
* | |
* It will return a document fragment with such a content: | |
* | |
* <quote><h>st</h></quote><p>se</p> | |
* | |
* @param {module:engine/model/selection~Selection} selection The selection of which content will be returned. | |
* @returns {module:engine/model/documentfragment~DocumentFragment} | |
*/ | |
function getSelectedContent( selection ) { | |
const frag = new documentfragment_DocumentFragment(); | |
const range = selection.getFirstRange(); | |
if ( !range || range.isCollapsed ) { | |
return frag; | |
} | |
const root = range.start.root; | |
const commonPath = range.start.getCommonPath( range.end ); | |
const commonParent = root.getNodeByPath( commonPath ); | |
// ## 1st step | |
// | |
// First, we'll clone a fragment represented by a minimal flat range | |
// containing the original range to be cloned. | |
// E.g. let's consider such a range: | |
// | |
// <p>x</p><quote><p>y</p><h>fir[st</h></quote><p>se]cond</p><p>z</p> | |
// | |
// A minimal flat range containing this one is: | |
// | |
// <p>x</p>[<quote><p>y</p><h>first</h></quote><p>second</p>]<p>z</p> | |
// | |
// We can easily clone this structure, preserving e.g. the <quote> element. | |
let flatSubtreeRange; | |
if ( range.start.parent == range.end.parent ) { | |
// The original range is flat, so take it. | |
flatSubtreeRange = range; | |
} else { | |
flatSubtreeRange = range_Range.createFromParentsAndOffsets( | |
commonParent, range.start.path[ commonPath.length ], | |
commonParent, range.end.path[ commonPath.length ] + 1 | |
); | |
} | |
const howMany = flatSubtreeRange.end.offset - flatSubtreeRange.start.offset; | |
// Clone the whole contents. | |
for ( const item of flatSubtreeRange.getItems( { shallow: true } ) ) { | |
if ( item.is( 'textProxy' ) ) { | |
frag.appendChildren( new text_Text( item.data, item.getAttributes() ) ); | |
} else { | |
frag.appendChildren( item.clone( true ) ); | |
} | |
} | |
// ## 2nd step | |
// | |
// If the original range wasn't flat, then we need to remove the excess nodes from the both ends of the cloned fragment. | |
// | |
// For example, for the range shown in the 1st step comment, we need to remove these pieces: | |
// | |
// <quote>[<p>y</p>]<h>[fir]st</h></quote><p>se[cond]</p> | |
// | |
// So this will be the final copied content: | |
// | |
// <quote><h>st</h></quote><p>se</p> | |
// | |
// In order to do that, we remove content from these two ranges: | |
// | |
// [<quote><p>y</p><h>fir]st</h></quote><p>se[cond</p>] | |
if ( flatSubtreeRange != range ) { | |
// Find the position of the original range in the cloned fragment. | |
const newRange = range._getTransformedByMove( flatSubtreeRange.start, position_Position.createAt( frag, 0 ), howMany )[ 0 ]; | |
const leftExcessRange = new range_Range( position_Position.createAt( frag ), newRange.start ); | |
const rightExcessRange = new range_Range( newRange.end, position_Position.createAt( frag, 'end' ) ); | |
removeRangeContent( rightExcessRange ); | |
removeRangeContent( leftExcessRange ); | |
} | |
return frag; | |
} | |
// After https://github.com/ckeditor/ckeditor5-engine/issues/690 is fixed, | |
// this function will, most likely, be able to rewritten using getMinimalFlatRanges(). | |
function removeRangeContent( range ) { | |
const parentsToCheck = []; | |
Array.from( range.getItems( { direction: 'backward' } ) ) | |
// We should better store ranges because text proxies will lose integrity | |
// with the text nodes when we'll start removing content. | |
.map( item => range_Range.createOn( item ) ) | |
// Filter only these items which are fully contained in the passed range. | |
// | |
// E.g. for the following range: [<quote><p>y</p><h>fir]st</h> | |
// the walker will return the entire <h> element, when only the "fir" item inside it is fully contained. | |
.filter( itemRange => { | |
// We should be able to use Range.containsRange, but https://github.com/ckeditor/ckeditor5-engine/issues/691. | |
const contained = | |
( itemRange.start.isAfter( range.start ) || itemRange.start.isEqual( range.start ) ) && | |
( itemRange.end.isBefore( range.end ) || itemRange.end.isEqual( range.end ) ); | |
return contained; | |
} ) | |
.forEach( itemRange => { | |
parentsToCheck.push( itemRange.start.parent ); | |
model_writer_remove( itemRange ); | |
} ); | |
// Remove ancestors of the removed items if they turned to be empty now | |
// (their whole content was contained in the range). | |
parentsToCheck.forEach( parentToCheck => { | |
let parent = parentToCheck; | |
while ( parent.parent && parent.isEmpty ) { | |
const removeRange = range_Range.createOn( parent ); | |
parent = parent.parent; | |
model_writer_remove( removeRange ); | |
} | |
} ); | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/controller/datacontroller.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/controller/datacontroller | |
*/ | |
/** | |
* Controller for the data pipeline. The data pipeline controls how data is retrieved from the document | |
* and set inside it. Hence, the controller features two methods which allow to {@link ~DataController#get get} | |
* and {@link ~DataController#set set} data of the {@link ~DataController#model model} | |
* using given: | |
* | |
* * {@link module:engine/dataprocessor/dataprocessor~DataProcessor data processor}, | |
* * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher model to view} and | |
* * {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher view to model} converters. | |
* | |
* @mixes module:utils/emittermixin~ObservableMixin | |
*/ | |
class datacontroller_DataController { | |
/** | |
* Creates data controller instance. | |
* | |
* @param {module:engine/model/document~Document} model Document model. | |
* @param {module:engine/dataprocessor/dataprocessor~DataProcessor} [dataProcessor] Data processor which should used by the controller. | |
*/ | |
constructor( model, dataProcessor ) { | |
/** | |
* Document model. | |
* | |
* @readonly | |
* @member {module:engine/model/document~Document} | |
*/ | |
this.model = model; | |
/** | |
* Data processor used during the conversion. | |
* | |
* @readonly | |
* @member {module:engine/dataProcessor~DataProcessor} | |
*/ | |
this.processor = dataProcessor; | |
/** | |
* Mapper used for the conversion. It has no permanent bindings, because they are created when getting data and | |
* cleared directly after data are converted. However, the mapper is defined as class property, because | |
* it needs to be passed to the `ModelConversionDispatcher` as a conversion API. | |
* | |
* @member {module:engine/conversion/mapper~Mapper} | |
*/ | |
this.mapper = new mapper_Mapper(); | |
/** | |
* Model to view conversion dispatcher used by the {@link #get get method}. | |
* To attach model to view converter to the data pipeline you need to add lister to this property: | |
* | |
* data.modelToView( 'insert:$element', customInsertConverter ); | |
* | |
* Or use {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder}: | |
* | |
* buildModelConverter().for( data.modelToView ).fromAttribute( 'bold' ).toElement( 'b' ); | |
* | |
* @readonly | |
* @member {module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher} | |
*/ | |
this.modelToView = new modelconversiondispatcher_ModelConversionDispatcher( this.model, { | |
mapper: this.mapper | |
} ); | |
this.modelToView.on( 'insert:$text', model_to_view_converters_insertText(), { priority: 'lowest' } ); | |
/** | |
* View to model conversion dispatcher used by the {@link #set set method}. | |
* To attach view to model converter to the data pipeline you need to add lister to this property: | |
* | |
* data.viewToModel( 'element', customElementConverter ); | |
* | |
* Or use {@link module:engine/conversion/buildviewconverter~ViewConverterBuilder}: | |
* | |
* buildViewConverter().for( data.viewToModel ).fromElement( 'b' ).toAttribute( 'bold', 'true' ); | |
* | |
* @readonly | |
* @member {module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher} | |
*/ | |
this.viewToModel = new viewconversiondispatcher_ViewConversionDispatcher( { | |
schema: model.schema | |
} ); | |
// Define default converters for text and elements. | |
// | |
// Note that if there is no default converter for the element it will be skipped, for instance `<b>foo</b>` will be | |
// converted to nothing. We add `convertToModelFragment` as a last converter so it converts children of that | |
// element to the document fragment so `<b>foo</b>` will be converted to `foo` if there is no converter for `<b>`. | |
this.viewToModel.on( 'text', convertText(), { priority: 'lowest' } ); | |
this.viewToModel.on( 'element', convertToModelFragment(), { priority: 'lowest' } ); | |
this.viewToModel.on( 'documentFragment', convertToModelFragment(), { priority: 'lowest' } ); | |
[ 'insertContent', 'deleteContent', 'modifySelection', 'getSelectedContent' ] | |
.forEach( methodName => this.decorate( methodName ) ); | |
} | |
/** | |
* Returns model's data converted by the {@link #modelToView model to view converters} and | |
* formatted by the {@link #processor data processor}. | |
* | |
* @param {String} [rootName='main'] Root name. | |
* @returns {String} Output data. | |
*/ | |
get( rootName = 'main' ) { | |
// Get model range. | |
return this.stringify( this.model.getRoot( rootName ) ); | |
} | |
/** | |
* Returns the content of the given {@link module:engine/model/element~Element model's element} or | |
* {@link module:engine/model/documentfragment~DocumentFragment model document fragment} converted by the | |
* {@link #modelToView model to view converters} and formatted by the | |
* {@link #processor data processor}. | |
* | |
* @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} modelElementOrFragment | |
* Element which content will be stringified. | |
* @returns {String} Output data. | |
*/ | |
stringify( modelElementOrFragment ) { | |
// model -> view | |
const viewDocumentFragment = this.toView( modelElementOrFragment ); | |
// view -> data | |
return this.processor.toData( viewDocumentFragment ); | |
} | |
/** | |
* Returns the content of the given {@link module:engine/model/element~Element model element} or | |
* {@link module:engine/model/documentfragment~DocumentFragment model document fragment} converted by the | |
* {@link #modelToView model to view converters} to a | |
* {@link module:engine/view/documentfragment~DocumentFragment view document fragment}. | |
* | |
* @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} modelElementOrFragment | |
* Element or document fragment which content will be converted. | |
* @returns {module:engine/view/documentfragment~DocumentFragment} Output view DocumentFragment. | |
*/ | |
toView( modelElementOrFragment ) { | |
const modelRange = range_Range.createIn( modelElementOrFragment ); | |
const viewDocumentFragment = new view_documentfragment_DocumentFragment(); | |
this.mapper.bindElements( modelElementOrFragment, viewDocumentFragment ); | |
this.modelToView.convertInsertion( modelRange ); | |
this.mapper.clearBindings(); | |
return viewDocumentFragment; | |
} | |
/** | |
* Sets input data parsed by the {@link #processor data processor} and | |
* converted by the {@link #viewToModel view to model converters}. | |
* | |
* This method also creates a batch with all the changes applied. If all you need is to parse data use | |
* the {@link #parse} method. | |
* | |
* @param {String} data Input data. | |
* @param {String} [rootName='main'] Root name. | |
*/ | |
set( data, rootName = 'main' ) { | |
// Save to model. | |
const modelRoot = this.model.getRoot( rootName ); | |
this.model.enqueueChanges( () => { | |
// Clearing selection is a workaround for ticket #569 (LiveRange loses position after removing data from document). | |
// After fixing it this code should be removed. | |
this.model.selection.removeAllRanges(); | |
this.model.selection.clearAttributes(); | |
// Initial batch should be ignored by features like undo, etc. | |
this.model.batch( 'transparent' ) | |
.remove( range_Range.createIn( modelRoot ) ) | |
.insert( position_Position.createAt( modelRoot, 0 ), this.parse( data ) ); | |
} ); | |
} | |
/** | |
* Returns data parsed by the {@link #processor data processor} and then | |
* converted by the {@link #viewToModel view to model converters}. | |
* | |
* @see #set | |
* @param {String} data Data to parse. | |
* @param {String} [context='$root'] Base context in which the view will be converted to the model. See: | |
* {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#convert}. | |
* @returns {module:engine/model/documentfragment~DocumentFragment} Parsed data. | |
*/ | |
parse( data, context = '$root' ) { | |
// data -> view | |
const viewDocumentFragment = this.processor.toView( data ); | |
// view -> model | |
return this.toModel( viewDocumentFragment, context ); | |
} | |
/** | |
* Returns wrapped by {module:engine/model/documentfragment~DocumentFragment} result of the given | |
* {@link module:engine/view/element~Element view element} or | |
* {@link module:engine/view/documentfragment~DocumentFragment view document fragment} converted by the | |
* {@link #viewToModel view to model converters}. | |
* | |
* When marker elements were converted during conversion process then will be set as DocumentFragment's | |
* {@link module:engine/model/documentfragment~DocumentFragment#markers static markers map}. | |
* | |
* @param {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment} viewElementOrFragment | |
* Element or document fragment which content will be converted. | |
* @param {String} [context='$root'] Base context in which the view will be converted to the model. See: | |
* {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#convert}. | |
* @returns {module:engine/model/documentfragment~DocumentFragment} Output document fragment. | |
*/ | |
toModel( viewElementOrFragment, context = '$root' ) { | |
return this.viewToModel.convert( viewElementOrFragment, { context: [ context ] } ); | |
} | |
/** | |
* Removes all event listeners set by the DataController. | |
*/ | |
destroy() {} | |
/** | |
* See {@link module:engine/controller/insertcontent.insertContent}. | |
* | |
* @fires insertContent | |
* @param {module:engine/model/documentfragment~DocumentFragment|module:engine/model/item~Item} content The content to insert. | |
* @param {module:engine/model/selection~Selection} selection Selection into which the content should be inserted. | |
* @param {module:engine/model/batch~Batch} [batch] Batch to which deltas will be added. If not specified, then | |
* changes will be added to a new batch. | |
*/ | |
insertContent( content, selection, batch ) { | |
insertContent( this, content, selection, batch ); | |
} | |
/** | |
* See {@link module:engine/controller/deletecontent.deleteContent}. | |
* | |
* Note: For the sake of predictability, the resulting selection should always be collapsed. | |
* In cases where a feature wants to modify deleting behavior so selection isn't collapsed | |
* (e.g. a table feature may want to keep row selection after pressing <kbd>Backspace</kbd>), | |
* then that behavior should be implemented in the view's listener. At the same time, the table feature | |
* will need to modify this method's behavior too, e.g. to "delete contents and then collapse | |
* the selection inside the last selected cell" or "delete the row and collapse selection somewhere near". | |
* That needs to be done in order to ensure that other features which use `deleteContent()` will work well with tables. | |
* | |
* @fires deleteContent | |
* @param {module:engine/model/selection~Selection} selection Selection of which the content should be deleted. | |
* @param {module:engine/model/batch~Batch} batch Batch to which deltas will be added. | |
* @param {Object} options See {@link module:engine/controller/deletecontent~deleteContent}'s options. | |
*/ | |
deleteContent( selection, batch, options ) { | |
deleteContent( selection, batch, options ); | |
} | |
/** | |
* See {@link module:engine/controller/modifyselection.modifySelection}. | |
* | |
* @fires modifySelection | |
* @param {module:engine/model/selection~Selection} selection The selection to modify. | |
* @param {Object} options See {@link module:engine/controller/modifyselection~modifySelection}'s options. | |
*/ | |
modifySelection( selection, options ) { | |
modifySelection( this, selection, options ); | |
} | |
/** | |
* See {@link module:engine/controller/getselectedcontent.getSelectedContent}. | |
* | |
* @fires module:engine/controller/datacontroller~DataController#getSelectedContent | |
* @param {module:engine/model/selection~Selection} selection The selection of which content will be retrieved. | |
* @returns {module:engine/model/documentfragment~DocumentFragment} Document fragment holding the clone of the selected content. | |
*/ | |
getSelectedContent( selection ) { | |
return getSelectedContent( selection ); | |
} | |
/** | |
* Checks whether given {@link module:engine/model/range~Range range} or {@link module:engine/model/element~Element element} | |
* has any content. | |
* | |
* Content is any text node or element which is registered in {@link module:engine/model/schema~Schema schema}. | |
* | |
* @param {module:engine/model/range~Range|module:engine/model/element~Element} rangeOrElement Range or element to check. | |
* @returns {Boolean} | |
*/ | |
hasContent( rangeOrElement ) { | |
if ( rangeOrElement instanceof element_Element ) { | |
rangeOrElement = range_Range.createIn( rangeOrElement ); | |
} | |
if ( rangeOrElement.isCollapsed ) { | |
return false; | |
} | |
for ( const item of rangeOrElement.getItems() ) { | |
// Remember, `TreeWalker` returns always `textProxy` nodes. | |
if ( item.is( 'textProxy' ) || this.model.schema.objects.has( item.name ) ) { | |
return true; | |
} | |
} | |
return false; | |
} | |
} | |
mix( datacontroller_DataController, observablemixin ); | |
/** | |
* Event fired when {@link #insertContent} method is called. | |
* | |
* The {@link #insertContent default action of that method} is implemented as a | |
* listener to this event so it can be fully customized by the features. | |
* | |
* @event insertContent | |
* @param {Array} args The arguments passed to the original method. | |
*/ | |
/** | |
* Event fired when {@link #deleteContent} method is called. | |
* | |
* The {@link #deleteContent default action of that method} is implemented as a | |
* listener to this event so it can be fully customized by the features. | |
* | |
* @event deleteContent | |
* @param {Array} args The arguments passed to the original method. | |
*/ | |
/** | |
* Event fired when {@link #modifySelection} method is called. | |
* | |
* The {@link #modifySelection default action of that method} is implemented as a | |
* listener to this event so it can be fully customized by the features. | |
* | |
* @event modifySelection | |
* @param {Array} args The arguments passed to the original method. | |
*/ | |
/** | |
* Event fired when {@link #getSelectedContent} method is called. | |
* | |
* The {@link #getSelectedContent default action of that method} is implemented as a | |
* listener to this event so it can be fully customized by the features. | |
* | |
* @event getSelectedContent | |
* @param {Array} args The arguments passed to the original method. | |
*/ | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/operation.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/model/operation/operation | |
*/ | |
/** | |
* Abstract base operation class. | |
* | |
* @abstract | |
*/ | |
class operation_Operation { | |
/** | |
* Base operation constructor. | |
* @param {Number} baseVersion {@link module:engine/model/document~Document#version} on which the operation can be applied. | |
*/ | |
constructor( baseVersion ) { | |
/** | |
* {@link module:engine/model/document~Document#version} on which operation can be applied. If you try to | |
* {@link module:engine/model/document~Document#applyOperation apply} operation with different base version than the | |
* {@link module:engine/model/document~Document#version document version} the | |
* {@link module:utils/ckeditorerror~CKEditorError model-document-applyOperation-wrong-version} error is thrown. | |
* | |
* @member {Number} | |
*/ | |
this.baseVersion = baseVersion; | |
/** | |
* Operation type. | |
* | |
* @readonly | |
* @member {String} #type | |
*/ | |
/** | |
* {@link module:engine/model/delta/delta~Delta Delta} which the operation is a part of. This property is set by the | |
* {@link module:engine/model/delta/delta~Delta delta} when the operations is added to it by the | |
* {@link module:engine/model/delta/delta~Delta#addOperation} method. | |
* | |
* @member {module:engine/model/delta/delta~Delta} #delta | |
*/ | |
/** | |
* Creates and returns an operation that has the same parameters as this operation. | |
* | |
* @method #clone | |
* @returns {module:engine/model/operation/operation~Operation} Clone of this operation. | |
*/ | |
/** | |
* Creates and returns a reverse operation. Reverse operation when executed right after | |
* the original operation will bring back tree model state to the point before the original | |
* operation execution. In other words, it reverses changes done by the original operation. | |
* | |
* Keep in mind that tree model state may change since executing the original operation, | |
* so reverse operation will be "outdated". In that case you will need to | |
* {@link module:engine/model/operation/transform~transform} it by all operations that were executed after the original operation. | |
* | |
* @method #getReversed | |
* @returns {module:engine/model/operation/operation~Operation} Reversed operation. | |
*/ | |
/** | |
* Executes the operation - modifications described by the operation attributes | |
* will be applied to the tree model. | |
* | |
* @protected | |
* @method #_execute | |
* @returns {Object} Object with additional information about the applied changes. It properties depends on the | |
* operation type. | |
*/ | |
} | |
/** | |
* Custom toJSON method to solve child-parent circular dependencies. | |
* | |
* @method #toJSON | |
* @returns {Object} Clone of this object with the delta property replaced with string. | |
*/ | |
toJSON() { | |
const json = lodash_clone( this, true ); | |
json.__className = this.constructor.className; | |
// Remove parent delta to avoid circular dependencies. | |
delete json.delta; | |
return json; | |
} | |
/** | |
* Name of the operation class used for serialization. | |
* | |
* @type {String} | |
*/ | |
static get className() { | |
return 'engine.model.operation.Operation'; | |
} | |
/** | |
* Creates Operation object from deserilized object, i.e. from parsed JSON string. | |
* | |
* @param {Object} json Deserialized JSON object. | |
* @param {module:engine/model/document~Document} doc Document on which this operation will be applied. | |
* @returns {module:engine/model/operation/operation~Operation} | |
*/ | |
static fromJSON( json ) { | |
return new this( json.baseVersion ); | |
} | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_setCacheAdd.js | |
/** Used to stand-in for `undefined` hash values. */ | |
var _setCacheAdd_HASH_UNDEFINED = '__lodash_hash_undefined__'; | |
/** | |
* Adds `value` to the array cache. | |
* | |
* @private | |
* @name add | |
* @memberOf SetCache | |
* @alias push | |
* @param {*} value The value to cache. | |
* @returns {Object} Returns the cache instance. | |
*/ | |
function setCacheAdd(value) { | |
this.__data__.set(value, _setCacheAdd_HASH_UNDEFINED); | |
return this; | |
} | |
/* harmony default export */ var _setCacheAdd = (setCacheAdd); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_setCacheHas.js | |
/** | |
* Checks if `value` is in the array cache. | |
* | |
* @private | |
* @name has | |
* @memberOf SetCache | |
* @param {*} value The value to search for. | |
* @returns {number} Returns `true` if `value` is found, else `false`. | |
*/ | |
function setCacheHas(value) { | |
return this.__data__.has(value); | |
} | |
/* harmony default export */ var _setCacheHas = (setCacheHas); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_SetCache.js | |
/** | |
* | |
* Creates an array cache object to store unique values. | |
* | |
* @private | |
* @constructor | |
* @param {Array} [values] The values to cache. | |
*/ | |
function SetCache(values) { | |
var index = -1, | |
length = values ? values.length : 0; | |
this.__data__ = new _MapCache; | |
while (++index < length) { | |
this.add(values[index]); | |
} | |
} | |
// Add methods to `SetCache`. | |
SetCache.prototype.add = SetCache.prototype.push = _setCacheAdd; | |
SetCache.prototype.has = _setCacheHas; | |
/* harmony default export */ var _SetCache = (SetCache); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_arraySome.js | |
/** | |
* A specialized version of `_.some` for arrays without support for iteratee | |
* shorthands. | |
* | |
* @private | |
* @param {Array} array The array to iterate over. | |
* @param {Function} predicate The function invoked per iteration. | |
* @returns {boolean} Returns `true` if any element passes the predicate check, | |
* else `false`. | |
*/ | |
function arraySome(array, predicate) { | |
var index = -1, | |
length = array.length; | |
while (++index < length) { | |
if (predicate(array[index], index, array)) { | |
return true; | |
} | |
} | |
return false; | |
} | |
/* harmony default export */ var _arraySome = (arraySome); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_equalArrays.js | |
/** Used to compose bitmasks for comparison styles. */ | |
var UNORDERED_COMPARE_FLAG = 1, | |
PARTIAL_COMPARE_FLAG = 2; | |
/** | |
* A specialized version of `baseIsEqualDeep` for arrays with support for | |
* partial deep comparisons. | |
* | |
* @private | |
* @param {Array} array The array to compare. | |
* @param {Array} other The other array to compare. | |
* @param {Function} equalFunc The function to determine equivalents of values. | |
* @param {Function} customizer The function to customize comparisons. | |
* @param {number} bitmask The bitmask of comparison flags. See `baseIsEqual` | |
* for more details. | |
* @param {Object} stack Tracks traversed `array` and `other` objects. | |
* @returns {boolean} Returns `true` if the arrays are equivalent, else `false`. | |
*/ | |
function equalArrays(array, other, equalFunc, customizer, bitmask, stack) { | |
var isPartial = bitmask & PARTIAL_COMPARE_FLAG, | |
arrLength = array.length, | |
othLength = other.length; | |
if (arrLength != othLength && !(isPartial && othLength > arrLength)) { | |
return false; | |
} | |
// Assume cyclic values are equal. | |
var stacked = stack.get(array); | |
if (stacked) { | |
return stacked == other; | |
} | |
var index = -1, | |
result = true, | |
seen = (bitmask & UNORDERED_COMPARE_FLAG) ? new _SetCache : undefined; | |
stack.set(array, other); | |
// Ignore non-index properties. | |
while (++index < arrLength) { | |
var arrValue = array[index], | |
othValue = other[index]; | |
if (customizer) { | |
var compared = isPartial | |
? customizer(othValue, arrValue, index, other, array, stack) | |
: customizer(arrValue, othValue, index, array, other, stack); | |
} | |
if (compared !== undefined) { | |
if (compared) { | |
continue; | |
} | |
result = false; | |
break; | |
} | |
// Recursively compare arrays (susceptible to call stack limits). | |
if (seen) { | |
if (!_arraySome(other, function(othValue, othIndex) { | |
if (!seen.has(othIndex) && | |
(arrValue === othValue || equalFunc(arrValue, othValue, customizer, bitmask, stack))) { | |
return seen.add(othIndex); | |
} | |
})) { | |
result = false; | |
break; | |
} | |
} else if (!( | |
arrValue === othValue || | |
equalFunc(arrValue, othValue, customizer, bitmask, stack) | |
)) { | |
result = false; | |
break; | |
} | |
} | |
stack['delete'](array); | |
return result; | |
} | |
/* harmony default export */ var _equalArrays = (equalArrays); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_equalByTag.js | |
/** Used to compose bitmasks for comparison styles. */ | |
var _equalByTag_UNORDERED_COMPARE_FLAG = 1, | |
_equalByTag_PARTIAL_COMPARE_FLAG = 2; | |
/** `Object#toString` result references. */ | |
var _equalByTag_boolTag = '[object Boolean]', | |
_equalByTag_dateTag = '[object Date]', | |
_equalByTag_errorTag = '[object Error]', | |
_equalByTag_mapTag = '[object Map]', | |
_equalByTag_numberTag = '[object Number]', | |
_equalByTag_regexpTag = '[object RegExp]', | |
_equalByTag_setTag = '[object Set]', | |
_equalByTag_stringTag = '[object String]', | |
_equalByTag_symbolTag = '[object Symbol]'; | |
var _equalByTag_arrayBufferTag = '[object ArrayBuffer]', | |
_equalByTag_dataViewTag = '[object DataView]'; | |
/** Used to convert symbols to primitives and strings. */ | |
var _equalByTag_symbolProto = _Symbol ? _Symbol.prototype : undefined, | |
_equalByTag_symbolValueOf = _equalByTag_symbolProto ? _equalByTag_symbolProto.valueOf : undefined; | |
/** | |
* A specialized version of `baseIsEqualDeep` for comparing objects of | |
* the same `toStringTag`. | |
* | |
* **Note:** This function only supports comparing values with tags of | |
* `Boolean`, `Date`, `Error`, `Number`, `RegExp`, or `String`. | |
* | |
* @private | |
* @param {Object} object The object to compare. | |
* @param {Object} other The other object to compare. | |
* @param {string} tag The `toStringTag` of the objects to compare. | |
* @param {Function} equalFunc The function to determine equivalents of values. | |
* @param {Function} customizer The function to customize comparisons. | |
* @param {number} bitmask The bitmask of comparison flags. See `baseIsEqual` | |
* for more details. | |
* @param {Object} stack Tracks traversed `object` and `other` objects. | |
* @returns {boolean} Returns `true` if the objects are equivalent, else `false`. | |
*/ | |
function equalByTag(object, other, tag, equalFunc, customizer, bitmask, stack) { | |
switch (tag) { | |
case _equalByTag_dataViewTag: | |
if ((object.byteLength != other.byteLength) || | |
(object.byteOffset != other.byteOffset)) { | |
return false; | |
} | |
object = object.buffer; | |
other = other.buffer; | |
case _equalByTag_arrayBufferTag: | |
if ((object.byteLength != other.byteLength) || | |
!equalFunc(new _Uint8Array(object), new _Uint8Array(other))) { | |
return false; | |
} | |
return true; | |
case _equalByTag_boolTag: | |
case _equalByTag_dateTag: | |
// Coerce dates and booleans to numbers, dates to milliseconds and | |
// booleans to `1` or `0` treating invalid dates coerced to `NaN` as | |
// not equal. | |
return +object == +other; | |
case _equalByTag_errorTag: | |
return object.name == other.name && object.message == other.message; | |
case _equalByTag_numberTag: | |
// Treat `NaN` vs. `NaN` as equal. | |
return (object != +object) ? other != +other : object == +other; | |
case _equalByTag_regexpTag: | |
case _equalByTag_stringTag: | |
// Coerce regexes to strings and treat strings, primitives and objects, | |
// as equal. See http://www.ecma-international.org/ecma-262/6.0/#sec-regexp.prototype.tostring | |
// for more details. | |
return object == (other + ''); | |
case _equalByTag_mapTag: | |
var convert = _mapToArray; | |
case _equalByTag_setTag: | |
var isPartial = bitmask & _equalByTag_PARTIAL_COMPARE_FLAG; | |
convert || (convert = _setToArray); | |
if (object.size != other.size && !isPartial) { | |
return false; | |
} | |
// Assume cyclic values are equal. | |
var stacked = stack.get(object); | |
if (stacked) { | |
return stacked == other; | |
} | |
bitmask |= _equalByTag_UNORDERED_COMPARE_FLAG; | |
stack.set(object, other); | |
// Recursively compare objects (susceptible to call stack limits). | |
return _equalArrays(convert(object), convert(other), equalFunc, customizer, bitmask, stack); | |
case _equalByTag_symbolTag: | |
if (_equalByTag_symbolValueOf) { | |
return _equalByTag_symbolValueOf.call(object) == _equalByTag_symbolValueOf.call(other); | |
} | |
} | |
return false; | |
} | |
/* harmony default export */ var _equalByTag = (equalByTag); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_equalObjects.js | |
/** Used to compose bitmasks for comparison styles. */ | |
var _equalObjects_PARTIAL_COMPARE_FLAG = 2; | |
/** | |
* A specialized version of `baseIsEqualDeep` for objects with support for | |
* partial deep comparisons. | |
* | |
* @private | |
* @param {Object} object The object to compare. | |
* @param {Object} other The other object to compare. | |
* @param {Function} equalFunc The function to determine equivalents of values. | |
* @param {Function} customizer The function to customize comparisons. | |
* @param {number} bitmask The bitmask of comparison flags. See `baseIsEqual` | |
* for more details. | |
* @param {Object} stack Tracks traversed `object` and `other` objects. | |
* @returns {boolean} Returns `true` if the objects are equivalent, else `false`. | |
*/ | |
function equalObjects(object, other, equalFunc, customizer, bitmask, stack) { | |
var isPartial = bitmask & _equalObjects_PARTIAL_COMPARE_FLAG, | |
objProps = lodash_keys(object), | |
objLength = objProps.length, | |
othProps = lodash_keys(other), | |
othLength = othProps.length; | |
if (objLength != othLength && !isPartial) { | |
return false; | |
} | |
var index = objLength; | |
while (index--) { | |
var key = objProps[index]; | |
if (!(isPartial ? key in other : _baseHas(other, key))) { | |
return false; | |
} | |
} | |
// Assume cyclic values are equal. | |
var stacked = stack.get(object); | |
if (stacked) { | |
return stacked == other; | |
} | |
var result = true; | |
stack.set(object, other); | |
var skipCtor = isPartial; | |
while (++index < objLength) { | |
key = objProps[index]; | |
var objValue = object[key], | |
othValue = other[key]; | |
if (customizer) { | |
var compared = isPartial | |
? customizer(othValue, objValue, key, other, object, stack) | |
: customizer(objValue, othValue, key, object, other, stack); | |
} | |
// Recursively compare objects (susceptible to call stack limits). | |
if (!(compared === undefined | |
? (objValue === othValue || equalFunc(objValue, othValue, customizer, bitmask, stack)) | |
: compared | |
)) { | |
result = false; | |
break; | |
} | |
skipCtor || (skipCtor = key == 'constructor'); | |
} | |
if (result && !skipCtor) { | |
var objCtor = object.constructor, | |
othCtor = other.constructor; | |
// Non `Object` object instances with different constructors are not equal. | |
if (objCtor != othCtor && | |
('constructor' in object && 'constructor' in other) && | |
!(typeof objCtor == 'function' && objCtor instanceof objCtor && | |
typeof othCtor == 'function' && othCtor instanceof othCtor)) { | |
result = false; | |
} | |
} | |
stack['delete'](object); | |
return result; | |
} | |
/* harmony default export */ var _equalObjects = (equalObjects); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/isTypedArray.js | |
/** `Object#toString` result references. */ | |
var isTypedArray_argsTag = '[object Arguments]', | |
isTypedArray_arrayTag = '[object Array]', | |
isTypedArray_boolTag = '[object Boolean]', | |
isTypedArray_dateTag = '[object Date]', | |
isTypedArray_errorTag = '[object Error]', | |
isTypedArray_funcTag = '[object Function]', | |
isTypedArray_mapTag = '[object Map]', | |
isTypedArray_numberTag = '[object Number]', | |
isTypedArray_objectTag = '[object Object]', | |
isTypedArray_regexpTag = '[object RegExp]', | |
isTypedArray_setTag = '[object Set]', | |
isTypedArray_stringTag = '[object String]', | |
isTypedArray_weakMapTag = '[object WeakMap]'; | |
var isTypedArray_arrayBufferTag = '[object ArrayBuffer]', | |
isTypedArray_dataViewTag = '[object DataView]', | |
isTypedArray_float32Tag = '[object Float32Array]', | |
isTypedArray_float64Tag = '[object Float64Array]', | |
isTypedArray_int8Tag = '[object Int8Array]', | |
isTypedArray_int16Tag = '[object Int16Array]', | |
isTypedArray_int32Tag = '[object Int32Array]', | |
isTypedArray_uint8Tag = '[object Uint8Array]', | |
isTypedArray_uint8ClampedTag = '[object Uint8ClampedArray]', | |
isTypedArray_uint16Tag = '[object Uint16Array]', | |
isTypedArray_uint32Tag = '[object Uint32Array]'; | |
/** Used to identify `toStringTag` values of typed arrays. */ | |
var typedArrayTags = {}; | |
typedArrayTags[isTypedArray_float32Tag] = typedArrayTags[isTypedArray_float64Tag] = | |
typedArrayTags[isTypedArray_int8Tag] = typedArrayTags[isTypedArray_int16Tag] = | |
typedArrayTags[isTypedArray_int32Tag] = typedArrayTags[isTypedArray_uint8Tag] = | |
typedArrayTags[isTypedArray_uint8ClampedTag] = typedArrayTags[isTypedArray_uint16Tag] = | |
typedArrayTags[isTypedArray_uint32Tag] = true; | |
typedArrayTags[isTypedArray_argsTag] = typedArrayTags[isTypedArray_arrayTag] = | |
typedArrayTags[isTypedArray_arrayBufferTag] = typedArrayTags[isTypedArray_boolTag] = | |
typedArrayTags[isTypedArray_dataViewTag] = typedArrayTags[isTypedArray_dateTag] = | |
typedArrayTags[isTypedArray_errorTag] = typedArrayTags[isTypedArray_funcTag] = | |
typedArrayTags[isTypedArray_mapTag] = typedArrayTags[isTypedArray_numberTag] = | |
typedArrayTags[isTypedArray_objectTag] = typedArrayTags[isTypedArray_regexpTag] = | |
typedArrayTags[isTypedArray_setTag] = typedArrayTags[isTypedArray_stringTag] = | |
typedArrayTags[isTypedArray_weakMapTag] = false; | |
/** Used for built-in method references. */ | |
var isTypedArray_objectProto = Object.prototype; | |
/** | |
* Used to resolve the | |
* [`toStringTag`](http://ecma-international.org/ecma-262/6.0/#sec-object.prototype.tostring) | |
* of values. | |
*/ | |
var isTypedArray_objectToString = isTypedArray_objectProto.toString; | |
/** | |
* Checks if `value` is classified as a typed array. | |
* | |
* @static | |
* @memberOf _ | |
* @since 3.0.0 | |
* @category Lang | |
* @param {*} value The value to check. | |
* @returns {boolean} Returns `true` if `value` is correctly classified, | |
* else `false`. | |
* @example | |
* | |
* _.isTypedArray(new Uint8Array); | |
* // => true | |
* | |
* _.isTypedArray([]); | |
* // => false | |
*/ | |
function isTypedArray(value) { | |
return lodash_isObjectLike(value) && | |
lodash_isLength(value.length) && !!typedArrayTags[isTypedArray_objectToString.call(value)]; | |
} | |
/* harmony default export */ var lodash_isTypedArray = (isTypedArray); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseIsEqualDeep.js | |
/** Used to compose bitmasks for comparison styles. */ | |
var _baseIsEqualDeep_PARTIAL_COMPARE_FLAG = 2; | |
/** `Object#toString` result references. */ | |
var _baseIsEqualDeep_argsTag = '[object Arguments]', | |
_baseIsEqualDeep_arrayTag = '[object Array]', | |
_baseIsEqualDeep_objectTag = '[object Object]'; | |
/** Used for built-in method references. */ | |
var _baseIsEqualDeep_objectProto = Object.prototype; | |
/** Used to check objects for own properties. */ | |
var _baseIsEqualDeep_hasOwnProperty = _baseIsEqualDeep_objectProto.hasOwnProperty; | |
/** | |
* A specialized version of `baseIsEqual` for arrays and objects which performs | |
* deep comparisons and tracks traversed objects enabling objects with circular | |
* references to be compared. | |
* | |
* @private | |
* @param {Object} object The object to compare. | |
* @param {Object} other The other object to compare. | |
* @param {Function} equalFunc The function to determine equivalents of values. | |
* @param {Function} [customizer] The function to customize comparisons. | |
* @param {number} [bitmask] The bitmask of comparison flags. See `baseIsEqual` | |
* for more details. | |
* @param {Object} [stack] Tracks traversed `object` and `other` objects. | |
* @returns {boolean} Returns `true` if the objects are equivalent, else `false`. | |
*/ | |
function baseIsEqualDeep(object, other, equalFunc, customizer, bitmask, stack) { | |
var objIsArr = lodash_isArray(object), | |
othIsArr = lodash_isArray(other), | |
objTag = _baseIsEqualDeep_arrayTag, | |
othTag = _baseIsEqualDeep_arrayTag; | |
if (!objIsArr) { | |
objTag = _getTag(object); | |
objTag = objTag == _baseIsEqualDeep_argsTag ? _baseIsEqualDeep_objectTag : objTag; | |
} | |
if (!othIsArr) { | |
othTag = _getTag(other); | |
othTag = othTag == _baseIsEqualDeep_argsTag ? _baseIsEqualDeep_objectTag : othTag; | |
} | |
var objIsObj = objTag == _baseIsEqualDeep_objectTag && !_isHostObject(object), | |
othIsObj = othTag == _baseIsEqualDeep_objectTag && !_isHostObject(other), | |
isSameTag = objTag == othTag; | |
if (isSameTag && !objIsObj) { | |
stack || (stack = new _Stack); | |
return (objIsArr || lodash_isTypedArray(object)) | |
? _equalArrays(object, other, equalFunc, customizer, bitmask, stack) | |
: _equalByTag(object, other, objTag, equalFunc, customizer, bitmask, stack); | |
} | |
if (!(bitmask & _baseIsEqualDeep_PARTIAL_COMPARE_FLAG)) { | |
var objIsWrapped = objIsObj && _baseIsEqualDeep_hasOwnProperty.call(object, '__wrapped__'), | |
othIsWrapped = othIsObj && _baseIsEqualDeep_hasOwnProperty.call(other, '__wrapped__'); | |
if (objIsWrapped || othIsWrapped) { | |
var objUnwrapped = objIsWrapped ? object.value() : object, | |
othUnwrapped = othIsWrapped ? other.value() : other; | |
stack || (stack = new _Stack); | |
return equalFunc(objUnwrapped, othUnwrapped, customizer, bitmask, stack); | |
} | |
} | |
if (!isSameTag) { | |
return false; | |
} | |
stack || (stack = new _Stack); | |
return _equalObjects(object, other, equalFunc, customizer, bitmask, stack); | |
} | |
/* harmony default export */ var _baseIsEqualDeep = (baseIsEqualDeep); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseIsEqual.js | |
/** | |
* The base implementation of `_.isEqual` which supports partial comparisons | |
* and tracks traversed objects. | |
* | |
* @private | |
* @param {*} value The value to compare. | |
* @param {*} other The other value to compare. | |
* @param {Function} [customizer] The function to customize comparisons. | |
* @param {boolean} [bitmask] The bitmask of comparison flags. | |
* The bitmask may be composed of the following flags: | |
* 1 - Unordered comparison | |
* 2 - Partial comparison | |
* @param {Object} [stack] Tracks traversed `value` and `other` objects. | |
* @returns {boolean} Returns `true` if the values are equivalent, else `false`. | |
*/ | |
function baseIsEqual(value, other, customizer, bitmask, stack) { | |
if (value === other) { | |
return true; | |
} | |
if (value == null || other == null || (!lodash_isObject(value) && !lodash_isObjectLike(other))) { | |
return value !== value && other !== other; | |
} | |
return _baseIsEqualDeep(value, other, baseIsEqual, customizer, bitmask, stack); | |
} | |
/* harmony default export */ var _baseIsEqual = (baseIsEqual); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/isEqual.js | |
/** | |
* Performs a deep comparison between two values to determine if they are | |
* equivalent. | |
* | |
* **Note:** This method supports comparing arrays, array buffers, booleans, | |
* date objects, error objects, maps, numbers, `Object` objects, regexes, | |
* sets, strings, symbols, and typed arrays. `Object` objects are compared | |
* by their own, not inherited, enumerable properties. Functions and DOM | |
* nodes are **not** supported. | |
* | |
* @static | |
* @memberOf _ | |
* @since 0.1.0 | |
* @category Lang | |
* @param {*} value The value to compare. | |
* @param {*} other The other value to compare. | |
* @returns {boolean} Returns `true` if the values are equivalent, | |
* else `false`. | |
* @example | |
* | |
* var object = { 'user': 'fred' }; | |
* var other = { 'user': 'fred' }; | |
* | |
* _.isEqual(object, other); | |
* // => true | |
* | |
* object === other; | |
* // => false | |
*/ | |
function isEqual(value, other) { | |
return _baseIsEqual(value, other); | |
} | |
/* harmony default export */ var lodash_isEqual = (isEqual); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/attributeoperation.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/model/operation/attributeoperation | |
*/ | |
/** | |
* Operation to change nodes' attribute. | |
* | |
* Using this class you can add, remove or change value of the attribute. | |
* | |
* @extends module:engine/model/operation/operation~Operation | |
*/ | |
class attributeoperation_AttributeOperation extends operation_Operation { | |
/** | |
* Creates an operation that changes, removes or adds attributes. | |
* | |
* If only `newValue` is set, attribute will be added on a node. Note that all nodes in operation's range must not | |
* have an attribute with the same key as the added attribute. | |
* | |
* If only `oldValue` is set, then attribute with given key will be removed. Note that all nodes in operation's range | |
* must have an attribute with that key added. | |
* | |
* If both `newValue` and `oldValue` are set, then the operation will change the attribute value. Note that all nodes in | |
* operation's ranges must already have an attribute with given key and `oldValue` as value | |
* | |
* @param {module:engine/model/range~Range} range Range on which the operation should be applied. | |
* @param {String} key Key of an attribute to change or remove. | |
* @param {*} oldValue Old value of the attribute with given key or `null`, if attribute was not set before. | |
* @param {*} newValue New value of the attribute with given key or `null`, if operation should remove attribute. | |
* @param {Number} baseVersion {@link module:engine/model/document~Document#version} on which the operation can be applied. | |
*/ | |
constructor( range, key, oldValue, newValue, baseVersion ) { | |
super( baseVersion ); | |
/** | |
* Range on which operation should be applied. | |
* | |
* @readonly | |
* @member {module:engine/model/range~Range} | |
*/ | |
this.range = range_Range.createFromRange( range ); | |
/** | |
* Key of an attribute to change or remove. | |
* | |
* @readonly | |
* @member {String} | |
*/ | |
this.key = key; | |
/** | |
* Old value of the attribute with given key or `null`, if attribute was not set before. | |
* | |
* @readonly | |
* @member {*} | |
*/ | |
this.oldValue = oldValue === undefined ? null : oldValue; | |
/** | |
* New value of the attribute with given key or `null`, if operation should remove attribute. | |
* | |
* @readonly | |
* @member {*} | |
*/ | |
this.newValue = newValue === undefined ? null : newValue; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
get type() { | |
if ( this.oldValue === null ) { | |
return 'addAttribute'; | |
} else if ( this.newValue === null ) { | |
return 'removeAttribute'; | |
} else { | |
return 'changeAttribute'; | |
} | |
} | |
/** | |
* Creates and returns an operation that has the same parameters as this operation. | |
* | |
* @returns {module:engine/model/operation/attributeoperation~AttributeOperation} Clone of this operation. | |
*/ | |
clone() { | |
return new attributeoperation_AttributeOperation( this.range, this.key, this.oldValue, this.newValue, this.baseVersion ); | |
} | |
/** | |
* See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}. | |
* | |
* @returns {module:engine/model/operation/attributeoperation~AttributeOperation} | |
*/ | |
getReversed() { | |
return new attributeoperation_AttributeOperation( this.range, this.key, this.newValue, this.oldValue, this.baseVersion + 1 ); | |
} | |
/** | |
* @inheritDoc | |
*/ | |
_execute() { | |
// Validation. | |
for ( const item of this.range.getItems() ) { | |
if ( this.oldValue !== null && !lodash_isEqual( item.getAttribute( this.key ), this.oldValue ) ) { | |
/** | |
* Changed node has different attribute value than operation's old attribute value. | |
* | |
* @error operation-attribute-wrong-old-value | |
* @param {module:engine/model/item~Item} item | |
* @param {String} key | |
* @param {*} value | |
*/ | |
throw new CKEditorError( | |
'attribute-operation-wrong-old-value: Changed node has different attribute value than operation\'s ' + | |
'old attribute value.', | |
{ item, key: this.key, value: this.oldValue } | |
); | |
} | |
if ( this.oldValue === null && this.newValue !== null && item.hasAttribute( this.key ) ) { | |
/** | |
* The attribute with given key already exists for the given node. | |
* | |
* @error attribute-operation-attribute-exists | |
* @param {module:engine/model/node~Node} node | |
* @param {String} key | |
*/ | |
throw new CKEditorError( | |
'attribute-operation-attribute-exists: The attribute with given key already exists.', | |
{ node: item, key: this.key } | |
); | |
} | |
} | |
// If value to set is same as old value, don't do anything. | |
if ( !lodash_isEqual( this.oldValue, this.newValue ) ) { | |
// Execution. | |
model_writer.setAttribute( this.range, this.key, this.newValue ); | |
} | |
return { range: this.range, key: this.key, oldValue: this.oldValue, newValue: this.newValue }; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
static get className() { | |
return 'engine.model.operation.AttributeOperation'; | |
} | |
/** | |
* Creates `AttributeOperation` object from deserilized object, i.e. from parsed JSON string. | |
* | |
* @param {Object} json Deserialized JSON object. | |
* @param {module:engine/model/document~Document} document Document on which this operation will be applied. | |
* @returns {module:engine/model/operation/attributeoperation~AttributeOperation} | |
*/ | |
static fromJSON( json, document ) { | |
return new attributeoperation_AttributeOperation( range_Range.fromJSON( json.range, document ), json.key, json.oldValue, json.newValue, json.baseVersion ); | |
} | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/moveoperation.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/model/operation/moveoperation | |
*/ | |
/** | |
* Operation to move a range of {@link module:engine/model/item~Item model items} | |
* to given {@link module:engine/model/position~Position target position}. | |
* | |
* @extends module:engine/model/operation/operation~Operation | |
*/ | |
class moveoperation_MoveOperation extends operation_Operation { | |
/** | |
* Creates a move operation. | |
* | |
* @param {module:engine/model/position~Position} sourcePosition | |
* Position before the first {@link module:engine/model/item~Item model item} to move. | |
* @param {Number} howMany Offset size of moved range. Moved range will start from `sourcePosition` and end at | |
* `sourcePosition` with offset shifted by `howMany`. | |
* @param {module:engine/model/position~Position} targetPosition Position at which moved nodes will be inserted. | |
* @param {Number} baseVersion {@link module:engine/model/document~Document#version} on which operation can be applied. | |
*/ | |
constructor( sourcePosition, howMany, targetPosition, baseVersion ) { | |
super( baseVersion ); | |
/** | |
* Position before the first {@link module:engine/model/item~Item model item} to move. | |
* | |
* @member {module:engine/model/position~Position} module:engine/model/operation/moveoperation~MoveOperation#sourcePosition | |
*/ | |
this.sourcePosition = position_Position.createFromPosition( sourcePosition ); | |
/** | |
* Offset size of moved range. | |
* | |
* @member {Number} module:engine/model/operation/moveoperation~MoveOperation#howMany | |
*/ | |
this.howMany = howMany; | |
/** | |
* Position at which moved nodes will be inserted. | |
* | |
* @member {module:engine/model/position~Position} module:engine/model/operation/moveoperation~MoveOperation#targetPosition | |
*/ | |
this.targetPosition = position_Position.createFromPosition( targetPosition ); | |
/** | |
* Defines whether `MoveOperation` is sticky. If `MoveOperation` is sticky, during | |
* {@link module:engine/model/operation/transform~transform operational transformation} if there will be an operation that | |
* inserts some nodes at the position equal to the boundary of this `MoveOperation`, that operation will | |
* get their insertion path updated to the position where this `MoveOperation` moves the range. | |
* | |
* @member {Boolean} module:engine/model/operation/moveoperation~MoveOperation#isSticky | |
*/ | |
this.isSticky = false; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
get type() { | |
return 'move'; | |
} | |
/** | |
* Creates and returns an operation that has the same parameters as this operation. | |
* | |
* @returns {module:engine/model/operation/moveoperation~MoveOperation} Clone of this operation. | |
*/ | |
clone() { | |
const op = new this.constructor( this.sourcePosition, this.howMany, this.targetPosition, this.baseVersion ); | |
op.isSticky = this.isSticky; | |
return op; | |
} | |
/** | |
* Returns the start position of the moved range after it got moved. This may be different than | |
* {@link module:engine/model/operation/moveoperation~MoveOperation#targetPosition} in some cases, i.e. when a range is moved | |
* inside the same parent but {@link module:engine/model/operation/moveoperation~MoveOperation#targetPosition targetPosition} | |
* is after {@link module:engine/model/operation/moveoperation~MoveOperation#sourcePosition sourcePosition}. | |
* | |
* vv vv | |
* abcdefg ===> adefbcg | |
* ^ ^ | |
* targetPos movedRangeStart | |
* offset 6 offset 4 | |
* | |
* @returns {module:engine/model/position~Position} | |
*/ | |
getMovedRangeStart() { | |
return this.targetPosition._getTransformedByDeletion( this.sourcePosition, this.howMany ); | |
} | |
/** | |
* See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}. | |
* | |
* @returns {module:engine/model/operation/moveoperation~MoveOperation} | |
*/ | |
getReversed() { | |
const newTargetPosition = this.sourcePosition._getTransformedByInsertion( this.targetPosition, this.howMany ); | |
const op = new this.constructor( this.getMovedRangeStart(), this.howMany, newTargetPosition, this.baseVersion + 1 ); | |
op.isSticky = this.isSticky; | |
return op; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
_execute() { | |
const sourceElement = this.sourcePosition.parent; | |
const targetElement = this.targetPosition.parent; | |
const sourceOffset = this.sourcePosition.offset; | |
const targetOffset = this.targetPosition.offset; | |
// Validate whether move operation has correct parameters. | |
// Validation is pretty complex but move operation is one of the core ways to manipulate the document state. | |
// We expect that many errors might be connected with one of scenarios described below. | |
if ( !sourceElement || !targetElement ) { | |
/** | |
* Source position or target position is invalid. | |
* | |
* @error move-operation-position-invalid | |
*/ | |
throw new CKEditorError( | |
'move-operation-position-invalid: Source position or target position is invalid.' | |
); | |
} else if ( sourceOffset + this.howMany > sourceElement.maxOffset ) { | |
/** | |
* The nodes which should be moved do not exist. | |
* | |
* @error move-operation-nodes-do-not-exist | |
*/ | |
throw new CKEditorError( | |
'move-operation-nodes-do-not-exist: The nodes which should be moved do not exist.' | |
); | |
} else if ( sourceElement === targetElement && sourceOffset < targetOffset && targetOffset < sourceOffset + this.howMany ) { | |
/** | |
* Trying to move a range of nodes into the middle of that range. | |
* | |
* @error move-operation-range-into-itself | |
*/ | |
throw new CKEditorError( | |
'move-operation-range-into-itself: Trying to move a range of nodes to the inside of that range.' | |
); | |
} else if ( this.sourcePosition.root == this.targetPosition.root ) { | |
if ( compareArrays( this.sourcePosition.getParentPath(), this.targetPosition.getParentPath() ) == 'prefix' ) { | |
const i = this.sourcePosition.path.length - 1; | |
if ( this.targetPosition.path[ i ] >= sourceOffset && this.targetPosition.path[ i ] < sourceOffset + this.howMany ) { | |
/** | |
* Trying to move a range of nodes into one of nodes from that range. | |
* | |
* @error move-operation-node-into-itself | |
*/ | |
throw new CKEditorError( | |
'move-operation-node-into-itself: Trying to move a range of nodes into one of nodes from that range.' | |
); | |
} | |
} | |
} | |
const range = model_writer.move( range_Range.createFromPositionAndShift( this.sourcePosition, this.howMany ), this.targetPosition ); | |
return { | |
sourcePosition: this.sourcePosition, | |
range | |
}; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
static get className() { | |
return 'engine.model.operation.MoveOperation'; | |
} | |
/** | |
* Creates `MoveOperation` object from deserilized object, i.e. from parsed JSON string. | |
* | |
* @param {Object} json Deserialized JSON object. | |
* @param {module:engine/model/document~Document} document Document on which this operation will be applied. | |
* @returns {module:engine/model/operation/moveoperation~MoveOperation} | |
*/ | |
static fromJSON( json, document ) { | |
const sourcePosition = position_Position.fromJSON( json.sourcePosition, document ); | |
const targetPosition = position_Position.fromJSON( json.targetPosition, document ); | |
const move = new this( sourcePosition, json.howMany, targetPosition, json.baseVersion ); | |
if ( json.isSticky ) { | |
move.isSticky = true; | |
} | |
return move; | |
} | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/reinsertoperation.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/model/operation/reinsertoperation | |
*/ | |
/** | |
* Operation to reinsert previously removed nodes back to the non-graveyard root. This operation acts like | |
* {@link module:engine/model/operation/moveoperation~MoveOperation} but it returns | |
* {@link module:engine/model/operation/removeoperation~RemoveOperation} when reversed | |
* and fires different change event. | |
*/ | |
class reinsertoperation_ReinsertOperation extends moveoperation_MoveOperation { | |
/** | |
* Position where nodes will be re-inserted. | |
* | |
* @type {module:engine/model/position~Position} | |
*/ | |
get position() { | |
return this.targetPosition; | |
} | |
/** | |
* @param {module:engine/model/position~Position} pos | |
*/ | |
set position( pos ) { | |
this.targetPosition = pos; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
get type() { | |
return 'reinsert'; | |
} | |
/** | |
* See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}. | |
* | |
* @returns {module:engine/model/operation/removeoperation~RemoveOperation} | |
*/ | |
getReversed() { | |
const newTargetPosition = this.sourcePosition._getTransformedByInsertion( this.targetPosition, this.howMany ); | |
return new removeoperation_RemoveOperation( this.getMovedRangeStart(), this.howMany, newTargetPosition, this.baseVersion + 1 ); | |
} | |
/** | |
* @inheritDoc | |
*/ | |
static get className() { | |
return 'engine.model.operation.ReinsertOperation'; | |
} | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/removeoperation.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/model/operation/removeoperation | |
*/ | |
/** | |
* Operation to remove a range of nodes. | |
*/ | |
class removeoperation_RemoveOperation extends moveoperation_MoveOperation { | |
/** | |
* @inheritDoc | |
*/ | |
get type() { | |
return 'remove'; | |
} | |
/** | |
* See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}. | |
* | |
* @returns {module:engine/model/operation/reinsertoperation~ReinsertOperation|module:engine/model/operation/nooperation~NoOperation} | |
*/ | |
getReversed() { | |
const newTargetPosition = this.sourcePosition._getTransformedByInsertion( this.targetPosition, this.howMany ); | |
return new reinsertoperation_ReinsertOperation( this.getMovedRangeStart(), this.howMany, newTargetPosition, this.baseVersion + 1 ); | |
} | |
/** | |
* @inheritDoc | |
*/ | |
static get className() { | |
return 'engine.model.operation.RemoveOperation'; | |
} | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/insertoperation.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/model/operation/insertoperation | |
*/ | |
/** | |
* Operation to insert one or more nodes at given position in the model. | |
* | |
* @extends module:engine/model/operation/operation~Operation | |
*/ | |
class insertoperation_InsertOperation extends operation_Operation { | |
/** | |
* Creates an insert operation. | |
* | |
* @param {module:engine/model/position~Position} position Position of insertion. | |
* @param {module:engine/model/node~NodeSet} nodes The list of nodes to be inserted. | |
* @param {Number} baseVersion {@link module:engine/model/document~Document#version} on which operation can be applied. | |
*/ | |
constructor( position, nodes, baseVersion ) { | |
super( baseVersion ); | |
/** | |
* Position of insertion. | |
* | |
* @readonly | |
* @member {module:engine/model/position~Position} module:engine/model/operation/insertoperation~InsertOperation#position | |
*/ | |
this.position = position_Position.createFromPosition( position ); | |
/** | |
* List of nodes to insert. | |
* | |
* @readonly | |
* @member {module:engine/model/nodelist~NodeList} module:engine/model/operation/insertoperation~InsertOperation#nodeList | |
*/ | |
this.nodes = new nodelist_NodeList( normalizeNodes( nodes ) ); | |
} | |
/** | |
* @inheritDoc | |
*/ | |
get type() { | |
return 'insert'; | |
} | |
/** | |
* Creates and returns an operation that has the same parameters as this operation. | |
* | |
* @returns {module:engine/model/operation/insertoperation~InsertOperation} Clone of this operation. | |
*/ | |
clone() { | |
const nodes = new nodelist_NodeList( [ ...this.nodes ].map( node => node.clone( true ) ) ); | |
return new insertoperation_InsertOperation( this.position, nodes, this.baseVersion ); | |
} | |
/** | |
* See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}. | |
* | |
* @returns {module:engine/model/operation/removeoperation~RemoveOperation} | |
*/ | |
getReversed() { | |
const graveyard = this.position.root.document.graveyard; | |
const gyPosition = new position_Position( graveyard, [ 0 ] ); | |
return new removeoperation_RemoveOperation( this.position, this.nodes.maxOffset, gyPosition, this.baseVersion + 1 ); | |
} | |
/** | |
* @inheritDoc | |
*/ | |
_execute() { | |
// What happens here is that we want original nodes be passed to writer because we want original nodes | |
// to be inserted to the model. But in InsertOperation, we want to keep those nodes as they were added | |
// to the operation, not modified. For example, text nodes can get merged or cropped while Elements can | |
// get children. It is important that InsertOperation has the copy of original nodes in intact state. | |
const originalNodes = this.nodes; | |
this.nodes = new nodelist_NodeList( [ ...originalNodes ].map( node => node.clone( true ) ) ); | |
const range = model_writer_insert( this.position, originalNodes ); | |
return { range }; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
static get className() { | |
return 'engine.model.operation.InsertOperation'; | |
} | |
/** | |
* Creates `InsertOperation` object from deserilized object, i.e. from parsed JSON string. | |
* | |
* @param {Object} json Deserialized JSON object. | |
* @param {module:engine/model/document~Document} document Document on which this operation will be applied. | |
* @returns {module:engine/model/operation/insertoperation~InsertOperation} | |
*/ | |
static fromJSON( json, document ) { | |
const children = []; | |
for ( const child of json.nodes ) { | |
if ( child.name ) { | |
// If child has name property, it is an Element. | |
children.push( element_Element.fromJSON( child ) ); | |
} else { | |
// Otherwise, it is a Text node. | |
children.push( text_Text.fromJSON( child ) ); | |
} | |
} | |
return new insertoperation_InsertOperation( position_Position.fromJSON( json.position, document ), children, json.baseVersion ); | |
} | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/markeroperation.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/model/operation/markeroperation | |
*/ | |
/** | |
* @extends module:engine/model/operation/operation~Operation | |
*/ | |
class markeroperation_MarkerOperation extends operation_Operation { | |
/** | |
* @param {String} name Marker name. | |
* @param {module:engine/model/range~Range} oldRange Marker range before the change. | |
* @param {module:engine/model/range~Range} newRange Marker range after the change. | |
* @param {module:engine/model/markercollection~MarkerCollection} markers Marker collection on which change should be executed. | |
* @param {Number} baseVersion {@link module:engine/model/document~Document#version} on which the operation can be applied. | |
*/ | |
constructor( name, oldRange, newRange, markers, baseVersion ) { | |
super( baseVersion ); | |
/** | |
* Marker name. | |
* | |
* @readonly | |
* @member {String} | |
*/ | |
this.name = name; | |
/** | |
* Marker range before the change. | |
* | |
* @readonly | |
* @member {module:engine/model/range~Range} | |
*/ | |
this.oldRange = oldRange ? range_Range.createFromRange( oldRange ) : null; | |
/** | |
* Marker range after the change. | |
* | |
* @readonly | |
* @member {module:engine/model/range~Range} | |
*/ | |
this.newRange = newRange ? range_Range.createFromRange( newRange ) : null; | |
/** | |
* Marker collection on which change should be executed. | |
* | |
* @private | |
* @member {module:engine/model/markercollection~MarkerCollection} | |
*/ | |
this._markers = markers; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
get type() { | |
return 'marker'; | |
} | |
/** | |
* Creates and returns an operation that has the same parameters as this operation. | |
* | |
* @returns {module:engine/model/operation/markeroperation~MarkerOperation} Clone of this operation. | |
*/ | |
clone() { | |
return new markeroperation_MarkerOperation( this.name, this.oldRange, this.newRange, this._markers, this.baseVersion ); | |
} | |
/** | |
* See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}. | |
* | |
* @returns {module:engine/model/operation/markeroperation~MarkerOperation} | |
*/ | |
getReversed() { | |
return new markeroperation_MarkerOperation( this.name, this.newRange, this.oldRange, this._markers, this.baseVersion + 1 ); | |
} | |
/** | |
* @inheritDoc | |
*/ | |
_execute() { | |
const type = this.newRange ? 'set' : 'remove'; | |
this._markers[ type ]( this.name, this.newRange ); | |
return { name: this.name, type }; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
toJSON() { | |
const json = super.toJSON(); | |
delete json._markers; | |
return json; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
static get className() { | |
return 'engine.model.operation.MarkerOperation'; | |
} | |
/** | |
* Creates `MarkerOperation` object from deserilized object, i.e. from parsed JSON string. | |
* | |
* @param {Object} json Deserialized JSON object. | |
* @param {module:engine/model/document~Document} document Document on which this operation will be applied. | |
* @returns {module:engine/model/operation/markeroperation~MarkerOperation} | |
*/ | |
static fromJSON( json, document ) { | |
return new markeroperation_MarkerOperation( | |
json.name, | |
json.oldRange ? range_Range.fromJSON( json.oldRange, document ) : null, | |
json.newRange ? range_Range.fromJSON( json.newRange, document ) : null, | |
document.markers, | |
json.baseVersion | |
); | |
} | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/nooperation.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/model/operation/nooperation | |
*/ | |
/** | |
* Operation which is doing nothing ("empty operation", "do-nothing operation", "noop"). This is an operation, | |
* which when executed does not change the tree model. It still has some parameters defined for transformation purposes. | |
* | |
* In most cases this operation is a result of transforming operations. When transformation returns | |
* {@link module:engine/model/operation/nooperation~NoOperation} it means that changes done by the transformed operation | |
* have already been applied. | |
* | |
* @extends module:engine/model/operation/operation~Operation | |
*/ | |
class NoOperation extends operation_Operation { | |
get type() { | |
return 'noop'; | |
} | |
/** | |
* Creates and returns an operation that has the same parameters as this operation. | |
* | |
* @returns {module:engine/model/operation/nooperation~NoOperation} Clone of this operation. | |
*/ | |
clone() { | |
return new NoOperation( this.baseVersion ); | |
} | |
/** | |
* See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}. | |
* | |
* @returns {module:engine/model/operation/nooperation~NoOperation} | |
*/ | |
getReversed() { | |
return new NoOperation( this.baseVersion + 1 ); | |
} | |
/** | |
* @inheritDoc | |
*/ | |
_execute() { | |
return {}; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
static get className() { | |
return 'engine.model.operation.NoOperation'; | |
} | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/renameoperation.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/model/operation/renameoperation | |
*/ | |
/** | |
* Operation to change element's name. | |
* | |
* Using this class you can change element's name. | |
* | |
* @extends module:engine/model/operation/operation~Operation | |
*/ | |
class renameoperation_RenameOperation extends operation_Operation { | |
/** | |
* Creates an operation that changes element's name. | |
* | |
* @param {module:engine/model/position~Position} position Position before an element to change. | |
* @param {String} oldName Current name of the element. | |
* @param {String} newName New name for the element. | |
* @param {Number} baseVersion {@link module:engine/model/document~Document#version} on which the operation can be applied. | |
*/ | |
constructor( position, oldName, newName, baseVersion ) { | |
super( baseVersion ); | |
/** | |
* Position before an element to change. | |
* | |
* @member {module:engine/model/position~Position} module:engine/model/operation/renameoperation~RenameOperation#position | |
*/ | |
this.position = position; | |
/** | |
* Current name of the element. | |
* | |
* @member {String} module:engine/model/operation/renameoperation~RenameOperation#oldName | |
*/ | |
this.oldName = oldName; | |
/** | |
* New name for the element. | |
* | |
* @member {String} module:engine/model/operation/renameoperation~RenameOperation#newName | |
*/ | |
this.newName = newName; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
get type() { | |
return 'rename'; | |
} | |
/** | |
* Creates and returns an operation that has the same parameters as this operation. | |
* | |
* @returns {module:engine/model/operation/renameoperation~RenameOperation} Clone of this operation. | |
*/ | |
clone() { | |
return new renameoperation_RenameOperation( position_Position.createFromPosition( this.position ), this.oldName, this.newName, this.baseVersion ); | |
} | |
/** | |
* See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}. | |
* | |
* @returns {module:engine/model/operation/renameoperation~RenameOperation} | |
*/ | |
getReversed() { | |
return new renameoperation_RenameOperation( position_Position.createFromPosition( this.position ), this.newName, this.oldName, this.baseVersion + 1 ); | |
} | |
/** | |
* @inheritDoc | |
*/ | |
_execute() { | |
// Validation. | |
const element = this.position.nodeAfter; | |
if ( !( element instanceof element_Element ) ) { | |
/** | |
* Given position is invalid or node after it is not instance of Element. | |
* | |
* @error rename-operation-wrong-position | |
*/ | |
throw new CKEditorError( | |
'rename-operation-wrong-position: Given position is invalid or node after it is not an instance of Element.' | |
); | |
} else if ( element.name !== this.oldName ) { | |
/** | |
* Element to change has different name than operation's old name. | |
* | |
* @error rename-operation-wrong-name | |
*/ | |
throw new CKEditorError( | |
'rename-operation-wrong-name: Element to change has different name than operation\'s old name.' | |
); | |
} | |
// If value to set is same as old value, don't do anything. | |
if ( element.name != this.newName ) { | |
// Execution. | |
element.name = this.newName; | |
} | |
return { element, oldName: this.oldName }; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
static get className() { | |
return 'engine.model.operation.RenameOperation'; | |
} | |
/** | |
* Creates `RenameOperation` object from deserialized object, i.e. from parsed JSON string. | |
* | |
* @param {Object} json Deserialized JSON object. | |
* @param {module:engine/model/document~Document} document Document on which this operation will be applied. | |
* @returns {module:engine/model/operation/attributeoperation~AttributeOperation} | |
*/ | |
static fromJSON( json, document ) { | |
return new renameoperation_RenameOperation( position_Position.fromJSON( json.position, document ), json.oldName, json.newName, json.baseVersion ); | |
} | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/rootattributeoperation.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/model/operation/rootattributeoperation | |
*/ | |
/** | |
* Operation to change root element's attribute. Using this class you can add, remove or change value of the attribute. | |
* | |
* This operation is needed, because root elements can't be changed through | |
* @link module:engine/model/operation/attributeoperation~AttributeOperation}. | |
* It is because {@link module:engine/model/operation/attributeoperation~AttributeOperation} | |
* requires a range to change and root element can't | |
* be a part of range because every {@link module:engine/model/position~Position} has to be inside a root. | |
* {@link module:engine/model/position~Position} can't be created before a root element. | |
* | |
* @extends module:engine/model/operation/operation~Operation | |
*/ | |
class rootattributeoperation_RootAttributeOperation extends operation_Operation { | |
/** | |
* Creates an operation that changes, removes or adds attributes on root element. | |
* | |
* @see module:engine/model/operation/attributeoperation~AttributeOperation | |
* @param {module:engine/model/rootelement~RootElement} root Root element to change. | |
* @param {String} key Key of an attribute to change or remove. | |
* @param {*} oldValue Old value of the attribute with given key or `null` if adding a new attribute. | |
* @param {*} newValue New value to set for the attribute. If `null`, then the operation just removes the attribute. | |
* @param {Number} baseVersion {@link module:engine/model/document~Document#version} on which the operation can be applied. | |
*/ | |
constructor( root, key, oldValue, newValue, baseVersion ) { | |
super( baseVersion ); | |
/** | |
* Root element to change. | |
* | |
* @readonly | |
* @member {module:engine/model/rootelement~RootElement} | |
*/ | |
this.root = root; | |
/** | |
* Key of an attribute to change or remove. | |
* | |
* @readonly | |
* @member {String} | |
*/ | |
this.key = key; | |
/** | |
* Old value of the attribute with given key or `null` if adding a new attribute. | |
* | |
* @readonly | |
* @member {*} | |
*/ | |
this.oldValue = oldValue; | |
/** | |
* New value to set for the attribute. If `null`, then the operation just removes the attribute. | |
* | |
* @readonly | |
* @member {*} | |
*/ | |
this.newValue = newValue; | |
} | |
get type() { | |
if ( this.oldValue === null ) { | |
return 'addRootAttribute'; | |
} else if ( this.newValue === null ) { | |
return 'removeRootAttribute'; | |
} else { | |
return 'changeRootAttribute'; | |
} | |
} | |
/** | |
* Creates and returns an operation that has the same parameters as this operation. | |
* | |
* @returns {module:engine/model/operation/rootattributeoperation~RootAttributeOperation} Clone of this operation. | |
*/ | |
clone() { | |
return new rootattributeoperation_RootAttributeOperation( this.root, this.key, this.oldValue, this.newValue, this.baseVersion ); | |
} | |
/** | |
* See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}. | |
* | |
* @returns {module:engine/model/operation/rootattributeoperation~RootAttributeOperation} | |
*/ | |
getReversed() { | |
return new rootattributeoperation_RootAttributeOperation( this.root, this.key, this.newValue, this.oldValue, this.baseVersion + 1 ); | |
} | |
_execute() { | |
if ( this.oldValue !== null && this.root.getAttribute( this.key ) !== this.oldValue ) { | |
/** | |
* The attribute which should be removed does not exists for the given node. | |
* | |
* @error rootattribute-operation-wrong-old-value | |
* @param {module:engine/model/rootelement~RootElement} root | |
* @param {String} key | |
* @param {*} value | |
*/ | |
throw new CKEditorError( | |
'rootattribute-operation-wrong-old-value: Changed node has different attribute value than operation\'s ' + | |
'old attribute value.', | |
{ root: this.root, key: this.key } | |
); | |
} | |
if ( this.oldValue === null && this.newValue !== null && this.root.hasAttribute( this.key ) ) { | |
/** | |
* The attribute with given key already exists for the given node. | |
* | |
* @error rootattribute-operation-attribute-exists | |
* @param {module:engine/model/rootelement~RootElement} root | |
* @param {String} key | |
*/ | |
throw new CKEditorError( | |
'rootattribute-operation-attribute-exists: The attribute with given key already exists.', | |
{ root: this.root, key: this.key } | |
); | |
} | |
if ( this.newValue !== null ) { | |
this.root.setAttribute( this.key, this.newValue ); | |
} else { | |
this.root.removeAttribute( this.key ); | |
} | |
return { root: this.root, key: this.key, oldValue: this.oldValue, newValue: this.newValue }; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
static get className() { | |
return 'engine.model.operation.RootAttributeOperation'; | |
} | |
/** | |
* Creates RootAttributeOperation object from deserilized object, i.e. from parsed JSON string. | |
* | |
* @param {Object} json Deserialized JSON object. | |
* @param {module:engine/model/document~Document} document Document on which this operation will be applied. | |
* @returns {module:engine/model/operation/rootattributeoperation~RootAttributeOperation} | |
*/ | |
static fromJSON( json, document ) { | |
if ( !document.hasRoot( json.root ) ) { | |
/** | |
* Cannot create RootAttributeOperation for document. Root with specified name does not exist. | |
* | |
* @error rootattributeoperation-fromjson-no-root | |
* @param {String} rootName | |
*/ | |
throw new CKEditorError( | |
'rootattribute-operation-fromjson-no-root: Cannot create RootAttributeOperation. Root with specified name does not exist.', | |
{ rootName: json } | |
); | |
} | |
return new rootattributeoperation_RootAttributeOperation( document.getRoot( json.root ), json.key, json.oldValue, json.newValue, json.baseVersion ); | |
} | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/operationfactory.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/model/operation/operationfactory | |
*/ | |
const operationfactory_operations = {}; | |
operationfactory_operations[ attributeoperation_AttributeOperation.className ] = attributeoperation_AttributeOperation; | |
operationfactory_operations[ insertoperation_InsertOperation.className ] = insertoperation_InsertOperation; | |
operationfactory_operations[ markeroperation_MarkerOperation.className ] = markeroperation_MarkerOperation; | |
operationfactory_operations[ moveoperation_MoveOperation.className ] = moveoperation_MoveOperation; | |
operationfactory_operations[ NoOperation.className ] = NoOperation; | |
operationfactory_operations[ operation_Operation.className ] = operation_Operation; | |
operationfactory_operations[ reinsertoperation_ReinsertOperation.className ] = reinsertoperation_ReinsertOperation; | |
operationfactory_operations[ removeoperation_RemoveOperation.className ] = removeoperation_RemoveOperation; | |
operationfactory_operations[ renameoperation_RenameOperation.className ] = renameoperation_RenameOperation; | |
operationfactory_operations[ rootattributeoperation_RootAttributeOperation.className ] = rootattributeoperation_RootAttributeOperation; | |
/** | |
* A factory class for creating operations. | |
* | |
* @abstract | |
*/ | |
class OperationFactory { | |
/** | |
* Creates concrete `Operation` object from deserilized object, i.e. from parsed JSON string. | |
* | |
* @param {Object} json Deserialized JSON object. | |
* @param {module:engine/model/document~Document} document Document on which this operation will be applied. | |
* @returns {module:engine/model/operation/operation~Operation} | |
*/ | |
static fromJSON( json, document ) { | |
return operationfactory_operations[ json.__className ].fromJSON( json, document ); | |
} | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/delta/deltafactory.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/model/delta/deltafactory | |
*/ | |
const deserializers = new Map(); | |
/** | |
* A factory class for creating operations. | |
* | |
* Delta is a single, from the user action point of view, change in the editable document, like insert, split or | |
* rename element. Delta is composed of operations, which are unit changes needed to be done to execute user action. | |
* | |
* Multiple deltas are grouped into a single {@link module:engine/model/batch~Batch}. | |
*/ | |
class deltafactory_DeltaFactory { | |
/** | |
* Creates InsertDelta from deserialized object, i.e. from parsed JSON string. | |
* | |
* @param {Object} json | |
* @param {module:engine/model/document~Document} doc Document on which this delta will be applied. | |
* @returns {module:engine/model/delta/insertdelta~InsertDelta} | |
*/ | |
static fromJSON( json, doc ) { | |
if ( !deserializers.has( json.__className ) ) { | |
/** | |
* This delta has no defined deserializer. | |
* | |
* @error delta-fromjson-no-deserializer | |
* @param {String} name | |
*/ | |
throw new CKEditorError( | |
'delta-fromjson-no-deserializer: This delta has no defined deserializer', | |
{ name: json.__className } | |
); | |
} | |
const Delta = deserializers.get( json.__className ); | |
const delta = new Delta(); | |
for ( const operation of json.operations ) { | |
delta.addOperation( OperationFactory.fromJSON( operation, doc ) ); | |
} | |
// Rewrite all other properties. | |
for ( const prop in json ) { | |
if ( prop != '__className' && delta[ prop ] === undefined ) { | |
delta[ prop ] = json[ prop ]; | |
} | |
} | |
return delta; | |
} | |
/** | |
* Registers a class for delta factory. | |
* | |
* @param {Function} Delta A delta class to register. | |
*/ | |
static register( Delta ) { | |
deserializers.set( Delta.className, Delta ); | |
} | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/delta/delta.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/model/delta/delta | |
*/ | |
/** | |
* Base class for all deltas. | |
* | |
* Delta is a single, from the user action point of view, change in the editable document, like insert, split or | |
* rename element. Delta is composed of operations, which are unit changes needed to be done to execute user action. | |
* | |
* Multiple deltas are grouped into a single {@link module:engine/model/batch~Batch}. | |
*/ | |
class delta_Delta { | |
/** | |
* Creates a delta instance. | |
*/ | |
constructor() { | |
/** | |
* {@link module:engine/model/batch~Batch} which delta is a part of. This property is null by default and set by the | |
* {@link module:engine/model/batch~Batch#addDelta} method. | |
* | |
* @readonly | |
* @member {module:engine/model/batch~Batch} module:engine/model/delta/delta~Delta#batch | |
*/ | |
this.batch = null; | |
/** | |
* Array of operations which compose delta. | |
* | |
* @readonly | |
* @member {module:engine/model/operation/operation~Operation[]} module:engine/model/delta/delta~Delta#operations | |
*/ | |
this.operations = []; | |
} | |
/** | |
* Returns delta base version which is equal to the base version of the first operation in delta. If there | |
* are no operations in delta, returns `null`. | |
* | |
* @see module:engine/model/document~Document | |
* @type {Number|null} | |
*/ | |
get baseVersion() { | |
if ( this.operations.length > 0 ) { | |
return this.operations[ 0 ].baseVersion; | |
} | |
return null; | |
} | |
/** | |
* @param {Number} baseVersion | |
*/ | |
set baseVersion( baseVersion ) { | |
for ( const operation of this.operations ) { | |
operation.baseVersion = baseVersion++; | |
} | |
} | |
/** | |
* A class that will be used when creating reversed delta. | |
* | |
* @private | |
* @type {Function} | |
*/ | |
get _reverseDeltaClass() { | |
return delta_Delta; | |
} | |
/** | |
* Delta type. | |
* | |
* @readonly | |
* @member {String} #type | |
*/ | |
/** | |
* Add operation to the delta. | |
* | |
* @param {module:engine/model/operation/operation~Operation} operation Operation instance. | |
*/ | |
addOperation( operation ) { | |
operation.delta = this; | |
this.operations.push( operation ); | |
return operation; | |
} | |
/** | |
* Creates and returns a delta that has the same parameters as this delta. | |
* | |
* @returns {module:engine/model/delta/delta~Delta} Clone of this delta. | |
*/ | |
clone() { | |
const delta = new this.constructor(); | |
for ( const op of this.operations ) { | |
delta.addOperation( op.clone() ); | |
} | |
return delta; | |
} | |
/** | |
* Creates and returns a reverse delta. Reverse delta when executed right after the original delta will bring back | |
* tree model state to the point before the original delta execution. In other words, it reverses changes done | |
* by the original delta. | |
* | |
* Keep in mind that tree model state may change since executing the original delta, so reverse delta may be "outdated". | |
* In that case you will need to {@link module:engine/model/delta/transform~transform} it by all deltas that were executed after | |
* the original delta. | |
* | |
* @returns {module:engine/model/delta/delta~Delta} Reversed delta. | |
*/ | |
getReversed() { | |
const delta = new this._reverseDeltaClass(); | |
for ( const op of this.operations ) { | |
delta.addOperation( op.getReversed() ); | |
} | |
delta.operations.reverse(); | |
for ( let i = 0; i < delta.operations.length; i++ ) { | |
delta.operations[ i ].baseVersion = this.operations[ this.operations.length - 1 ].baseVersion + i + 1; | |
} | |
return delta; | |
} | |
/** | |
* Custom toJSON method to make deltas serializable. | |
* | |
* @returns {Object} Clone of this delta with added class name. | |
*/ | |
toJSON() { | |
const json = lodash_clone( this ); | |
json.__className = this.constructor.className; | |
// Remove parent batch to avoid circular dependencies. | |
delete json.batch; | |
return json; | |
} | |
/** | |
* Delta class name. Used by {@link #toJSON} method for serialization and | |
* {@link module:engine/model/delta/deltafactory~DeltaFactory.fromJSON} during deserialization. | |
* | |
* @type {String} | |
* @readonly | |
*/ | |
static get className() { | |
return 'engine.model.delta.Delta'; | |
} | |
} | |
deltafactory_DeltaFactory.register( delta_Delta ); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/batch.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/model/batch | |
*/ | |
/** | |
* `Batch` instance groups document changes ({@link module:engine/model/delta/delta~Delta deltas}). All deltas grouped in a single `Batch` | |
* can be reverted together, so you can think about `Batch` as of a single undo step. If you want to extend given undo step you | |
* can call another method on the same `Batch` object. If you want to create a separate undo step you can create a new `Batch`. | |
* | |
* For example to create two separate undo steps you can call: | |
* | |
* doc.batch().insert( firstPosition, 'foo' ); | |
* doc.batch().insert( secondPosition, 'bar' ); | |
* | |
* To create a single undo step: | |
* | |
* const batch = doc.batch(); | |
* batch.insert( firstPosition, 'foo' ); | |
* batch.insert( secondPosition, 'bar' ); | |
* | |
* Note that all document modification methods (insert, remove, split, etc.) are chainable so you can shorten code to: | |
* | |
* doc.batch().insert( firstPosition, 'foo' ).insert( secondPosition, 'bar' ); | |
*/ | |
class Batch { | |
/** | |
* Creates `Batch` instance. Not recommended to use directly, use {@link module:engine/model/document~Document#batch} instead. | |
* | |
* @param {module:engine/model/document~Document} document Document which this Batch changes. | |
* @param {'transparent'|'default'} [type='default'] Type of the batch. | |
*/ | |
constructor( document, type = 'default' ) { | |
/** | |
* Document which this batch changes. | |
* | |
* @readonly | |
* @member {module:engine/model/document~Document} module:engine/model/batch~Batch#document | |
*/ | |
this.document = document; | |
/** | |
* Array of deltas which compose this batch. | |
* | |
* @readonly | |
* @member {Array.<module:engine/model/delta/delta~Delta>} module:engine/model/batch~Batch#deltas | |
*/ | |
this.deltas = []; | |
/** | |
* Type of the batch. | |
* | |
* Can be one of the following values: | |
* * `'default'` - all "normal" batches, most commonly used type. | |
* * `'transparent'` - batch that should be ignored by other features, i.e. initial batch or collaborative editing changes. | |
* | |
* @readonly | |
* @member {'transparent'|'default'} module:engine/model/batch~Batch#type | |
*/ | |
this.type = type; | |
} | |
/** | |
* Returns this batch base version, which is equal to the base version of first delta in the batch. | |
* If there are no deltas in the batch, it returns `null`. | |
* | |
* @readonly | |
* @type {Number|null} | |
*/ | |
get baseVersion() { | |
return this.deltas.length > 0 ? this.deltas[ 0 ].baseVersion : null; | |
} | |
/** | |
* Adds delta to the batch instance. All modification methods (insert, remove, split, etc.) use this method | |
* to add created deltas. | |
* | |
* @param {module:engine/model/delta/delta~Delta} delta Delta to add. | |
* @return {module:engine/model/delta/delta~Delta} Added delta. | |
*/ | |
addDelta( delta ) { | |
delta.batch = this; | |
this.deltas.push( delta ); | |
return delta; | |
} | |
/** | |
* Gets an iterable collection of operations. | |
* | |
* @returns {Iterable.<module:engine/model/operation/operation~Operation>} | |
*/ | |
* getOperations() { | |
for ( const delta of this.deltas ) { | |
yield* delta.operations; | |
} | |
} | |
} | |
/** | |
* Function to register batch methods. To make code scalable `Batch` do not have modification | |
* methods built in. They can be registered using this method. | |
* | |
* This method checks if there is no naming collision and throws `batch-register-taken` if the method name | |
* is already taken. | |
* | |
* Besides that no magic happens here, the method is added to the `Batch` class prototype. | |
* | |
* For example: | |
* | |
* Batch.register( 'insert', function( position, nodes ) { | |
* // You can use a class inheriting from `Delta` if that class should handle OT in a special way. | |
* const delta = new Delta(); | |
* | |
* // Add delta to the Batch instance. It is important to add a delta to the batch before applying any operation. | |
* this.addDelta( delta ); | |
* | |
* // Create operations which should be components of this delta. | |
* const operation = new InsertOperation( position, nodes, this.document.version ); | |
* | |
* // Add operation to the delta. It is important to add operation before applying it. | |
* delta.addOperation( operation ); | |
* | |
* // Remember to apply every operation, no magic, you need to do it manually. | |
* this.document.applyOperation( operation ); | |
* | |
* // Make this method chainable. | |
* return this; | |
* } ); | |
* | |
* @method module:engine/model/batch~Batch.register | |
* @param {String} name Method name. | |
* @param {Function} creator Method body. | |
*/ | |
function register( name, creator ) { | |
if ( Batch.prototype[ name ] ) { | |
/** | |
* This batch method name is already taken. | |
* | |
* @error batch-register-taken | |
* @param {String} name | |
*/ | |
throw new CKEditorError( | |
'model-batch-register-taken: This batch method name is already taken.', | |
{ name } ); | |
} | |
Batch.prototype[ name ] = creator; | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/delta/attributedelta.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/model/delta/attributedelta | |
*/ | |
/** | |
* To provide specific OT behavior and better collisions solving, methods to change attributes | |
* ({@link module:engine/model/batch~Batch#setAttribute} and {@link module:engine/model/batch~Batch#removeAttribute}) | |
* use `AttributeDelta` class which inherits from the `Delta` class and may overwrite some methods. | |
* | |
* @extends module:engine/model/delta/delta~Delta | |
*/ | |
class attributedelta_AttributeDelta extends delta_Delta { | |
/** | |
* @inheritDoc | |
*/ | |
get type() { | |
return 'attribute'; | |
} | |
/** | |
* The attribute key that is changed by the delta or `null` if the delta has no operations. | |
* | |
* @readonly | |
* @type {String|null} | |
*/ | |
get key() { | |
return this.operations[ 0 ] ? this.operations[ 0 ].key : null; | |
} | |
/** | |
* The attribute value that is set by the delta or `null` if the delta has no operations. | |
* | |
* @readonly | |
* @type {*|null} | |
*/ | |
get value() { | |
return this.operations[ 0 ] ? this.operations[ 0 ].newValue : null; | |
} | |
/** | |
* The range on which delta operates or `null` if the delta has no operations. | |
* | |
* @readonly | |
* @type {module:engine/model/range~Range|null} | |
*/ | |
get range() { | |
// Check if it is cached. | |
if ( this._range ) { | |
return this._range; | |
} | |
let start = null; | |
let end = null; | |
for ( const operation of this.operations ) { | |
if ( operation instanceof NoOperation ) { | |
continue; | |
} | |
if ( start === null || start.isAfter( operation.range.start ) ) { | |
start = operation.range.start; | |
} | |
if ( end === null || end.isBefore( operation.range.end ) ) { | |
end = operation.range.end; | |
} | |
} | |
if ( start && end ) { | |
this._range = new range_Range( start, end ); | |
return this._range; | |
} | |
return null; | |
} | |
get _reverseDeltaClass() { | |
return attributedelta_AttributeDelta; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
toJSON() { | |
const json = super.toJSON(); | |
delete json._range; | |
return json; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
static get className() { | |
return 'engine.model.delta.AttributeDelta'; | |
} | |
} | |
/** | |
* To provide specific OT behavior and better collisions solving, methods to change attributes | |
* ({@link module:engine/model/batch~Batch#setAttribute} and {@link module:engine/model/batch~Batch#removeAttribute}) | |
* use `RootAttributeDelta` class which inherits from the `Delta` class and may | |
* overwrite some methods. | |
* | |
* @extends module:engine/model/delta/delta~Delta | |
*/ | |
class RootAttributeDelta extends delta_Delta { | |
/** | |
* @inheritDoc | |
*/ | |
static get className() { | |
return 'engine.model.delta.RootAttributeDelta'; | |
} | |
} | |
/** | |
* Sets value of the attribute with given key on a {@link module:engine/model/item~Item model item} | |
* or on a {@link module:engine/model/range~Range range}. | |
* | |
* @chainable | |
* @method module:engine/model/batch~Batch#setAttribute | |
* @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange | |
* Model item or range on which the attribute will be set. | |
* @param {String} key Attribute key. | |
* @param {*} value Attribute new value. | |
*/ | |
register( 'setAttribute', function( itemOrRange, key, value ) { | |
attributedelta_attribute( this, key, value, itemOrRange ); | |
return this; | |
} ); | |
/** | |
* Removes an attribute with given key from a {@link module:engine/model/item~Item model item} | |
* or from a {@link module:engine/model/range~Range range}. | |
* | |
* @chainable | |
* @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange | |
* Model item or range from which the attribute will be removed. | |
* @method module:engine/model/batch~Batch#removeAttribute | |
* @param {String} key Attribute key. | |
*/ | |
register( 'removeAttribute', function( itemOrRange, key ) { | |
attributedelta_attribute( this, key, null, itemOrRange ); | |
return this; | |
} ); | |
function attributedelta_attribute( batch, key, value, itemOrRange ) { | |
if ( itemOrRange instanceof range_Range ) { | |
changeRange( batch, batch.document, key, value, itemOrRange ); | |
} else { | |
changeItem( batch, batch.document, key, value, itemOrRange ); | |
} | |
} | |
function changeItem( batch, doc, key, value, item ) { | |
const previousValue = item.getAttribute( key ); | |
let range, operation; | |
const delta = item.is( 'rootElement' ) ? new RootAttributeDelta() : new attributedelta_AttributeDelta(); | |
if ( previousValue != value ) { | |
batch.addDelta( delta ); | |
if ( item.is( 'rootElement' ) ) { | |
// If we change attributes of root element, we have to use `RootAttributeOperation`. | |
operation = new rootattributeoperation_RootAttributeOperation( item, key, previousValue, value, doc.version ); | |
} else { | |
if ( item.is( 'element' ) ) { | |
// If we change the attribute of the element, we do not want to change attributes of its children, so | |
// the end of the range cannot be after the closing tag, it should be inside that element, before any of | |
// it's children, so the range will contain only the opening tag. | |
range = new range_Range( position_Position.createBefore( item ), position_Position.createFromParentAndOffset( item, 0 ) ); | |
} else { | |
// If `item` is text proxy, we create a range from the beginning to the end of that text proxy, to change | |
// all characters represented by it. | |
range = new range_Range( position_Position.createBefore( item ), position_Position.createAfter( item ) ); | |
} | |
operation = new attributeoperation_AttributeOperation( range, key, previousValue, value, doc.version ); | |
} | |
delta.addOperation( operation ); | |
doc.applyOperation( operation ); | |
} | |
} | |
// Because attribute operation needs to have the same attribute value on the whole range, this function splits the range | |
// into smaller parts. | |
function changeRange( batch, doc, attributeKey, attributeValue, range ) { | |
const delta = new attributedelta_AttributeDelta(); | |
// Position of the last split, the beginning of the new range. | |
let lastSplitPosition = range.start; | |
// Currently position in the scanning range. Because we need value after the position, it is not a current | |
// position of the iterator but the previous one (we need to iterate one more time to get the value after). | |
let position, | |
// Value before the currently position. | |
attributeValueBefore, | |
// Value after the currently position. | |
attributeValueAfter; | |
for ( const value of range ) { | |
attributeValueAfter = value.item.getAttribute( attributeKey ); | |
// At the first run of the iterator the position in undefined. We also do not have a attributeValueBefore, but | |
// because attributeValueAfter may be null, attributeValueBefore may be equal attributeValueAfter ( undefined == null ). | |
if ( position && attributeValueBefore != attributeValueAfter ) { | |
// if attributeValueBefore == attributeValue there is nothing to change, so we add operation only if these values are different. | |
if ( attributeValueBefore != attributeValue ) { | |
addOperation(); | |
} | |
lastSplitPosition = position; | |
} | |
position = value.nextPosition; | |
attributeValueBefore = attributeValueAfter; | |
} | |
// Because position in the loop is not the iterator position (see let position comment), the last position in | |
// the while loop will be last but one position in the range. We need to check the last position manually. | |
if ( position instanceof position_Position && position != lastSplitPosition && attributeValueBefore != attributeValue ) { | |
addOperation(); | |
} | |
function addOperation() { | |
// Add delta to the batch only if there is at least operation in the delta. Add delta only once. | |
if ( delta.operations.length === 0 ) { | |
batch.addDelta( delta ); | |
} | |
const range = new range_Range( lastSplitPosition, position ); | |
const operation = new attributeoperation_AttributeOperation( range, attributeKey, attributeValueBefore, attributeValue, doc.version ); | |
delta.addOperation( operation ); | |
doc.applyOperation( operation ); | |
} | |
} | |
deltafactory_DeltaFactory.register( attributedelta_AttributeDelta ); | |
deltafactory_DeltaFactory.register( RootAttributeDelta ); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/delta/movedelta.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/model/delta/movedelta | |
*/ | |
/** | |
* To provide specific OT behavior and better collisions solving, {@link module:engine/model/batch~Batch#move} method | |
* uses the `MoveDelta` class which inherits from the `Delta` class and may overwrite some methods. | |
* | |
* @extends module:engine/model/delta/delta~Delta | |
*/ | |
class MoveDelta extends delta_Delta { | |
/** | |
* @inheritDoc | |
*/ | |
get type() { | |
return 'move'; | |
} | |
/** | |
* Offset size of moved range or `null` if there are no operations in the delta. | |
* | |
* @type {Number|null} | |
*/ | |
get howMany() { | |
return this._moveOperation ? this._moveOperation.howMany : null; | |
} | |
/** | |
* {@link module:engine/model/delta/movedelta~MoveDelta#_moveOperation Move operation} | |
* {@link module:engine/model/operation/moveoperation~MoveOperation#sourcePosition source position} or `null` if there are | |
* no operations in the delta. | |
* | |
* @type {module:engine/model/position~Position|null} | |
*/ | |
get sourcePosition() { | |
return this._moveOperation ? this._moveOperation.sourcePosition : null; | |
} | |
/** | |
* {@link module:engine/model/delta/movedelta~MoveDelta#_moveOperation Move operation} | |
* {@link module:engine/model/operation/moveoperation~MoveOperation#targetPosition target position} or `null` if there are | |
* no operations in the delta. | |
* | |
* @type {module:engine/model/position~Position|null} | |
*/ | |
get targetPosition() { | |
return this._moveOperation ? this._moveOperation.targetPosition : null; | |
} | |
/** | |
* {@link module:engine/model/delta/movedelta~MoveDelta#_moveOperation Move operation} that is saved in this delta or `null` | |
* if there are no operations in the delta. | |
* | |
* @protected | |
* @type {module:engine/model/operation/moveoperation~MoveOperation|null} | |
*/ | |
get _moveOperation() { | |
return this.operations[ 0 ] || null; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
get _reverseDeltaClass() { | |
return MoveDelta; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
static get className() { | |
return 'engine.model.delta.MoveDelta'; | |
} | |
} | |
function addMoveOperation( batch, delta, sourcePosition, howMany, targetPosition ) { | |
const operation = new moveoperation_MoveOperation( sourcePosition, howMany, targetPosition, batch.document.version ); | |
delta.addOperation( operation ); | |
batch.document.applyOperation( operation ); | |
} | |
/** | |
* Moves given {@link module:engine/model/item~Item model item} or given range to target position. | |
* | |
* @chainable | |
* @method module:engine/model/batch~Batch#move | |
* @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange Model item or range of nodes to move. | |
* @param {module:engine/model/position~Position} targetPosition Position where moved nodes will be inserted. | |
*/ | |
register( 'move', function( itemOrRange, targetPosition ) { | |
const delta = new MoveDelta(); | |
this.addDelta( delta ); | |
if ( itemOrRange instanceof range_Range ) { | |
if ( !itemOrRange.isFlat ) { | |
/** | |
* Range to move is not flat. | |
* | |
* @error batch-move-range-not-flat | |
*/ | |
throw new CKEditorError( 'batch-move-range-not-flat: Range to move is not flat.' ); | |
} | |
addMoveOperation( this, delta, itemOrRange.start, itemOrRange.end.offset - itemOrRange.start.offset, targetPosition ); | |
} else { | |
addMoveOperation( this, delta, position_Position.createBefore( itemOrRange ), 1, targetPosition ); | |
} | |
return this; | |
} ); | |
deltafactory_DeltaFactory.register( MoveDelta ); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/delta/removedelta.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/model/delta/removedelta | |
*/ | |
/** | |
* To provide specific OT behavior and better collisions solving, {@link module:engine/model/batch~Batch#remove} method | |
* uses the `RemoveDelta` class which inherits from the `Delta` class and may overwrite some methods. | |
* | |
* @extends module:engine/model/delta/delta~Delta | |
*/ | |
class RemoveDelta extends MoveDelta { | |
/** | |
* @inheritDoc | |
*/ | |
static get className() { | |
return 'engine.model.delta.RemoveDelta'; | |
} | |
} | |
function addRemoveDelta( batch, position, howMany ) { | |
const delta = new RemoveDelta(); | |
batch.addDelta( delta ); | |
const graveyard = batch.document.graveyard; | |
const gyPosition = new position_Position( graveyard, [ 0 ] ); | |
const operation = new removeoperation_RemoveOperation( position, howMany, gyPosition, batch.document.version ); | |
delta.addOperation( operation ); | |
batch.document.applyOperation( operation ); | |
} | |
/** | |
* Removes given {@link module:engine/model/item~Item model item} or given range. | |
* | |
* @chainable | |
* @method module:engine/model/batch~Batch#remove | |
* @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange Model item or range to remove. | |
*/ | |
register( 'remove', function( itemOrRange ) { | |
if ( itemOrRange instanceof range_Range ) { | |
// The array is reversed, so the ranges to remove are in correct order and do not have to be updated. | |
const ranges = itemOrRange.getMinimalFlatRanges().reverse(); | |
for ( const flat of ranges ) { | |
addRemoveDelta( this, flat.start, flat.end.offset - flat.start.offset ); | |
} | |
} else { | |
addRemoveDelta( this, position_Position.createBefore( itemOrRange ), 1 ); | |
} | |
return this; | |
} ); | |
deltafactory_DeltaFactory.register( RemoveDelta ); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/delta/insertdelta.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/model/delta/insertdelta | |
*/ | |
/** | |
* To provide specific OT behavior and better collisions solving, the {@link module:engine/model/batch~Batch#insert Batch#insert} method | |
* uses the `InsertDelta` class which inherits from the `Delta` class and may overwrite some methods. | |
* | |
* @extends module:engine/model/delta/delta~Delta | |
*/ | |
class insertdelta_InsertDelta extends delta_Delta { | |
/** | |
* @inheritDoc | |
*/ | |
get type() { | |
return 'insert'; | |
} | |
/** | |
* Position where the delta inserts nodes or `null` if there are no operations in the delta. | |
* | |
* @readonly | |
* @type {module:engine/model/position~Position|null} | |
*/ | |
get position() { | |
return this._insertOperation ? this._insertOperation.position : null; | |
} | |
/** | |
* Node list containing all the nodes inserted by the delta or `null` if there are no operations in the delta. | |
* | |
* @readonly | |
* @type {module:engine/model/nodelist~NodeList|null} | |
*/ | |
get nodes() { | |
return this._insertOperation ? this._insertOperation.nodes : null; | |
} | |
/** | |
* Insert operation that is saved in this delta or `null` if there are no operations in the delta. | |
* | |
* @readonly | |
* @protected | |
* @type {module:engine/model/operation/insertoperation~InsertOperation|null} | |
*/ | |
get _insertOperation() { | |
return this.operations[ 0 ] || null; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
get _reverseDeltaClass() { | |
return RemoveDelta; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
static get className() { | |
return 'engine.model.delta.InsertDelta'; | |
} | |
} | |
/** | |
* Inserts a node or nodes at the given position. | |
* | |
* When inserted element is a {@link module:engine/model/documentfragment~DocumentFragment} and has markers its markers will | |
* be set to {@link module:engine/model/document~Document#markers}. | |
* | |
* @chainable | |
* @method module:engine/model/batch~Batch#insert | |
* @param {module:engine/model/position~Position} position Position of insertion. | |
* @param {module:engine/model/node~NodeSet} nodes The list of nodes to be inserted. | |
*/ | |
register( 'insert', function( position, nodes ) { | |
const normalizedNodes = normalizeNodes( nodes ); | |
// If nothing is inserted do not create delta and operation. | |
if ( normalizedNodes.length === 0 ) { | |
return this; | |
} | |
const delta = new insertdelta_InsertDelta(); | |
const insert = new insertoperation_InsertOperation( position, normalizedNodes, this.document.version ); | |
this.addDelta( delta ); | |
delta.addOperation( insert ); | |
this.document.applyOperation( insert ); | |
// When element is a DocumentFragment we need to move its markers to Document#markers. | |
if ( nodes instanceof documentfragment_DocumentFragment ) { | |
for ( const [ markerName, markerRange ] of nodes.markers ) { | |
// We need to migrate marker range from DocumentFragment to Document. | |
const rangeRootPosition = position_Position.createAt( markerRange.root ); | |
const range = new range_Range( | |
markerRange.start._getCombined( rangeRootPosition, position ), | |
markerRange.end._getCombined( rangeRootPosition, position ) | |
); | |
this.setMarker( markerName, range ); | |
} | |
} | |
return this; | |
} ); | |
deltafactory_DeltaFactory.register( insertdelta_InsertDelta ); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/delta/splitdelta.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/model/delta/splitdelta | |
*/ | |
/** | |
* To provide specific OT behavior and better collisions solving, the {@link module:engine/model/batch~Batch#split} method | |
* uses `SplitDelta` class which inherits from the `Delta` class and may overwrite some methods. | |
* | |
* @extends module:engine/model/delta/delta~Delta | |
*/ | |
class splitdelta_SplitDelta extends delta_Delta { | |
/** | |
* @inheritDoc | |
*/ | |
get type() { | |
return 'split'; | |
} | |
/** | |
* Position of split or `null` if there are no operations in the delta. | |
* | |
* @type {module:engine/model/position~Position|null} | |
*/ | |
get position() { | |
return this._moveOperation ? this._moveOperation.sourcePosition : null; | |
} | |
/** | |
* Operation in the delta that adds to model an element into which split nodes will be moved, or `null` if | |
* there are no operations in the delta. | |
* | |
* Most commonly this will be {@link module:engine/model/operation/insertoperation~InsertOperation an insert operation}, | |
* as `SplitDelta` has to create a new node. If `SplitDelta` was created through | |
* {@link module:engine/model/delta/delta~Delta#getReversed reversing} | |
* a {@link module:engine/model/delta/mergedelta~MergeDelta merge delta}, | |
* this will be a {@link module:engine/model/operation/reinsertoperation~ReinsertOperation reinsert operation}, | |
* as we will want to re-insert the exact element that was removed by that merge delta. | |
* | |
* @protected | |
* @type {module:engine/model/operation/insertoperation~InsertOperation| | |
* module:engine/model/operation/reinsertoperation~ReinsertOperation|null} | |
*/ | |
get _cloneOperation() { | |
return this.operations[ 0 ] || null; | |
} | |
/** | |
* Operation in the delta that moves model items, that are after split position, to their new parent or `null` | |
* if there are no operations in the delta. | |
* | |
* @protected | |
* @type {module:engine/model/operation/moveoperation~MoveOperation|null} | |
*/ | |
get _moveOperation() { | |
return this.operations[ 1 ] && this.operations[ 1 ] instanceof moveoperation_MoveOperation ? this.operations[ 1 ] : null; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
get _reverseDeltaClass() { | |
return mergedelta_MergeDelta; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
static get className() { | |
return 'engine.model.delta.SplitDelta'; | |
} | |
} | |
/** | |
* Splits an element at the given position. | |
* | |
* The element cannot be a root element, as root element cannot be split. The `batch-split-root` error will be thrown if | |
* you try to split the root element. | |
* | |
* @chainable | |
* @method module:engine/model/batch~Batch#split | |
* @param {module:engine/model/position~Position} position Position of split. | |
*/ | |
register( 'split', function( position ) { | |
const delta = new splitdelta_SplitDelta(); | |
this.addDelta( delta ); | |
const splitElement = position.parent; | |
if ( !splitElement.parent ) { | |
/** | |
* Root element can not be split. | |
* | |
* @error batch-split-root | |
*/ | |
throw new CKEditorError( 'batch-split-root: Root element can not be split.' ); | |
} | |
const copy = new element_Element( splitElement.name, splitElement.getAttributes() ); | |
const insert = new insertoperation_InsertOperation( | |
position_Position.createAfter( splitElement ), | |
copy, | |
this.document.version | |
); | |
delta.addOperation( insert ); | |
this.document.applyOperation( insert ); | |
const move = new moveoperation_MoveOperation( | |
position, | |
splitElement.maxOffset - position.offset, | |
position_Position.createFromParentAndOffset( copy, 0 ), | |
this.document.version | |
); | |
move.isSticky = true; | |
delta.addOperation( move ); | |
this.document.applyOperation( move ); | |
return this; | |
} ); | |
deltafactory_DeltaFactory.register( splitdelta_SplitDelta ); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/delta/mergedelta.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/model/delta/mergedelta | |
*/ | |
/** | |
* To provide specific OT behavior and better collisions solving, {@link module:engine/model/batch~Batch#merge} method | |
* uses the `MergeDelta` class which inherits from the `Delta` class and may overwrite some methods. | |
* | |
* @extends module:engine/model/delta/delta~Delta | |
*/ | |
class mergedelta_MergeDelta extends delta_Delta { | |
/** | |
* @inheritDoc | |
*/ | |
get type() { | |
return 'merge'; | |
} | |
/** | |
* Position between to merged nodes or `null` if the delta has no operations. | |
* | |
* @readonly | |
* @type {module:engine/model/position~Position|null} | |
*/ | |
get position() { | |
return this._removeOperation ? this._removeOperation.sourcePosition : null; | |
} | |
/** | |
* Operation in this delta that removes the node after merge position (which will be empty at that point) or | |
* `null` if the delta has no operations. Note, that after {@link module:engine/model/delta/transform~transform transformation} | |
* this might be an instance of {@link module:engine/model/operation/moveoperation~MoveOperation} instead of | |
* {@link module:engine/model/operation/removeoperation~RemoveOperation}. | |
* | |
* @readonly | |
* @protected | |
* @type {module:engine/model/operation/moveoperation~MoveOperation|null} | |
*/ | |
get _removeOperation() { | |
return this.operations[ 1 ] || null; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
get _reverseDeltaClass() { | |
return splitdelta_SplitDelta; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
static get className() { | |
return 'engine.model.delta.MergeDelta'; | |
} | |
} | |
/** | |
* Merges two siblings at the given position. | |
* | |
* Node before and after the position have to be an element. Otherwise `batch-merge-no-element-before` or | |
* `batch-merge-no-element-after` error will be thrown. | |
* | |
* @chainable | |
* @method module:engine/model/batch~Batch#merge | |
* @param {module:engine/model/position~Position} position Position of merge. | |
*/ | |
register( 'merge', function( position ) { | |
const delta = new mergedelta_MergeDelta(); | |
this.addDelta( delta ); | |
const nodeBefore = position.nodeBefore; | |
const nodeAfter = position.nodeAfter; | |
if ( !( nodeBefore instanceof element_Element ) ) { | |
/** | |
* Node before merge position must be an element. | |
* | |
* @error batch-merge-no-element-before | |
*/ | |
throw new CKEditorError( 'batch-merge-no-element-before: Node before merge position must be an element.' ); | |
} | |
if ( !( nodeAfter instanceof element_Element ) ) { | |
/** | |
* Node after merge position must be an element. | |
* | |
* @error batch-merge-no-element-after | |
*/ | |
throw new CKEditorError( 'batch-merge-no-element-after: Node after merge position must be an element.' ); | |
} | |
const positionAfter = position_Position.createFromParentAndOffset( nodeAfter, 0 ); | |
const positionBefore = position_Position.createFromParentAndOffset( nodeBefore, nodeBefore.maxOffset ); | |
const move = new moveoperation_MoveOperation( | |
positionAfter, | |
nodeAfter.maxOffset, | |
positionBefore, | |
this.document.version | |
); | |
move.isSticky = true; | |
delta.addOperation( move ); | |
this.document.applyOperation( move ); | |
const graveyard = this.document.graveyard; | |
const gyPosition = new position_Position( graveyard, [ 0 ] ); | |
const remove = new removeoperation_RemoveOperation( position, 1, gyPosition, this.document.version ); | |
delta.addOperation( remove ); | |
this.document.applyOperation( remove ); | |
return this; | |
} ); | |
deltafactory_DeltaFactory.register( mergedelta_MergeDelta ); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/delta/renamedelta.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/model/delta/renamedelta | |
*/ | |
/** | |
* To provide specific OT behavior and better collisions solving, the {@link module:engine/model/batch~Batch#rename Batch#rename} method | |
* uses the `RenameDelta` class which inherits from the `Delta` class and may overwrite some methods. | |
* | |
* @extends module:engine/model/delta/delta~Delta | |
*/ | |
class RenameDelta extends delta_Delta { | |
/** | |
* @inheritDoc | |
*/ | |
get type() { | |
return 'rename'; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
get _reverseDeltaClass() { | |
return RenameDelta; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
static get className() { | |
return 'engine.model.delta.RenameDelta'; | |
} | |
} | |
function renamedelta_apply( batch, delta, operation ) { | |
delta.addOperation( operation ); | |
batch.document.applyOperation( operation ); | |
} | |
/** | |
* Renames given element. | |
* | |
* @chainable | |
* @method module:engine/model/batch~Batch#rename | |
* @param {module:engine/model/element~Element} element The element to rename. | |
* @param {String} newName New element name. | |
*/ | |
register( 'rename', function( element, newName ) { | |
if ( !( element instanceof element_Element ) ) { | |
/** | |
* Trying to rename an object which is not an instance of Element. | |
* | |
* @error batch-rename-not-element-instance | |
*/ | |
throw new CKEditorError( 'batch-rename-not-element-instance: Trying to rename an object which is not an instance of Element.' ); | |
} | |
const delta = new RenameDelta(); | |
this.addDelta( delta ); | |
const renameOperation = new renameoperation_RenameOperation( position_Position.createBefore( element ), element.name, newName, this.document.version ); | |
renamedelta_apply( this, delta, renameOperation ); | |
return this; | |
} ); | |
deltafactory_DeltaFactory.register( RenameDelta ); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/delta/wrapdelta.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/model/delta/wrapdelta | |
*/ | |
/** | |
* To provide specific OT behavior and better collisions solving, {@link module:engine/model/batch~Batch#merge} method | |
* uses the `WrapDelta` class which inherits from the `Delta` class and may overwrite some methods. | |
* | |
* @extends module:engine/model/delta/delta~Delta | |
*/ | |
class wrapdelta_WrapDelta extends delta_Delta { | |
/** | |
* @inheritDoc | |
*/ | |
get type() { | |
return 'wrap'; | |
} | |
/** | |
* Range to wrap or `null` if there are no operations in the delta. | |
* | |
* @type {module:engine/model/range~Range|null} | |
*/ | |
get range() { | |
const moveOp = this._moveOperation; | |
return moveOp ? range_Range.createFromPositionAndShift( moveOp.sourcePosition, moveOp.howMany ) : null; | |
} | |
/** | |
* Offset size of range to wrap by the delta or `null` if there are no operations in delta. | |
* | |
* @type {Number} | |
*/ | |
get howMany() { | |
const range = this.range; | |
return range ? range.end.offset - range.start.offset : 0; | |
} | |
/* eslint-disable max-len */ | |
/** | |
* Operation that inserts wrapping element or `null` if there are no operations in the delta. | |
* | |
* @protected | |
* @type {module:engine/model/operation/insertoperation~InsertOperation|module:engine/model/operation/reinsertoperation~ReinsertOperation} | |
*/ | |
/* eslint-enable max-len */ | |
get _insertOperation() { | |
return this.operations[ 0 ] || null; | |
} | |
/** | |
* Operation that moves wrapped nodes to their new parent or `null` if there are no operations in the delta. | |
* | |
* @protected | |
* @type {module:engine/model/operation/moveoperation~MoveOperation|null} | |
*/ | |
get _moveOperation() { | |
return this.operations[ 1 ] || null; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
get _reverseDeltaClass() { | |
return unwrapdelta_UnwrapDelta; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
static get className() { | |
return 'engine.model.delta.WrapDelta'; | |
} | |
} | |
/** | |
* Wraps given range with given element or with a new element with specified name, if string has been passed. | |
* **Note:** range to wrap should be a "flat range" (see {@link module:engine/model/range~Range#isFlat}). If not, error will be thrown. | |
* | |
* @chainable | |
* @method module:engine/model/batch~Batch#wrap | |
* @param {module:engine/model/range~Range} range Range to wrap. | |
* @param {module:engine/model/element~Element|String} elementOrString Element or name of element to wrap the range with. | |
*/ | |
register( 'wrap', function( range, elementOrString ) { | |
if ( !range.isFlat ) { | |
/** | |
* Range to wrap is not flat. | |
* | |
* @error batch-wrap-range-not-flat | |
*/ | |
throw new CKEditorError( 'batch-wrap-range-not-flat: Range to wrap is not flat.' ); | |
} | |
const element = elementOrString instanceof element_Element ? elementOrString : new element_Element( elementOrString ); | |
if ( element.childCount > 0 ) { | |
/** | |
* Element to wrap with is not empty. | |
* | |
* @error batch-wrap-element-not-empty | |
*/ | |
throw new CKEditorError( 'batch-wrap-element-not-empty: Element to wrap with is not empty.' ); | |
} | |
if ( element.parent !== null ) { | |
/** | |
* Element to wrap with is already attached to a tree model. | |
* | |
* @error batch-wrap-element-attached | |
*/ | |
throw new CKEditorError( 'batch-wrap-element-attached: Element to wrap with is already attached to tree model.' ); | |
} | |
const delta = new wrapdelta_WrapDelta(); | |
this.addDelta( delta ); | |
const insert = new insertoperation_InsertOperation( range.end, element, this.document.version ); | |
delta.addOperation( insert ); | |
this.document.applyOperation( insert ); | |
const targetPosition = position_Position.createFromParentAndOffset( element, 0 ); | |
const move = new moveoperation_MoveOperation( | |
range.start, | |
range.end.offset - range.start.offset, | |
targetPosition, | |
this.document.version | |
); | |
delta.addOperation( move ); | |
this.document.applyOperation( move ); | |
return this; | |
} ); | |
deltafactory_DeltaFactory.register( wrapdelta_WrapDelta ); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/delta/unwrapdelta.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/model/delta/unwrapdelta | |
*/ | |
/** | |
* To provide specific OT behavior and better collisions solving, {@link module:engine/model/batch~Batch#merge} method | |
* uses the `UnwrapDelta` class which inherits from the `Delta` class and may overwrite some methods. | |
* | |
* @extends module:engine/model/delta/delta~Delta | |
*/ | |
class unwrapdelta_UnwrapDelta extends delta_Delta { | |
/** | |
* @inheritDoc | |
*/ | |
get type() { | |
return 'unwrap'; | |
} | |
/** | |
* Position before unwrapped element or `null` if there are no operations in the delta. | |
* | |
* @type {module:engine/model/position~Position|null} | |
*/ | |
get position() { | |
return this._moveOperation ? this._moveOperation.targetPosition : null; | |
} | |
/** | |
* Operation in the delta that moves unwrapped nodes to their new parent or `null` if there are no operations in the delta. | |
* | |
* @protected | |
* @type {module:engine/model/operation/moveoperation~MoveOperation|null} | |
*/ | |
get _moveOperation() { | |
return this.operations[ 0 ] || null; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
get _reverseDeltaClass() { | |
return wrapdelta_WrapDelta; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
static get className() { | |
return 'engine.model.delta.UnwrapDelta'; | |
} | |
} | |
/** | |
* Unwraps children of the given element – all its children are moved before it and then the element is removed. | |
* Throws error if you try to unwrap an element which does not have a parent. | |
* | |
* @chainable | |
* @method module:engine/model/batch~Batch#unwrap | |
* @param {module:engine/model/element~Element} position Element to unwrap. | |
*/ | |
register( 'unwrap', function( element ) { | |
if ( element.parent === null ) { | |
/** | |
* Trying to unwrap an element which has no parent. | |
* | |
* @error batch-unwrap-element-no-parent | |
*/ | |
throw new CKEditorError( 'batch-unwrap-element-no-parent: Trying to unwrap an element which has no parent.' ); | |
} | |
const delta = new unwrapdelta_UnwrapDelta(); | |
this.addDelta( delta ); | |
const sourcePosition = position_Position.createFromParentAndOffset( element, 0 ); | |
const move = new moveoperation_MoveOperation( | |
sourcePosition, | |
element.maxOffset, | |
position_Position.createBefore( element ), | |
this.document.version | |
); | |
move.isSticky = true; | |
delta.addOperation( move ); | |
this.document.applyOperation( move ); | |
// Computing new position because we moved some nodes before `element`. | |
// If we would cache `Position.createBefore( element )` we remove wrong node. | |
const graveyard = this.document.graveyard; | |
const gyPosition = new position_Position( graveyard, [ 0 ] ); | |
const remove = new removeoperation_RemoveOperation( position_Position.createBefore( element ), 1, gyPosition, this.document.version ); | |
delta.addOperation( remove ); | |
this.document.applyOperation( remove ); | |
return this; | |
} ); | |
deltafactory_DeltaFactory.register( unwrapdelta_UnwrapDelta ); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/delta/weakinsertdelta.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/model/delta/weakinsertdelta | |
*/ | |
/** | |
* To provide specific OT behavior and better collisions solving, the {@link module:engine/model/batch~Batch#insert} method | |
* uses the `WeakInsertDelta` class which inherits from the `Delta` class and may overwrite some methods. | |
* | |
* @extends module:engine/model/delta/delta~Delta | |
*/ | |
class WeakInsertDelta extends insertdelta_InsertDelta { | |
/** | |
* @inheritDoc | |
*/ | |
static get className() { | |
return 'engine.model.delta.WeakInsertDelta'; | |
} | |
} | |
/** | |
* Inserts a node or nodes at given position. {@link module:engine/model/batch~Batch#weakInsert weakInsert} is commonly used for actions | |
* like typing or plain-text paste (without formatting). There are two differences between | |
* {@link module:engine/model/batch~Batch#insert insert} and {@link module:engine/model/batch~Batch#weakInsert weakInsert}: | |
* | |
* * When using `weakInsert`, inserted nodes will have same attributes as the current attributes of | |
* {@link module:engine/model/document~Document#selection document selection}. | |
* * If {@link module:engine/model/operation/insertoperation~InsertOperation insert operation} position is inside a range changed by | |
* {@link module:engine/model/operation/attributeoperation~AttributeOperation attribute operation}, | |
* the attribute operation is split into two operations. | |
* Thanks to this, attribute change "omits" the inserted nodes. The correct behavior for `WeakInsertDelta` is that | |
* {@link module:engine/model/operation/attributeoperation~AttributeOperation AttributeOperation} does not "break" and also | |
* applies attributes for inserted nodes. This behavior has to be reflected during | |
* {@link module:engine/model/delta/transform~transform delta transformation}. | |
* | |
* @chainable | |
* @method module:engine/model/batch~Batch#weakInsert | |
* @param {module:engine/model/position~Position} position Position of insertion. | |
* @param {module:engine/model/node~NodeSet} nodes The list of nodes to be inserted. | |
*/ | |
register( 'weakInsert', function( position, nodes ) { | |
const delta = new WeakInsertDelta(); | |
this.addDelta( delta ); | |
nodes = normalizeNodes( nodes ); | |
for ( const node of nodes ) { | |
node.setAttributesTo( this.document.selection.getAttributes() ); | |
} | |
const operation = new insertoperation_InsertOperation( position, nodes, this.document.version ); | |
delta.addOperation( operation ); | |
this.document.applyOperation( operation ); | |
return this; | |
} ); | |
deltafactory_DeltaFactory.register( WeakInsertDelta ); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/delta/basic-deltas.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/model/delta/basic-deltas | |
*/ | |
// Deltas require `register` method that require `Batch` class and is defined in batch-base.js. | |
// We would like to group all deltas files in one place, so we would only have to include batch.js | |
// which would already have all default deltas registered. | |
// Import default suite of deltas so a feature have to include only Batch class file. | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/transform.js | |
/** | |
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md. | |
*/ | |
/** | |
* @module engine/model/operation/transform | |
*/ | |
/** | |
* Transforms given {@link module:engine/model/operation/operation~Operation operation} | |
* by another {@link module:engine/model/operation/operation~Operation operation} | |
* and returns the result of that transformation as an array containing | |
* one or more {@link module:engine/model/operation/operation~Operation operations}. | |
* | |
* Operations work on specified positions, passed to them when they are created. | |
* Whenever {@link module:engine/model/document~Document document} | |
* changes, we have to reflect those modifications by updating or "transforming" operations which are not yet applied. | |
* When an operation is transformed, its parameters may change based on the operation by which it is transformed. | |
* If the transform-by operation applied any modifications to the Tree Data Model which affect positions or nodes | |
* connected with transformed operation, those changes will be reflected in the parameters of the returned operation(s). | |
* | |
* Whenever the {@link module:engine/model/document~Document document} | |
* has different {@link module:engine/model/document~Document#version} | |
* than the operation you want to {@link module:engine/model/document~Document#applyOperation apply}, you need to transform that | |
* operation by all operations which were already applied to the {@link module:engine/model/document~Document document} and have greater | |
* {@link module:engine/model/document~Document#version} than the operation being applied. Transform them in the same order as those | |
* operations which were applied. This way all modifications done to the Tree Data Model will be reflected | |
* in the operation parameters and the operation will "operate" on "up-to-date" version of the Tree Data Model. | |
* This is mostly the case with Operational Transformations but it might be needed in particular features as well. | |
* | |
* In some cases, when given operation apply changes to the same nodes as this operation, two or more operations need | |
* to be created as one would not be able to reflect the combination of these operations. | |
* This is why an array is returned instead of a single object. All returned operations have to be applied | |
* (or further transformed) to get an effect which was intended in pre-transformed operation. | |
* | |
* Sometimes two operations are in conflict. This happens when they modify the same node in a different way, i.e. | |
* set different value for the same attribute or move the node into different positions. When this happens, | |
* we need to decide which operation is more important. We can't assume that operation `a` or operation `b` is always | |
* more important. In Operational Transformations algorithms we often need to get a result of transforming | |
* `a` by `b` and also `b` by `a`. In both transformations the same operation has to be the important one. If we assume | |
* that first or the second passed operation is always more important we won't be able to solve this case. | |
* | |
* @function module:engine/model/operation/transform~transform | |
* @param {module:engine/model/operation/operation~Operation} a Operation that will be transformed. | |
* @param {module:engine/model/operation/operation~Operation} b Operation to transform by. | |
* @param {module:engine/model/delta/transform~transformationContext} [context] Transformation context. | |
* @returns {Array.<module:engine/model/operation/operation~Operation>} Result of the transformation. | |
*/ | |
/* harmony default export */ var operation_transform = (transform_transform); | |
const ot = { | |
InsertOperation: { | |
// Transforms InsertOperation `a` by InsertOperation `b`. Accepts a flag stating whether `a` is more important | |
// than `b` when it comes to resolving conflicts. Returns results as an array of operations. | |
InsertOperation( a, b, context ) { | |
// Transformed operations are always new instances, not references to the original operations. | |
const transformed = a.clone(); | |
// Check whether there is a forced order of nodes or use `context.isStrong` flag for conflict resolving. | |
const insertBefore = context.insertBefore === undefined ? !context.isStrong : context.insertBefore; | |
// Transform insert position by the other operation position. | |
transformed.position = transformed.position._getTransformedByInsertion( b.position, b.nodes.maxOffset, insertBefore ); | |
return [ transformed ]; | |
}, | |
AttributeOperation: doNotUpdate, | |
RootAttributeOperation: doNotUpdate, | |
RenameOperation: doNotUpdate, | |
MarkerOperation: doNotUpdate, | |
// Transforms InsertOperation `a` by MoveOperation `b`. Accepts a flag stating whether `a` is more important | |
// than `b` when it comes to resolving conflicts. Returns results as an array of operations. | |
MoveOperation( a, b, context ) { | |
const transformed = a.clone(); | |
// Check whether there is a forced order of nodes or use `context.isStrong` flag for conflict resolving. | |
const insertBefore = context.insertBefore === undefined ? !context.isStrong : context.insertBefore; | |
// Transform insert position by the other operation parameters. | |
transformed.position = a.position._getTransformedByMove( | |
b.sourcePosition, | |
b.targetPosition, | |
b.howMany, | |
insertBefore, | |
b.isSticky && !context.forceNotSticky | |
); | |
return [ transformed ]; | |
} | |
}, | |
AttributeOperation: { | |
// Transforms AttributeOperation `a` by InsertOperation `b`. Returns results as an array of operations. | |
InsertOperation( a, b ) { | |
// Transform this operation's range. | |
const ranges = a.range._getTransformedByInsertion( b.position, b.nodes.maxOffset, true, false ); | |
// Map transformed range(s) to operations and return them. | |
return ranges.reverse().map( range => { | |
return new attributeoperation_AttributeOperation( range, a.key, a.oldValue, a.newValue, a.baseVersion ); | |
} ); | |
}, | |
// Transforms AttributeOperation `a` by AttributeOperation `b`. Accepts a flag stating whether `a` is more important | |
// than `b` when it comes to resolving conflicts. Returns results as an array of operations. | |
AttributeOperation( a, b, context ) { | |
if ( a.key === b.key ) { | |
// If operations attributes are in conflict, check if their ranges intersect and manage them properly. | |
// First, we want to apply change to the part of a range that has not been changed by the other operation. | |
const operations = a.range.getDifference( b.range ).map( range => { | |
return new attributeoperation_AttributeOperation( range, a.key, a.oldValue, a.newValue, a.baseVersion ); | |
} ); | |
// Then we take care of the common part of ranges. | |
const common = a.range.getIntersection( b.range ); | |
if ( common ) { | |
// If this operation is more important, we also want to apply change to the part of the | |
// original range that has already been changed by the other operation. Since that range | |
// got changed we also have to update `oldValue`. | |
if ( context.isStrong ) { | |
operations.push( new attributeoperation_AttributeOperation( common, b.key, b.newValue, a.newValue, a.baseVersion ) ); | |
} else if ( operations.length === 0 ) { | |
operations.push( new NoOperation( 0 ) ); | |
} | |
} | |
return operations; | |
} else { | |
// If operations don't conflict, simply return an array containing just a clone of this operation. | |
return [ a.clone() ]; | |
} | |
}, | |
RootAttributeOperation: doNotUpdate, | |
RenameOperation: doNotUpdate, | |
MarkerOperation: doNotUpdate, | |
// Transforms AttributeOperation `a` by MoveOperation `b`. Returns results as an array of operations. | |
MoveOperation( a, b ) { | |
// Convert MoveOperation properties into a range. | |
const rangeB = range_Range.createFromPositionAndShift( b.sourcePosition, b.howMany ); | |
// This will aggregate transformed ranges. | |
let ranges = []; | |
// Difference is a part of changed range that is modified by AttributeOperation but is not affected | |
// by MoveOperation. This can be zero, one or two ranges (if moved range is inside changed range). | |
// Right now we will make a simplification and join difference ranges and transform them as one. We will cover rangeB later. | |
const difference = joinRanges( a.range.getDifference( rangeB ) ); | |
// Common is a range of nodes that is affected by MoveOperation. So it got moved to other place. | |
const common = a.range.getIntersection( rangeB ); | |
if ( difference !== null ) { | |
// MoveOperation removes nodes from their original position. We acknowledge this by proper transformation. | |
// Take the start and the end of the range and transform them by deletion of moved nodes. | |
// Note that if rangeB was inside AttributeOperation range, only difference.end will be transformed. | |
// This nicely covers the joining simplification we did in the previous step. | |
difference.start = difference.start._getTransformedByDeletion( b.sourcePosition, b.howMany ); | |
difference.end = difference.end._getTransformedByDeletion( b.sourcePosition, b.howMany ); | |
// MoveOperation pastes nodes into target position. We acknowledge this by proper transformation. | |
// Note that since we operate on transformed difference range, we should transform by | |
// previously transformed target position. | |
// Note that we do not use Position._getTransformedByMove on range boundaries because we need to | |
// transform by insertion a range as a whole, since newTargetPosition might be inside that range. | |
ranges = difference._getTransformedByInsertion( b.getMovedRangeStart(), b.howMany, true, false ).reverse(); | |
} | |
if ( common !== null ) { | |
// Here we do not need to worry that newTargetPosition is inside moved range, because that | |
// would mean that the MoveOperation targets into itself, and that is incorrect operation. | |
// Instead, we calculate the new position of that part of original range. | |
common.start = common.start._getCombined( b.sourcePosition, b.getMovedRangeStart() ); | |
common.end = common.end._getCombined( b.sourcePosition, b.getMovedRangeStart() ); | |
ranges.push( common ); | |
} | |
// Map transformed range(s) to operations and return them. | |
return ranges.map( range => { | |
return new attributeoperation_AttributeOperation( range, a.key, a.oldValue, a.newValue, a.baseVersion ); | |
} ); | |
} | |
}, | |
RootAttributeOperation: { | |
InsertOperation: doNotUpdate, | |
AttributeOperation: doNotUpdate, | |
// Transforms RootAttributeOperation `a` by RootAttributeOperation `b`. Accepts a flag stating whether `a` is more important | |
// than `b` when it comes to resolving conflicts. Returns results as an array of operations. | |
RootAttributeOperation( a, b, context ) { | |
if ( a.root === b.root && a.key === b.key ) { | |
if ( ( a.newValue !== b.newValue && !context.isStrong ) || a.newValue === b.newValue ) { | |
return [ new NoOperation( a.baseVersion ) ]; | |
} | |
} | |
return [ a.clone() ]; | |
}, | |
RenameOperation: doNotUpdate, | |
MarkerOperation: doNotUpdate, | |
MoveOperation: doNotUpdate | |
}, | |
RenameOperation: { | |
// Transforms RenameOperation `a` by InsertOperation `b`. Returns results as an array of operations. | |
InsertOperation( a, b ) { | |
// Clone the operation, we don't want to alter the original operation. | |
const clone = a.clone(); | |
// Transform this operation's position. | |
clone.position = clone.position._getTransformedByInsertion( b.position, b.nodes.maxOffset, true ); | |
return [ clone ]; | |
}, | |
AttributeOperation: doNotUpdate, | |
RootAttributeOperation: doNotUpdate, | |
// Transforms RenameOperation `a` by RenameOperation `b`. Accepts a flag stating whether `a` is more important | |
// than `b` when it comes to resolving conflicts. Returns results as an array of operations. | |
RenameOperation( a, b, context ) { | |
// Clone the operation, we don't want to alter the original operation. | |
const clone = a.clone(); | |
if ( a.position.isEqual( b.position ) ) { | |
if ( context.isStrong ) { | |
clone.oldName = b.newName; | |
} else { | |
return [ new NoOperation( a.baseVersion ) ]; | |
} | |
} | |
return [ clone ]; | |
}, | |
MarkerOperation: doNotUpdate, | |
// Transforms RenameOperation `a` by MoveOperation `b`. Returns results as an array of operations. | |
MoveOperation( a, b ) { | |
const clone = a.clone(); | |
const isSticky = clone.position.isEqual( b.sourcePosition ); | |
clone.position = clone.position._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, true, isSticky ); | |
return [ clone ]; | |
} | |
}, | |
MarkerOperation: { | |
// Transforms MarkerOperation `a` by InsertOperation `b`. Returns results as an array of operations. | |
InsertOperation( a, b ) { | |
// Clone the operation, we don't want to alter the original operation. | |
const clone = a.clone(); | |
if ( clone.oldRange ) { | |
clone.oldRange = clone.oldRange._getTransformedByInsertion( b.position, b.nodes.maxOffset, false, false )[ 0 ]; | |
} | |
if ( clone.newRange ) { | |
clone.newRange = clone.newRange._getTransformedByInsertion( b.position, b.nodes.maxOffset, false, false )[ 0 ]; | |
} | |
return [ clone ]; | |
}, | |
AttributeOperation: doNotUpdate, | |
RootAttributeOperation: doNotUpdate, | |
RenameOperation: doNotUpdate, | |
// Transforms MarkerOperation `a` by MarkerOperation `b`. Accepts a flag stating whether `a` is more important | |
// than `b` when it comes to resolving conflicts. Returns results as an array of operations. | |
MarkerOperation( a, b, context ) { | |
// Clone the operation, we don't want to alter the original operation. | |
const clone = a.clone(); | |
if ( a.name == b.name ) { | |
if ( context.isStrong ) { | |
clone.oldRange = b.newRange; | |
} else { | |
return [ new NoOperation( a.baseVersion ) ]; | |
} | |
} | |
return [ clone ]; | |
}, | |
// Transforms MarkerOperation `a` by MoveOperation `b`. Returns results as an array of operations. | |
MoveOperation( a, b ) { | |
// Clone the operation, we don't want to alter the original operation. | |
const clone = a.clone(); | |
if ( clone.oldRange ) { | |
const oldRanges = clone.oldRange._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany ); | |
clone.oldRange = range_Range.createFromRanges( oldRanges ); | |
} | |
if ( clone.newRange ) { | |
const newRanges = clone.newRange._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany ); | |
clone.newRange = range_Range.createFromRanges( newRanges ); | |
} | |
return [ clone ]; | |
} | |
}, | |
MoveOperation: { | |
// Transforms MoveOperation `a` by InsertOperation `b`. Accepts a flag stating whether `a` is more important | |
// than `b` when it comes to resolving conflicts. Returns results as an array of operations. | |
InsertOperation( a, b, context ) { | |
// Create range from MoveOperation properties and transform it by insertion. | |
let range = range_Range.createFromPositionAndShift( a.sourcePosition, a.howMany ); | |
const includeB = a.isSticky && !context.forceNotSticky; | |
range = range._getTransformedByInsertion( b.position, b.nodes.maxOffset, false, includeB )[ 0 ]; | |
// Check whether there is a forced order of nodes or use `context.isStrong` flag for conflict resolving. | |
const insertBefore = context.insertBefore === undefined ? !context.isStrong : context.insertBefore; | |
const result = new a.constructor( | |
range.start, | |
range.end.offset - range.start.offset, | |
a.targetPosition._getTransformedByInsertion( b.position, b.nodes.maxOffset, insertBefore ), | |
a.baseVersion | |
); | |
result.isSticky = a.isSticky; | |
return [ result ]; | |
}, | |
AttributeOperation: doNotUpdate, | |
RootAttributeOperation: doNotUpdate, | |
RenameOperation: doNotUpdate, | |
MarkerOperation: doNotUpdate, | |
// Transforms MoveOperation `a` by MoveOperation `b`. Accepts a flag stating whether `a` is more important | |
// than `b` when it comes to resolving conflicts. Returns results as an array of operations. | |
MoveOperation( a, b, context ) { | |
// | |
// Setting and evaluating some variables that will be used in special cases and default algorithm. | |
// | |
// Create ranges from `MoveOperations` properties. | |
const rangeA = range_Range.createFromPositionAndShift( a.sourcePosition, a.howMany ); | |
const rangeB = range_Range.createFromPositionAndShift( b.sourcePosition, b.howMany ); | |
// Assign `context.isStrong` to a different variable, because the value may change during execution of | |
// this algorithm and we do not want to override original `context.isStrong` that will be used in later transformations. | |
let isStrong = context.isStrong; | |
// Whether range moved by operation `b` is includable in operation `a` move range. | |
// For this, `a` operation has to be sticky (so `b` sticks to the range) and context has to allow stickiness. | |
const includeB = a.isSticky && !context.forceNotSticky; | |
// Evaluate new target position for transformed operation. | |
// Check whether there is a forced order of nodes or use `isStrong` flag for conflict resolving. | |
const insertBefore = context.insertBefore === undefined ? !isStrong : context.insertBefore; | |
// `a.targetPosition` could be affected by the `b` operation. We will transform it. | |
const newTargetPosition = a.targetPosition._getTransformedByMove( | |
b.sourcePosition, | |
b.targetPosition, | |
b.howMany, | |
insertBefore, | |
b.isSticky && !context.forceNotSticky | |
); | |
// | |
// Special case #1 + mirror. | |
// | |
// Special case when both move operations' target positions are inside nodes that are | |
// being moved by the other move operation. So in other words, we move ranges into inside of each other. | |
// This case can't be solved reasonably (on the other hand, it should not happen often). | |
if ( moveTargetIntoMovedRange( a, b ) && moveTargetIntoMovedRange( b, a ) ) { | |
// Instead of transforming operation, we return a reverse of the operation that we transform by. | |
// So when the results of this "transformation" will be applied, `b` MoveOperation will get reversed. | |
return [ b.getReversed() ]; | |
} | |
// | |
// End of special case #1. | |
// | |
// | |
// Special case #2. | |
// | |
// Check if `b` operation targets inside `rangeA`. Use stickiness if possible. | |
const bTargetsToA = rangeA.containsPosition( b.targetPosition ) || | |
( rangeA.start.isEqual( b.targetPosition ) && includeB ) || | |
( rangeA.end.isEqual( b.targetPosition ) && includeB ); | |
// If `b` targets to `rangeA` and `rangeA` contains `rangeB`, `b` operation has no influence on `a` operation. | |
// You might say that operation `b` is captured inside operation `a`. | |
if ( bTargetsToA && rangeA.containsRange( rangeB, true ) ) { | |
// There is a mini-special case here, where `rangeB` is on other level than `rangeA`. That's why | |
// we need to transform `a` operation anyway. | |
rangeA.start = rangeA.start._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, !includeB ); | |
rangeA.end = rangeA.end._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, includeB ); | |
return makeMoveOperationsFromRanges( [ rangeA ], newTargetPosition, a ); | |
} | |
// | |
// Special case #2 mirror. | |
// | |
const aTargetsToB = rangeB.containsPosition( a.targetPosition ) || | |
( rangeB.start.isEqual( a.targetPosition ) && b.isSticky && !context.forceNotSticky ) || | |
( rangeB.end.isEqual( a.targetPosition ) && b.isSticky && !context.forceNotSticky ); | |
if ( aTargetsToB && rangeB.containsRange( rangeA, true ) ) { | |
// `a` operation is "moved together" with `b` operation. | |
// Here, just move `rangeA` "inside" `rangeB`. | |
rangeA.start = rangeA.start._getCombined( b.sourcePosition, b.getMovedRangeStart() ); | |
rangeA.end = rangeA.end._getCombined( b.sourcePosition, b.getMovedRangeStart() ); | |
return makeMoveOperationsFromRanges( [ rangeA ], newTargetPosition, a ); | |
} | |
// | |
// End of special case #2. | |
// | |
// | |
// Special case #3 + mirror. | |
// | |
// `rangeA` has a node which is an ancestor of `rangeB`. In other words, `rangeB` is inside `rangeA` | |
// but not on the same tree level. In such case ranges have common part but we have to treat it | |
// differently, because in such case those ranges are not really conflicting and should be treated like | |
// two separate ranges. Also we have to discard two difference parts. | |
const aCompB = compareArrays( a.sourcePosition.getParentPath(), b.sourcePosition.getParentPath() ); | |
if ( aCompB == 'prefix' || aCompB == 'extension' ) { | |
// Transform `rangeA` by `b` operation and make operation out of it, and that's all. | |
// Note that this is a simplified version of default case, but here we treat the common part (whole `rangeA`) | |
// like a one difference part. | |
rangeA.start = rangeA.start._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, !includeB ); | |
rangeA.end = rangeA.end._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, includeB ); | |
return makeMoveOperationsFromRanges( [ rangeA ], newTargetPosition, a ); | |
} | |
// | |
// End of special case #3. | |
// | |
// | |
// Default case - ranges are on the same level or are not connected with each other. | |
// | |
// Modifier for default case. | |
// Modifies `isStrong` flag in certain conditions. | |
// | |
// If only one of operations is a remove operation, we force remove operation to be the "stronger" one | |
// to provide more expected results. This is done only if `context.forceWeakRemove` is set to `false`. | |
// `context.forceWeakRemove` is set to `true` in certain conditions when transformation takes place during undo. | |
if ( !context.forceWeakRemove ) { | |
if ( a instanceof removeoperation_RemoveOperation && !( b instanceof removeoperation_RemoveOperation ) ) { | |
isStrong = true; | |
} else if ( !( a instanceof removeoperation_RemoveOperation ) && b instanceof removeoperation_RemoveOperation ) { | |
isStrong = false; | |
} | |
} | |
// Handle operation's source ranges - check how `rangeA` is affected by `b` operation. | |
// This will aggregate transformed ranges. | |
const ranges = []; | |
// Get the "difference part" of `a` operation source range. | |
// This is an array with one or two ranges. Two ranges if `rangeB` is inside `rangeA`. | |
const difference = rangeA.getDifference( rangeB ); | |
for ( const range of difference ) { | |
// Transform those ranges by `b` operation. For example if `b` moved range from before those ranges, fix those ranges. | |
range.start = range.start._getTransformedByDeletion( b.sourcePosition, b.howMany ); | |
range.end = range.end._getTransformedByDeletion( b.sourcePosition, b.howMany ); | |
// If `b` operation targets into `rangeA` on the same level, spread `rangeA` into two ranges. | |
const shouldSpread = compareArrays( range.start.getParentPath(), b.getMovedRangeStart().getParentPath() ) == 'same'; | |
const newRanges = range._getTransformedByInsertion( b.getMovedRangeStart(), b.howMany, shouldSpread, includeB ); | |
ranges.push( ...newRanges ); | |
} | |
// Then, we have to manage the "common part" of both move ranges. | |
const common = rangeA.getIntersection( rangeB ); | |
if ( common !== null && isStrong && !bTargetsToA ) { | |
// Calculate the new position of that part of original range. | |
common.start = common.start._getCombined( b.sourcePosition, b.getMovedRangeStart() ); | |
common.end = common.end._getCombined( b.sourcePosition, b.getMovedRangeStart() ); | |
// Take care of proper range order. | |
// | |
// Put `common` at appropriate place. Keep in mind that we are interested in original order. | |
// Basically there are only three cases: there is zero, one or two difference ranges. | |
// | |
// If there is zero difference ranges, just push `common` in the array. | |
if ( ranges.length === 0 ) { | |
ranges.push( common ); | |
} | |
// If there is one difference range, we need to check whether common part was before it or after it. | |
else if ( ranges.length == 1 ) { | |
if ( rangeB.start.isBefore( rangeA.start ) || rangeB.start.isEqual( rangeA.start ) ) { | |
ranges.unshift( common ); | |
} else { | |
ranges.push( common ); | |
} | |
} | |
// If there are more ranges (which means two), put common part between them. This is the only scenario | |
// where there could be two difference ranges so we don't have to make any comparisons. | |
else { | |
ranges.splice( 1, 0, common ); | |
} | |
} | |
if ( ranges.length === 0 ) { | |
// If there are no "source ranges", nothing should be changed. | |
// Note that this can happen only if `isStrong == false` and `rangeA.isEqual( rangeB )`. | |
return [ new NoOperation( a.baseVersion ) ]; | |
} | |
return makeMoveOperationsFromRanges( ranges, newTargetPosition, a ); | |
} | |
} | |
}; | |
function transform_transform( a, b, context = { isStrong: false } ) { | |
let group, algorithm; | |
if ( a instanceof insertoperation_InsertOperation ) { | |
group = ot.InsertOperation; | |
} else if ( a instanceof attributeoperation_AttributeOperation ) { | |
group = ot.AttributeOperation; | |
} else if ( a instanceof rootattributeoperation_RootAttributeOperation ) { | |
group = ot.RootAttributeOperation; | |
} else if ( a instanceof renameoperation_RenameOperation ) { | |
group = ot.RenameOperation; | |
} else if ( a instanceof markeroperation_MarkerOperation ) { | |
group = ot.MarkerOperation; | |
} else if ( a instanceof moveoperation_MoveOperation ) { | |
group = ot.MoveOperation; | |
} else { | |
algorithm = doNotUpdate; | |
} | |
if ( group ) { | |
if ( b instanceof insertoperation_InsertOperation ) { | |
algorithm = group.InsertOperation; | |
} else if ( b instanceof attributeoperation_AttributeOperation ) { | |
algorithm = group.AttributeOperation; | |
} else if ( b instanceof rootattributeoperation_RootAttributeOperation ) { | |
algorithm = group.RootAttributeOperation; | |
} else if ( b instanceof renameoperation_RenameOperation ) { | |
algorithm = group.RenameOperation; | |
} else if ( b instanceof markeroperation_MarkerOperation ) { | |
algorithm = group.MarkerOperation; | |
} else if ( b instanceof moveoperation_MoveOperation ) { | |
algorithm = group.MoveOperation; | |
} else { | |
algorithm = doNotUpdate; | |
} | |
} | |
const transformed = algorithm( a, b, context ); | |
return updateBaseVersions( a.baseVersion, transformed ); | |
} | |
// When we don't want to update an operation, we create and return a clone of it. | |
// Returns the operation in "unified format" - wrapped in an Array. | |
function doNotUpdate( operation ) { | |
return [ operation.clone() ]; | |
} | |
// Takes an Array of operations and sets consecutive base versions for them, starting from given base version. | |
// Returns the passed array. | |
function updateBaseVersions( baseVersion, operations ) { | |
for ( let i = 0; i < operations.length; i++ ) { | |
operations[ i ].baseVersion = baseVersion + i + 1; | |
} | |
return operations; | |
} | |
// Checks whether MoveOperation targetPosition is inside a node from the moved range of the other MoveOperation. | |
function moveTargetIntoMovedRange( a, b ) { | |
return a.targetPosition._getTransformedByDeletion( b.sourcePosition, b.howMany ) === null; | |
} | |
// Gets an array of Ranges and produces one Range out of it. The root of a new range will be same as | |
// the root of the first range in the array. If any of given ranges has different root than the first range, | |
// it will be discarded. | |
function joinRanges( ranges ) { | |
if ( ranges.length === 0 ) { | |
return null; | |
} else if ( ranges.length == 1 ) { | |
return ranges[ 0 ]; | |
} else { | |
ranges[ 0 ].end = ranges[ ranges.length - 1 ].end; | |
return ranges[ 0 ]; | |
} | |
} | |
// Helper function for `MoveOperation` x `MoveOperation` transformation. | |
// Convert given ranges and target position to move operations and return them. | |
// Ranges and target position will be transformed on-the-fly when generating operations. | |
// Given `ranges` should be in the order of how they were in the original transformed operation. | |
// Given `targetPosition` is the target position of the first range from `ranges`. | |
function makeMoveOperationsFromRanges( ranges, targetPosition, a ) { | |
// At this moment we have some ranges and a target position, to which those ranges should be moved. | |
// Order in `ranges` array is the go-to order of after transformation. | |
// | |
// We are almost done. We have `ranges` and `targetPosition` to make operations from. | |
// Unfortunately, those operations may affect each other. Precisely, first operation after move | |
// may affect source range and target position of second and third operation. Same with second | |
// operation affecting third. | |
// | |
// We need to fix those source ranges and target positions once again, before converting `ranges` to operations. | |
const operations = []; | |
// Keep in mind that nothing will be transformed if there is just one range in `ranges`. | |
for ( let i = 0; i < ranges.length; i++ ) { | |
// Create new operation out of a range and target position. | |
const op = makeMoveOperation( ranges[ i ], targetPosition, a.isSticky ); | |
operations.push( op ); | |
// Transform other ranges by the generated operation. | |
for ( let j = i + 1; j < ranges.length; j++ ) { | |
// All ranges in `ranges` array should be: | |
// * non-intersecting (these are part of original operation source range), and | |
// * `targetPosition` does not target into them (opposite would mean that transformed operation targets "inside itself"). | |
// | |
// This means that the transformation will be "clean" and always return one result. | |
ranges[ j ] = ranges[ j ]._getTransformedByMove( op.sourcePosition, op.targetPosition, op.howMany )[ 0 ]; | |
} | |
targetPosition = targetPosition._getTransformedByMove( op.sourcePosition, op.targetPosition, op.howMany, true, false ); | |
} | |
return operations; | |
} | |
function makeMoveOperation( range, targetPosition, isSticky ) { | |
// We want to keep correct operation class. | |
let OperationClass; | |
if ( targetPosition.root.rootName == '$graveyard' ) { | |
OperationClass = removeoperation_RemoveOperation; | |
} else if ( range.start.root.rootName == '$graveyard' ) { | |
OperationClass = reinsertoperation_ReinsertOperation; | |
} else { | |
OperationClass = moveoperation_MoveOperation; | |
} | |
const result = new OperationClass( | |
range.start, | |
range.end.offset - range.start.offset, | |
targetPosition, | |
0 // Is corrected anyway later. | |
); | |
result.isSticky = isSticky; | |
return result; | |
} | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseSlice.js | |
/** | |
* The base implementation of `_.slice` without an iteratee call guard. | |
* | |
* @private | |
* @param {Array} array The array to slice. | |
* @param {number} [start=0] The start position. | |
* @param {number} [end=array.length] The end position. | |
* @returns {Array} Returns the slice of `array`. | |
*/ | |
function baseSlice(array, start, end) { | |
var index = -1, | |
length = array.length; | |
if (start < 0) { | |
start = -start > length ? 0 : (length + start); | |
} | |
end = end > length ? length : end; | |
if (end < 0) { | |
end += length; | |
} | |
length = start > end ? 0 : ((end - start) >>> 0); | |
start >>>= 0; | |
var result = Array(length); | |
while (++index < length) { | |
result[index] = array[index + start]; | |
} | |
return result; | |
} | |
/* harmony default export */ var _baseSlice = (baseSlice); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/chunk.js | |
/* Built-in method references for those with the same name as other `lodash` methods. */ | |
var nativeCeil = Math.ceil, | |
chunk_nativeMax = Math.max; | |
/** | |
* Creates an array of elements split into groups the length of `size`. | |
* If `array` can't be split evenly, the final chunk will be the remaining | |
* elements. | |
* | |
* @static | |
* @memberOf _ | |
* @since 3.0.0 | |
* @category Array | |
* @param {Array} array The array to process. | |
* @param {number} [size=1] The length of each chunk | |
* @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. | |
* @returns {Array} Returns the new array of chunks. | |
* @example | |
* | |
* _.chunk(['a', 'b', 'c', 'd'], 2); | |
* // => [['a', 'b'], ['c', 'd']] | |
* | |
* _.chunk(['a', 'b', 'c', 'd'], 3); | |
* // => [['a', 'b', 'c'], ['d']] | |
*/ | |
function chunk(array, size, guard) { | |
if ((guard ? _isIterateeCall(array, size, guard) : size === undefined)) { | |
size = 1; | |
} else { | |
size = chunk_nativeMax(lodash_toInteger(size), 0); | |
} | |
var length = array ? array.length : 0; | |
if (!length || size < 1) { | |
return []; | |
} | |
var index = 0, | |
resIndex = 0, | |
result = Array(nativeCeil(length / size)); | |
while (index < length) { | |
result[resIndex++] = _baseSlice(array, index, (index += size)); | |
} | |
return result; | |
} | |
/* harmony default export */ var lodash_chunk = (chunk); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/compact.js | |
/** | |
* Creates an array with all falsey values removed. The values `false`, `null`, | |
* `0`, `""`, `undefined`, and `NaN` are falsey. | |
* | |
* @static | |
* @memberOf _ | |
* @since 0.1.0 | |
* @category Array | |
* @param {Array} array The array to compact. | |
* @returns {Array} Returns the new array of filtered values. | |
* @example | |
* | |
* _.compact([0, 1, false, 2, '', 3]); | |
* // => [1, 2, 3] | |
*/ | |
function compact(array) { | |
var index = -1, | |
length = array ? array.length : 0, | |
resIndex = 0, | |
result = []; | |
while (++index < length) { | |
var value = array[index]; | |
if (value) { | |
result[resIndex++] = value; | |
} | |
} | |
return result; | |
} | |
/* harmony default export */ var lodash_compact = (compact); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_isFlattenable.js | |
/** | |
* Checks if `value` is a flattenable `arguments` object or array. | |
* | |
* @private | |
* @param {*} value The value to check. | |
* @returns {boolean} Returns `true` if `value` is flattenable, else `false`. | |
*/ | |
function isFlattenable(value) { | |
return lodash_isArray(value) || lodash_isArguments(value); | |
} | |
/* harmony default export */ var _isFlattenable = (isFlattenable); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseFlatten.js | |
/** | |
* The base implementation of `_.flatten` with support for restricting flattening. | |
* | |
* @private | |
* @param {Array} array The array to flatten. | |
* @param {number} depth The maximum recursion depth. | |
* @param {boolean} [predicate=isFlattenable] The function invoked per iteration. | |
* @param {boolean} [isStrict] Restrict to values that pass `predicate` checks. | |
* @param {Array} [result=[]] The initial result value. | |
* @returns {Array} Returns the new flattened array. | |
*/ | |
function baseFlatten(array, depth, predicate, isStrict, result) { | |
var index = -1, | |
length = array.length; | |
predicate || (predicate = _isFlattenable); | |
result || (result = []); | |
while (++index < length) { | |
var value = array[index]; | |
if (depth > 0 && predicate(value)) { | |
if (depth > 1) { | |
// Recursively flatten arrays (susceptible to call stack limits). | |
baseFlatten(value, depth - 1, predicate, isStrict, result); | |
} else { | |
_arrayPush(result, value); | |
} | |
} else if (!isStrict) { | |
result[result.length] = value; | |
} | |
} | |
return result; | |
} | |
/* harmony default export */ var _baseFlatten = (baseFlatten); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/concat.js | |
/** | |
* Creates a new array concatenating `array` with any additional arrays | |
* and/or values. | |
* | |
* @static | |
* @memberOf _ | |
* @since 4.0.0 | |
* @category Array | |
* @param {Array} array The array to concatenate. | |
* @param {...*} [values] The values to concatenate. | |
* @returns {Array} Returns the new concatenated array. | |
* @example | |
* | |
* var array = [1]; | |
* var other = _.concat(array, 2, [3], [[4]]); | |
* | |
* console.log(other); | |
* // => [1, 2, 3, [4]] | |
* | |
* console.log(array); | |
* // => [1] | |
*/ | |
function concat() { | |
var length = arguments.length, | |
args = Array(length ? length - 1 : 0), | |
array = arguments[0], | |
index = length; | |
while (index--) { | |
args[index - 1] = arguments[index]; | |
} | |
return length | |
? _arrayPush(lodash_isArray(array) ? _copyArray(array) : [array], _baseFlatten(args, 1)) | |
: []; | |
} | |
/* harmony default export */ var lodash_concat = (concat); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_indexOfNaN.js | |
/** | |
* Gets the index at which the first occurrence of `NaN` is found in `array`. | |
* | |
* @private | |
* @param {Array} array The array to search. | |
* @param {number} fromIndex The index to search from. | |
* @param {boolean} [fromRight] Specify iterating from right to left. | |
* @returns {number} Returns the index of the matched `NaN`, else `-1`. | |
*/ | |
function indexOfNaN(array, fromIndex, fromRight) { | |
var length = array.length, | |
index = fromIndex + (fromRight ? 0 : -1); | |
while ((fromRight ? index-- : ++index < length)) { | |
var other = array[index]; | |
if (other !== other) { | |
return index; | |
} | |
} | |
return -1; | |
} | |
/* harmony default export */ var _indexOfNaN = (indexOfNaN); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseIndexOf.js | |
/** | |
* The base implementation of `_.indexOf` without `fromIndex` bounds checks. | |
* | |
* @private | |
* @param {Array} array The array to search. | |
* @param {*} value The value to search for. | |
* @param {number} fromIndex The index to search from. | |
* @returns {number} Returns the index of the matched value, else `-1`. | |
*/ | |
function baseIndexOf(array, value, fromIndex) { | |
if (value !== value) { | |
return _indexOfNaN(array, fromIndex); | |
} | |
var index = fromIndex - 1, | |
length = array.length; | |
while (++index < length) { | |
if (array[index] === value) { | |
return index; | |
} | |
} | |
return -1; | |
} | |
/* harmony default export */ var _baseIndexOf = (baseIndexOf); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_arrayIncludes.js | |
/** | |
* A specialized version of `_.includes` for arrays without support for | |
* specifying an index to search from. | |
* | |
* @private | |
* @param {Array} array The array to search. | |
* @param {*} target The value to search for. | |
* @returns {boolean} Returns `true` if `target` is found, else `false`. | |
*/ | |
function arrayIncludes(array, value) { | |
return !!array.length && _baseIndexOf(array, value, 0) > -1; | |
} | |
/* harmony default export */ var _arrayIncludes = (arrayIncludes); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_arrayIncludesWith.js | |
/** | |
* This function is like `arrayIncludes` except that it accepts a comparator. | |
* | |
* @private | |
* @param {Array} array The array to search. | |
* @param {*} target The value to search for. | |
* @param {Function} comparator The comparator invoked per element. | |
* @returns {boolean} Returns `true` if `target` is found, else `false`. | |
*/ | |
function arrayIncludesWith(array, value, comparator) { | |
var index = -1, | |
length = array.length; | |
while (++index < length) { | |
if (comparator(value, array[index])) { | |
return true; | |
} | |
} | |
return false; | |
} | |
/* harmony default export */ var _arrayIncludesWith = (arrayIncludesWith); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_arrayMap.js | |
/** | |
* A specialized version of `_.map` for arrays without support for iteratee | |
* shorthands. | |
* | |
* @private | |
* @param {Array} array The array to iterate over. | |
* @param {Function} iteratee The function invoked per iteration. | |
* @returns {Array} Returns the new mapped array. | |
*/ | |
function arrayMap(array, iteratee) { | |
var index = -1, | |
length = array.length, | |
result = Array(length); | |
while (++index < length) { | |
result[index] = iteratee(array[index], index, array); | |
} | |
return result; | |
} | |
/* harmony default export */ var _arrayMap = (arrayMap); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseUnary.js | |
/** | |
* The base implementation of `_.unary` without support for storing wrapper metadata. | |
* | |
* @private | |
* @param {Function} func The function to cap arguments for. | |
* @returns {Function} Returns the new capped function. | |
*/ | |
function baseUnary(func) { | |
return function(value) { | |
return func(value); | |
}; | |
} | |
/* harmony default export */ var _baseUnary = (baseUnary); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_cacheHas.js | |
/** | |
* Checks if a cache value for `key` exists. | |
* | |
* @private | |
* @param {Object} cache The cache to query. | |
* @param {string} key The key of the entry to check. | |
* @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. | |
*/ | |
function cacheHas(cache, key) { | |
return cache.has(key); | |
} | |
/* harmony default export */ var _cacheHas = (cacheHas); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseDifference.js | |
/** Used as the size to enable large array optimizations. */ | |
var _baseDifference_LARGE_ARRAY_SIZE = 200; | |
/** | |
* The base implementation of methods like `_.difference` without support | |
* for excluding multiple arrays or iteratee shorthands. | |
* | |
* @private | |
* @param {Array} array The array to inspect. | |
* @param {Array} values The values to exclude. | |
* @param {Function} [iteratee] The iteratee invoked per element. | |
* @param {Function} [comparator] The comparator invoked per element. | |
* @returns {Array} Returns the new array of filtered values. | |
*/ | |
function baseDifference(array, values, iteratee, comparator) { | |
var index = -1, | |
includes = _arrayIncludes, | |
isCommon = true, | |
length = array.length, | |
result = [], | |
valuesLength = values.length; | |
if (!length) { | |
return result; | |
} | |
if (iteratee) { | |
values = _arrayMap(values, _baseUnary(iteratee)); | |
} | |
if (comparator) { | |
includes = _arrayIncludesWith; | |
isCommon = false; | |
} | |
else if (values.length >= _baseDifference_LARGE_ARRAY_SIZE) { | |
includes = _cacheHas; | |
isCommon = false; | |
values = new _SetCache(values); | |
} | |
outer: | |
while (++index < length) { | |
var value = array[index], | |
computed = iteratee ? iteratee(value) : value; | |
value = (comparator || value !== 0) ? value : 0; | |
if (isCommon && computed === computed) { | |
var valuesIndex = valuesLength; | |
while (valuesIndex--) { | |
if (values[valuesIndex] === computed) { | |
continue outer; | |
} | |
} | |
result.push(value); | |
} | |
else if (!includes(values, computed, comparator)) { | |
result.push(value); | |
} | |
} | |
return result; | |
} | |
/* harmony default export */ var _baseDifference = (baseDifference); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/difference.js | |
/** | |
* Creates an array of unique `array` values not included in the other given | |
* arrays using [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero) | |
* for equality comparisons. The order of result values is determined by the | |
* order they occur in the first array. | |
* | |
* @static | |
* @memberOf _ | |
* @since 0.1.0 | |
* @category Array | |
* @param {Array} array The array to inspect. | |
* @param {...Array} [values] The values to exclude. | |
* @returns {Array} Returns the new array of filtered values. | |
* @see _.without, _.xor | |
* @example | |
* | |
* _.difference([3, 2, 1], [4, 2]); | |
* // => [3, 1] | |
*/ | |
var difference_difference = lodash_rest(function(array, values) { | |
return lodash_isArrayLikeObject(array) | |
? _baseDifference(array, _baseFlatten(values, 1, lodash_isArrayLikeObject, true)) | |
: []; | |
}); | |
/* harmony default export */ var lodash_difference = (difference_difference); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseIsMatch.js | |
/** Used to compose bitmasks for comparison styles. */ | |
var _baseIsMatch_UNORDERED_COMPARE_FLAG = 1, | |
_baseIsMatch_PARTIAL_COMPARE_FLAG = 2; | |
/** | |
* The base implementation of `_.isMatch` without support for iteratee shorthands. | |
* | |
* @private | |
* @param {Object} object The object to inspect. | |
* @param {Object} source The object of property values to match. | |
* @param {Array} matchData The property names, values, and compare flags to match. | |
* @param {Function} [customizer] The function to customize comparisons. | |
* @returns {boolean} Returns `true` if `object` is a match, else `false`. | |
*/ | |
function baseIsMatch(object, source, matchData, customizer) { | |
var index = matchData.length, | |
length = index, | |
noCustomizer = !customizer; | |
if (object == null) { | |
return !length; | |
} | |
object = Object(object); | |
while (index--) { | |
var data = matchData[index]; | |
if ((noCustomizer && data[2]) | |
? data[1] !== object[data[0]] | |
: !(data[0] in object) | |
) { | |
return false; | |
} | |
} | |
while (++index < length) { | |
data = matchData[index]; | |
var key = data[0], | |
objValue = object[key], | |
srcValue = data[1]; | |
if (noCustomizer && data[2]) { | |
if (objValue === undefined && !(key in object)) { | |
return false; | |
} | |
} else { | |
var stack = new _Stack; | |
if (customizer) { | |
var result = customizer(objValue, srcValue, key, object, source, stack); | |
} | |
if (!(result === undefined | |
? _baseIsEqual(srcValue, objValue, customizer, _baseIsMatch_UNORDERED_COMPARE_FLAG | _baseIsMatch_PARTIAL_COMPARE_FLAG, stack) | |
: result | |
)) { | |
return false; | |
} | |
} | |
} | |
return true; | |
} | |
/* harmony default export */ var _baseIsMatch = (baseIsMatch); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_isStrictComparable.js | |
/** | |
* Checks if `value` is suitable for strict equality comparisons, i.e. `===`. | |
* | |
* @private | |
* @param {*} value The value to check. | |
* @returns {boolean} Returns `true` if `value` if suitable for strict | |
* equality comparisons, else `false`. | |
*/ | |
function isStrictComparable(value) { | |
return value === value && !lodash_isObject(value); | |
} | |
/* harmony default export */ var _isStrictComparable = (isStrictComparable); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseToPairs.js | |
/** | |
* The base implementation of `_.toPairs` and `_.toPairsIn` which creates an array | |
* of key-value pairs for `object` corresponding to the property names of `props`. | |
* | |
* @private | |
* @param {Object} object The object to query. | |
* @param {Array} props The property names to get values for. | |
* @returns {Object} Returns the key-value pairs. | |
*/ | |
function baseToPairs(object, props) { | |
return _arrayMap(props, function(key) { | |
return [key, object[key]]; | |
}); | |
} | |
/* harmony default export */ var _baseToPairs = (baseToPairs); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_setToPairs.js | |
/** | |
* Converts `set` to its value-value pairs. | |
* | |
* @private | |
* @param {Object} set The set to convert. | |
* @returns {Array} Returns the value-value pairs. | |
*/ | |
function setToPairs(set) { | |
var index = -1, | |
result = Array(set.size); | |
set.forEach(function(value) { | |
result[++index] = [value, value]; | |
}); | |
return result; | |
} | |
/* harmony default export */ var _setToPairs = (setToPairs); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_createToPairs.js | |
/** `Object#toString` result references. */ | |
var _createToPairs_mapTag = '[object Map]', | |
_createToPairs_setTag = '[object Set]'; | |
/** | |
* Creates a `_.toPairs` or `_.toPairsIn` function. | |
* | |
* @private | |
* @param {Function} keysFunc The function to get the keys of a given object. | |
* @returns {Function} Returns the new pairs function. | |
*/ | |
function createToPairs(keysFunc) { | |
return function(object) { | |
var tag = _getTag(object); | |
if (tag == _createToPairs_mapTag) { | |
return _mapToArray(object); | |
} | |
if (tag == _createToPairs_setTag) { | |
return _setToPairs(object); | |
} | |
return _baseToPairs(object, keysFunc(object)); | |
}; | |
} | |
/* harmony default export */ var _createToPairs = (createToPairs); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/toPairs.js | |
/** | |
* Creates an array of own enumerable string keyed-value pairs for `object` | |
* which can be consumed by `_.fromPairs`. If `object` is a map or set, its | |
* entries are returned. | |
* | |
* @static | |
* @memberOf _ | |
* @since 4.0.0 | |
* @alias entries | |
* @category Object | |
* @param {Object} object The object to query. | |
* @returns {Array} Returns the key-value pairs. | |
* @example | |
* | |
* function Foo() { | |
* this.a = 1; | |
* this.b = 2; | |
* } | |
* | |
* Foo.prototype.c = 3; | |
* | |
* _.toPairs(new Foo); | |
* // => [['a', 1], ['b', 2]] (iteration order is not guaranteed) | |
*/ | |
var toPairs = _createToPairs(lodash_keys); | |
/* harmony default export */ var lodash_toPairs = (toPairs); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_getMatchData.js | |
/** | |
* Gets the property names, values, and compare flags of `object`. | |
* | |
* @private | |
* @param {Object} object The object to query. | |
* @returns {Array} Returns the match data of `object`. | |
*/ | |
function getMatchData(object) { | |
var result = lodash_toPairs(object), | |
length = result.length; | |
while (length--) { | |
result[length][2] = _isStrictComparable(result[length][1]); | |
} | |
return result; | |
} | |
/* harmony default export */ var _getMatchData = (getMatchData); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_matchesStrictComparable.js | |
/** | |
* A specialized version of `matchesProperty` for source values suitable | |
* for strict equality comparisons, i.e. `===`. | |
* | |
* @private | |
* @param {string} key The key of the property to get. | |
* @param {*} srcValue The value to match. | |
* @returns {Function} Returns the new spec function. | |
*/ | |
function matchesStrictComparable(key, srcValue) { | |
return function(object) { | |
if (object == null) { | |
return false; | |
} | |
return object[key] === srcValue && | |
(srcValue !== undefined || (key in Object(object))); | |
}; | |
} | |
/* harmony default export */ var _matchesStrictComparable = (matchesStrictComparable); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseMatches.js | |
/** | |
* The base implementation of `_.matches` which doesn't clone `source`. | |
* | |
* @private | |
* @param {Object} source The object of property values to match. | |
* @returns {Function} Returns the new spec function. | |
*/ | |
function baseMatches(source) { | |
var matchData = _getMatchData(source); | |
if (matchData.length == 1 && matchData[0][2]) { | |
return _matchesStrictComparable(matchData[0][0], matchData[0][1]); | |
} | |
return function(object) { | |
return object === source || _baseIsMatch(object, source, matchData); | |
}; | |
} | |
/* harmony default export */ var _baseMatches = (baseMatches); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/memoize.js | |
/** Used as the `TypeError` message for "Functions" methods. */ | |
var memoize_FUNC_ERROR_TEXT = 'Expected a function'; | |
/** | |
* Creates a function that memoizes the result of `func`. If `resolver` is | |
* provided, it determines the cache key for storing the result based on the | |
* arguments provided to the memoized function. By default, the first argument | |
* provided to the memoized function is used as the map cache key. The `func` | |
* is invoked with the `this` binding of the memoized function. | |
* | |
* **Note:** The cache is exposed as the `cache` property on the memoized | |
* function. Its creation may be customized by replacing the `_.memoize.Cache` | |
* constructor with one whose instances implement the | |
* [`Map`](http://ecma-international.org/ecma-262/6.0/#sec-properties-of-the-map-prototype-object) | |
* method interface of `delete`, `get`, `has`, and `set`. | |
* | |
* @static | |
* @memberOf _ | |
* @since 0.1.0 | |
* @category Function | |
* @param {Function} func The function to have its output memoized. | |
* @param {Function} [resolver] The function to resolve the cache key. | |
* @returns {Function} Returns the new memoized function. | |
* @example | |
* | |
* var object = { 'a': 1, 'b': 2 }; | |
* var other = { 'c': 3, 'd': 4 }; | |
* | |
* var values = _.memoize(_.values); | |
* values(object); | |
* // => [1, 2] | |
* | |
* values(other); | |
* // => [3, 4] | |
* | |
* object.a = 2; | |
* values(object); | |
* // => [1, 2] | |
* | |
* // Modify the result cache. | |
* values.cache.set(object, ['a', 'b']); | |
* values(object); | |
* // => ['a', 'b'] | |
* | |
* // Replace `_.memoize.Cache`. | |
* _.memoize.Cache = WeakMap; | |
*/ | |
function memoize(func, resolver) { | |
if (typeof func != 'function' || (resolver && typeof resolver != 'function')) { | |
throw new TypeError(memoize_FUNC_ERROR_TEXT); | |
} | |
var memoized = function() { | |
var args = arguments, | |
key = resolver ? resolver.apply(this, args) : args[0], | |
cache = memoized.cache; | |
if (cache.has(key)) { | |
return cache.get(key); | |
} | |
var result = func.apply(this, args); | |
memoized.cache = cache.set(key, result); | |
return result; | |
}; | |
memoized.cache = new (memoize.Cache || _MapCache); | |
return memoized; | |
} | |
// Assign cache to `_.memoize`. | |
memoize.Cache = _MapCache; | |
/* harmony default export */ var lodash_memoize = (memoize); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseToString.js | |
/** Used as references for various `Number` constants. */ | |
var _baseToString_INFINITY = 1 / 0; | |
/** Used to convert symbols to primitives and strings. */ | |
var _baseToString_symbolProto = _Symbol ? _Symbol.prototype : undefined, | |
symbolToString = _baseToString_symbolProto ? _baseToString_symbolProto.toString : undefined; | |
/** | |
* The base implementation of `_.toString` which doesn't convert nullish | |
* values to empty strings. | |
* | |
* @private | |
* @param {*} value The value to process. | |
* @returns {string} Returns the string. | |
*/ | |
function baseToString(value) { | |
// Exit early for strings to avoid a performance hit in some environments. | |
if (typeof value == 'string') { | |
return value; | |
} | |
if (lodash_isSymbol(value)) { | |
return symbolToString ? symbolToString.call(value) : ''; | |
} | |
var result = (value + ''); | |
return (result == '0' && (1 / value) == -_baseToString_INFINITY) ? '-0' : result; | |
} | |
/* harmony default export */ var _baseToString = (baseToString); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/toString.js | |
/** | |
* Converts `value` to a string. An empty string is returned for `null` | |
* and `undefined` values. The sign of `-0` is preserved. | |
* | |
* @static | |
* @memberOf _ | |
* @since 4.0.0 | |
* @category Lang | |
* @param {*} value The value to process. | |
* @returns {string} Returns the string. | |
* @example | |
* | |
* _.toString(null); | |
* // => '' | |
* | |
* _.toString(-0); | |
* // => '-0' | |
* | |
* _.toString([1, 2, 3]); | |
* // => '1,2,3' | |
*/ | |
function toString_toString(value) { | |
return value == null ? '' : _baseToString(value); | |
} | |
/* harmony default export */ var lodash_toString = (toString_toString); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_stringToPath.js | |
/** Used to match property names within property paths. */ | |
var rePropName = /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]/g; | |
/** Used to match backslashes in property paths. */ | |
var reEscapeChar = /\\(\\)?/g; | |
/** | |
* Converts `string` to a property path array. | |
* | |
* @private | |
* @param {string} string The string to convert. | |
* @returns {Array} Returns the property path array. | |
*/ | |
var stringToPath = lodash_memoize(function(string) { | |
var result = []; | |
lodash_toString(string).replace(rePropName, function(match, number, quote, string) { | |
result.push(quote ? string.replace(reEscapeChar, '$1') : (number || match)); | |
}); | |
return result; | |
}); | |
/* harmony default export */ var _stringToPath = (stringToPath); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_castPath.js | |
/** | |
* Casts `value` to a path array if it's not one. | |
* | |
* @private | |
* @param {*} value The value to inspect. | |
* @returns {Array} Returns the cast property path array. | |
*/ | |
function castPath(value) { | |
return lodash_isArray(value) ? value : _stringToPath(value); | |
} | |
/* harmony default export */ var _castPath = (castPath); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_isKey.js | |
/** Used to match property names within property paths. */ | |
var reIsDeepProp = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/, | |
reIsPlainProp = /^\w*$/; | |
/** | |
* Checks if `value` is a property name and not a property path. | |
* | |
* @private | |
* @param {*} value The value to check. | |
* @param {Object} [object] The object to query keys on. | |
* @returns {boolean} Returns `true` if `value` is a property name, else `false`. | |
*/ | |
function isKey(value, object) { | |
if (lodash_isArray(value)) { | |
return false; | |
} | |
var type = typeof value; | |
if (type == 'number' || type == 'symbol' || type == 'boolean' || | |
value == null || lodash_isSymbol(value)) { | |
return true; | |
} | |
return reIsPlainProp.test(value) || !reIsDeepProp.test(value) || | |
(object != null && value in Object(object)); | |
} | |
/* harmony default export */ var _isKey = (isKey); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_toKey.js | |
/** Used as references for various `Number` constants. */ | |
var _toKey_INFINITY = 1 / 0; | |
/** | |
* Converts `value` to a string key if it's not a string or symbol. | |
* | |
* @private | |
* @param {*} value The value to inspect. | |
* @returns {string|symbol} Returns the key. | |
*/ | |
function toKey(value) { | |
if (typeof value == 'string' || lodash_isSymbol(value)) { | |
return value; | |
} | |
var result = (value + ''); | |
return (result == '0' && (1 / value) == -_toKey_INFINITY) ? '-0' : result; | |
} | |
/* harmony default export */ var _toKey = (toKey); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseGet.js | |
/** | |
* The base implementation of `_.get` without support for default values. | |
* | |
* @private | |
* @param {Object} object The object to query. | |
* @param {Array|string} path The path of the property to get. | |
* @returns {*} Returns the resolved value. | |
*/ | |
function baseGet(object, path) { | |
path = _isKey(path, object) ? [path] : _castPath(path); | |
var index = 0, | |
length = path.length; | |
while (object != null && index < length) { | |
object = object[_toKey(path[index++])]; | |
} | |
return (index && index == length) ? object : undefined; | |
} | |
/* harmony default export */ var _baseGet = (baseGet); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/get.js | |
/** | |
* Gets the value at `path` of `object`. If the resolved value is | |
* `undefined`, the `defaultValue` is used in its place. | |
* | |
* @static | |
* @memberOf _ | |
* @since 3.7.0 | |
* @category Object | |
* @param {Object} object The object to query. | |
* @param {Array|string} path The path of the property to get. | |
* @param {*} [defaultValue] The value returned for `undefined` resolved values. | |
* @returns {*} Returns the resolved value. | |
* @example | |
* | |
* var object = { 'a': [{ 'b': { 'c': 3 } }] }; | |
* | |
* _.get(object, 'a[0].b.c'); | |
* // => 3 | |
* | |
* _.get(object, ['a', '0', 'b', 'c']); | |
* // => 3 | |
* | |
* _.get(object, 'a.b.c', 'default'); | |
* // => 'default' | |
*/ | |
function get(object, path, defaultValue) { | |
var result = object == null ? undefined : _baseGet(object, path); | |
return result === undefined ? defaultValue : result; | |
} | |
/* harmony default export */ var lodash_get = (get); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseHasIn.js | |
/** | |
* The base implementation of `_.hasIn` without support for deep paths. | |
* | |
* @private | |
* @param {Object} object The object to query. | |
* @param {Array|string} key The key to check. | |
* @returns {boolean} Returns `true` if `key` exists, else `false`. | |
*/ | |
function baseHasIn(object, key) { | |
return key in Object(object); | |
} | |
/* harmony default export */ var _baseHasIn = (baseHasIn); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_hasPath.js | |
/** | |
* Checks if `path` exists on `object`. | |
* | |
* @private | |
* @param {Object} object The object to query. | |
* @param {Array|string} path The path to check. | |
* @param {Function} hasFunc The function to check properties. | |
* @returns {boolean} Returns `true` if `path` exists, else `false`. | |
*/ | |
function hasPath(object, path, hasFunc) { | |
path = _isKey(path, object) ? [path] : _castPath(path); | |
var result, | |
index = -1, | |
length = path.length; | |
while (++index < length) { | |
var key = _toKey(path[index]); | |
if (!(result = object != null && hasFunc(object, key))) { | |
break; | |
} | |
object = object[key]; | |
} | |
if (result) { | |
return result; | |
} | |
var length = object ? object.length : 0; | |
return !!length && lodash_isLength(length) && _isIndex(key, length) && | |
(lodash_isArray(object) || lodash_isString(object) || lodash_isArguments(object)); | |
} | |
/* harmony default export */ var _hasPath = (hasPath); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/hasIn.js | |
/** | |
* Checks if `path` is a direct or inherited property of `object`. | |
* | |
* @static | |
* @memberOf _ | |
* @since 4.0.0 | |
* @category Object | |
* @param {Object} object The object to query. | |
* @param {Array|string} path The path to check. | |
* @returns {boolean} Returns `true` if `path` exists, else `false`. | |
* @example | |
* | |
* var object = _.create({ 'a': _.create({ 'b': 2 }) }); | |
* | |
* _.hasIn(object, 'a'); | |
* // => true | |
* | |
* _.hasIn(object, 'a.b'); | |
* // => true | |
* | |
* _.hasIn(object, ['a', 'b']); | |
* // => true | |
* | |
* _.hasIn(object, 'b'); | |
* // => false | |
*/ | |
function hasIn(object, path) { | |
return object != null && _hasPath(object, path, _baseHasIn); | |
} | |
/* harmony default export */ var lodash_hasIn = (hasIn); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseMatchesProperty.js | |
/** Used to compose bitmasks for comparison styles. */ | |
var _baseMatchesProperty_UNORDERED_COMPARE_FLAG = 1, | |
_baseMatchesProperty_PARTIAL_COMPARE_FLAG = 2; | |
/** | |
* The base implementation of `_.matchesProperty` which doesn't clone `srcValue`. | |
* | |
* @private | |
* @param {string} path The path of the property to get. | |
* @param {*} srcValue The value to match. | |
* @returns {Function} Returns the new spec function. | |
*/ | |
function baseMatchesProperty(path, srcValue) { | |
if (_isKey(path) && _isStrictComparable(srcValue)) { | |
return _matchesStrictComparable(_toKey(path), srcValue); | |
} | |
return function(object) { | |
var objValue = lodash_get(object, path); | |
return (objValue === undefined && objValue === srcValue) | |
? lodash_hasIn(object, path) | |
: _baseIsEqual(srcValue, objValue, undefined, _baseMatchesProperty_UNORDERED_COMPARE_FLAG | _baseMatchesProperty_PARTIAL_COMPARE_FLAG); | |
}; | |
} | |
/* harmony default export */ var _baseMatchesProperty = (baseMatchesProperty); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/identity.js | |
/** | |
* This method returns the first argument given to it. | |
* | |
* @static | |
* @since 0.1.0 | |
* @memberOf _ | |
* @category Util | |
* @param {*} value Any value. | |
* @returns {*} Returns `value`. | |
* @example | |
* | |
* var object = { 'user': 'fred' }; | |
* | |
* _.identity(object) === object; | |
* // => true | |
*/ | |
function identity(value) { | |
return value; | |
} | |
/* harmony default export */ var lodash_identity = (identity); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_basePropertyDeep.js | |
/** | |
* A specialized version of `baseProperty` which supports deep paths. | |
* | |
* @private | |
* @param {Array|string} path The path of the property to get. | |
* @returns {Function} Returns the new accessor function. | |
*/ | |
function basePropertyDeep(path) { | |
return function(object) { | |
return _baseGet(object, path); | |
}; | |
} | |
/* harmony default export */ var _basePropertyDeep = (basePropertyDeep); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/property.js | |
/** | |
* Creates a function that returns the value at `path` of a given object. | |
* | |
* @static | |
* @memberOf _ | |
* @since 2.4.0 | |
* @category Util | |
* @param {Array|string} path The path of the property to get. | |
* @returns {Function} Returns the new accessor function. | |
* @example | |
* | |
* var objects = [ | |
* { 'a': { 'b': 2 } }, | |
* { 'a': { 'b': 1 } } | |
* ]; | |
* | |
* _.map(objects, _.property('a.b')); | |
* // => [2, 1] | |
* | |
* _.map(_.sortBy(objects, _.property(['a', 'b'])), 'a.b'); | |
* // => [1, 2] | |
*/ | |
function property_property(path) { | |
return _isKey(path) ? _baseProperty(_toKey(path)) : _basePropertyDeep(path); | |
} | |
/* harmony default export */ var lodash_property = (property_property); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseIteratee.js | |
/** | |
* The base implementation of `_.iteratee`. | |
* | |
* @private | |
* @param {*} [value=_.identity] The value to convert to an iteratee. | |
* @returns {Function} Returns the iteratee. | |
*/ | |
function baseIteratee(value) { | |
// Don't store the `typeof` result in a variable to avoid a JIT bug in Safari 9. | |
// See https://bugs.webkit.org/show_bug.cgi?id=156034 for more details. | |
if (typeof value == 'function') { | |
return value; | |
} | |
if (value == null) { | |
return lodash_identity; | |
} | |
if (typeof value == 'object') { | |
return lodash_isArray(value) | |
? _baseMatchesProperty(value[0], value[1]) | |
: _baseMatches(value); | |
} | |
return lodash_property(value); | |
} | |
/* harmony default export */ var _baseIteratee = (baseIteratee); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/differenceBy.js | |
/** | |
* This method is like `_.difference` except that it accepts `iteratee` which | |
* is invoked for each element of `array` and `values` to generate the criterion | |
* by which they're compared. Result values are chosen from the first array. | |
* The iteratee is invoked with one argument: (value). | |
* | |
* @static | |
* @memberOf _ | |
* @since 4.0.0 | |
* @category Array | |
* @param {Array} array The array to inspect. | |
* @param {...Array} [values] The values to exclude. | |
* @param {Array|Function|Object|string} [iteratee=_.identity] | |
* The iteratee invoked per element. | |
* @returns {Array} Returns the new array of filtered values. | |
* @example | |
* | |
* _.differenceBy([3.1, 2.2, 1.3], [4.4, 2.5], Math.floor); | |
* // => [3.1, 1.3] | |
* | |
* // The `_.property` iteratee shorthand. | |
* _.differenceBy([{ 'x': 2 }, { 'x': 1 }], [{ 'x': 1 }], 'x'); | |
* // => [{ 'x': 2 }] | |
*/ | |
var differenceBy = lodash_rest(function(array, values) { | |
var iteratee = lodash_last(values); | |
if (lodash_isArrayLikeObject(iteratee)) { | |
iteratee = undefined; | |
} | |
return lodash_isArrayLikeObject(array) | |
? _baseDifference(array, _baseFlatten(values, 1, lodash_isArrayLikeObject, true), _baseIteratee(iteratee)) | |
: []; | |
}); | |
/* harmony default export */ var lodash_differenceBy = (differenceBy); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/differenceWith.js | |
/** | |
* This method is like `_.difference` except that it accepts `comparator` | |
* which is invoked to compare elements of `array` to `values`. Result values | |
* are chosen from the first array. The comparator is invoked with two arguments: | |
* (arrVal, othVal). | |
* | |
* @static | |
* @memberOf _ | |
* @since 4.0.0 | |
* @category Array | |
* @param {Array} array The array to inspect. | |
* @param {...Array} [values] The values to exclude. | |
* @param {Function} [comparator] The comparator invoked per element. | |
* @returns {Array} Returns the new array of filtered values. | |
* @example | |
* | |
* var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }]; | |
* | |
* _.differenceWith(objects, [{ 'x': 1, 'y': 2 }], _.isEqual); | |
* // => [{ 'x': 2, 'y': 1 }] | |
*/ | |
var differenceWith = lodash_rest(function(array, values) { | |
var comparator = lodash_last(values); | |
if (lodash_isArrayLikeObject(comparator)) { | |
comparator = undefined; | |
} | |
return lodash_isArrayLikeObject(array) | |
? _baseDifference(array, _baseFlatten(values, 1, lodash_isArrayLikeObject, true), undefined, comparator) | |
: []; | |
}); | |
/* harmony default export */ var lodash_differenceWith = (differenceWith); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/drop.js | |
/** | |
* Creates a slice of `array` with `n` elements dropped from the beginning. | |
* | |
* @static | |
* @memberOf _ | |
* @since 0.5.0 | |
* @category Array | |
* @param {Array} array The array to query. | |
* @param {number} [n=1] The number of elements to drop. | |
* @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. | |
* @returns {Array} Returns the slice of `array`. | |
* @example | |
* | |
* _.drop([1, 2, 3]); | |
* // => [2, 3] | |
* | |
* _.drop([1, 2, 3], 2); | |
* // => [3] | |
* | |
* _.drop([1, 2, 3], 5); | |
* // => [] | |
* | |
* _.drop([1, 2, 3], 0); | |
* // => [1, 2, 3] | |
*/ | |
function drop(array, n, guard) { | |
var length = array ? array.length : 0; | |
if (!length) { | |
return []; | |
} | |
n = (guard || n === undefined) ? 1 : lodash_toInteger(n); | |
return _baseSlice(array, n < 0 ? 0 : n, length); | |
} | |
/* harmony default export */ var lodash_drop = (drop); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/dropRight.js | |
/** | |
* Creates a slice of `array` with `n` elements dropped from the end. | |
* | |
* @static | |
* @memberOf _ | |
* @since 3.0.0 | |
* @category Array | |
* @param {Array} array The array to query. | |
* @param {number} [n=1] The number of elements to drop. | |
* @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. | |
* @returns {Array} Returns the slice of `array`. | |
* @example | |
* | |
* _.dropRight([1, 2, 3]); | |
* // => [1, 2] | |
* | |
* _.dropRight([1, 2, 3], 2); | |
* // => [1] | |
* | |
* _.dropRight([1, 2, 3], 5); | |
* // => [] | |
* | |
* _.dropRight([1, 2, 3], 0); | |
* // => [1, 2, 3] | |
*/ | |
function dropRight(array, n, guard) { | |
var length = array ? array.length : 0; | |
if (!length) { | |
return []; | |
} | |
n = (guard || n === undefined) ? 1 : lodash_toInteger(n); | |
n = length - n; | |
return _baseSlice(array, 0, n < 0 ? 0 : n); | |
} | |
/* harmony default export */ var lodash_dropRight = (dropRight); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseWhile.js | |
/** | |
* The base implementation of methods like `_.dropWhile` and `_.takeWhile` | |
* without support for iteratee shorthands. | |
* | |
* @private | |
* @param {Array} array The array to query. | |
* @param {Function} predicate The function invoked per iteration. | |
* @param {boolean} [isDrop] Specify dropping elements instead of taking them. | |
* @param {boolean} [fromRight] Specify iterating from right to left. | |
* @returns {Array} Returns the slice of `array`. | |
*/ | |
function baseWhile(array, predicate, isDrop, fromRight) { | |
var length = array.length, | |
index = fromRight ? length : -1; | |
while ((fromRight ? index-- : ++index < length) && | |
predicate(array[index], index, array)) {} | |
return isDrop | |
? _baseSlice(array, (fromRight ? 0 : index), (fromRight ? index + 1 : length)) | |
: _baseSlice(array, (fromRight ? index + 1 : 0), (fromRight ? length : index)); | |
} | |
/* harmony default export */ var _baseWhile = (baseWhile); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/dropRightWhile.js | |
/** | |
* Creates a slice of `array` excluding elements dropped from the end. | |
* Elements are dropped until `predicate` returns falsey. The predicate is | |
* invoked with three arguments: (value, index, array). | |
* | |
* @static | |
* @memberOf _ | |
* @since 3.0.0 | |
* @category Array | |
* @param {Array} array The array to query. | |
* @param {Array|Function|Object|string} [predicate=_.identity] | |
* The function invoked per iteration. | |
* @returns {Array} Returns the slice of `array`. | |
* @example | |
* | |
* var users = [ | |
* { 'user': 'barney', 'active': true }, | |
* { 'user': 'fred', 'active': false }, | |
* { 'user': 'pebbles', 'active': false } | |
* ]; | |
* | |
* _.dropRightWhile(users, function(o) { return !o.active; }); | |
* // => objects for ['barney'] | |
* | |
* // The `_.matches` iteratee shorthand. | |
* _.dropRightWhile(users, { 'user': 'pebbles', 'active': false }); | |
* // => objects for ['barney', 'fred'] | |
* | |
* // The `_.matchesProperty` iteratee shorthand. | |
* _.dropRightWhile(users, ['active', false]); | |
* // => objects for ['barney'] | |
* | |
* // The `_.property` iteratee shorthand. | |
* _.dropRightWhile(users, 'active'); | |
* // => objects for ['barney', 'fred', 'pebbles'] | |
*/ | |
function dropRightWhile(array, predicate) { | |
return (array && array.length) | |
? _baseWhile(array, _baseIteratee(predicate, 3), true, true) | |
: []; | |
} | |
/* harmony default export */ var lodash_dropRightWhile = (dropRightWhile); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/dropWhile.js | |
/** | |
* Creates a slice of `array` excluding elements dropped from the beginning. | |
* Elements are dropped until `predicate` returns falsey. The predicate is | |
* invoked with three arguments: (value, index, array). | |
* | |
* @static | |
* @memberOf _ | |
* @since 3.0.0 | |
* @category Array | |
* @param {Array} array The array to query. | |
* @param {Array|Function|Object|string} [predicate=_.identity] | |
* The function invoked per iteration. | |
* @returns {Array} Returns the slice of `array`. | |
* @example | |
* | |
* var users = [ | |
* { 'user': 'barney', 'active': false }, | |
* { 'user': 'fred', 'active': false }, | |
* { 'user': 'pebbles', 'active': true } | |
* ]; | |
* | |
* _.dropWhile(users, function(o) { return !o.active; }); | |
* // => objects for ['pebbles'] | |
* | |
* // The `_.matches` iteratee shorthand. | |
* _.dropWhile(users, { 'user': 'barney', 'active': false }); | |
* // => objects for ['fred', 'pebbles'] | |
* | |
* // The `_.matchesProperty` iteratee shorthand. | |
* _.dropWhile(users, ['active', false]); | |
* // => objects for ['pebbles'] | |
* | |
* // The `_.property` iteratee shorthand. | |
* _.dropWhile(users, 'active'); | |
* // => objects for ['barney', 'fred', 'pebbles'] | |
*/ | |
function dropWhile(array, predicate) { | |
return (array && array.length) | |
? _baseWhile(array, _baseIteratee(predicate, 3), true) | |
: []; | |
} | |
/* harmony default export */ var lodash_dropWhile = (dropWhile); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseClamp.js | |
/** | |
* The base implementation of `_.clamp` which doesn't coerce arguments to numbers. | |
* | |
* @private | |
* @param {number} number The number to clamp. | |
* @param {number} [lower] The lower bound. | |
* @param {number} upper The upper bound. | |
* @returns {number} Returns the clamped number. | |
*/ | |
function baseClamp(number, lower, upper) { | |
if (number === number) { | |
if (upper !== undefined) { | |
number = number <= upper ? number : upper; | |
} | |
if (lower !== undefined) { | |
number = number >= lower ? number : lower; | |
} | |
} | |
return number; | |
} | |
/* harmony default export */ var _baseClamp = (baseClamp); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/toLength.js | |
/** Used as references for the maximum length and index of an array. */ | |
var MAX_ARRAY_LENGTH = 4294967295; | |
/** | |
* Converts `value` to an integer suitable for use as the length of an | |
* array-like object. | |
* | |
* **Note:** This method is based on | |
* [`ToLength`](http://ecma-international.org/ecma-262/6.0/#sec-tolength). | |
* | |
* @static | |
* @memberOf _ | |
* @since 4.0.0 | |
* @category Lang | |
* @param {*} value The value to convert. | |
* @returns {number} Returns the converted integer. | |
* @example | |
* | |
* _.toLength(3.2); | |
* // => 3 | |
* | |
* _.toLength(Number.MIN_VALUE); | |
* // => 0 | |
* | |
* _.toLength(Infinity); | |
* // => 4294967295 | |
* | |
* _.toLength('3.2'); | |
* // => 3 | |
*/ | |
function toLength(value) { | |
return value ? _baseClamp(lodash_toInteger(value), 0, MAX_ARRAY_LENGTH) : 0; | |
} | |
/* harmony default export */ var lodash_toLength = (toLength); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseFill.js | |
/** | |
* The base implementation of `_.fill` without an iteratee call guard. | |
* | |
* @private | |
* @param {Array} array The array to fill. | |
* @param {*} value The value to fill `array` with. | |
* @param {number} [start=0] The start position. | |
* @param {number} [end=array.length] The end position. | |
* @returns {Array} Returns `array`. | |
*/ | |
function baseFill(array, value, start, end) { | |
var length = array.length; | |
start = lodash_toInteger(start); | |
if (start < 0) { | |
start = -start > length ? 0 : (length + start); | |
} | |
end = (end === undefined || end > length) ? length : lodash_toInteger(end); | |
if (end < 0) { | |
end += length; | |
} | |
end = start > end ? 0 : lodash_toLength(end); | |
while (start < end) { | |
array[start++] = value; | |
} | |
return array; | |
} | |
/* harmony default export */ var _baseFill = (baseFill); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/fill.js | |
/** | |
* Fills elements of `array` with `value` from `start` up to, but not | |
* including, `end`. | |
* | |
* **Note:** This method mutates `array`. | |
* | |
* @static | |
* @memberOf _ | |
* @since 3.2.0 | |
* @category Array | |
* @param {Array} array The array to fill. | |
* @param {*} value The value to fill `array` with. | |
* @param {number} [start=0] The start position. | |
* @param {number} [end=array.length] The end position. | |
* @returns {Array} Returns `array`. | |
* @example | |
* | |
* var array = [1, 2, 3]; | |
* | |
* _.fill(array, 'a'); | |
* console.log(array); | |
* // => ['a', 'a', 'a'] | |
* | |
* _.fill(Array(3), 2); | |
* // => [2, 2, 2] | |
* | |
* _.fill([4, 6, 8, 10], '*', 1, 3); | |
* // => [4, '*', '*', 10] | |
*/ | |
function fill(array, value, start, end) { | |
var length = array ? array.length : 0; | |
if (!length) { | |
return []; | |
} | |
if (start && typeof start != 'number' && _isIterateeCall(array, value, start)) { | |
start = 0; | |
end = length; | |
} | |
return _baseFill(array, value, start, end); | |
} | |
/* harmony default export */ var lodash_fill = (fill); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseFindIndex.js | |
/** | |
* The base implementation of `_.findIndex` and `_.findLastIndex` without | |
* support for iteratee shorthands. | |
* | |
* @private | |
* @param {Array} array The array to search. | |
* @param {Function} predicate The function invoked per iteration. | |
* @param {boolean} [fromRight] Specify iterating from right to left. | |
* @returns {number} Returns the index of the matched value, else `-1`. | |
*/ | |
function baseFindIndex(array, predicate, fromRight) { | |
var length = array.length, | |
index = fromRight ? length : -1; | |
while ((fromRight ? index-- : ++index < length)) { | |
if (predicate(array[index], index, array)) { | |
return index; | |
} | |
} | |
return -1; | |
} | |
/* harmony default export */ var _baseFindIndex = (baseFindIndex); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/findIndex.js | |
/** | |
* This method is like `_.find` except that it returns the index of the first | |
* element `predicate` returns truthy for instead of the element itself. | |
* | |
* @static | |
* @memberOf _ | |
* @since 1.1.0 | |
* @category Array | |
* @param {Array} array The array to search. | |
* @param {Array|Function|Object|string} [predicate=_.identity] | |
* The function invoked per iteration. | |
* @returns {number} Returns the index of the found element, else `-1`. | |
* @example | |
* | |
* var users = [ | |
* { 'user': 'barney', 'active': false }, | |
* { 'user': 'fred', 'active': false }, | |
* { 'user': 'pebbles', 'active': true } | |
* ]; | |
* | |
* _.findIndex(users, function(o) { return o.user == 'barney'; }); | |
* // => 0 | |
* | |
* // The `_.matches` iteratee shorthand. | |
* _.findIndex(users, { 'user': 'fred', 'active': false }); | |
* // => 1 | |
* | |
* // The `_.matchesProperty` iteratee shorthand. | |
* _.findIndex(users, ['active', false]); | |
* // => 0 | |
* | |
* // The `_.property` iteratee shorthand. | |
* _.findIndex(users, 'active'); | |
* // => 2 | |
*/ | |
function findIndex(array, predicate) { | |
return (array && array.length) | |
? _baseFindIndex(array, _baseIteratee(predicate, 3)) | |
: -1; | |
} | |
/* harmony default export */ var lodash_findIndex = (findIndex); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/findLastIndex.js | |
/** | |
* This method is like `_.findIndex` except that it iterates over elements | |
* of `collection` from right to left. | |
* | |
* @static | |
* @memberOf _ | |
* @since 2.0.0 | |
* @category Array | |
* @param {Array} array The array to search. | |
* @param {Array|Function|Object|string} [predicate=_.identity] | |
* The function invoked per iteration. | |
* @returns {number} Returns the index of the found element, else `-1`. | |
* @example | |
* | |
* var users = [ | |
* { 'user': 'barney', 'active': true }, | |
* { 'user': 'fred', 'active': false }, | |
* { 'user': 'pebbles', 'active': false } | |
* ]; | |
* | |
* _.findLastIndex(users, function(o) { return o.user == 'pebbles'; }); | |
* // => 2 | |
* | |
* // The `_.matches` iteratee shorthand. | |
* _.findLastIndex(users, { 'user': 'barney', 'active': true }); | |
* // => 0 | |
* | |
* // The `_.matchesProperty` iteratee shorthand. | |
* _.findLastIndex(users, ['active', false]); | |
* // => 2 | |
* | |
* // The `_.property` iteratee shorthand. | |
* _.findLastIndex(users, 'active'); | |
* // => 0 | |
*/ | |
function findLastIndex(array, predicate) { | |
return (array && array.length) | |
? _baseFindIndex(array, _baseIteratee(predicate, 3), true) | |
: -1; | |
} | |
/* harmony default export */ var lodash_findLastIndex = (findLastIndex); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/head.js | |
/** | |
* Gets the first element of `array`. | |
* | |
* @static | |
* @memberOf _ | |
* @since 0.1.0 | |
* @alias first | |
* @category Array | |
* @param {Array} array The array to query. | |
* @returns {*} Returns the first element of `array`. | |
* @example | |
* | |
* _.head([1, 2, 3]); | |
* // => 1 | |
* | |
* _.head([]); | |
* // => undefined | |
*/ | |
function head(array) { | |
return (array && array.length) ? array[0] : undefined; | |
} | |
/* harmony default export */ var lodash_head = (head); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/first.js | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/flatten.js | |
/** | |
* Flattens `array` a single level deep. | |
* | |
* @static | |
* @memberOf _ | |
* @since 0.1.0 | |
* @category Array | |
* @param {Array} array The array to flatten. | |
* @returns {Array} Returns the new flattened array. | |
* @example | |
* | |
* _.flatten([1, [2, [3, [4]], 5]]); | |
* // => [1, 2, [3, [4]], 5] | |
*/ | |
function flatten(array) { | |
var length = array ? array.length : 0; | |
return length ? _baseFlatten(array, 1) : []; | |
} | |
/* harmony default export */ var lodash_flatten = (flatten); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/flattenDeep.js | |
/** Used as references for various `Number` constants. */ | |
var flattenDeep_INFINITY = 1 / 0; | |
/** | |
* Recursively flattens `array`. | |
* | |
* @static | |
* @memberOf _ | |
* @since 3.0.0 | |
* @category Array | |
* @param {Array} array The array to flatten. | |
* @returns {Array} Returns the new flattened array. | |
* @example | |
* | |
* _.flattenDeep([1, [2, [3, [4]], 5]]); | |
* // => [1, 2, 3, 4, 5] | |
*/ | |
function flattenDeep(array) { | |
var length = array ? array.length : 0; | |
return length ? _baseFlatten(array, flattenDeep_INFINITY) : []; | |
} | |
/* harmony default export */ var lodash_flattenDeep = (flattenDeep); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/flattenDepth.js | |
/** | |
* Recursively flatten `array` up to `depth` times. | |
* | |
* @static | |
* @memberOf _ | |
* @since 4.4.0 | |
* @category Array | |
* @param {Array} array The array to flatten. | |
* @param {number} [depth=1] The maximum recursion depth. | |
* @returns {Array} Returns the new flattened array. | |
* @example | |
* | |
* var array = [1, [2, [3, [4]], 5]]; | |
* | |
* _.flattenDepth(array, 1); | |
* // => [1, 2, [3, [4]], 5] | |
* | |
* _.flattenDepth(array, 2); | |
* // => [1, 2, 3, [4], 5] | |
*/ | |
function flattenDepth(array, depth) { | |
var length = array ? array.length : 0; | |
if (!length) { | |
return []; | |
} | |
depth = depth === undefined ? 1 : lodash_toInteger(depth); | |
return _baseFlatten(array, depth); | |
} | |
/* harmony default export */ var lodash_flattenDepth = (flattenDepth); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/fromPairs.js | |
/** | |
* The inverse of `_.toPairs`; this method returns an object composed | |
* from key-value `pairs`. | |
* | |
* @static | |
* @memberOf _ | |
* @since 4.0.0 | |
* @category Array | |
* @param {Array} pairs The key-value pairs. | |
* @returns {Object} Returns the new object. | |
* @example | |
* | |
* _.fromPairs([['fred', 30], ['barney', 40]]); | |
* // => { 'fred': 30, 'barney': 40 } | |
*/ | |
function fromPairs(pairs) { | |
var index = -1, | |
length = pairs ? pairs.length : 0, | |
result = {}; | |
while (++index < length) { | |
var pair = pairs[index]; | |
result[pair[0]] = pair[1]; | |
} | |
return result; | |
} | |
/* harmony default export */ var lodash_fromPairs = (fromPairs); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/indexOf.js | |
/* Built-in method references for those with the same name as other `lodash` methods. */ | |
var indexOf_nativeMax = Math.max; | |
/** | |
* Gets the index at which the first occurrence of `value` is found in `array` | |
* using [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero) | |
* for equality comparisons. If `fromIndex` is negative, it's used as the | |
* offset from the end of `array`. | |
* | |
* @static | |
* @memberOf _ | |
* @since 0.1.0 | |
* @category Array | |
* @param {Array} array The array to search. | |
* @param {*} value The value to search for. | |
* @param {number} [fromIndex=0] The index to search from. | |
* @returns {number} Returns the index of the matched value, else `-1`. | |
* @example | |
* | |
* _.indexOf([1, 2, 1, 2], 2); | |
* // => 1 | |
* | |
* // Search from the `fromIndex`. | |
* _.indexOf([1, 2, 1, 2], 2, 2); | |
* // => 3 | |
*/ | |
function indexOf_indexOf(array, value, fromIndex) { | |
var length = array ? array.length : 0; | |
if (!length) { | |
return -1; | |
} | |
fromIndex = lodash_toInteger(fromIndex); | |
if (fromIndex < 0) { | |
fromIndex = indexOf_nativeMax(length + fromIndex, 0); | |
} | |
return _baseIndexOf(array, value, fromIndex); | |
} | |
/* harmony default export */ var lodash_indexOf = (indexOf_indexOf); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/initial.js | |
/** | |
* Gets all but the last element of `array`. | |
* | |
* @static | |
* @memberOf _ | |
* @since 0.1.0 | |
* @category Array | |
* @param {Array} array The array to query. | |
* @returns {Array} Returns the slice of `array`. | |
* @example | |
* | |
* _.initial([1, 2, 3]); | |
* // => [1, 2] | |
*/ | |
function initial(array) { | |
return lodash_dropRight(array, 1); | |
} | |
/* harmony default export */ var lodash_initial = (initial); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseIntersection.js | |
/* Built-in method references for those with the same name as other `lodash` methods. */ | |
var nativeMin = Math.min; | |
/** | |
* The base implementation of methods like `_.intersection`, without support | |
* for iteratee shorthands, that accepts an array of arrays to inspect. | |
* | |
* @private | |
* @param {Array} arrays The arrays to inspect. | |
* @param {Function} [iteratee] The iteratee invoked per element. | |
* @param {Function} [comparator] The comparator invoked per element. | |
* @returns {Array} Returns the new array of shared values. | |
*/ | |
function baseIntersection(arrays, iteratee, comparator) { | |
var includes = comparator ? _arrayIncludesWith : _arrayIncludes, | |
length = arrays[0].length, | |
othLength = arrays.length, | |
othIndex = othLength, | |
caches = Array(othLength), | |
maxLength = Infinity, | |
result = []; | |
while (othIndex--) { | |
var array = arrays[othIndex]; | |
if (othIndex && iteratee) { | |
array = _arrayMap(array, _baseUnary(iteratee)); | |
} | |
maxLength = nativeMin(array.length, maxLength); | |
caches[othIndex] = !comparator && (iteratee || (length >= 120 && array.length >= 120)) | |
? new _SetCache(othIndex && array) | |
: undefined; | |
} | |
array = arrays[0]; | |
var index = -1, | |
seen = caches[0]; | |
outer: | |
while (++index < length && result.length < maxLength) { | |
var value = array[index], | |
computed = iteratee ? iteratee(value) : value; | |
value = (comparator || value !== 0) ? value : 0; | |
if (!(seen | |
? _cacheHas(seen, computed) | |
: includes(result, computed, comparator) | |
)) { | |
othIndex = othLength; | |
while (--othIndex) { | |
var cache = caches[othIndex]; | |
if (!(cache | |
? _cacheHas(cache, computed) | |
: includes(arrays[othIndex], computed, comparator)) | |
) { | |
continue outer; | |
} | |
} | |
if (seen) { | |
seen.push(computed); | |
} | |
result.push(value); | |
} | |
} | |
return result; | |
} | |
/* harmony default export */ var _baseIntersection = (baseIntersection); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_castArrayLikeObject.js | |
/** | |
* Casts `value` to an empty array if it's not an array like object. | |
* | |
* @private | |
* @param {*} value The value to inspect. | |
* @returns {Array|Object} Returns the cast array-like object. | |
*/ | |
function castArrayLikeObject(value) { | |
return lodash_isArrayLikeObject(value) ? value : []; | |
} | |
/* harmony default export */ var _castArrayLikeObject = (castArrayLikeObject); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/intersection.js | |
/** | |
* Creates an array of unique values that are included in all given arrays | |
* using [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero) | |
* for equality comparisons. The order of result values is determined by the | |
* order they occur in the first array. | |
* | |
* @static | |
* @memberOf _ | |
* @since 0.1.0 | |
* @category Array | |
* @param {...Array} [arrays] The arrays to inspect. | |
* @returns {Array} Returns the new array of intersecting values. | |
* @example | |
* | |
* _.intersection([2, 1], [4, 2], [1, 2]); | |
* // => [2] | |
*/ | |
var intersection = lodash_rest(function(arrays) { | |
var mapped = _arrayMap(arrays, _castArrayLikeObject); | |
return (mapped.length && mapped[0] === arrays[0]) | |
? _baseIntersection(mapped) | |
: []; | |
}); | |
/* harmony default export */ var lodash_intersection = (intersection); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/intersectionBy.js | |
/** | |
* This method is like `_.intersection` except that it accepts `iteratee` | |
* which is invoked for each element of each `arrays` to generate the criterion | |
* by which they're compared. Result values are chosen from the first array. | |
* The iteratee is invoked with one argument: (value). | |
* | |
* @static | |
* @memberOf _ | |
* @since 4.0.0 | |
* @category Array | |
* @param {...Array} [arrays] The arrays to inspect. | |
* @param {Array|Function|Object|string} [iteratee=_.identity] | |
* The iteratee invoked per element. | |
* @returns {Array} Returns the new array of intersecting values. | |
* @example | |
* | |
* _.intersectionBy([2.1, 1.2], [4.3, 2.4], Math.floor); | |
* // => [2.1] | |
* | |
* // The `_.property` iteratee shorthand. | |
* _.intersectionBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x'); | |
* // => [{ 'x': 1 }] | |
*/ | |
var intersectionBy = lodash_rest(function(arrays) { | |
var iteratee = lodash_last(arrays), | |
mapped = _arrayMap(arrays, _castArrayLikeObject); | |
if (iteratee === lodash_last(mapped)) { | |
iteratee = undefined; | |
} else { | |
mapped.pop(); | |
} | |
return (mapped.length && mapped[0] === arrays[0]) | |
? _baseIntersection(mapped, _baseIteratee(iteratee)) | |
: []; | |
}); | |
/* harmony default export */ var lodash_intersectionBy = (intersectionBy); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/intersectionWith.js | |
/** | |
* This method is like `_.intersection` except that it accepts `comparator` | |
* which is invoked to compare elements of `arrays`. Result values are chosen | |
* from the first array. The comparator is invoked with two arguments: | |
* (arrVal, othVal). | |
* | |
* @static | |
* @memberOf _ | |
* @since 4.0.0 | |
* @category Array | |
* @param {...Array} [arrays] The arrays to inspect. | |
* @param {Function} [comparator] The comparator invoked per element. | |
* @returns {Array} Returns the new array of intersecting values. | |
* @example | |
* | |
* var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }]; | |
* var others = [{ 'x': 1, 'y': 1 }, { 'x': 1, 'y': 2 }]; | |
* | |
* _.intersectionWith(objects, others, _.isEqual); | |
* // => [{ 'x': 1, 'y': 2 }] | |
*/ | |
var intersectionWith = lodash_rest(function(arrays) { | |
var comparator = lodash_last(arrays), | |
mapped = _arrayMap(arrays, _castArrayLikeObject); | |
if (comparator === lodash_last(mapped)) { | |
comparator = undefined; | |
} else { | |
mapped.pop(); | |
} | |
return (mapped.length && mapped[0] === arrays[0]) | |
? _baseIntersection(mapped, undefined, comparator) | |
: []; | |
}); | |
/* harmony default export */ var lodash_intersectionWith = (intersectionWith); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/join.js | |
/** Used for built-in method references. */ | |
var join_arrayProto = Array.prototype; | |
/* Built-in method references for those with the same name as other `lodash` methods. */ | |
var nativeJoin = join_arrayProto.join; | |
/** | |
* Converts all elements in `array` into a string separated by `separator`. | |
* | |
* @static | |
* @memberOf _ | |
* @since 4.0.0 | |
* @category Array | |
* @param {Array} array The array to convert. | |
* @param {string} [separator=','] The element separator. | |
* @returns {string} Returns the joined string. | |
* @example | |
* | |
* _.join(['a', 'b', 'c'], '~'); | |
* // => 'a~b~c' | |
*/ | |
function join(array, separator) { | |
return array ? nativeJoin.call(array, separator) : ''; | |
} | |
/* harmony default export */ var lodash_join = (join); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/lastIndexOf.js | |
/* Built-in method references for those with the same name as other `lodash` methods. */ | |
var lastIndexOf_nativeMax = Math.max, | |
lastIndexOf_nativeMin = Math.min; | |
/** | |
* This method is like `_.indexOf` except that it iterates over elements of | |
* `array` from right to left. | |
* | |
* @static | |
* @memberOf _ | |
* @since 0.1.0 | |
* @category Array | |
* @param {Array} array The array to search. | |
* @param {*} value The value to search for. | |
* @param {number} [fromIndex=array.length-1] The index to search from. | |
* @returns {number} Returns the index of the matched value, else `-1`. | |
* @example | |
* | |
* _.lastIndexOf([1, 2, 1, 2], 2); | |
* // => 3 | |
* | |
* // Search from the `fromIndex`. | |
* _.lastIndexOf([1, 2, 1, 2], 2, 2); | |
* // => 1 | |
*/ | |
function lastIndexOf(array, value, fromIndex) { | |
var length = array ? array.length : 0; | |
if (!length) { | |
return -1; | |
} | |
var index = length; | |
if (fromIndex !== undefined) { | |
index = lodash_toInteger(fromIndex); | |
index = ( | |
index < 0 | |
? lastIndexOf_nativeMax(length + index, 0) | |
: lastIndexOf_nativeMin(index, length - 1) | |
) + 1; | |
} | |
if (value !== value) { | |
return _indexOfNaN(array, index, true); | |
} | |
while (index--) { | |
if (array[index] === value) { | |
return index; | |
} | |
} | |
return -1; | |
} | |
/* harmony default export */ var lodash_lastIndexOf = (lastIndexOf); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseNth.js | |
/** | |
* The base implementation of `_.nth` which doesn't coerce `n` to an integer. | |
* | |
* @private | |
* @param {Array} array The array to query. | |
* @param {number} n The index of the element to return. | |
* @returns {*} Returns the nth element of `array`. | |
*/ | |
function baseNth(array, n) { | |
var length = array.length; | |
if (!length) { | |
return; | |
} | |
n += n < 0 ? length : 0; | |
return _isIndex(n, length) ? array[n] : undefined; | |
} | |
/* harmony default export */ var _baseNth = (baseNth); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/nth.js | |
/** | |
* Gets the element at `n` index of `array`. If `n` is negative, the nth | |
* element from the end is returned. | |
* | |
* @static | |
* @memberOf _ | |
* @since 4.11.0 | |
* @category Array | |
* @param {Array} array The array to query. | |
* @param {number} [n=0] The index of the element to return. | |
* @returns {*} Returns the nth element of `array`. | |
* @example | |
* | |
* var array = ['a', 'b', 'c', 'd']; | |
* | |
* _.nth(array, 1); | |
* // => 'b' | |
* | |
* _.nth(array, -2); | |
* // => 'c'; | |
*/ | |
function nth(array, n) { | |
return (array && array.length) ? _baseNth(array, lodash_toInteger(n)) : undefined; | |
} | |
/* harmony default export */ var lodash_nth = (nth); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseIndexOfWith.js | |
/** | |
* This function is like `baseIndexOf` except that it accepts a comparator. | |
* | |
* @private | |
* @param {Array} array The array to search. | |
* @param {*} value The value to search for. | |
* @param {number} fromIndex The index to search from. | |
* @param {Function} comparator The comparator invoked per element. | |
* @returns {number} Returns the index of the matched value, else `-1`. | |
*/ | |
function baseIndexOfWith(array, value, fromIndex, comparator) { | |
var index = fromIndex - 1, | |
length = array.length; | |
while (++index < length) { | |
if (comparator(array[index], value)) { | |
return index; | |
} | |
} | |
return -1; | |
} | |
/* harmony default export */ var _baseIndexOfWith = (baseIndexOfWith); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_basePullAll.js | |
/** Used for built-in method references. */ | |
var _basePullAll_arrayProto = Array.prototype; | |
/** Built-in value references. */ | |
var _basePullAll_splice = _basePullAll_arrayProto.splice; | |
/** | |
* The base implementation of `_.pullAllBy` without support for iteratee | |
* shorthands. | |
* | |
* @private | |
* @param {Array} array The array to modify. | |
* @param {Array} values The values to remove. | |
* @param {Function} [iteratee] The iteratee invoked per element. | |
* @param {Function} [comparator] The comparator invoked per element. | |
* @returns {Array} Returns `array`. | |
*/ | |
function basePullAll(array, values, iteratee, comparator) { | |
var indexOf = comparator ? _baseIndexOfWith : _baseIndexOf, | |
index = -1, | |
length = values.length, | |
seen = array; | |
if (iteratee) { | |
seen = _arrayMap(array, _baseUnary(iteratee)); | |
} | |
while (++index < length) { | |
var fromIndex = 0, | |
value = values[index], | |
computed = iteratee ? iteratee(value) : value; | |
while ((fromIndex = indexOf(seen, computed, fromIndex, comparator)) > -1) { | |
if (seen !== array) { | |
_basePullAll_splice.call(seen, fromIndex, 1); | |
} | |
_basePullAll_splice.call(array, fromIndex, 1); | |
} | |
} | |
return array; | |
} | |
/* harmony default export */ var _basePullAll = (basePullAll); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/pullAll.js | |
/** | |
* This method is like `_.pull` except that it accepts an array of values to remove. | |
* | |
* **Note:** Unlike `_.difference`, this method mutates `array`. | |
* | |
* @static | |
* @memberOf _ | |
* @since 4.0.0 | |
* @category Array | |
* @param {Array} array The array to modify. | |
* @param {Array} values The values to remove. | |
* @returns {Array} Returns `array`. | |
* @example | |
* | |
* var array = [1, 2, 3, 1, 2, 3]; | |
* | |
* _.pullAll(array, [2, 3]); | |
* console.log(array); | |
* // => [1, 1] | |
*/ | |
function pullAll(array, values) { | |
return (array && array.length && values && values.length) | |
? _basePullAll(array, values) | |
: array; | |
} | |
/* harmony default export */ var lodash_pullAll = (pullAll); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/pull.js | |
/** | |
* Removes all given values from `array` using | |
* [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero) | |
* for equality comparisons. | |
* | |
* **Note:** Unlike `_.without`, this method mutates `array`. Use `_.remove` | |
* to remove elements from an array by predicate. | |
* | |
* @static | |
* @memberOf _ | |
* @since 2.0.0 | |
* @category Array | |
* @param {Array} array The array to modify. | |
* @param {...*} [values] The values to remove. | |
* @returns {Array} Returns `array`. | |
* @example | |
* | |
* var array = [1, 2, 3, 1, 2, 3]; | |
* | |
* _.pull(array, 2, 3); | |
* console.log(array); | |
* // => [1, 1] | |
*/ | |
var pull = lodash_rest(lodash_pullAll); | |
/* harmony default export */ var lodash_pull = (pull); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/pullAllBy.js | |
/** | |
* This method is like `_.pullAll` except that it accepts `iteratee` which is | |
* invoked for each element of `array` and `values` to generate the criterion | |
* by which they're compared. The iteratee is invoked with one argument: (value). | |
* | |
* **Note:** Unlike `_.differenceBy`, this method mutates `array`. | |
* | |
* @static | |
* @memberOf _ | |
* @since 4.0.0 | |
* @category Array | |
* @param {Array} array The array to modify. | |
* @param {Array} values The values to remove. | |
* @param {Array|Function|Object|string} [iteratee=_.identity] | |
* The iteratee invoked per element. | |
* @returns {Array} Returns `array`. | |
* @example | |
* | |
* var array = [{ 'x': 1 }, { 'x': 2 }, { 'x': 3 }, { 'x': 1 }]; | |
* | |
* _.pullAllBy(array, [{ 'x': 1 }, { 'x': 3 }], 'x'); | |
* console.log(array); | |
* // => [{ 'x': 2 }] | |
*/ | |
function pullAllBy(array, values, iteratee) { | |
return (array && array.length && values && values.length) | |
? _basePullAll(array, values, _baseIteratee(iteratee)) | |
: array; | |
} | |
/* harmony default export */ var lodash_pullAllBy = (pullAllBy); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/pullAllWith.js | |
/** | |
* This method is like `_.pullAll` except that it accepts `comparator` which | |
* is invoked to compare elements of `array` to `values`. The comparator is | |
* invoked with two arguments: (arrVal, othVal). | |
* | |
* **Note:** Unlike `_.differenceWith`, this method mutates `array`. | |
* | |
* @static | |
* @memberOf _ | |
* @since 4.6.0 | |
* @category Array | |
* @param {Array} array The array to modify. | |
* @param {Array} values The values to remove. | |
* @param {Function} [comparator] The comparator invoked per element. | |
* @returns {Array} Returns `array`. | |
* @example | |
* | |
* var array = [{ 'x': 1, 'y': 2 }, { 'x': 3, 'y': 4 }, { 'x': 5, 'y': 6 }]; | |
* | |
* _.pullAllWith(array, [{ 'x': 3, 'y': 4 }], _.isEqual); | |
* console.log(array); | |
* // => [{ 'x': 1, 'y': 2 }, { 'x': 5, 'y': 6 }] | |
*/ | |
function pullAllWith(array, values, comparator) { | |
return (array && array.length && values && values.length) | |
? _basePullAll(array, values, undefined, comparator) | |
: array; | |
} | |
/* harmony default export */ var lodash_pullAllWith = (pullAllWith); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseAt.js | |
/** | |
* The base implementation of `_.at` without support for individual paths. | |
* | |
* @private | |
* @param {Object} object The object to iterate over. | |
* @param {string[]} paths The property paths of elements to pick. | |
* @returns {Array} Returns the picked elements. | |
*/ | |
function baseAt(object, paths) { | |
var index = -1, | |
isNil = object == null, | |
length = paths.length, | |
result = Array(length); | |
while (++index < length) { | |
result[index] = isNil ? undefined : lodash_get(object, paths[index]); | |
} | |
return result; | |
} | |
/* harmony default export */ var _baseAt = (baseAt); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_parent.js | |
/** | |
* Gets the parent value at `path` of `object`. | |
* | |
* @private | |
* @param {Object} object The object to query. | |
* @param {Array} path The path to get the parent value of. | |
* @returns {*} Returns the parent value. | |
*/ | |
function _parent_parent(object, path) { | |
return path.length == 1 ? object : _baseGet(object, _baseSlice(path, 0, -1)); | |
} | |
/* harmony default export */ var _parent = (_parent_parent); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_basePullAt.js | |
/** Used for built-in method references. */ | |
var _basePullAt_arrayProto = Array.prototype; | |
/** Built-in value references. */ | |
var _basePullAt_splice = _basePullAt_arrayProto.splice; | |
/** | |
* The base implementation of `_.pullAt` without support for individual | |
* indexes or capturing the removed elements. | |
* | |
* @private | |
* @param {Array} array The array to modify. | |
* @param {number[]} indexes The indexes of elements to remove. | |
* @returns {Array} Returns `array`. | |
*/ | |
function basePullAt(array, indexes) { | |
var length = array ? indexes.length : 0, | |
lastIndex = length - 1; | |
while (length--) { | |
var index = indexes[length]; | |
if (length == lastIndex || index !== previous) { | |
var previous = index; | |
if (_isIndex(index)) { | |
_basePullAt_splice.call(array, index, 1); | |
} | |
else if (!_isKey(index, array)) { | |
var path = _castPath(index), | |
object = _parent(array, path); | |
if (object != null) { | |
delete object[_toKey(lodash_last(path))]; | |
} | |
} | |
else { | |
delete array[_toKey(index)]; | |
} | |
} | |
} | |
return array; | |
} | |
/* harmony default export */ var _basePullAt = (basePullAt); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_compareAscending.js | |
/** | |
* Compares values to sort them in ascending order. | |
* | |
* @private | |
* @param {*} value The value to compare. | |
* @param {*} other The other value to compare. | |
* @returns {number} Returns the sort order indicator for `value`. | |
*/ | |
function compareAscending(value, other) { | |
if (value !== other) { | |
var valIsDefined = value !== undefined, | |
valIsNull = value === null, | |
valIsReflexive = value === value, | |
valIsSymbol = lodash_isSymbol(value); | |
var othIsDefined = other !== undefined, | |
othIsNull = other === null, | |
othIsReflexive = other === other, | |
othIsSymbol = lodash_isSymbol(other); | |
if ((!othIsNull && !othIsSymbol && !valIsSymbol && value > other) || | |
(valIsSymbol && othIsDefined && othIsReflexive && !othIsNull && !othIsSymbol) || | |
(valIsNull && othIsDefined && othIsReflexive) || | |
(!valIsDefined && othIsReflexive) || | |
!valIsReflexive) { | |
return 1; | |
} | |
if ((!valIsNull && !valIsSymbol && !othIsSymbol && value < other) || | |
(othIsSymbol && valIsDefined && valIsReflexive && !valIsNull && !valIsSymbol) || | |
(othIsNull && valIsDefined && valIsReflexive) || | |
(!othIsDefined && valIsReflexive) || | |
!othIsReflexive) { | |
return -1; | |
} | |
} | |
return 0; | |
} | |
/* harmony default export */ var _compareAscending = (compareAscending); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/pullAt.js | |
/** | |
* Removes elements from `array` corresponding to `indexes` and returns an | |
* array of removed elements. | |
* | |
* **Note:** Unlike `_.at`, this method mutates `array`. | |
* | |
* @static | |
* @memberOf _ | |
* @since 3.0.0 | |
* @category Array | |
* @param {Array} array The array to modify. | |
* @param {...(number|number[])} [indexes] The indexes of elements to remove. | |
* @returns {Array} Returns the new array of removed elements. | |
* @example | |
* | |
* var array = [5, 10, 15, 20]; | |
* var evens = _.pullAt(array, 1, 3); | |
* | |
* console.log(array); | |
* // => [5, 15] | |
* | |
* console.log(evens); | |
* // => [10, 20] | |
*/ | |
var pullAt = lodash_rest(function(array, indexes) { | |
indexes = _baseFlatten(indexes, 1); | |
var length = array ? array.length : 0, | |
result = _baseAt(array, indexes); | |
_basePullAt(array, _arrayMap(indexes, function(index) { | |
return _isIndex(index, length) ? +index : index; | |
}).sort(_compareAscending)); | |
return result; | |
}); | |
/* harmony default export */ var lodash_pullAt = (pullAt); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/remove.js | |
/** | |
* Removes all elements from `array` that `predicate` returns truthy for | |
* and returns an array of the removed elements. The predicate is invoked | |
* with three arguments: (value, index, array). | |
* | |
* **Note:** Unlike `_.filter`, this method mutates `array`. Use `_.pull` | |
* to pull elements from an array by value. | |
* | |
* @static | |
* @memberOf _ | |
* @since 2.0.0 | |
* @category Array | |
* @param {Array} array The array to modify. | |
* @param {Array|Function|Object|string} [predicate=_.identity] | |
* The function invoked per iteration. | |
* @returns {Array} Returns the new array of removed elements. | |
* @example | |
* | |
* var array = [1, 2, 3, 4]; | |
* var evens = _.remove(array, function(n) { | |
* return n % 2 == 0; | |
* }); | |
* | |
* console.log(array); | |
* // => [1, 3] | |
* | |
* console.log(evens); | |
* // => [2, 4] | |
*/ | |
function remove_remove(array, predicate) { | |
var result = []; | |
if (!(array && array.length)) { | |
return result; | |
} | |
var index = -1, | |
indexes = [], | |
length = array.length; | |
predicate = _baseIteratee(predicate, 3); | |
while (++index < length) { | |
var value = array[index]; | |
if (predicate(value, index, array)) { | |
result.push(value); | |
indexes.push(index); | |
} | |
} | |
_basePullAt(array, indexes); | |
return result; | |
} | |
/* harmony default export */ var lodash_remove = (remove_remove); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/reverse.js | |
/** Used for built-in method references. */ | |
var reverse_arrayProto = Array.prototype; | |
/* Built-in method references for those with the same name as other `lodash` methods. */ | |
var nativeReverse = reverse_arrayProto.reverse; | |
/** | |
* Reverses `array` so that the first element becomes the last, the second | |
* element becomes the second to last, and so on. | |
* | |
* **Note:** This method mutates `array` and is based on | |
* [`Array#reverse`](https://mdn.io/Array/reverse). | |
* | |
* @static | |
* @memberOf _ | |
* @since 4.0.0 | |
* @category Array | |
* @param {Array} array The array to modify. | |
* @returns {Array} Returns `array`. | |
* @example | |
* | |
* var array = [1, 2, 3]; | |
* | |
* _.reverse(array); | |
* // => [3, 2, 1] | |
* | |
* console.log(array); | |
* // => [3, 2, 1] | |
*/ | |
function reverse(array) { | |
return array ? nativeReverse.call(array) : array; | |
} | |
/* harmony default export */ var lodash_reverse = (reverse); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/slice.js | |
/** | |
* Creates a slice of `array` from `start` up to, but not including, `end`. | |
* | |
* **Note:** This method is used instead of | |
* [`Array#slice`](https://mdn.io/Array/slice) to ensure dense arrays are | |
* returned. | |
* | |
* @static | |
* @memberOf _ | |
* @since 3.0.0 | |
* @category Array | |
* @param {Array} array The array to slice. | |
* @param {number} [start=0] The start position. | |
* @param {number} [end=array.length] The end position. | |
* @returns {Array} Returns the slice of `array`. | |
*/ | |
function slice(array, start, end) { | |
var length = array ? array.length : 0; | |
if (!length) { | |
return []; | |
} | |
if (end && typeof end != 'number' && _isIterateeCall(array, start, end)) { | |
start = 0; | |
end = length; | |
} | |
else { | |
start = start == null ? 0 : lodash_toInteger(start); | |
end = end === undefined ? length : lodash_toInteger(end); | |
} | |
return _baseSlice(array, start, end); | |
} | |
/* harmony default export */ var lodash_slice = (slice); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseSortedIndexBy.js | |
/** Used as references for the maximum length and index of an array. */ | |
var _baseSortedIndexBy_MAX_ARRAY_LENGTH = 4294967295, | |
MAX_ARRAY_INDEX = _baseSortedIndexBy_MAX_ARRAY_LENGTH - 1; | |
/* Built-in method references for those with the same name as other `lodash` methods. */ | |
var nativeFloor = Math.floor, | |
_baseSortedIndexBy_nativeMin = Math.min; | |
/** | |
* The base implementation of `_.sortedIndexBy` and `_.sortedLastIndexBy` | |
* which invokes `iteratee` for `value` and each element of `array` to compute | |
* their sort ranking. The iteratee is invoked with one argument; (value). | |
* | |
* @private | |
* @param {Array} array The sorted array to inspect. | |
* @param {*} value The value to evaluate. | |
* @param {Function} iteratee The iteratee invoked per element. | |
* @param {boolean} [retHighest] Specify returning the highest qualified index. | |
* @returns {number} Returns the index at which `value` should be inserted | |
* into `array`. | |
*/ | |
function baseSortedIndexBy(array, value, iteratee, retHighest) { | |
value = iteratee(value); | |
var low = 0, | |
high = array ? array.length : 0, | |
valIsNaN = value !== value, | |
valIsNull = value === null, | |
valIsSymbol = lodash_isSymbol(value), | |
valIsUndefined = value === undefined; | |
while (low < high) { | |
var mid = nativeFloor((low + high) / 2), | |
computed = iteratee(array[mid]), | |
othIsDefined = computed !== undefined, | |
othIsNull = computed === null, | |
othIsReflexive = computed === computed, | |
othIsSymbol = lodash_isSymbol(computed); | |
if (valIsNaN) { | |
var setLow = retHighest || othIsReflexive; | |
} else if (valIsUndefined) { | |
setLow = othIsReflexive && (retHighest || othIsDefined); | |
} else if (valIsNull) { | |
setLow = othIsReflexive && othIsDefined && (retHighest || !othIsNull); | |
} else if (valIsSymbol) { | |
setLow = othIsReflexive && othIsDefined && !othIsNull && (retHighest || !othIsSymbol); | |
} else if (othIsNull || othIsSymbol) { | |
setLow = false; | |
} else { | |
setLow = retHighest ? (computed <= value) : (computed < value); | |
} | |
if (setLow) { | |
low = mid + 1; | |
} else { | |
high = mid; | |
} | |
} | |
return _baseSortedIndexBy_nativeMin(high, MAX_ARRAY_INDEX); | |
} | |
/* harmony default export */ var _baseSortedIndexBy = (baseSortedIndexBy); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/_baseSortedIndex.js | |
/** Used as references for the maximum length and index of an array. */ | |
var _baseSortedIndex_MAX_ARRAY_LENGTH = 4294967295, | |
HALF_MAX_ARRAY_LENGTH = _baseSortedIndex_MAX_ARRAY_LENGTH >>> 1; | |
/** | |
* The base implementation of `_.sortedIndex` and `_.sortedLastIndex` which | |
* performs a binary search of `array` to determine the index at which `value` | |
* should be inserted into `array` in order to maintain its sort order. | |
* | |
* @private | |
* @param {Array} array The sorted array to inspect. | |
* @param {*} value The value to evaluate. | |
* @param {boolean} [retHighest] Specify returning the highest qualified index. | |
* @returns {number} Returns the index at which `value` should be inserted | |
* into `array`. | |
*/ | |
function baseSortedIndex(array, value, retHighest) { | |
var low = 0, | |
high = array ? array.length : low; | |
if (typeof value == 'number' && value === value && high <= HALF_MAX_ARRAY_LENGTH) { | |
while (low < high) { | |
var mid = (low + high) >>> 1, | |
computed = array[mid]; | |
if (computed !== null && !lodash_isSymbol(computed) && | |
(retHighest ? (computed <= value) : (computed < value))) { | |
low = mid + 1; | |
} else { | |
high = mid; | |
} | |
} | |
return high; | |
} | |
return _baseSortedIndexBy(array, value, lodash_identity, retHighest); | |
} | |
/* harmony default export */ var _baseSortedIndex = (baseSortedIndex); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/sortedIndex.js | |
/** | |
* Uses a binary search to determine the lowest index at which `value` | |
* should be inserted into `array` in order to maintain its sort order. | |
* | |
* @static | |
* @memberOf _ | |
* @since 0.1.0 | |
* @category Array | |
* @param {Array} array The sorted array to inspect. | |
* @param {*} value The value to evaluate. | |
* @returns {number} Returns the index at which `value` should be inserted | |
* into `array`. | |
* @example | |
* | |
* _.sortedIndex([30, 50], 40); | |
* // => 1 | |
* | |
* _.sortedIndex([4, 5], 4); | |
* // => 0 | |
*/ | |
function sortedIndex(array, value) { | |
return _baseSortedIndex(array, value); | |
} | |
/* harmony default export */ var lodash_sortedIndex = (sortedIndex); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/sortedIndexBy.js | |
/** | |
* This method is like `_.sortedIndex` except that it accepts `iteratee` | |
* which is invoked for `value` and each element of `array` to compute their | |
* sort ranking. The iteratee is invoked with one argument: (value). | |
* | |
* @static | |
* @memberOf _ | |
* @since 4.0.0 | |
* @category Array | |
* @param {Array} array The sorted array to inspect. | |
* @param {*} value The value to evaluate. | |
* @param {Array|Function|Object|string} [iteratee=_.identity] | |
* The iteratee invoked per element. | |
* @returns {number} Returns the index at which `value` should be inserted | |
* into `array`. | |
* @example | |
* | |
* var dict = { 'thirty': 30, 'forty': 40, 'fifty': 50 }; | |
* | |
* _.sortedIndexBy(['thirty', 'fifty'], 'forty', _.propertyOf(dict)); | |
* // => 1 | |
* | |
* // The `_.property` iteratee shorthand. | |
* _.sortedIndexBy([{ 'x': 4 }, { 'x': 5 }], { 'x': 4 }, 'x'); | |
* // => 0 | |
*/ | |
function sortedIndexBy(array, value, iteratee) { | |
return _baseSortedIndexBy(array, value, _baseIteratee(iteratee)); | |
} | |
/* harmony default export */ var lodash_sortedIndexBy = (sortedIndexBy); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/sortedIndexOf.js | |
/** | |
* This method is like `_.indexOf` except that it performs a binary | |
* search on a sorted `array`. | |
* | |
* @static | |
* @memberOf _ | |
* @since 4.0.0 | |
* @category Array | |
* @param {Array} array The array to search. | |
* @param {*} value The value to search for. | |
* @returns {number} Returns the index of the matched value, else `-1`. | |
* @example | |
* | |
* _.sortedIndexOf([1, 1, 2, 2], 2); | |
* // => 2 | |
*/ | |
function sortedIndexOf(array, value) { | |
var length = array ? array.length : 0; | |
if (length) { | |
var index = _baseSortedIndex(array, value); | |
if (index < length && lodash_eq(array[index], value)) { | |
return index; | |
} | |
} | |
return -1; | |
} | |
/* harmony default export */ var lodash_sortedIndexOf = (sortedIndexOf); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/sortedLastIndex.js | |
/** | |
* This method is like `_.sortedIndex` except that it returns the highest | |
* index at which `value` should be inserted into `array` in order to | |
* maintain its sort order. | |
* | |
* @static | |
* @memberOf _ | |
* @since 3.0.0 | |
* @category Array | |
* @param {Array} array The sorted array to inspect. | |
* @param {*} value The value to evaluate. | |
* @returns {number} Returns the index at which `value` should be inserted | |
* into `array`. | |
* @example | |
* | |
* _.sortedLastIndex([4, 5], 4); | |
* // => 1 | |
*/ | |
function sortedLastIndex(array, value) { | |
return _baseSortedIndex(array, value, true); | |
} | |
/* harmony default export */ var lodash_sortedLastIndex = (sortedLastIndex); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/sortedLastIndexBy.js | |
/** | |
* This method is like `_.sortedLastIndex` except that it accepts `iteratee` | |
* which is invoked for `value` and each element of `array` to compute their | |
* sort ranking. The iteratee is invoked with one argument: (value). | |
* | |
* @static | |
* @memberOf _ | |
* @since 4.0.0 | |
* @category Array | |
* @param {Array} array The sorted array to inspect. | |
* @param {*} value The value to evaluate. | |
* @param {Array|Function|Object|string} [iteratee=_.identity] | |
* The iteratee invoked per element. | |
* @returns {number} Returns the index at which `value` should be inserted | |
* into `array`. | |
* @example | |
* | |
* // The `_.property` iteratee shorthand. | |
* _.sortedLastIndexBy([{ 'x': 4 }, { 'x': 5 }], { 'x': 4 }, 'x'); | |
* // => 1 | |
*/ | |
function sortedLastIndexBy(array, value, iteratee) { | |
return _baseSortedIndexBy(array, value, _baseIteratee(iteratee), true); | |
} | |
/* harmony default export */ var lodash_sortedLastIndexBy = (sortedLastIndexBy); | |
// CONCATENATED MODULE: ./node_modules/@ckeditor/ckeditor5-utils/src/lib/lodash/sortedLastIndexOf.js | |
/** | |
* This method is like `_.lastIndexOf` except that it performs a binary | |
* search on a sorted `array`. | |
* | |
* @static | |
* @memberOf _ | |
* @since 4.0.0 | |
* @category Array | |
* @param {Array} array The array to search. | |
* @param {*} value The value to search for. | |
* @returns {number} Returns the index of the matched value, else `-1`. | |
* @example | |
* | |
* _.sortedLastIndexOf([1, 1, 2, 2], 2); | |
* // => 3 | |
*/ | |
function sortedLastIndexOf(array, value) { | |
var length = array ? array.length : 0; | |
if (length) { | |
var index = _baseSortedIndex(array, value, true) - 1; | |
if (lodash_eq(array[index], value |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment