...); if\n // they are, we want to unwrap the inner block element.\n var isNotTopContainer = !! parentNode.parentNode;\n var isNestedBlockElement =\n isBlockElement(parentNode) &&\n isBlockElement(node) &&\n isNotTopContainer;\n\n var nodeName = node.nodeName.toLowerCase();\n\n var allowedAttrs = getAllowedAttrs(this.config, nodeName, node);\n\n var isInvalid = isInline && containsBlockElement;\n\n // Drop tag entirely according to the whitelist *and* if the markup\n // is invalid.\n if (isInvalid || shouldRejectNode(node, allowedAttrs)\n || (!this.config.keepNestedBlockElements && isNestedBlockElement)) {\n // Do not keep the inner text of SCRIPT/STYLE elements.\n if (! (node.nodeName === 'SCRIPT' || node.nodeName === 'STYLE')) {\n while (node.childNodes.length > 0) {\n parentNode.insertBefore(node.childNodes[0], node);\n }\n }\n parentNode.removeChild(node);\n\n this._sanitize(document, parentNode);\n break;\n }\n\n // Sanitize attributes\n for (var a = 0; a < node.attributes.length; a += 1) {\n var attr = node.attributes[a];\n\n if (shouldRejectAttr(attr, allowedAttrs, node)) {\n node.removeAttribute(attr.name);\n // Shift the array to continue looping.\n a = a - 1;\n }\n }\n\n // Sanitize children\n this._sanitize(document, node);\n\n } while ((node = treeWalker.nextSibling()));\n };\n\n function createTreeWalker(document, node) {\n return document.createTreeWalker(node,\n NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT,\n null, false);\n }\n\n function getAllowedAttrs(config, nodeName, node){\n if (typeof config.tags[nodeName] === 'function') {\n return config.tags[nodeName](node);\n } else {\n return config.tags[nodeName];\n }\n }\n\n function shouldRejectNode(node, allowedAttrs){\n if (typeof allowedAttrs === 'undefined') {\n return true;\n } else if (typeof allowedAttrs === 'boolean') {\n return !allowedAttrs;\n }\n\n return false;\n }\n\n function shouldRejectAttr(attr, allowedAttrs, node){\n var attrName = attr.name.toLowerCase();\n\n if (allowedAttrs === true){\n return false;\n } else if (typeof allowedAttrs[attrName] === 'function'){\n return !allowedAttrs[attrName](attr.value, node);\n } else if (typeof allowedAttrs[attrName] === 'undefined'){\n return true;\n } else if (allowedAttrs[attrName] === false) {\n return true;\n } else if (typeof allowedAttrs[attrName] === 'string') {\n return (allowedAttrs[attrName] !== attr.value);\n }\n\n return false;\n }\n\n return HTMLJanitor;\n\n}));\n","/**\n * Copyright (c) 2014-present, Facebook, Inc.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nvar runtime = (function (exports) {\n \"use strict\";\n\n var Op = Object.prototype;\n var hasOwn = Op.hasOwnProperty;\n var undefined; // More compressible than void 0.\n var $Symbol = typeof Symbol === \"function\" ? Symbol : {};\n var iteratorSymbol = $Symbol.iterator || \"@@iterator\";\n var asyncIteratorSymbol = $Symbol.asyncIterator || \"@@asyncIterator\";\n var toStringTagSymbol = $Symbol.toStringTag || \"@@toStringTag\";\n\n function wrap(innerFn, outerFn, self, tryLocsList) {\n // If outerFn provided and outerFn.prototype is a Generator, then outerFn.prototype instanceof Generator.\n var protoGenerator = outerFn && outerFn.prototype instanceof Generator ? outerFn : Generator;\n var generator = Object.create(protoGenerator.prototype);\n var context = new Context(tryLocsList || []);\n\n // The ._invoke method unifies the implementations of the .next,\n // .throw, and .return methods.\n generator._invoke = makeInvokeMethod(innerFn, self, context);\n\n return generator;\n }\n exports.wrap = wrap;\n\n // Try/catch helper to minimize deoptimizations. Returns a completion\n // record like context.tryEntries[i].completion. This interface could\n // have been (and was previously) designed to take a closure to be\n // invoked without arguments, but in all the cases we care about we\n // already have an existing method we want to call, so there's no need\n // to create a new function object. We can even get away with assuming\n // the method takes exactly one argument, since that happens to be true\n // in every case, so we don't have to touch the arguments object. The\n // only additional allocation required is the completion record, which\n // has a stable shape and so hopefully should be cheap to allocate.\n function tryCatch(fn, obj, arg) {\n try {\n return { type: \"normal\", arg: fn.call(obj, arg) };\n } catch (err) {\n return { type: \"throw\", arg: err };\n }\n }\n\n var GenStateSuspendedStart = \"suspendedStart\";\n var GenStateSuspendedYield = \"suspendedYield\";\n var GenStateExecuting = \"executing\";\n var GenStateCompleted = \"completed\";\n\n // Returning this object from the innerFn has the same effect as\n // breaking out of the dispatch switch statement.\n var ContinueSentinel = {};\n\n // Dummy constructor functions that we use as the .constructor and\n // .constructor.prototype properties for functions that return Generator\n // objects. For full spec compliance, you may wish to configure your\n // minifier not to mangle the names of these two functions.\n function Generator() {}\n function GeneratorFunction() {}\n function GeneratorFunctionPrototype() {}\n\n // This is a polyfill for %IteratorPrototype% for environments that\n // don't natively support it.\n var IteratorPrototype = {};\n IteratorPrototype[iteratorSymbol] = function () {\n return this;\n };\n\n var getProto = Object.getPrototypeOf;\n var NativeIteratorPrototype = getProto && getProto(getProto(values([])));\n if (NativeIteratorPrototype &&\n NativeIteratorPrototype !== Op &&\n hasOwn.call(NativeIteratorPrototype, iteratorSymbol)) {\n // This environment has a native %IteratorPrototype%; use it instead\n // of the polyfill.\n IteratorPrototype = NativeIteratorPrototype;\n }\n\n var Gp = GeneratorFunctionPrototype.prototype =\n Generator.prototype = Object.create(IteratorPrototype);\n GeneratorFunction.prototype = Gp.constructor = GeneratorFunctionPrototype;\n GeneratorFunctionPrototype.constructor = GeneratorFunction;\n GeneratorFunctionPrototype[toStringTagSymbol] =\n GeneratorFunction.displayName = \"GeneratorFunction\";\n\n // Helper for defining the .next, .throw, and .return methods of the\n // Iterator interface in terms of a single ._invoke method.\n function defineIteratorMethods(prototype) {\n [\"next\", \"throw\", \"return\"].forEach(function(method) {\n prototype[method] = function(arg) {\n return this._invoke(method, arg);\n };\n });\n }\n\n exports.isGeneratorFunction = function(genFun) {\n var ctor = typeof genFun === \"function\" && genFun.constructor;\n return ctor\n ? ctor === GeneratorFunction ||\n // For the native GeneratorFunction constructor, the best we can\n // do is to check its .name property.\n (ctor.displayName || ctor.name) === \"GeneratorFunction\"\n : false;\n };\n\n exports.mark = function(genFun) {\n if (Object.setPrototypeOf) {\n Object.setPrototypeOf(genFun, GeneratorFunctionPrototype);\n } else {\n genFun.__proto__ = GeneratorFunctionPrototype;\n if (!(toStringTagSymbol in genFun)) {\n genFun[toStringTagSymbol] = \"GeneratorFunction\";\n }\n }\n genFun.prototype = Object.create(Gp);\n return genFun;\n };\n\n // Within the body of any async function, `await x` is transformed to\n // `yield regeneratorRuntime.awrap(x)`, so that the runtime can test\n // `hasOwn.call(value, \"__await\")` to determine if the yielded value is\n // meant to be awaited.\n exports.awrap = function(arg) {\n return { __await: arg };\n };\n\n function AsyncIterator(generator) {\n function invoke(method, arg, resolve, reject) {\n var record = tryCatch(generator[method], generator, arg);\n if (record.type === \"throw\") {\n reject(record.arg);\n } else {\n var result = record.arg;\n var value = result.value;\n if (value &&\n typeof value === \"object\" &&\n hasOwn.call(value, \"__await\")) {\n return Promise.resolve(value.__await).then(function(value) {\n invoke(\"next\", value, resolve, reject);\n }, function(err) {\n invoke(\"throw\", err, resolve, reject);\n });\n }\n\n return Promise.resolve(value).then(function(unwrapped) {\n // When a yielded Promise is resolved, its final value becomes\n // the .value of the Promise<{value,done}> result for the\n // current iteration.\n result.value = unwrapped;\n resolve(result);\n }, function(error) {\n // If a rejected Promise was yielded, throw the rejection back\n // into the async generator function so it can be handled there.\n return invoke(\"throw\", error, resolve, reject);\n });\n }\n }\n\n var previousPromise;\n\n function enqueue(method, arg) {\n function callInvokeWithMethodAndArg() {\n return new Promise(function(resolve, reject) {\n invoke(method, arg, resolve, reject);\n });\n }\n\n return previousPromise =\n // If enqueue has been called before, then we want to wait until\n // all previous Promises have been resolved before calling invoke,\n // so that results are always delivered in the correct order. If\n // enqueue has not been called before, then it is important to\n // call invoke immediately, without waiting on a callback to fire,\n // so that the async generator function has the opportunity to do\n // any necessary setup in a predictable way. This predictability\n // is why the Promise constructor synchronously invokes its\n // executor callback, and why async functions synchronously\n // execute code before the first await. Since we implement simple\n // async functions in terms of async generators, it is especially\n // important to get this right, even though it requires care.\n previousPromise ? previousPromise.then(\n callInvokeWithMethodAndArg,\n // Avoid propagating failures to Promises returned by later\n // invocations of the iterator.\n callInvokeWithMethodAndArg\n ) : callInvokeWithMethodAndArg();\n }\n\n // Define the unified helper method that is used to implement .next,\n // .throw, and .return (see defineIteratorMethods).\n this._invoke = enqueue;\n }\n\n defineIteratorMethods(AsyncIterator.prototype);\n AsyncIterator.prototype[asyncIteratorSymbol] = function () {\n return this;\n };\n exports.AsyncIterator = AsyncIterator;\n\n // Note that simple async functions are implemented on top of\n // AsyncIterator objects; they just return a Promise for the value of\n // the final result produced by the iterator.\n exports.async = function(innerFn, outerFn, self, tryLocsList) {\n var iter = new AsyncIterator(\n wrap(innerFn, outerFn, self, tryLocsList)\n );\n\n return exports.isGeneratorFunction(outerFn)\n ? iter // If outerFn is a generator, return the full iterator.\n : iter.next().then(function(result) {\n return result.done ? result.value : iter.next();\n });\n };\n\n function makeInvokeMethod(innerFn, self, context) {\n var state = GenStateSuspendedStart;\n\n return function invoke(method, arg) {\n if (state === GenStateExecuting) {\n throw new Error(\"Generator is already running\");\n }\n\n if (state === GenStateCompleted) {\n if (method === \"throw\") {\n throw arg;\n }\n\n // Be forgiving, per 25.3.3.3.3 of the spec:\n // https://people.mozilla.org/~jorendorff/es6-draft.html#sec-generatorresume\n return doneResult();\n }\n\n context.method = method;\n context.arg = arg;\n\n while (true) {\n var delegate = context.delegate;\n if (delegate) {\n var delegateResult = maybeInvokeDelegate(delegate, context);\n if (delegateResult) {\n if (delegateResult === ContinueSentinel) continue;\n return delegateResult;\n }\n }\n\n if (context.method === \"next\") {\n // Setting context._sent for legacy support of Babel's\n // function.sent implementation.\n context.sent = context._sent = context.arg;\n\n } else if (context.method === \"throw\") {\n if (state === GenStateSuspendedStart) {\n state = GenStateCompleted;\n throw context.arg;\n }\n\n context.dispatchException(context.arg);\n\n } else if (context.method === \"return\") {\n context.abrupt(\"return\", context.arg);\n }\n\n state = GenStateExecuting;\n\n var record = tryCatch(innerFn, self, context);\n if (record.type === \"normal\") {\n // If an exception is thrown from innerFn, we leave state ===\n // GenStateExecuting and loop back for another invocation.\n state = context.done\n ? GenStateCompleted\n : GenStateSuspendedYield;\n\n if (record.arg === ContinueSentinel) {\n continue;\n }\n\n return {\n value: record.arg,\n done: context.done\n };\n\n } else if (record.type === \"throw\") {\n state = GenStateCompleted;\n // Dispatch the exception by looping back around to the\n // context.dispatchException(context.arg) call above.\n context.method = \"throw\";\n context.arg = record.arg;\n }\n }\n };\n }\n\n // Call delegate.iterator[context.method](context.arg) and handle the\n // result, either by returning a { value, done } result from the\n // delegate iterator, or by modifying context.method and context.arg,\n // setting context.delegate to null, and returning the ContinueSentinel.\n function maybeInvokeDelegate(delegate, context) {\n var method = delegate.iterator[context.method];\n if (method === undefined) {\n // A .throw or .return when the delegate iterator has no .throw\n // method always terminates the yield* loop.\n context.delegate = null;\n\n if (context.method === \"throw\") {\n // Note: [\"return\"] must be used for ES3 parsing compatibility.\n if (delegate.iterator[\"return\"]) {\n // If the delegate iterator has a return method, give it a\n // chance to clean up.\n context.method = \"return\";\n context.arg = undefined;\n maybeInvokeDelegate(delegate, context);\n\n if (context.method === \"throw\") {\n // If maybeInvokeDelegate(context) changed context.method from\n // \"return\" to \"throw\", let that override the TypeError below.\n return ContinueSentinel;\n }\n }\n\n context.method = \"throw\";\n context.arg = new TypeError(\n \"The iterator does not provide a 'throw' method\");\n }\n\n return ContinueSentinel;\n }\n\n var record = tryCatch(method, delegate.iterator, context.arg);\n\n if (record.type === \"throw\") {\n context.method = \"throw\";\n context.arg = record.arg;\n context.delegate = null;\n return ContinueSentinel;\n }\n\n var info = record.arg;\n\n if (! info) {\n context.method = \"throw\";\n context.arg = new TypeError(\"iterator result is not an object\");\n context.delegate = null;\n return ContinueSentinel;\n }\n\n if (info.done) {\n // Assign the result of the finished delegate to the temporary\n // variable specified by delegate.resultName (see delegateYield).\n context[delegate.resultName] = info.value;\n\n // Resume execution at the desired location (see delegateYield).\n context.next = delegate.nextLoc;\n\n // If context.method was \"throw\" but the delegate handled the\n // exception, let the outer generator proceed normally. If\n // context.method was \"next\", forget context.arg since it has been\n // \"consumed\" by the delegate iterator. If context.method was\n // \"return\", allow the original .return call to continue in the\n // outer generator.\n if (context.method !== \"return\") {\n context.method = \"next\";\n context.arg = undefined;\n }\n\n } else {\n // Re-yield the result returned by the delegate method.\n return info;\n }\n\n // The delegate iterator is finished, so forget it and continue with\n // the outer generator.\n context.delegate = null;\n return ContinueSentinel;\n }\n\n // Define Generator.prototype.{next,throw,return} in terms of the\n // unified ._invoke helper method.\n defineIteratorMethods(Gp);\n\n Gp[toStringTagSymbol] = \"Generator\";\n\n // A Generator should always return itself as the iterator object when the\n // @@iterator function is called on it. Some browsers' implementations of the\n // iterator prototype chain incorrectly implement this, causing the Generator\n // object to not be returned from this call. This ensures that doesn't happen.\n // See https://github.com/facebook/regenerator/issues/274 for more details.\n Gp[iteratorSymbol] = function() {\n return this;\n };\n\n Gp.toString = function() {\n return \"[object Generator]\";\n };\n\n function pushTryEntry(locs) {\n var entry = { tryLoc: locs[0] };\n\n if (1 in locs) {\n entry.catchLoc = locs[1];\n }\n\n if (2 in locs) {\n entry.finallyLoc = locs[2];\n entry.afterLoc = locs[3];\n }\n\n this.tryEntries.push(entry);\n }\n\n function resetTryEntry(entry) {\n var record = entry.completion || {};\n record.type = \"normal\";\n delete record.arg;\n entry.completion = record;\n }\n\n function Context(tryLocsList) {\n // The root entry object (effectively a try statement without a catch\n // or a finally block) gives us a place to store values thrown from\n // locations where there is no enclosing try statement.\n this.tryEntries = [{ tryLoc: \"root\" }];\n tryLocsList.forEach(pushTryEntry, this);\n this.reset(true);\n }\n\n exports.keys = function(object) {\n var keys = [];\n for (var key in object) {\n keys.push(key);\n }\n keys.reverse();\n\n // Rather than returning an object with a next method, we keep\n // things simple and return the next function itself.\n return function next() {\n while (keys.length) {\n var key = keys.pop();\n if (key in object) {\n next.value = key;\n next.done = false;\n return next;\n }\n }\n\n // To avoid creating an additional object, we just hang the .value\n // and .done properties off the next function object itself. This\n // also ensures that the minifier will not anonymize the function.\n next.done = true;\n return next;\n };\n };\n\n function values(iterable) {\n if (iterable) {\n var iteratorMethod = iterable[iteratorSymbol];\n if (iteratorMethod) {\n return iteratorMethod.call(iterable);\n }\n\n if (typeof iterable.next === \"function\") {\n return iterable;\n }\n\n if (!isNaN(iterable.length)) {\n var i = -1, next = function next() {\n while (++i < iterable.length) {\n if (hasOwn.call(iterable, i)) {\n next.value = iterable[i];\n next.done = false;\n return next;\n }\n }\n\n next.value = undefined;\n next.done = true;\n\n return next;\n };\n\n return next.next = next;\n }\n }\n\n // Return an iterator with no values.\n return { next: doneResult };\n }\n exports.values = values;\n\n function doneResult() {\n return { value: undefined, done: true };\n }\n\n Context.prototype = {\n constructor: Context,\n\n reset: function(skipTempReset) {\n this.prev = 0;\n this.next = 0;\n // Resetting context._sent for legacy support of Babel's\n // function.sent implementation.\n this.sent = this._sent = undefined;\n this.done = false;\n this.delegate = null;\n\n this.method = \"next\";\n this.arg = undefined;\n\n this.tryEntries.forEach(resetTryEntry);\n\n if (!skipTempReset) {\n for (var name in this) {\n // Not sure about the optimal order of these conditions:\n if (name.charAt(0) === \"t\" &&\n hasOwn.call(this, name) &&\n !isNaN(+name.slice(1))) {\n this[name] = undefined;\n }\n }\n }\n },\n\n stop: function() {\n this.done = true;\n\n var rootEntry = this.tryEntries[0];\n var rootRecord = rootEntry.completion;\n if (rootRecord.type === \"throw\") {\n throw rootRecord.arg;\n }\n\n return this.rval;\n },\n\n dispatchException: function(exception) {\n if (this.done) {\n throw exception;\n }\n\n var context = this;\n function handle(loc, caught) {\n record.type = \"throw\";\n record.arg = exception;\n context.next = loc;\n\n if (caught) {\n // If the dispatched exception was caught by a catch block,\n // then let that catch block handle the exception normally.\n context.method = \"next\";\n context.arg = undefined;\n }\n\n return !! caught;\n }\n\n for (var i = this.tryEntries.length - 1; i >= 0; --i) {\n var entry = this.tryEntries[i];\n var record = entry.completion;\n\n if (entry.tryLoc === \"root\") {\n // Exception thrown outside of any try block that could handle\n // it, so set the completion value of the entire function to\n // throw the exception.\n return handle(\"end\");\n }\n\n if (entry.tryLoc <= this.prev) {\n var hasCatch = hasOwn.call(entry, \"catchLoc\");\n var hasFinally = hasOwn.call(entry, \"finallyLoc\");\n\n if (hasCatch && hasFinally) {\n if (this.prev < entry.catchLoc) {\n return handle(entry.catchLoc, true);\n } else if (this.prev < entry.finallyLoc) {\n return handle(entry.finallyLoc);\n }\n\n } else if (hasCatch) {\n if (this.prev < entry.catchLoc) {\n return handle(entry.catchLoc, true);\n }\n\n } else if (hasFinally) {\n if (this.prev < entry.finallyLoc) {\n return handle(entry.finallyLoc);\n }\n\n } else {\n throw new Error(\"try statement without catch or finally\");\n }\n }\n }\n },\n\n abrupt: function(type, arg) {\n for (var i = this.tryEntries.length - 1; i >= 0; --i) {\n var entry = this.tryEntries[i];\n if (entry.tryLoc <= this.prev &&\n hasOwn.call(entry, \"finallyLoc\") &&\n this.prev < entry.finallyLoc) {\n var finallyEntry = entry;\n break;\n }\n }\n\n if (finallyEntry &&\n (type === \"break\" ||\n type === \"continue\") &&\n finallyEntry.tryLoc <= arg &&\n arg <= finallyEntry.finallyLoc) {\n // Ignore the finally entry if control is not jumping to a\n // location outside the try/catch block.\n finallyEntry = null;\n }\n\n var record = finallyEntry ? finallyEntry.completion : {};\n record.type = type;\n record.arg = arg;\n\n if (finallyEntry) {\n this.method = \"next\";\n this.next = finallyEntry.finallyLoc;\n return ContinueSentinel;\n }\n\n return this.complete(record);\n },\n\n complete: function(record, afterLoc) {\n if (record.type === \"throw\") {\n throw record.arg;\n }\n\n if (record.type === \"break\" ||\n record.type === \"continue\") {\n this.next = record.arg;\n } else if (record.type === \"return\") {\n this.rval = this.arg = record.arg;\n this.method = \"return\";\n this.next = \"end\";\n } else if (record.type === \"normal\" && afterLoc) {\n this.next = afterLoc;\n }\n\n return ContinueSentinel;\n },\n\n finish: function(finallyLoc) {\n for (var i = this.tryEntries.length - 1; i >= 0; --i) {\n var entry = this.tryEntries[i];\n if (entry.finallyLoc === finallyLoc) {\n this.complete(entry.completion, entry.afterLoc);\n resetTryEntry(entry);\n return ContinueSentinel;\n }\n }\n },\n\n \"catch\": function(tryLoc) {\n for (var i = this.tryEntries.length - 1; i >= 0; --i) {\n var entry = this.tryEntries[i];\n if (entry.tryLoc === tryLoc) {\n var record = entry.completion;\n if (record.type === \"throw\") {\n var thrown = record.arg;\n resetTryEntry(entry);\n }\n return thrown;\n }\n }\n\n // The context.catch method must only be called with a location\n // argument that corresponds to a known catch block.\n throw new Error(\"illegal catch attempt\");\n },\n\n delegateYield: function(iterable, resultName, nextLoc) {\n this.delegate = {\n iterator: values(iterable),\n resultName: resultName,\n nextLoc: nextLoc\n };\n\n if (this.method === \"next\") {\n // Deliberately forget the last sent value so that we don't\n // accidentally pass it on to the delegate.\n this.arg = undefined;\n }\n\n return ContinueSentinel;\n }\n };\n\n // Regardless of whether this script is executing as a CommonJS module\n // or not, return the runtime object so that we can declare the variable\n // regeneratorRuntime in the outer scope, which allows this module to be\n // injected easily by `bin/regenerator --include-runtime script.js`.\n return exports;\n\n}(\n // If this script is executing as a CommonJS module, use module.exports\n // as the regeneratorRuntime namespace. Otherwise create a new empty\n // object. Either way, the resulting object will be used to initialize\n // the regeneratorRuntime variable at the top of this file.\n typeof module === \"object\" ? module.exports : {}\n));\n\ntry {\n regeneratorRuntime = runtime;\n} catch (accidentalStrictMode) {\n // This module should not be running in strict mode, so the above\n // assignment should always work unless something is misconfigured. Just\n // in case runtime.js accidentally runs in strict mode, we can escape\n // strict mode using a global Function call. This could conceivably fail\n // if a Content Security Policy forbids using Function, but in that case\n // the proper solution is to fix the accidental strict mode problem. If\n // you've misconfigured your bundler to force strict mode and applied a\n // CSP to forbid Function, and you're not willing to fix either of those\n // problems, please detail your unique predicament in a GitHub issue.\n Function(\"r\", \"regeneratorRuntime = r\")(runtime);\n}\n","'use strict';\nimport {EditorConfig} from '../types';\n\ndeclare const VERSION: string;\n\n/**\n * Apply polyfills\n */\nimport '@babel/register';\n\nimport 'components/polyfills';\nimport Core from './components/core';\n\n/**\n * Editor.js\n *\n * Short Description (눈_눈;)\n * @version 2.0\n *\n * @licence Apache-2.0\n * @author CodeX-Team \n */\nexport default class EditorJS {\n /**\n * Promise that resolves when core modules are ready and UI is rendered on the page\n */\n public isReady: Promise;\n\n /**\n * Stores destroy method implementation.\n * Clear heap occupied by Editor and remove UI components from the DOM.\n */\n public destroy: () => void;\n\n /** Editor version */\n static get version(): string {\n return VERSION;\n }\n\n /**\n * @constructor\n *\n * @param {EditorConfig|String|undefined} [configuration] - user configuration\n */\n public constructor(configuration?: EditorConfig|string) {\n /**\n * Set default onReady function\n */\n let onReady = () => {};\n\n /**\n * If `onReady` was passed in `configuration` then redefine onReady function\n */\n if (typeof configuration === 'object' && typeof configuration.onReady === 'function') {\n onReady = configuration.onReady;\n }\n\n /**\n * Create a Editor.js instance\n */\n const editor = new Core(configuration);\n\n /**\n * We need to export isReady promise in the constructor\n * as it can be used before other API methods are exported\n * @type {Promise}\n */\n this.isReady = editor.isReady.then(() => {\n this.exportAPI(editor);\n onReady();\n });\n }\n\n /**\n * Export external API methods\n *\n * @param editor\n */\n public exportAPI(editor: Core): void {\n const fieldsToExport = [ 'configuration' ];\n const destroy = () => {\n editor.moduleInstances.Listeners.removeAll();\n editor.moduleInstances.UI.destroy();\n editor.moduleInstances.ModificationsObserver.destroy();\n editor = null;\n\n for (const field in this) {\n if (this.hasOwnProperty(field)) {\n delete this[field];\n }\n }\n\n Object.setPrototypeOf(this, null);\n };\n\n fieldsToExport.forEach((field) => {\n this[field] = editor[field];\n });\n\n this.destroy = destroy;\n\n Object.setPrototypeOf(this, editor.moduleInstances.API.methods);\n\n delete this.exportAPI;\n\n const shorthands = {\n blocks: {\n clear: 'clear',\n render: 'render',\n },\n caret: {\n focus: 'focus',\n },\n events: {\n on: 'on',\n off: 'off',\n emit: 'emit',\n },\n saver: {\n save: 'save',\n },\n };\n\n Object.entries(shorthands)\n .forEach(([key, methods]) => {\n Object.entries(methods)\n .forEach(([name, alias]) => {\n this[alias] = editor.moduleInstances.API.methods[key][name];\n });\n });\n }\n}\n","import {EditorModules} from '../types-internal/editor-modules';\nimport {EditorConfig} from '../../types';\nimport {ModuleConfig} from '../types-internal/module-config';\n\n/**\n * @abstract\n * @class Module\n * @classdesc All modules inherits from this class.\n *\n * @typedef {Module} Module\n * @property {Object} config - Editor user settings\n * @property {EditorModules} Editor - List of Editor modules\n */\nexport default class Module {\n\n /**\n * Editor modules list\n * @type {EditorModules}\n */\n protected Editor: EditorModules;\n\n /**\n * Editor configuration object\n * @type {EditorConfig}\n */\n protected config: EditorConfig;\n\n /**\n * @constructor\n * @param {EditorConfig}\n */\n constructor({config}: ModuleConfig) {\n if (new.target === Module) {\n throw new TypeError('Constructors for abstract class Module are not allowed.');\n }\n\n this.config = config;\n }\n\n /**\n * Editor modules setter\n * @param {EditorModules} Editor\n */\n set state(Editor: EditorModules) {\n this.Editor = Editor;\n }\n}\n","/**\n * @class DeleteTune\n * @classdesc Editor's default tune that moves up selected block\n *\n * @copyright 2018\n */\nimport {API, BlockTune} from '../../../types';\nimport $ from '../dom';\n\nexport default class DeleteTune implements BlockTune {\n\n /**\n * Property that contains Editor.js API methods\n * @see {docs/api.md}\n */\n private readonly api: API;\n\n /**\n * Styles\n */\n private CSS = {\n button: 'ce-settings__button',\n buttonDelete: 'ce-settings__button--delete',\n buttonConfirm: 'ce-settings__button--confirm',\n };\n\n /**\n * Delete confirmation\n */\n private needConfirmation: boolean;\n\n /**\n * set false confirmation state\n */\n private resetConfirmation: () => void;\n\n /**\n * Tune nodes\n */\n private nodes: {button: HTMLElement} = {\n button: null,\n };\n\n /**\n * DeleteTune constructor\n *\n * @param {{api: API}} api\n */\n constructor({api}) {\n this.api = api;\n\n this.resetConfirmation = () => {\n this.setConfirmation(false);\n };\n }\n\n /**\n * Create \"Delete\" button and add click event listener\n * @returns [Element}\n */\n public render() {\n this.nodes.button = $.make('div', [this.CSS.button, this.CSS.buttonDelete], {});\n this.nodes.button.appendChild($.svg('cross', 12, 12));\n this.api.listeners.on(this.nodes.button, 'click', (event: MouseEvent) => this.handleClick(event), false);\n\n /**\n * Enable tooltip module\n */\n this.api.tooltip.onHover(this.nodes.button, 'Delete');\n\n return this.nodes.button;\n }\n\n /**\n * Delete block conditions passed\n * @param {MouseEvent} event\n */\n public handleClick(event: MouseEvent): void {\n\n /**\n * if block is not waiting the confirmation, subscribe on block-settings-closing event to reset\n * otherwise delete block\n */\n if (!this.needConfirmation) {\n this.setConfirmation(true);\n\n /**\n * Subscribe on event.\n * When toolbar block settings is closed but block deletion is not confirmed,\n * then reset confirmation state\n */\n this.api.events.on('block-settings-closed', this.resetConfirmation);\n\n } else {\n\n /**\n * Unsubscribe from block-settings closing event\n */\n this.api.events.off('block-settings-closed', this.resetConfirmation);\n\n this.api.blocks.delete();\n this.api.toolbar.close();\n this.api.tooltip.hide();\n\n /**\n * Prevent firing ui~documentClicked that can drop currentBlock pointer\n */\n event.stopPropagation();\n }\n }\n\n /**\n * change tune state\n */\n private setConfirmation(state): void {\n this.needConfirmation = state;\n this.nodes.button.classList.add(this.CSS.buttonConfirm);\n }\n\n}\n","/**\n * @class MoveDownTune\n * @classdesc Editor's default tune - Moves down highlighted block\n *\n * @copyright 2018\n */\n\nimport $ from '../dom';\nimport {API, BlockTune} from '../../../types';\n\nexport default class MoveDownTune implements BlockTune {\n /**\n * Property that contains Editor.js API methods\n * @see {api.md}\n */\n private readonly api: API;\n\n /**\n * Styles\n * @type {{wrapper: string}}\n */\n private CSS = {\n button: 'ce-settings__button',\n wrapper: 'ce-tune-move-down',\n animation: 'wobble',\n };\n\n /**\n * MoveDownTune constructor\n *\n * @param {{api: API}} api\n */\n public constructor({api}) {\n this.api = api;\n }\n\n /**\n * Return 'move down' button\n */\n public render() {\n const moveDownButton = $.make('div', [this.CSS.button, this.CSS.wrapper], {});\n moveDownButton.appendChild($.svg('arrow-down', 14, 14));\n this.api.listeners.on(\n moveDownButton,\n 'click',\n (event) => this.handleClick(event as MouseEvent, moveDownButton),\n false,\n );\n\n /**\n * Enable tooltip module on button\n */\n this.api.tooltip.onHover(moveDownButton, 'Move down');\n\n return moveDownButton;\n }\n\n /**\n * Handle clicks on 'move down' button\n * @param {MouseEvent} event\n * @param {HTMLElement} button\n */\n public handleClick(event: MouseEvent, button: HTMLElement) {\n\n const currentBlockIndex = this.api.blocks.getCurrentBlockIndex();\n\n // If Block is last do nothing\n if (currentBlockIndex === this.api.blocks.getBlocksCount() - 1) {\n button.classList.add(this.CSS.animation);\n\n window.setTimeout( () => {\n button.classList.remove(this.CSS.animation);\n }, 500);\n return;\n }\n\n const nextBlockElement = this.api.blocks.getBlockByIndex(currentBlockIndex + 1);\n const nextBlockCoords = nextBlockElement.getBoundingClientRect();\n\n let scrollOffset = Math.abs(window.innerHeight - nextBlockElement.offsetHeight);\n\n /**\n * Next block ends on screen.\n * Increment scroll by next block's height to save element onscreen-position\n */\n if (nextBlockCoords.top < window.innerHeight) {\n\n scrollOffset = window.scrollY + nextBlockElement.offsetHeight;\n\n }\n\n window.scrollTo(0, scrollOffset);\n\n /** Change blocks positions */\n this.api.blocks.swap(currentBlockIndex, currentBlockIndex + 1);\n\n /** Hide the Tooltip */\n this.api.tooltip.hide();\n }\n}\n","/**\n * @class MoveUpTune\n * @classdesc Editor's default tune that moves up selected block\n *\n * @copyright 2018\n */\nimport $ from '../dom';\nimport {API, BlockTune} from '../../../types';\n\nexport default class MoveUpTune implements BlockTune {\n\n /**\n * Property that contains Editor.js API methods\n * @see {api.md}\n */\n private readonly api: API;\n\n /**\n * Styles\n * @type {{wrapper: string}}\n */\n private CSS = {\n button: 'ce-settings__button',\n wrapper: 'ce-tune-move-up',\n animation: 'wobble',\n };\n\n /**\n * MoveUpTune constructor\n *\n * @param {{api: API}} api\n */\n public constructor({api}) {\n this.api = api;\n }\n\n /**\n * Create \"MoveUp\" button and add click event listener\n * @returns [HTMLElement}\n */\n public render(): HTMLElement {\n const moveUpButton = $.make('div', [this.CSS.button, this.CSS.wrapper], {});\n moveUpButton.appendChild($.svg('arrow-up', 14, 14));\n this.api.listeners.on(\n moveUpButton,\n 'click',\n (event) => this.handleClick(event as MouseEvent, moveUpButton),\n false,\n );\n\n /**\n * Enable tooltip module on button\n */\n this.api.tooltip.onHover(moveUpButton, 'Move up');\n\n return moveUpButton;\n }\n\n /**\n * Move current block up\n * @param {MouseEvent} event\n * @param {HTMLElement} button\n */\n public handleClick(event: MouseEvent, button: HTMLElement): void {\n\n const currentBlockIndex = this.api.blocks.getCurrentBlockIndex();\n\n if (currentBlockIndex === 0) {\n button.classList.add(this.CSS.animation);\n\n window.setTimeout( () => {\n button.classList.remove(this.CSS.animation);\n }, 500);\n return;\n }\n\n const currentBlockElement = this.api.blocks.getBlockByIndex(currentBlockIndex);\n const previousBlockElement = this.api.blocks.getBlockByIndex(currentBlockIndex - 1);\n\n /**\n * Here is two cases:\n * - when previous block has negative offset and part of it is visible on window, then we scroll\n * by window's height and add offset which is mathematically difference between two blocks\n *\n * - when previous block is visible and has offset from the window,\n * than we scroll window to the difference between this offsets.\n */\n const currentBlockCoords = currentBlockElement.getBoundingClientRect(),\n previousBlockCoords = previousBlockElement.getBoundingClientRect();\n\n let scrollUpOffset;\n\n if (previousBlockCoords.top > 0) {\n scrollUpOffset = Math.abs(currentBlockCoords.top) - Math.abs(previousBlockCoords.top);\n } else {\n scrollUpOffset = window.innerHeight - Math.abs(currentBlockCoords.top) + Math.abs(previousBlockCoords.top);\n }\n\n window.scrollBy(0, -1 * scrollUpOffset);\n\n /** Change blocks positions */\n this.api.blocks.swap(currentBlockIndex, currentBlockIndex - 1);\n\n /** Hide the Tooltip */\n this.api.tooltip.hide();\n }\n}\n","import {\n API,\n BlockTool,\n BlockToolConstructable,\n BlockToolData,\n BlockTune,\n BlockTuneConstructable,\n SanitizerConfig,\n ToolConfig,\n} from '../../types';\n\nimport {SavedData} from '../types-internal/block-data';\nimport $ from './dom';\nimport * as _ from './utils';\n\n/**\n * @class Block\n * @classdesc This class describes editor`s block, including block`s HTMLElement, data and tool\n *\n * @property {BlockTool} tool — current block tool (Paragraph, for example)\n * @property {Object} CSS — block`s css classes\n *\n */\n\n/** Import default tunes */\nimport MoveUpTune from './block-tunes/block-tune-move-up';\nimport DeleteTune from './block-tunes/block-tune-delete';\nimport MoveDownTune from './block-tunes/block-tune-move-down';\nimport SelectionUtils from './selection';\n\n/**\n * Available Block Tool API methods\n */\nexport enum BlockToolAPI {\n /**\n * @todo remove method in 3.0.0\n * @deprecated — use 'rendered' hook instead\n */\n APPEND_CALLBACK = 'appendCallback',\n RENDERED = 'rendered',\n UPDATED = 'updated',\n REMOVED = 'removed',\n ON_PASTE = 'onPaste',\n}\n\n/**\n * @classdesc Abstract Block class that contains Block information, Tool name and Tool class instance\n *\n * @property tool - Tool instance\n * @property html - Returns HTML content of plugin\n * @property holder - Div element that wraps block content with Tool's content. Has `ce-block` CSS class\n * @property pluginsContent - HTML content that returns by Tool's render function\n */\nexport default class Block {\n\n /**\n * CSS classes for the Block\n * @return {{wrapper: string, content: string}}\n */\n static get CSS() {\n return {\n wrapper: 'ce-block',\n wrapperStretched: 'ce-block--stretched',\n content: 'ce-block__content',\n focused: 'ce-block--focused',\n selected: 'ce-block--selected',\n dropTarget: 'ce-block--drop-target',\n };\n }\n\n /**\n * Find and return all editable elements (contenteditables and native inputs) in the Tool HTML\n *\n * @returns {HTMLElement[]}\n */\n get inputs(): HTMLElement[] {\n /**\n * Return from cache if existed\n */\n if (this.cachedInputs.length !== 0) {\n return this.cachedInputs;\n }\n\n const content = this.holder;\n const allowedInputTypes = ['text', 'password', 'email', 'number', 'search', 'tel', 'url'];\n\n const selector = '[contenteditable], textarea, input:not([type]), '\n + allowedInputTypes.map((type) => `input[type=\"${type}\"]`).join(', ');\n\n let inputs = _.array(content.querySelectorAll(selector));\n\n /**\n * If contenteditable element contains block elements, treat them as inputs.\n */\n inputs = inputs.reduce((result, input) => {\n if ($.isNativeInput(input) || $.containsOnlyInlineElements(input)) {\n return [...result, input];\n }\n\n return [...result, ...$.getDeepestBlockElements(input)];\n }, []);\n\n /**\n * If inputs amount was changed we need to check if input index is bigger then inputs array length\n */\n if (this.inputIndex > inputs.length - 1) {\n this.inputIndex = inputs.length - 1;\n }\n\n /**\n * Cache inputs\n */\n this.cachedInputs = inputs;\n\n return inputs;\n }\n\n /**\n * Return current Tool`s input\n *\n * @returns {HTMLElement}\n */\n get currentInput(): HTMLElement | Node {\n return this.inputs[this.inputIndex];\n }\n\n /**\n * Set input index to the passed element\n *\n * @param {HTMLElement} element\n */\n set currentInput(element: HTMLElement | Node) {\n const index = this.inputs.findIndex((input) => input === element || input.contains(element));\n\n if (index !== -1) {\n this.inputIndex = index;\n }\n }\n\n /**\n * Return first Tool`s input\n *\n * @returns {HTMLElement}\n */\n get firstInput(): HTMLElement {\n return this.inputs[0];\n }\n\n /**\n * Return first Tool`s input\n *\n * @returns {HTMLElement}\n */\n get lastInput(): HTMLElement {\n const inputs = this.inputs;\n\n return inputs[inputs.length - 1];\n }\n\n /**\n * Return next Tool`s input or undefined if it doesn't exist\n *\n * @returns {HTMLElement}\n */\n get nextInput(): HTMLElement {\n return this.inputs[this.inputIndex + 1];\n }\n\n /**\n * Return previous Tool`s input or undefined if it doesn't exist\n *\n * @returns {HTMLElement}\n */\n get previousInput(): HTMLElement {\n return this.inputs[this.inputIndex - 1];\n }\n\n /**\n * Returns Plugins content\n * @return {HTMLElement}\n */\n get pluginsContent(): HTMLElement {\n const blockContentNodes = this.holder.querySelector(`.${Block.CSS.content}`);\n\n if (blockContentNodes && blockContentNodes.childNodes.length) {\n /**\n * Editors Block content can contain different Nodes from extensions\n * We use DOM isExtensionNode to ignore such Nodes and return first Block that does not match filtering list\n */\n for (let child = blockContentNodes.childNodes.length - 1; child >= 0; child--) {\n const contentNode = blockContentNodes.childNodes[child];\n\n if (!$.isExtensionNode(contentNode)) {\n return contentNode as HTMLElement;\n }\n }\n }\n\n return null;\n }\n\n /**\n * Get Block's JSON data\n * @return {Object}\n */\n get data(): BlockToolData {\n return this.save().then((savedObject) => {\n if (savedObject && !_.isEmpty(savedObject.data)) {\n return savedObject.data;\n } else {\n return {};\n }\n });\n }\n\n /**\n * Returns tool's sanitizer config\n * @return {object}\n */\n get sanitize(): SanitizerConfig {\n return this.tool.sanitize;\n }\n\n /**\n * is block mergeable\n * We plugin have merge function then we call it mergable\n * @return {boolean}\n */\n get mergeable(): boolean {\n return typeof this.tool.merge === 'function';\n }\n\n /**\n * Check block for emptiness\n * @return {Boolean}\n */\n get isEmpty(): boolean {\n const emptyText = $.isEmpty(this.pluginsContent);\n const emptyMedia = !this.hasMedia;\n\n return emptyText && emptyMedia;\n }\n\n /**\n * Check if block has a media content such as images, iframes and other\n * @return {Boolean}\n */\n get hasMedia(): boolean {\n /**\n * This tags represents media-content\n * @type {string[]}\n */\n const mediaTags = [\n 'img',\n 'iframe',\n 'video',\n 'audio',\n 'source',\n 'input',\n 'textarea',\n 'twitterwidget',\n ];\n\n return !!this.holder.querySelector(mediaTags.join(','));\n }\n\n /**\n * Set focused state\n * @param {Boolean} state - 'true' to select, 'false' to remove selection\n */\n set focused(state: boolean) {\n this.holder.classList.toggle(Block.CSS.focused, state);\n }\n\n /**\n * Set selected state\n * We don't need to mark Block as Selected when it is empty\n * @param {Boolean} state - 'true' to select, 'false' to remove selection\n */\n set selected(state: boolean) {\n if (state) {\n this.holder.classList.add(Block.CSS.selected);\n } else {\n this.holder.classList.remove(Block.CSS.selected);\n }\n }\n\n /**\n * Returns True if it is Selected\n * @return {boolean}\n */\n get selected(): boolean {\n return this.holder.classList.contains(Block.CSS.selected);\n }\n\n /**\n * Set stretched state\n * @param {Boolean} state - 'true' to enable, 'false' to disable stretched statte\n */\n set stretched(state: boolean) {\n this.holder.classList.toggle(Block.CSS.wrapperStretched, state);\n }\n\n /**\n * Toggle drop target state\n * @param {boolean} state\n */\n public set dropTarget(state) {\n this.holder.classList.toggle(Block.CSS.dropTarget, state);\n }\n\n /**\n * Block Tool`s name\n */\n public name: string;\n\n /**\n * Instance of the Tool Block represents\n */\n public tool: BlockTool;\n\n /**\n * Class blueprint of the ool Block represents\n */\n public class: BlockToolConstructable;\n\n /**\n * User Tool configuration\n */\n public settings: ToolConfig;\n\n /**\n * Wrapper for Block`s content\n */\n public holder: HTMLDivElement;\n\n /**\n * Tunes used by Tool\n */\n public tunes: BlockTune[];\n\n /**\n * Cached inputs\n * @type {HTMLElement[]}\n */\n private cachedInputs: HTMLElement[] = [];\n\n /**\n * Editor`s API\n */\n private readonly api: API;\n\n /**\n * Focused input index\n * @type {number}\n */\n private inputIndex = 0;\n\n /**\n * Mutation observer to handle DOM mutations\n * @type {MutationObserver}\n */\n private mutationObserver: MutationObserver;\n\n /**\n * Debounce Timer\n * @type {number}\n */\n private readonly modificationDebounceTimer = 450;\n\n /**\n * Is fired when DOM mutation has been happened\n */\n private didMutated = _.debounce((): void => {\n /**\n * Drop cache\n */\n this.cachedInputs = [];\n\n /**\n * Update current input\n */\n this.updateCurrentInput();\n\n this.call(BlockToolAPI.UPDATED);\n }, this.modificationDebounceTimer);\n\n /**\n * @constructor\n * @param {String} toolName - Tool name that passed on initialization\n * @param {Object} toolInstance — passed Tool`s instance that rendered the Block\n * @param {Object} toolClass — Tool's class\n * @param {Object} settings - default settings\n * @param {Object} apiMethods - Editor API\n */\n constructor(\n toolName: string,\n toolInstance: BlockTool,\n toolClass: BlockToolConstructable,\n settings: ToolConfig,\n apiMethods: API,\n ) {\n this.name = toolName;\n this.tool = toolInstance;\n this.class = toolClass;\n this.settings = settings;\n this.api = apiMethods;\n this.holder = this.compose();\n\n this.mutationObserver = new MutationObserver(this.didMutated);\n\n /**\n * @type {BlockTune[]}\n */\n this.tunes = this.makeTunes();\n }\n\n /**\n * Calls Tool's method\n *\n * Method checks tool property {MethodName}. Fires method with passes params If it is instance of Function\n *\n * @param {String} methodName\n * @param {Object} params\n */\n public call(methodName: string, params?: object) {\n /**\n * call Tool's method with the instance context\n */\n if (this.tool[methodName] && this.tool[methodName] instanceof Function) {\n try {\n this.tool[methodName].call(this.tool, params);\n } catch (e) {\n _.log(`Error during '${methodName}' call: ${e.message}`, 'error');\n }\n }\n }\n\n /**\n * Call plugins merge method\n * @param {Object} data\n */\n public async mergeWith(data: BlockToolData): Promise {\n await this.tool.merge(data);\n }\n /**\n * Extracts data from Block\n * Groups Tool's save processing time\n * @return {Object}\n */\n public async save(): Promise {\n const extractedBlock = await this.tool.save(this.pluginsContent as HTMLElement);\n\n /**\n * Measuring execution time\n */\n const measuringStart = window.performance.now();\n let measuringEnd;\n\n return Promise.resolve(extractedBlock)\n .then((finishedExtraction) => {\n /** measure promise execution */\n measuringEnd = window.performance.now();\n\n return {\n tool: this.name,\n data: finishedExtraction,\n time : measuringEnd - measuringStart,\n };\n })\n .catch((error) => {\n _.log(`Saving proccess for ${this.name} tool failed due to the ${error}`, 'log', 'red');\n });\n }\n\n /**\n * Uses Tool's validation method to check the correctness of output data\n * Tool's validation method is optional\n *\n * @description Method returns true|false whether data passed the validation or not\n *\n * @param {BlockToolData} data\n * @returns {Promise} valid\n */\n public async validate(data: BlockToolData): Promise {\n let isValid = true;\n\n if (this.tool.validate instanceof Function) {\n isValid = await this.tool.validate(data);\n }\n\n return isValid;\n }\n\n /**\n * Make an array with default settings\n * Each block has default tune instance that have states\n * @return {BlockTune[]}\n */\n public makeTunes(): BlockTune[] {\n const tunesList = [MoveUpTune, DeleteTune, MoveDownTune];\n\n // Pluck tunes list and return tune instances with passed Editor API and settings\n return tunesList.map( (tune: BlockTuneConstructable) => {\n return new tune({\n api: this.api,\n settings: this.settings,\n });\n });\n }\n\n /**\n * Enumerates initialized tunes and returns fragment that can be appended to the toolbars area\n * @return {DocumentFragment}\n */\n public renderTunes(): DocumentFragment {\n const tunesElement = document.createDocumentFragment();\n\n this.tunes.forEach( (tune) => {\n $.append(tunesElement, tune.render());\n });\n\n return tunesElement;\n }\n\n /**\n * Update current input index with selection anchor node\n */\n public updateCurrentInput(): void {\n this.currentInput = SelectionUtils.anchorNode;\n }\n\n /**\n * Is fired when Block will be selected as current\n */\n public willSelect(): void {\n /**\n * Observe DOM mutations to update Block inputs\n */\n this.mutationObserver.observe(\n this.holder.firstElementChild,\n {\n childList: true,\n subtree: true,\n characterData: true,\n attributes: true,\n },\n );\n }\n\n /**\n * Is fired when Block will be unselected\n */\n public willUnselect() {\n this.mutationObserver.disconnect();\n }\n\n /**\n * Make default Block wrappers and put Tool`s content there\n * @returns {HTMLDivElement}\n */\n private compose(): HTMLDivElement {\n const wrapper = $.make('div', Block.CSS.wrapper) as HTMLDivElement,\n contentNode = $.make('div', Block.CSS.content),\n pluginsContent = this.tool.render();\n\n contentNode.appendChild(pluginsContent);\n wrapper.appendChild(contentNode);\n return wrapper;\n }\n}\n","import * as _ from './utils';\nimport $ from './dom';\nimport Block, {BlockToolAPI} from './block';\n\n/**\n * @class Blocks\n * @classdesc Class to work with Block instances array\n *\n * @private\n *\n * @property {HTMLElement} workingArea — editor`s working node\n *\n */\nexport default class Blocks {\n /**\n * Get length of Block instances array\n *\n * @returns {Number}\n */\n public get length(): number {\n return this.blocks.length;\n }\n\n /**\n * Get Block instances array\n *\n * @returns {Block[]}\n */\n public get array(): Block[] {\n return this.blocks;\n }\n\n /**\n * Get blocks html elements array\n *\n * @returns {HTMLElement[]}\n */\n public get nodes(): HTMLElement[] {\n return _.array(this.workingArea.children);\n }\n\n /**\n * Proxy trap to implement array-like setter\n *\n * @example\n * blocks[0] = new Block(...)\n *\n * @param {Blocks} instance — Blocks instance\n * @param {Number|String} property — block index or any Blocks class property to set\n * @param {Block} value — value to set\n * @returns {Boolean}\n */\n public static set(instance: Blocks, property: number | string, value: Block | any) {\n\n /**\n * If property name is not a number (method or other property, access it via reflect\n */\n if (isNaN(Number(property))) {\n Reflect.set(instance, property, value);\n return true;\n }\n\n /**\n * If property is number, call insert method to emulate array behaviour\n *\n * @example\n * blocks[0] = new Block();\n */\n instance.insert(+property, value);\n\n return true;\n }\n\n /**\n * Proxy trap to implement array-like getter\n *\n * @param {Blocks} instance — Blocks instance\n * @param {Number|String} property — Blocks class property\n * @returns {Block|*}\n */\n public static get(instance: Blocks, property: any | number) {\n\n /**\n * If property is not a number, get it via Reflect object\n */\n if (isNaN(Number(property))) {\n return Reflect.get(instance, property);\n }\n\n /**\n * If property is a number (Block index) return Block by passed index\n */\n return instance.get(+property);\n }\n\n /**\n * Array of Block instances in order of addition\n */\n public blocks: Block[];\n\n /**\n * Editor`s area where to add Block`s HTML\n */\n public workingArea: HTMLElement;\n\n /**\n * @constructor\n *\n * @param {HTMLElement} workingArea — editor`s working node\n */\n constructor(workingArea: HTMLElement) {\n this.blocks = [];\n this.workingArea = workingArea;\n }\n\n /**\n * Push new Block to the blocks array and append it to working area\n *\n * @param {Block} block\n */\n public push(block: Block): void {\n this.blocks.push(block);\n this.insertToDOM(block);\n }\n\n /**\n * Swaps blocks with indexes first and second\n * @param {Number} first - first block index\n * @param {Number} second - second block index\n */\n public swap(first: number, second: number): void {\n const secondBlock = this.blocks[second];\n\n /**\n * Change in DOM\n */\n $.swap(this.blocks[first].holder, secondBlock.holder);\n\n /**\n * Change in array\n */\n this.blocks[second] = this.blocks[first];\n this.blocks[first] = secondBlock;\n }\n\n /**\n * Insert new Block at passed index\n *\n * @param {Number} index — index to insert Block\n * @param {Block} block — Block to insert\n * @param {Boolean} replace — it true, replace block on given index\n */\n public insert(index: number, block: Block, replace: boolean = false): void {\n if (!this.length) {\n this.push(block);\n return;\n }\n\n if (index > this.length) {\n index = this.length;\n }\n\n if (replace) {\n this.blocks[index].holder.remove();\n this.blocks[index].call(BlockToolAPI.REMOVED);\n }\n\n const deleteCount = replace ? 1 : 0;\n\n this.blocks.splice(index, deleteCount, block);\n\n if (index > 0) {\n const previousBlock = this.blocks[index - 1];\n\n this.insertToDOM(block, 'afterend', previousBlock);\n } else {\n const nextBlock = this.blocks[index + 1];\n\n if (nextBlock) {\n this.insertToDOM(block, 'beforebegin', nextBlock);\n } else {\n this.insertToDOM(block);\n }\n }\n }\n\n /**\n * Remove block\n * @param {Number|null} index\n */\n public remove(index: number): void {\n if (isNaN(index)) {\n index = this.length - 1;\n }\n\n this.blocks[index].holder.remove();\n\n this.blocks[index].call(BlockToolAPI.REMOVED);\n\n this.blocks.splice(index, 1);\n }\n\n /**\n * Remove all blocks\n */\n public removeAll(): void {\n this.workingArea.innerHTML = '';\n\n this.blocks.forEach((block) => block.call(BlockToolAPI.REMOVED));\n\n this.blocks.length = 0;\n }\n\n /**\n * Insert Block after passed target\n *\n * @todo decide if this method is necessary\n *\n * @param {Block} targetBlock — target after wich Block should be inserted\n * @param {Block} newBlock — Block to insert\n */\n public insertAfter(targetBlock: Block, newBlock: Block): void {\n const index = this.blocks.indexOf(targetBlock);\n\n this.insert(index + 1, newBlock);\n }\n\n /**\n * Get Block by index\n *\n * @param {Number} index — Block index\n * @returns {Block}\n */\n public get(index: number): Block {\n return this.blocks[index];\n }\n\n /**\n * Return index of passed Block\n *\n * @param {Block} block\n * @returns {Number}\n */\n public indexOf(block: Block): number {\n return this.blocks.indexOf(block);\n }\n\n /**\n * Insert new Block into DOM\n *\n * @param {Block} block - Block to insert\n * @param {InsertPosition} position — insert position (if set, will use insertAdjacentElement)\n * @param {Block} target — Block related to position\n */\n private insertToDOM(block: Block, position?: InsertPosition, target?: Block): void {\n if (position) {\n target.holder.insertAdjacentElement(position, block.holder);\n } else {\n this.workingArea.appendChild(block.holder);\n }\n\n block.call(BlockToolAPI.RENDERED);\n }\n}\n","import $ from './dom';\nimport * as _ from './utils';\nimport {EditorConfig, OutputData, SanitizerConfig} from '../../types';\nimport {EditorModules} from '../types-internal/editor-modules';\nimport {LogLevels} from './utils';\n\n/**\n * @typedef {Core} Core - editor core class\n */\n\n/**\n * Require Editor modules places in components/modules dir\n */\nconst contextRequire = require.context('./modules', true);\n\nconst modules = [];\n\ncontextRequire.keys().forEach((filename) => {\n /**\n * Include files if:\n * - extension is .js or .ts\n * - does not starts with _\n */\n if (filename.match(/^\\.\\/[^_][\\w/]*\\.([tj])s$/)) {\n modules.push(contextRequire(filename));\n }\n});\n\n/**\n * @class Core\n *\n * @classdesc Editor.js core class\n *\n * @property this.config - all settings\n * @property this.moduleInstances - constructed editor components\n *\n * @type {Core}\n */\nexport default class Core {\n\n /**\n * Editor configuration passed by user to the constructor\n */\n public config: EditorConfig;\n\n /**\n * Object with core modules instances\n */\n public moduleInstances: EditorModules = {} as EditorModules;\n\n /**\n * Promise that resolves when all core modules are prepared and UI is rendered on the page\n */\n public isReady: Promise;\n\n /**\n * @param {EditorConfig} config - user configuration\n *\n */\n constructor(config?: EditorConfig|string) {\n /**\n * Ready promise. Resolved if Editor.js is ready to work, rejected otherwise\n */\n let onReady, onFail;\n\n this.isReady = new Promise((resolve, reject) => {\n onReady = resolve;\n onFail = reject;\n });\n\n Promise.resolve()\n .then(async () => {\n this.configuration = config;\n\n await this.validate();\n await this.init();\n await this.start();\n\n _.logLabeled('I\\'m ready! (ノ◕ヮ◕)ノ*:・゚✧', 'log', '', 'color: #E24A75');\n\n setTimeout(async () => {\n await this.render();\n\n if ((this.configuration as EditorConfig).autofocus) {\n const {BlockManager, Caret} = this.moduleInstances;\n\n Caret.setToBlock(BlockManager.blocks[0], Caret.positions.START);\n }\n\n /**\n * Remove loader, show content\n */\n this.moduleInstances.UI.removeLoader();\n\n /**\n * Resolve this.isReady promise\n */\n onReady();\n }, 500);\n })\n .catch((error) => {\n _.log(`Editor.js is not ready because of ${error}`, 'error');\n\n /**\n * Reject this.isReady promise\n */\n onFail(error);\n });\n }\n\n /**\n * Setting for configuration\n * @param {EditorConfig|string|undefined} config\n */\n set configuration(config: EditorConfig|string) {\n /**\n * Process zero-configuration or with only holderId\n * Make config object\n */\n if (typeof config !== 'object') {\n config = {\n holder: config,\n };\n }\n\n /**\n * If holderId is preset, assign him to holder property and work next only with holder\n */\n if (config.holderId && !config.holder) {\n config.holder = config.holderId;\n config.holderId = null;\n _.log('holderId property will deprecated in next major release, use holder property instead.', 'warn');\n }\n\n /**\n * Place config into the class property\n * @type {EditorConfig}\n */\n this.config = config;\n\n /**\n * If holder is empty then set a default value\n */\n if (this.config.holder == null) {\n this.config.holder = 'editorjs';\n }\n\n if (!this.config.logLevel) {\n this.config.logLevel = LogLevels.VERBOSE;\n }\n\n _.setLogLevel(this.config.logLevel);\n\n /**\n * If initial Block's Tool was not passed, use the Paragraph Tool\n */\n this.config.initialBlock = this.config.initialBlock || 'paragraph';\n\n /**\n * Height of Editor's bottom area that allows to set focus on the last Block\n * @type {number}\n */\n this.config.minHeight = this.config.minHeight !== undefined ? this.config.minHeight : 300 ;\n\n /**\n * Initial block type\n * Uses in case when there is no blocks passed\n * @type {{type: (*), data: {text: null}}}\n */\n const initialBlockData = {\n type : this.config.initialBlock,\n data : {},\n };\n\n this.config.placeholder = this.config.placeholder || false;\n this.config.sanitizer = this.config.sanitizer || {\n p: true,\n b: true,\n a: true,\n } as SanitizerConfig;\n\n this.config.hideToolbar = this.config.hideToolbar ? this.config.hideToolbar : false;\n this.config.tools = this.config.tools || {};\n this.config.data = this.config.data || {} as OutputData;\n this.config.onReady = this.config.onReady || (() => {});\n this.config.onChange = this.config.onChange || (() => {});\n\n /**\n * Initialize Blocks to pass data to the Renderer\n */\n if (_.isEmpty(this.config.data)) {\n this.config.data = {} as OutputData;\n this.config.data.blocks = [ initialBlockData ];\n } else {\n if (!this.config.data.blocks || this.config.data.blocks.length === 0) {\n this.config.data.blocks = [ initialBlockData ];\n }\n }\n }\n\n /**\n * Returns private property\n * @returns {EditorConfig}\n */\n get configuration(): EditorConfig|string {\n return this.config;\n }\n\n /**\n * Checks for required fields in Editor's config\n * @returns {Promise}\n */\n public async validate(): Promise {\n const { holderId, holder } = this.config;\n\n if (holderId && holder) {\n throw Error('«holderId» and «holder» param can\\'t assign at the same time.');\n }\n\n /**\n * Check for a holder element's existence\n */\n if (typeof holder === 'string' && !$.get(holder)) {\n throw Error(`element with ID «${holder}» is missing. Pass correct holder's ID.`);\n }\n\n if (holder && typeof holder === 'object' && !$.isElement(holder)) {\n throw Error('holder as HTMLElement if provided must be inherit from Element class.');\n }\n }\n\n /**\n * Initializes modules:\n * - make and save instances\n * - configure\n */\n public init() {\n /**\n * Make modules instances and save it to the @property this.moduleInstances\n */\n this.constructModules();\n\n /**\n * Modules configuration\n */\n this.configureModules();\n }\n\n /**\n * Start Editor!\n *\n * Get list of modules that needs to be prepared and return a sequence (Promise)\n * @return {Promise}\n */\n public async start() {\n const modulesToPrepare = [\n 'Tools',\n 'UI',\n 'BlockManager',\n 'Paste',\n 'DragNDrop',\n 'ModificationsObserver',\n 'BlockSelection',\n 'RectangleSelection',\n ];\n\n await modulesToPrepare.reduce(\n (promise, module) => promise.then(async () => {\n // _.log(`Preparing ${module} module`, 'time');\n\n try {\n await this.moduleInstances[module].prepare();\n } catch (e) {\n _.log(`Module ${module} was skipped because of %o`, 'warn', e);\n }\n // _.log(`Preparing ${module} module`, 'timeEnd');\n }),\n Promise.resolve(),\n );\n }\n\n /**\n * Render initial data\n */\n private render(): Promise {\n return this.moduleInstances.Renderer.render(this.config.data.blocks);\n }\n\n /**\n * Make modules instances and save it to the @property this.moduleInstances\n */\n private constructModules(): void {\n modules.forEach( (module) => {\n /**\n * If module has non-default exports, passed object contains them all and default export as 'default' property\n */\n const Module = typeof module === 'function' ? module : module.default;\n\n try {\n\n /**\n * We use class name provided by displayName property\n *\n * On build, Babel will transform all Classes to the Functions so, name will always be 'Function'\n * To prevent this, we use 'babel-plugin-class-display-name' plugin\n * @see https://www.npmjs.com/package/babel-plugin-class-display-name\n */\n this.moduleInstances[Module.displayName] = new Module({\n config : this.configuration,\n });\n } catch ( e ) {\n _.log(`Module ${Module.displayName} skipped because`, 'warn', e);\n }\n });\n }\n\n /**\n * Modules instances configuration:\n * - pass other modules to the 'state' property\n * - ...\n */\n private configureModules(): void {\n for (const name in this.moduleInstances) {\n if (this.moduleInstances.hasOwnProperty(name)) {\n /**\n * Module does not need self-instance\n */\n this.moduleInstances[name].state = this.getModulesDiff(name);\n }\n }\n }\n\n /**\n * Return modules without passed name\n * @param {string} name - module for witch modules difference should be calculated\n */\n private getModulesDiff(name: string): EditorModules {\n const diff = {} as EditorModules;\n\n for (const moduleName in this.moduleInstances) {\n /**\n * Skip module with passed name\n */\n if (moduleName === name) {\n continue;\n }\n diff[moduleName] = this.moduleInstances[moduleName];\n }\n\n return diff;\n }\n}\n","/**\n * DOM manipulations helper\n */\nexport default class Dom {\n /**\n * Check if passed tag has no closed tag\n * @param {HTMLElement} tag\n * @return {Boolean}\n */\n public static isSingleTag(tag: HTMLElement): boolean {\n return tag.tagName && [\n 'AREA',\n 'BASE',\n 'BR',\n 'COL',\n 'COMMAND',\n 'EMBED',\n 'HR',\n 'IMG',\n 'INPUT',\n 'KEYGEN',\n 'LINK',\n 'META',\n 'PARAM',\n 'SOURCE',\n 'TRACK',\n 'WBR',\n ].includes(tag.tagName);\n }\n\n /**\n * Check if element is BR or WBR\n *\n * @param {HTMLElement} element\n * @return {boolean}\n */\n public static isLineBreakTag(element: HTMLElement) {\n return element && element.tagName && [\n 'BR',\n 'WBR',\n ].includes(element.tagName);\n }\n\n /**\n * Helper for making Elements with classname and attributes\n *\n * @param {string} tagName - new Element tag name\n * @param {array|string} classNames - list or name of CSS classname(s)\n * @param {Object} attributes - any attributes\n * @return {HTMLElement}\n */\n public static make(tagName: string, classNames: string|string[] = null, attributes: object = {}): HTMLElement {\n const el = document.createElement(tagName);\n\n if ( Array.isArray(classNames) ) {\n el.classList.add(...classNames);\n } else if ( classNames ) {\n el.classList.add(classNames);\n }\n\n for (const attrName in attributes) {\n if (attributes.hasOwnProperty(attrName)) {\n el[attrName] = attributes[attrName];\n }\n }\n\n return el;\n }\n\n /**\n * Creates Text Node with the passed content\n * @param {String} content - text content\n * @return {Text}\n */\n public static text(content: string): Text {\n return document.createTextNode(content);\n }\n\n /**\n * Creates SVG icon linked to the sprite\n * @param {string} name - name (id) of icon from sprite\n * @param {number} width\n * @param {number} height\n * @return {SVGElement}\n */\n public static svg(name: string, width: number = 14, height: number = 14): SVGElement {\n const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n\n icon.classList.add('icon', 'icon--' + name);\n icon.setAttribute('width', width + 'px');\n icon.setAttribute('height', height + 'px');\n icon.innerHTML = ``;\n\n return icon;\n }\n\n /**\n * Append one or several elements to the parent\n *\n * @param {Element|DocumentFragment} parent - where to append\n * @param {Element|Element[]|Text|Text[]} elements - element or elements list\n */\n public static append(\n parent: Element|DocumentFragment,\n elements: Element|Element[]|DocumentFragment|Text|Text[],\n ): void {\n if ( Array.isArray(elements) ) {\n elements.forEach( (el) => parent.appendChild(el) );\n } else {\n parent.appendChild(elements);\n }\n }\n\n /**\n * Append element or a couple to the beginning of the parent elements\n *\n * @param {Element} parent - where to append\n * @param {Element|Element[]} elements - element or elements list\n */\n public static prepend(parent: Element, elements: Element|Element[]): void {\n if ( Array.isArray(elements) ) {\n elements = elements.reverse();\n elements.forEach( (el) => parent.prepend(el) );\n } else {\n parent.prepend(elements);\n }\n }\n\n /**\n * Swap two elements in parent\n * @param {HTMLElement} el1 - from\n * @param {HTMLElement} el2 - to\n */\n public static swap(el1: HTMLElement, el2: HTMLElement): void {\n // create marker element and insert it where el1 is\n const temp = document.createElement('div'),\n parent = el1.parentNode;\n\n parent.insertBefore(temp, el1);\n\n // move el1 to right before el2\n parent.insertBefore(el1, el2);\n\n // move el2 to right before where el1 used to be\n parent.insertBefore(el2, temp);\n\n // remove temporary marker node\n parent.removeChild(temp);\n }\n\n /**\n * Selector Decorator\n *\n * Returns first match\n *\n * @param {Element} el - element we searching inside. Default - DOM Document\n * @param {String} selector - searching string\n *\n * @returns {Element}\n */\n public static find(el: Element|Document = document, selector: string): Element {\n return el.querySelector(selector);\n }\n\n /**\n * Get Element by Id\n *\n * @param {string} id\n * @returns {HTMLElement | null}\n */\n public static get(id: string): HTMLElement {\n return document.getElementById(id);\n }\n\n /**\n * Selector Decorator.\n *\n * Returns all matches\n *\n * @param {Element} el - element we searching inside. Default - DOM Document\n * @param {String} selector - searching string\n * @returns {NodeList}\n */\n public static findAll(el: Element|Document = document, selector: string): NodeList {\n return el.querySelectorAll(selector);\n }\n\n /**\n * Search for deepest node which is Leaf.\n * Leaf is the vertex that doesn't have any child nodes\n *\n * @description Method recursively goes throw the all Node until it finds the Leaf\n *\n * @param {Node} node - root Node. From this vertex we start Deep-first search\n * {@link https://en.wikipedia.org/wiki/Depth-first_search}\n * @param {Boolean} atLast - find last text node\n * @return {Node} - it can be text Node or Element Node, so that caret will able to work with it\n */\n public static getDeepestNode(node: Node, atLast: boolean = false): Node {\n /**\n * Current function have two directions:\n * - starts from first child and every time gets first or nextSibling in special cases\n * - starts from last child and gets last or previousSibling\n * @type {string}\n */\n const child = atLast ? 'lastChild' : 'firstChild',\n sibling = atLast ? 'previousSibling' : 'nextSibling';\n\n if (node && node.nodeType === Node.ELEMENT_NODE && node[child]) {\n let nodeChild = node[child] as Node;\n\n /**\n * special case when child is single tag that can't contain any content\n */\n if (\n Dom.isSingleTag(nodeChild as HTMLElement) &&\n !Dom.isNativeInput(nodeChild) &&\n !Dom.isLineBreakTag(nodeChild as HTMLElement)\n ) {\n /**\n * 1) We need to check the next sibling. If it is Node Element then continue searching for deepest\n * from sibling\n *\n * 2) If single tag's next sibling is null, then go back to parent and check his sibling\n * In case of Node Element continue searching\n *\n * 3) If none of conditions above happened return parent Node Element\n */\n if (nodeChild[sibling]) {\n nodeChild = nodeChild[sibling];\n } else if (nodeChild.parentNode[sibling]) {\n nodeChild = nodeChild.parentNode[sibling];\n } else {\n return nodeChild.parentNode;\n }\n }\n\n return this.getDeepestNode(nodeChild, atLast);\n }\n\n return node;\n }\n\n /**\n * Check if object is DOM node\n *\n * @param {Object} node\n * @returns {boolean}\n */\n public static isElement(node: any): node is Element {\n return node && typeof node === 'object' && node.nodeType && node.nodeType === Node.ELEMENT_NODE;\n }\n\n /**\n * Check if object is DocumentFragmemt node\n *\n * @param {Object} node\n * @returns {boolean}\n */\n public static isFragment(node: any): boolean {\n return node && typeof node === 'object' && node.nodeType && node.nodeType === Node.DOCUMENT_FRAGMENT_NODE;\n }\n\n /**\n * Check if passed element is contenteditable\n * @param {HTMLElement} element\n * @return {boolean}\n */\n public static isContentEditable(element: HTMLElement): boolean {\n return element.contentEditable === 'true';\n }\n\n /**\n * Checks target if it is native input\n * @param {Element|String|Node} target - HTML element or string\n * @return {Boolean}\n */\n public static isNativeInput(target: any): boolean {\n const nativeInputs = [\n 'INPUT',\n 'TEXTAREA',\n ];\n\n return target && target.tagName ? nativeInputs.includes(target.tagName) : false;\n }\n\n /**\n * Checks if we can set caret\n * @param {HTMLElement} target\n * @return {boolean}\n */\n public static canSetCaret(target: HTMLElement): boolean {\n let result = true;\n if (Dom.isNativeInput(target)) {\n const inputElement = target as HTMLInputElement;\n switch (inputElement.type) {\n case 'file':\n case 'checkbox':\n case 'radio':\n case 'hidden':\n case 'submit':\n case 'button':\n case 'image':\n case 'reset':\n result = false;\n break;\n }\n } else {\n result = Dom.isContentEditable(target);\n }\n return result;\n }\n\n /**\n * Checks node if it is empty\n *\n * @description Method checks simple Node without any childs for emptiness\n * If you have Node with 2 or more children id depth, you better use {@link Dom#isEmpty} method\n *\n * @param {Node} node\n * @return {Boolean} true if it is empty\n */\n public static isNodeEmpty(node: Node): boolean {\n let nodeText;\n\n if (this.isSingleTag(node as HTMLElement) && !this.isLineBreakTag(node as HTMLElement)) {\n return false;\n }\n\n if ( this.isElement(node) && this.isNativeInput(node) ) {\n nodeText = (node as HTMLInputElement).value;\n } else {\n nodeText = node.textContent.replace('\\u200B', '');\n }\n\n return nodeText.trim().length === 0;\n }\n\n /**\n * checks node if it is doesn't have any child nodes\n * @param {Node} node\n * @return {boolean}\n */\n public static isLeaf(node: Node): boolean {\n if (!node) {\n return false;\n }\n\n return node.childNodes.length === 0;\n }\n\n /**\n * breadth-first search (BFS)\n * {@link https://en.wikipedia.org/wiki/Breadth-first_search}\n *\n * @description Pushes to stack all DOM leafs and checks for emptiness\n *\n * @param {Node} node\n * @return {boolean}\n */\n public static isEmpty(node: Node): boolean {\n const treeWalker = [],\n leafs = [];\n\n if (!node) {\n return true;\n }\n\n if (!node.childNodes.length) {\n return this.isNodeEmpty(node);\n }\n\n /**\n * Normalize node to merge several text nodes to one to reduce tree walker iterations\n */\n node.normalize();\n\n treeWalker.push(node.firstChild);\n\n while ( treeWalker.length > 0 ) {\n node = treeWalker.shift();\n\n if (!node) { continue; }\n\n if ( this.isLeaf(node) ) {\n leafs.push(node);\n } else {\n treeWalker.push(node.firstChild);\n }\n\n while ( node && node.nextSibling ) {\n node = node.nextSibling;\n\n if (!node) { continue; }\n\n treeWalker.push(node);\n }\n\n /**\n * If one of childs is not empty, checked Node is not empty too\n */\n if (node && !this.isNodeEmpty(node)) {\n return false;\n }\n }\n\n return leafs.every( (leaf) => this.isNodeEmpty(leaf) );\n }\n\n /**\n * Check if string contains html elements\n *\n * @returns {boolean}\n * @param {String} str\n */\n public static isHTMLString(str: string): boolean {\n const wrapper = Dom.make('div');\n\n wrapper.innerHTML = str;\n\n return wrapper.childElementCount > 0;\n }\n\n /**\n * Return length of node`s text content\n *\n * @param {Node} node\n * @returns {number}\n */\n public static getContentLength(node: Node): number {\n if (Dom.isNativeInput(node)) {\n return (node as HTMLInputElement).value.length;\n }\n\n if (node.nodeType === Node.TEXT_NODE) {\n return (node as Text).length;\n }\n\n return node.textContent.length;\n }\n\n /**\n * Return array of names of block html elements\n *\n * @returns {string[]}\n */\n static get blockElements(): string[] {\n return [\n 'address',\n 'article',\n 'aside',\n 'blockquote',\n 'canvas',\n 'div',\n 'dl',\n 'dt',\n 'fieldset',\n 'figcaption',\n 'figure',\n 'footer',\n 'form',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'header',\n 'hgroup',\n 'hr',\n 'li',\n 'main',\n 'nav',\n 'noscript',\n 'ol',\n 'output',\n 'p',\n 'pre',\n 'ruby',\n 'section',\n 'table',\n 'tr',\n 'tfoot',\n 'ul',\n 'video',\n ];\n }\n\n /**\n * Check if passed content includes only inline elements\n *\n * @param {string|HTMLElement} data - element or html string\n * @return {boolean}\n */\n public static containsOnlyInlineElements(data: string | HTMLElement): boolean {\n let wrapper: HTMLElement;\n\n if (typeof data === 'string') {\n wrapper = document.createElement('div');\n wrapper.innerHTML = data;\n } else {\n wrapper = data;\n }\n\n const check = (element: HTMLElement) => {\n return !Dom.blockElements.includes(element.tagName.toLowerCase())\n && Array.from(element.children).every(check);\n };\n\n return Array.from(wrapper.children).every(check);\n }\n\n /**\n * Find and return all block elements in the passed parent (including subtree)\n *\n * @param {HTMLElement} parent\n *\n * @return {HTMLElement[]}\n */\n public static getDeepestBlockElements(parent: HTMLElement): HTMLElement[] {\n if (Dom.containsOnlyInlineElements(parent)) {\n return [parent];\n }\n\n return Array.from(parent.children).reduce((result, element) => {\n return [...result, ...Dom.getDeepestBlockElements(element as HTMLElement)];\n }, []);\n }\n\n /*\n * Helper for get holder from {string} or return HTMLElement\n * @param element\n */\n public static getHolder(element: string | HTMLElement): HTMLElement {\n if (typeof element === 'string') { return document.getElementById(element); }\n return element;\n }\n\n /**\n * Method checks passed Node if it is some extension Node\n * @param {Node} node - any node\n */\n public static isExtensionNode(node: Node): boolean {\n const extensions = [\n 'GRAMMARLY-EXTENSION',\n ];\n\n return node && extensions.includes(node.nodeName);\n }\n}\n","import Dom from './dom';\n\n/**\n * Iterator above passed Elements list.\n * Each next or previous action adds provides CSS-class and sets cursor to this item\n */\nexport default class DomIterator {\n /**\n * This is a static property that defines iteration directions\n * @type {{RIGHT: string, LEFT: string}}\n */\n public static directions = {\n RIGHT: 'right',\n LEFT: 'left',\n };\n\n /**\n * User-provided CSS-class name for focused button\n */\n private focusedCssClass: string;\n\n /**\n * Focused button index.\n * Default is -1 which means nothing is active\n * @type {number}\n */\n private cursor: number = -1;\n\n /**\n * Items to flip\n */\n private items: HTMLElement[] = [];\n\n /**\n * @param {HTMLElement[]} nodeList — the list of iterable HTML-items\n * @param {string} focusedCssClass - user-provided CSS-class that will be set in flipping process\n */\n constructor(\n nodeList: HTMLElement[],\n focusedCssClass: string,\n ) {\n this.items = nodeList || [];\n this.focusedCssClass = focusedCssClass;\n }\n\n /**\n * Returns Focused button Node\n * @return {HTMLElement}\n */\n public get currentItem(): HTMLElement {\n if (this.cursor === -1) {\n return null;\n }\n\n return this.items[this.cursor];\n }\n\n /**\n * Sets items. Can be used when iterable items changed dynamically\n * @param {HTMLElement[]} nodeList\n */\n public setItems(nodeList: HTMLElement[]): void {\n this.items = nodeList;\n }\n\n /**\n * Sets cursor next to the current\n */\n public next(): void {\n this.cursor = this.leafNodesAndReturnIndex(DomIterator.directions.RIGHT);\n }\n\n /**\n * Sets cursor before current\n */\n public previous(): void {\n this.cursor = this.leafNodesAndReturnIndex(DomIterator.directions.LEFT);\n }\n\n /**\n * Sets cursor to the default position and removes CSS-class from previously focused item\n */\n public dropCursor(): void {\n if (this.cursor === -1) {\n return;\n }\n\n this.items[this.cursor].classList.remove(this.focusedCssClass);\n this.cursor = -1;\n }\n\n /**\n * Leafs nodes inside the target list from active element\n *\n * @param {string} direction - leaf direction. Can be 'left' or 'right'\n * @return {Number} index of focused node\n */\n private leafNodesAndReturnIndex(direction: string): number {\n /**\n * if items are empty then there is nothing to leaf\n */\n if (this.items.length === 0) {\n return this.cursor;\n }\n\n let focusedButtonIndex = this.cursor;\n\n /**\n * If activeButtonIndex === -1 then we have no chosen Tool in Toolbox\n */\n if (focusedButtonIndex === -1) {\n /**\n * Normalize \"previous\" Tool index depending on direction.\n * We need to do this to highlight \"first\" Tool correctly\n *\n * Order of Tools: [0] [1] ... [n - 1]\n * [0 = n] because of: n % n = 0 % n\n *\n * Direction 'right': for [0] the [n - 1] is a previous index\n * [n - 1] -> [0]\n *\n * Direction 'left': for [n - 1] the [0] is a previous index\n * [n - 1] <- [0]\n *\n * @type {number}\n */\n focusedButtonIndex = direction === DomIterator.directions.RIGHT ? -1 : 0;\n } else {\n /**\n * If we have chosen Tool then remove highlighting\n */\n this.items[focusedButtonIndex].classList.remove(this.focusedCssClass);\n }\n\n /**\n * Count index for next Tool\n */\n if (direction === DomIterator.directions.RIGHT) {\n /**\n * If we go right then choose next (+1) Tool\n * @type {number}\n */\n focusedButtonIndex = (focusedButtonIndex + 1) % this.items.length;\n } else {\n /**\n * If we go left then choose previous (-1) Tool\n * Before counting module we need to add length before because of \"The JavaScript Modulo Bug\"\n * @type {number}\n */\n focusedButtonIndex = (this.items.length + focusedButtonIndex - 1) % this.items.length;\n }\n\n if (Dom.isNativeInput(this.items[focusedButtonIndex])) {\n /**\n * Focus input\n */\n this.items[focusedButtonIndex].focus();\n }\n\n /**\n * Highlight new chosen Tool\n */\n this.items[focusedButtonIndex].classList.add(this.focusedCssClass);\n\n /**\n * Return focused button's index\n */\n return focusedButtonIndex;\n }\n}\n","import DomIterator from './domIterator';\nimport * as _ from './utils';\n\n/**\n * Flipper construction options\n */\nexport interface FlipperOptions {\n /**\n * CSS-modifier for focused item\n */\n focusedItemClass?: string;\n\n /**\n * If flipping items are the same for all Block (for ex. Toolbox), ypu can pass it on constructing\n */\n items?: HTMLElement[];\n\n /**\n * Defines arrows usage. By default Flipper leafs items also via RIGHT/LEFT.\n *\n * true by default\n *\n * Pass 'false' if you don't need this behaviour\n * (for example, Inline Toolbar should be closed by arrows,\n * because it means caret moving with selection clearing)\n */\n allowArrows?: boolean;\n\n /**\n * Optional callback for button click\n */\n activateCallback?: () => void;\n}\n\n/**\n * Flipper is a component that iterates passed items array by TAB or Arrows and clicks it by ENTER\n */\nexport default class Flipper {\n\n /**\n * Instance of flipper iterator\n * @type {DomIterator|null}\n */\n private readonly iterator: DomIterator = null;\n\n /**\n * Flag that defines activation status\n * @type {boolean}\n */\n private activated: boolean = false;\n\n /**\n * Flag that allows arrows usage to flip items\n * @type {boolean}\n */\n private readonly allowArrows: boolean = true;\n\n /**\n * Call back for button click/enter\n */\n private readonly activateCallback: () => void;\n\n /**\n * @constructor\n *\n * @param {FlipperOptions} options - different constructing settings\n * @\n */\n constructor(options: FlipperOptions) {\n this.allowArrows = typeof options.allowArrows === 'boolean' ? options.allowArrows : true;\n this.iterator = new DomIterator(options.items, options.focusedItemClass);\n this.activateCallback = options.activateCallback;\n\n /**\n * Listening all keydowns on document and react on TAB/Enter press\n * TAB will leaf iterator items\n * ENTER will click the focused item\n */\n document.addEventListener('keydown', (event) => {\n const isReady = this.isEventReadyForHandling(event);\n\n if (!isReady) {\n return;\n }\n\n /**\n * Prevent only used keys default behaviour\n * (allows to navigate by ARROW DOWN, for example)\n */\n if (Flipper.usedKeys.includes(event.keyCode)) {\n event.preventDefault();\n }\n\n switch (event.keyCode) {\n case _.keyCodes.TAB:\n this.handleTabPress(event);\n break;\n case _.keyCodes.LEFT:\n case _.keyCodes.UP:\n this.flipLeft();\n break;\n case _.keyCodes.RIGHT:\n case _.keyCodes.DOWN:\n this.flipRight();\n break;\n case _.keyCodes.ENTER:\n this.handleEnterPress(event);\n break;\n }\n }, false);\n }\n\n /**\n * Array of keys (codes) that is handled by Flipper\n * Used to:\n * - preventDefault only for this keys, not all keywdowns (@see constructor)\n * - to skip external behaviours only for these keys, when filler is activated (@see BlockEvents@arrowRightAndDown)\n */\n public static get usedKeys(): number[] {\n return [\n _.keyCodes.TAB,\n _.keyCodes.LEFT,\n _.keyCodes.RIGHT,\n _.keyCodes.ENTER,\n _.keyCodes.UP,\n _.keyCodes.DOWN,\n ];\n }\n\n /**\n * Active tab/arrows handling by flipper\n * @param {HTMLElement[]} items - Some modules (like, InlineToolbar, BlockSettings) might refresh buttons dynamically\n */\n public activate(items?: HTMLElement[]): void {\n this.activated = true;\n\n if (items) {\n this.iterator.setItems(items);\n }\n }\n\n /**\n * Disable tab/arrows handling by flipper\n */\n public deactivate(): void {\n this.activated = false;\n this.dropCursor();\n }\n\n /**\n * Return current focused button\n * @return {HTMLElement|null}\n */\n public get currentItem(): HTMLElement|null {\n return this.iterator.currentItem;\n }\n\n /**\n * Focus first item\n */\n public focusFirst(): void {\n this.dropCursor();\n this.flipRight();\n }\n\n /**\n * Drops flipper's iterator cursor\n * @see DomIterator#dropCursor\n */\n private dropCursor(): void {\n this.iterator.dropCursor();\n }\n\n /**\n * This function is fired before handling flipper keycodes\n * The result of this function defines if it is need to be handled or not\n * @param {KeyboardEvent} event\n * @return {boolean}\n */\n private isEventReadyForHandling(event: KeyboardEvent): boolean {\n const handlingKeyCodeList = [\n _.keyCodes.TAB,\n _.keyCodes.ENTER,\n ];\n\n if (this.allowArrows) {\n handlingKeyCodeList.push(\n _.keyCodes.LEFT,\n _.keyCodes.RIGHT,\n _.keyCodes.UP,\n _.keyCodes.DOWN,\n );\n }\n\n if (!this.activated || handlingKeyCodeList.indexOf(event.keyCode) === -1) {\n return false;\n }\n\n return true;\n }\n\n /**\n * When flipper is activated tab press will leaf the items\n * @param {KeyboardEvent} event\n */\n private handleTabPress(event: KeyboardEvent): void {\n /** this property defines leaf direction */\n const shiftKey = event.shiftKey,\n direction = shiftKey ? DomIterator.directions.LEFT : DomIterator.directions.RIGHT;\n\n switch (direction) {\n case DomIterator.directions.RIGHT:\n this.flipRight();\n break;\n case DomIterator.directions.LEFT:\n this.flipLeft();\n break;\n }\n }\n\n /**\n * Focuses previous flipper iterator item\n */\n private flipLeft(): void {\n this.iterator.previous();\n }\n\n /**\n * Focuses next flipper iterator item\n */\n private flipRight(): void {\n this.iterator.next();\n }\n\n /**\n * Enter press will click current item if flipper is activated\n * @param {KeyboardEvent} event\n */\n private handleEnterPress(event: KeyboardEvent): void {\n if (!this.activated) {\n return;\n }\n\n if (this.iterator.currentItem) {\n this.iterator.currentItem.click();\n }\n\n if (typeof this.activateCallback === 'function') {\n this.activateCallback();\n }\n\n event.preventDefault();\n event.stopPropagation();\n }\n}\n","import $ from '../dom';\nimport {API, InlineTool, SanitizerConfig} from '../../../types';\n\n/**\n * Bold Tool\n *\n * Inline Toolbar Tool\n *\n * Makes selected text bolder\n */\nexport default class BoldInlineTool implements InlineTool {\n\n /**\n * Specifies Tool as Inline Toolbar Tool\n *\n * @return {boolean}\n */\n public static isInline = true;\n\n /**\n * Title for hover-tooltip\n */\n public static title: string = 'Bold';\n\n /**\n * Sanitizer Rule\n * Leave tags\n * @return {object}\n */\n static get sanitize(): SanitizerConfig {\n return {\n b: {},\n } as SanitizerConfig;\n }\n\n /**\n * Native Document's command that uses for Bold\n */\n private readonly commandName: string = 'bold';\n\n /**\n * Styles\n */\n private readonly CSS = {\n button: 'ce-inline-tool',\n buttonActive: 'ce-inline-tool--active',\n buttonModifier: 'ce-inline-tool--bold',\n };\n\n /**\n * Elements\n */\n private nodes: {button: HTMLButtonElement} = {\n button: undefined,\n };\n\n /**\n * Create button for Inline Toolbar\n */\n public render(): HTMLElement {\n this.nodes.button = document.createElement('button') as HTMLButtonElement;\n this.nodes.button.type = 'button';\n this.nodes.button.classList.add(this.CSS.button, this.CSS.buttonModifier);\n this.nodes.button.appendChild($.svg('bold', 12, 14));\n return this.nodes.button;\n }\n\n /**\n * Wrap range with tag\n * @param {Range} range\n */\n public surround(range: Range): void {\n document.execCommand(this.commandName);\n }\n\n /**\n * Check selection and set activated state to button if there are tag\n * @param {Selection} selection\n */\n public checkState(selection: Selection): boolean {\n const isActive = document.queryCommandState(this.commandName);\n\n this.nodes.button.classList.toggle(this.CSS.buttonActive, isActive);\n return isActive;\n }\n\n /**\n * Set a shortcut\n */\n public get shortcut(): string {\n return 'CMD+B';\n }\n}\n","import $ from '../dom';\nimport {InlineTool, SanitizerConfig} from '../../../types';\n\n/**\n * Italic Tool\n *\n * Inline Toolbar Tool\n *\n * Style selected text with italic\n */\nexport default class ItalicInlineTool implements InlineTool {\n\n /**\n * Specifies Tool as Inline Toolbar Tool\n *\n * @return {boolean}\n */\n public static isInline = true;\n\n /**\n * Title for hover-tooltip\n */\n public static title: string = 'Italic';\n\n /**\n * Sanitizer Rule\n * Leave tags\n * @return {object}\n */\n static get sanitize(): SanitizerConfig {\n return {\n i: {},\n } as SanitizerConfig;\n }\n\n /**\n * Native Document's command that uses for Italic\n */\n private readonly commandName: string = 'italic';\n\n /**\n * Styles\n */\n private readonly CSS = {\n button: 'ce-inline-tool',\n buttonActive: 'ce-inline-tool--active',\n buttonModifier: 'ce-inline-tool--italic',\n };\n\n /**\n * Elements\n */\n private nodes: {button: HTMLButtonElement} = {\n button: null,\n };\n\n /**\n * Create button for Inline Toolbar\n */\n public render(): HTMLElement {\n this.nodes.button = document.createElement('button') as HTMLButtonElement;\n this.nodes.button.type = 'button';\n this.nodes.button.classList.add(this.CSS.button, this.CSS.buttonModifier);\n this.nodes.button.appendChild($.svg('italic', 4, 11));\n return this.nodes.button;\n }\n\n /**\n * Wrap range with tag\n * @param {Range} range\n */\n public surround(range: Range): void {\n document.execCommand(this.commandName);\n }\n\n /**\n * Check selection and set activated state to button if there are tag\n * @param {Selection} selection\n */\n public checkState(selection: Selection): boolean {\n const isActive = document.queryCommandState(this.commandName);\n\n this.nodes.button.classList.toggle(this.CSS.buttonActive, isActive);\n return isActive;\n }\n\n /**\n * Set a shortcut\n */\n public get shortcut(): string {\n return 'CMD+I';\n }\n}\n","import SelectionUtils from '../selection';\n\nimport $ from '../dom';\nimport * as _ from '../utils';\nimport {API, InlineTool, SanitizerConfig} from '../../../types';\nimport {Notifier, Toolbar} from '../../../types/api';\n\n/**\n * Link Tool\n *\n * Inline Toolbar Tool\n *\n * Wrap selected text with tag\n */\nexport default class LinkInlineTool implements InlineTool {\n\n /**\n * Specifies Tool as Inline Toolbar Tool\n *\n * @return {boolean}\n */\n public static isInline = true;\n\n /**\n * Title for hover-tooltip\n */\n public static title: string = 'Link';\n\n /**\n * Sanitizer Rule\n * Leave tags\n * @return {object}\n */\n static get sanitize(): SanitizerConfig {\n return {\n a: {\n href: true,\n target: '_blank',\n rel: 'nofollow',\n },\n } as SanitizerConfig;\n }\n\n /**\n * Native Document's commands for link/unlink\n */\n private readonly commandLink: string = 'createLink';\n private readonly commandUnlink: string = 'unlink';\n\n /**\n * Enter key code\n */\n private readonly ENTER_KEY: number = 13;\n\n /**\n * Styles\n */\n private readonly CSS = {\n button: 'ce-inline-tool',\n buttonActive: 'ce-inline-tool--active',\n buttonModifier: 'ce-inline-tool--link',\n buttonUnlink: 'ce-inline-tool--unlink',\n input: 'ce-inline-tool-input',\n inputShowed: 'ce-inline-tool-input--showed',\n };\n\n /**\n * Elements\n */\n private nodes: {\n button: HTMLButtonElement;\n input: HTMLInputElement;\n } = {\n button: null,\n input: null,\n };\n\n /**\n * SelectionUtils instance\n */\n private selection: SelectionUtils;\n\n /**\n * Input opening state\n */\n private inputOpened: boolean = false;\n\n /**\n * Available Toolbar methods (open/close)\n */\n private toolbar: Toolbar;\n\n /**\n * Available inline toolbar methods (open/close)\n */\n private inlineToolbar: Toolbar;\n\n /**\n * Notifier API methods\n */\n private notifier: Notifier;\n\n /**\n * @param {{api: API}} - Editor.js API\n */\n constructor({api}) {\n this.toolbar = api.toolbar;\n this.inlineToolbar = api.inlineToolbar;\n this.notifier = api.notifier;\n this.selection = new SelectionUtils();\n }\n\n /**\n * Create button for Inline Toolbar\n */\n public render(): HTMLElement {\n this.nodes.button = document.createElement('button') as HTMLButtonElement;\n this.nodes.button.type = 'button';\n this.nodes.button.classList.add(this.CSS.button, this.CSS.buttonModifier);\n this.nodes.button.appendChild($.svg('link', 14, 10));\n this.nodes.button.appendChild($.svg('unlink', 15, 11));\n return this.nodes.button;\n }\n\n /**\n * Input for the link\n */\n public renderActions(): HTMLElement {\n this.nodes.input = document.createElement('input') as HTMLInputElement;\n this.nodes.input.placeholder = 'Add a link';\n this.nodes.input.classList.add(this.CSS.input);\n this.nodes.input.addEventListener('keydown', (event: KeyboardEvent) => {\n if (event.keyCode === this.ENTER_KEY) {\n this.enterPressed(event);\n }\n });\n return this.nodes.input;\n }\n\n /**\n * Handle clicks on the Inline Toolbar icon\n * @param {Range} range\n */\n public surround(range: Range): void {\n /**\n * Range will be null when user makes second click on the 'link icon' to close opened input\n */\n if (range) {\n /**\n * Save selection before change focus to the input\n */\n if (!this.inputOpened) {\n /** Create blue background instead of selection */\n this.selection.setFakeBackground();\n this.selection.save();\n } else {\n this.selection.restore();\n this.selection.removeFakeBackground();\n }\n const parentAnchor = this.selection.findParentTag('A');\n\n /**\n * Unlink icon pressed\n */\n if (parentAnchor) {\n this.selection.expandToTag(parentAnchor);\n this.unlink();\n this.closeActions();\n this.checkState();\n this.toolbar.close();\n return;\n }\n }\n\n this.toggleActions();\n }\n\n /**\n * Check selection and set activated state to button if there are tag\n * @param {Selection} selection\n */\n public checkState(selection?: Selection): boolean {\n const anchorTag = this.selection.findParentTag('A');\n\n if (anchorTag) {\n this.nodes.button.classList.add(this.CSS.buttonUnlink);\n this.nodes.button.classList.add(this.CSS.buttonActive);\n this.openActions();\n\n /**\n * Fill input value with link href\n */\n const hrefAttr = anchorTag.getAttribute('href');\n this.nodes.input.value = hrefAttr !== 'null' ? hrefAttr : '';\n\n this.selection.save();\n } else {\n this.nodes.button.classList.remove(this.CSS.buttonUnlink);\n this.nodes.button.classList.remove(this.CSS.buttonActive);\n }\n\n return !!anchorTag;\n }\n\n /**\n * Function called with Inline Toolbar closing\n */\n public clear(): void {\n this.closeActions();\n }\n\n /**\n * Set a shortcut\n */\n public get shortcut(): string {\n return 'CMD+K';\n }\n\n private toggleActions(): void {\n if (!this.inputOpened) {\n this.openActions(true);\n } else {\n this.closeActions(false);\n }\n }\n\n /**\n * @param {boolean} needFocus - on link creation we need to focus input. On editing - nope.\n */\n private openActions(needFocus: boolean = false): void {\n this.nodes.input.classList.add(this.CSS.inputShowed);\n if (needFocus) {\n this.nodes.input.focus();\n }\n this.inputOpened = true;\n }\n\n /**\n * Close input\n * @param {boolean} clearSavedSelection — we don't need to clear saved selection\n * on toggle-clicks on the icon of opened Toolbar\n */\n private closeActions(clearSavedSelection: boolean = true): void {\n if (this.selection.isFakeBackgroundEnabled) {\n // if actions is broken by other selection We need to save new selection\n const currentSelection = new SelectionUtils();\n currentSelection.save();\n\n this.selection.restore();\n this.selection.removeFakeBackground();\n\n // and recover new selection after removing fake background\n currentSelection.restore();\n }\n\n this.nodes.input.classList.remove(this.CSS.inputShowed);\n this.nodes.input.value = '';\n if (clearSavedSelection) {\n this.selection.clearSaved();\n }\n this.inputOpened = false;\n }\n\n /**\n * Enter pressed on input\n * @param {KeyboardEvent} event\n */\n private enterPressed(event: KeyboardEvent): void {\n let value = this.nodes.input.value || '';\n\n if (!value.trim()) {\n this.selection.restore();\n this.unlink();\n event.preventDefault();\n this.closeActions();\n }\n\n if (!this.validateURL(value)) {\n\n this.notifier.show({\n message: 'Pasted link is not valid.',\n style: 'error',\n });\n\n _.log('Incorrect Link pasted', 'warn', value);\n return;\n }\n\n value = this.prepareLink(value);\n\n this.selection.restore();\n this.selection.removeFakeBackground();\n\n this.insertLink(value);\n\n /**\n * Preventing events that will be able to happen\n */\n event.preventDefault();\n event.stopPropagation();\n event.stopImmediatePropagation();\n this.selection.collapseToEnd();\n this.inlineToolbar.close();\n }\n\n /**\n * Detects if passed string is URL\n * @param {string} str\n * @return {Boolean}\n */\n private validateURL(str: string): boolean {\n /**\n * Don't allow spaces\n */\n return !/\\s/.test(str);\n }\n\n /**\n * Process link before injection\n * - sanitize\n * - add protocol for links like 'google.com'\n * @param {string} link - raw user input\n */\n private prepareLink(link: string): string {\n link = link.trim();\n link = this.addProtocol(link);\n return link;\n }\n\n /**\n * Add 'http' protocol to the links like 'vc.ru', 'google.com'\n * @param {String} link\n */\n private addProtocol(link: string): string {\n /**\n * If protocol already exists, do nothing\n */\n if (/^(\\w+):(\\/\\/)?/.test(link)) {\n return link;\n }\n\n /**\n * We need to add missed HTTP protocol to the link, but skip 2 cases:\n * 1) Internal links like \"/general\"\n * 2) Anchors looks like \"#results\"\n * 3) Protocol-relative URLs like \"//google.com\"\n */\n const isInternal = /^\\/[^\\/\\s]/.test(link),\n isAnchor = link.substring(0, 1) === '#',\n isProtocolRelative = /^\\/\\/[^\\/\\s]/.test(link);\n\n if (!isInternal && !isAnchor && !isProtocolRelative) {\n link = 'http://' + link;\n }\n\n return link;\n }\n\n /**\n * Inserts tag with \"href\"\n * @param {string} link - \"href\" value\n */\n private insertLink(link: string): void {\n\n /**\n * Edit all link, not selected part\n */\n const anchorTag = this.selection.findParentTag('A');\n\n if (anchorTag) {\n this.selection.expandToTag(anchorTag);\n }\n\n document.execCommand(this.commandLink, false, link);\n }\n\n /**\n * Removes tag\n */\n private unlink(): void {\n document.execCommand(this.commandUnlink);\n }\n}\n","var map = {\n\t\"./api\": \"./src/components/modules/api/index.ts\",\n\t\"./api/\": \"./src/components/modules/api/index.ts\",\n\t\"./api/blocks\": \"./src/components/modules/api/blocks.ts\",\n\t\"./api/blocks.ts\": \"./src/components/modules/api/blocks.ts\",\n\t\"./api/caret\": \"./src/components/modules/api/caret.ts\",\n\t\"./api/caret.ts\": \"./src/components/modules/api/caret.ts\",\n\t\"./api/events\": \"./src/components/modules/api/events.ts\",\n\t\"./api/events.ts\": \"./src/components/modules/api/events.ts\",\n\t\"./api/index\": \"./src/components/modules/api/index.ts\",\n\t\"./api/index.ts\": \"./src/components/modules/api/index.ts\",\n\t\"./api/inlineToolbar\": \"./src/components/modules/api/inlineToolbar.ts\",\n\t\"./api/inlineToolbar.ts\": \"./src/components/modules/api/inlineToolbar.ts\",\n\t\"./api/listeners\": \"./src/components/modules/api/listeners.ts\",\n\t\"./api/listeners.ts\": \"./src/components/modules/api/listeners.ts\",\n\t\"./api/notifier\": \"./src/components/modules/api/notifier.ts\",\n\t\"./api/notifier.ts\": \"./src/components/modules/api/notifier.ts\",\n\t\"./api/sanitizer\": \"./src/components/modules/api/sanitizer.ts\",\n\t\"./api/sanitizer.ts\": \"./src/components/modules/api/sanitizer.ts\",\n\t\"./api/saver\": \"./src/components/modules/api/saver.ts\",\n\t\"./api/saver.ts\": \"./src/components/modules/api/saver.ts\",\n\t\"./api/selection\": \"./src/components/modules/api/selection.ts\",\n\t\"./api/selection.ts\": \"./src/components/modules/api/selection.ts\",\n\t\"./api/styles\": \"./src/components/modules/api/styles.ts\",\n\t\"./api/styles.ts\": \"./src/components/modules/api/styles.ts\",\n\t\"./api/toolbar\": \"./src/components/modules/api/toolbar.ts\",\n\t\"./api/toolbar.ts\": \"./src/components/modules/api/toolbar.ts\",\n\t\"./api/tooltip\": \"./src/components/modules/api/tooltip.ts\",\n\t\"./api/tooltip.ts\": \"./src/components/modules/api/tooltip.ts\",\n\t\"./blockEvents\": \"./src/components/modules/blockEvents.ts\",\n\t\"./blockEvents.ts\": \"./src/components/modules/blockEvents.ts\",\n\t\"./blockManager\": \"./src/components/modules/blockManager.ts\",\n\t\"./blockManager.ts\": \"./src/components/modules/blockManager.ts\",\n\t\"./blockSelection\": \"./src/components/modules/blockSelection.ts\",\n\t\"./blockSelection.ts\": \"./src/components/modules/blockSelection.ts\",\n\t\"./caret\": \"./src/components/modules/caret.ts\",\n\t\"./caret.ts\": \"./src/components/modules/caret.ts\",\n\t\"./crossBlockSelection\": \"./src/components/modules/crossBlockSelection.ts\",\n\t\"./crossBlockSelection.ts\": \"./src/components/modules/crossBlockSelection.ts\",\n\t\"./dragNDrop\": \"./src/components/modules/dragNDrop.ts\",\n\t\"./dragNDrop.ts\": \"./src/components/modules/dragNDrop.ts\",\n\t\"./events\": \"./src/components/modules/events.ts\",\n\t\"./events.ts\": \"./src/components/modules/events.ts\",\n\t\"./listeners\": \"./src/components/modules/listeners.ts\",\n\t\"./listeners.ts\": \"./src/components/modules/listeners.ts\",\n\t\"./modificationsObserver\": \"./src/components/modules/modificationsObserver.ts\",\n\t\"./modificationsObserver.ts\": \"./src/components/modules/modificationsObserver.ts\",\n\t\"./notifier\": \"./src/components/modules/notifier.ts\",\n\t\"./notifier.ts\": \"./src/components/modules/notifier.ts\",\n\t\"./paste\": \"./src/components/modules/paste.ts\",\n\t\"./paste.ts\": \"./src/components/modules/paste.ts\",\n\t\"./rectangleSelection\": \"./src/components/modules/rectangleSelection.ts\",\n\t\"./rectangleSelection.ts\": \"./src/components/modules/rectangleSelection.ts\",\n\t\"./renderer\": \"./src/components/modules/renderer.ts\",\n\t\"./renderer.ts\": \"./src/components/modules/renderer.ts\",\n\t\"./sanitizer\": \"./src/components/modules/sanitizer.ts\",\n\t\"./sanitizer.ts\": \"./src/components/modules/sanitizer.ts\",\n\t\"./saver\": \"./src/components/modules/saver.ts\",\n\t\"./saver.ts\": \"./src/components/modules/saver.ts\",\n\t\"./shortcuts\": \"./src/components/modules/shortcuts.ts\",\n\t\"./shortcuts.ts\": \"./src/components/modules/shortcuts.ts\",\n\t\"./toolbar\": \"./src/components/modules/toolbar/index.ts\",\n\t\"./toolbar/\": \"./src/components/modules/toolbar/index.ts\",\n\t\"./toolbar/blockSettings\": \"./src/components/modules/toolbar/blockSettings.ts\",\n\t\"./toolbar/blockSettings.ts\": \"./src/components/modules/toolbar/blockSettings.ts\",\n\t\"./toolbar/conversion\": \"./src/components/modules/toolbar/conversion.ts\",\n\t\"./toolbar/conversion.ts\": \"./src/components/modules/toolbar/conversion.ts\",\n\t\"./toolbar/index\": \"./src/components/modules/toolbar/index.ts\",\n\t\"./toolbar/index.ts\": \"./src/components/modules/toolbar/index.ts\",\n\t\"./toolbar/inline\": \"./src/components/modules/toolbar/inline.ts\",\n\t\"./toolbar/inline.ts\": \"./src/components/modules/toolbar/inline.ts\",\n\t\"./toolbar/toolbox\": \"./src/components/modules/toolbar/toolbox.ts\",\n\t\"./toolbar/toolbox.ts\": \"./src/components/modules/toolbar/toolbox.ts\",\n\t\"./tools\": \"./src/components/modules/tools.ts\",\n\t\"./tools.ts\": \"./src/components/modules/tools.ts\",\n\t\"./tooltip\": \"./src/components/modules/tooltip.ts\",\n\t\"./tooltip.ts\": \"./src/components/modules/tooltip.ts\",\n\t\"./ui\": \"./src/components/modules/ui.ts\",\n\t\"./ui.ts\": \"./src/components/modules/ui.ts\"\n};\n\n\nfunction webpackContext(req) {\n\tvar id = webpackContextResolve(req);\n\treturn __webpack_require__(id);\n}\nfunction webpackContextResolve(req) {\n\tif(!__webpack_require__.o(map, req)) {\n\t\tvar e = new Error(\"Cannot find module '\" + req + \"'\");\n\t\te.code = 'MODULE_NOT_FOUND';\n\t\tthrow e;\n\t}\n\treturn map[req];\n}\nwebpackContext.keys = function webpackContextKeys() {\n\treturn Object.keys(map);\n};\nwebpackContext.resolve = webpackContextResolve;\nmodule.exports = webpackContext;\nwebpackContext.id = \"./src/components/modules sync recursive ^\\\\.\\\\/.*$\";","import Module from '../../__module';\n\nimport {Blocks} from '../../../../types/api';\nimport {BlockToolData, OutputData, ToolConfig} from '../../../../types';\nimport * as _ from './../../utils';\n\n/**\n * @class BlocksAPI\n * provides with methods working with Block\n */\nexport default class BlocksAPI extends Module {\n /**\n * Available methods\n * @return {Blocks}\n */\n get methods(): Blocks {\n return {\n clear: () => this.clear(),\n render: (data: OutputData) => this.render(data),\n renderFromHTML: (data: string) => this.renderFromHTML(data),\n delete: () => this.delete(),\n swap: (fromIndex: number, toIndex: number) => this.swap(fromIndex, toIndex),\n getBlockByIndex: (index: number) => this.getBlockByIndex(index),\n getCurrentBlockIndex: () => this.getCurrentBlockIndex(),\n getBlocksCount: () => this.getBlocksCount(),\n stretchBlock: (index: number, status: boolean = true) => this.stretchBlock(index, status),\n insertNewBlock: () => this.insertNewBlock(),\n insert: this.insert,\n };\n }\n\n /**\n * Returns Blocks count\n * @return {number}\n */\n public getBlocksCount(): number {\n return this.Editor.BlockManager.blocks.length;\n }\n\n /**\n * Returns current block index\n * @return {number}\n */\n public getCurrentBlockIndex(): number {\n return this.Editor.BlockManager.currentBlockIndex;\n }\n\n /**\n * Returns Block holder by Block index\n * @param {Number} index\n *\n * @return {HTMLElement}\n */\n public getBlockByIndex(index: number): HTMLElement {\n const block = this.Editor.BlockManager.getBlockByIndex(index);\n return block.holder;\n }\n\n /**\n * Call Block Manager method that swap Blocks\n * @param {number} fromIndex - position of first Block\n * @param {number} toIndex - position of second Block\n */\n public swap(fromIndex: number, toIndex: number): void {\n this.Editor.BlockManager.swap(fromIndex, toIndex);\n\n /**\n * Move toolbar\n * DO not close the settings\n */\n this.Editor.Toolbar.move(false);\n }\n\n /**\n * Deletes Block\n * @param blockIndex\n */\n public delete(blockIndex?: number): void {\n this.Editor.BlockManager.removeBlock(blockIndex);\n\n /**\n * in case of last block deletion\n * Insert new initial empty block\n */\n if (this.Editor.BlockManager.blocks.length === 0) {\n this.Editor.BlockManager.insert();\n }\n\n /**\n * After Block deletion currentBlock is updated\n */\n this.Editor.Caret.setToBlock(this.Editor.BlockManager.currentBlock, this.Editor.Caret.positions.END);\n\n this.Editor.Toolbar.close();\n }\n\n /**\n * Clear Editor's area\n */\n public clear(): void {\n this.Editor.BlockManager.clear(true);\n this.Editor.InlineToolbar.close();\n }\n\n /**\n * Fills Editor with Blocks data\n * @param {OutputData} data — Saved Editor data\n */\n public render(data: OutputData): Promise {\n this.Editor.BlockManager.clear();\n return this.Editor.Renderer.render(data.blocks);\n }\n\n /**\n * Render passed HTML string\n * @param {string} data\n * @return {Promise}\n */\n public renderFromHTML(data: string): Promise {\n this.Editor.BlockManager.clear();\n return this.Editor.Paste.processText(data, true);\n }\n\n /**\n * Stretch Block's content\n * @param {number} index\n * @param {boolean} status - true to enable, false to disable\n */\n public stretchBlock(index: number, status: boolean = true): void {\n const block = this.Editor.BlockManager.getBlockByIndex(index);\n\n if (!block) {\n return;\n }\n\n block.stretched = status;\n }\n\n /**\n * Insert new Block\n *\n * @param {string} type — Tool name\n * @param {BlockToolData} data — Tool data to insert\n * @param {ToolConfig} config — Tool config\n * @param {number?} index — index where to insert new Block\n * @param {boolean?} needToFocus - flag to focus inserted Block\n */\n public insert = (\n type: string = this.config.initialBlock,\n data: BlockToolData = {},\n config: ToolConfig = {},\n index?: number,\n needToFocus?: boolean,\n ): void => {\n this.Editor.BlockManager.insert(\n type,\n data,\n config,\n index,\n needToFocus,\n );\n }\n\n /**\n * Insert new Block\n * After set caret to this Block\n *\n * @todo: remove in 3.0.0\n *\n * @deprecated with insert() method\n */\n public insertNewBlock(): void {\n _.log('Method blocks.insertNewBlock() is deprecated and it will be removed in next major release. ' +\n 'Use blocks.insert() instead.', 'warn');\n this.insert();\n }\n}\n","import Module from '../../__module';\nimport {Caret} from '../../../../types/api';\n\n/**\n * @class CaretAPI\n * provides with methods to work with caret\n */\nexport default class CaretAPI extends Module {\n /**\n * Available methods\n * @return {Caret}\n */\n get methods(): Caret {\n return {\n setToFirstBlock: this.setToFirstBlock,\n setToLastBlock: this.setToLastBlock,\n setToPreviousBlock: this.setToPreviousBlock,\n setToNextBlock: this.setToNextBlock,\n setToBlock: this.setToBlock,\n focus: this.focus,\n };\n }\n\n /**\n * Sets caret to the first Block\n *\n * @param {string} position - position where to set caret\n * @param {number} offset - caret offset\n *\n * @return {boolean}\n */\n private setToFirstBlock = (position: string = this.Editor.Caret.positions.DEFAULT, offset: number = 0): boolean => {\n if (!this.Editor.BlockManager.firstBlock) {\n return false;\n }\n\n this.Editor.Caret.setToBlock(this.Editor.BlockManager.firstBlock, position, offset);\n return true;\n }\n\n /**\n * Sets caret to the last Block\n *\n * @param {string} position - position where to set caret\n * @param {number} offset - caret offset\n *\n * @return {boolean}\n */\n private setToLastBlock = (position: string = this.Editor.Caret.positions.DEFAULT, offset: number = 0): boolean => {\n if (!this.Editor.BlockManager.lastBlock) {\n return false;\n }\n\n this.Editor.Caret.setToBlock(this.Editor.BlockManager.lastBlock, position, offset);\n return true;\n }\n\n /**\n * Sets caret to the previous Block\n *\n * @param {string} position - position where to set caret\n * @param {number} offset - caret offset\n *\n * @return {boolean}\n */\n private setToPreviousBlock = (\n position: string = this.Editor.Caret.positions.DEFAULT,\n offset: number = 0,\n ): boolean => {\n if (!this.Editor.BlockManager.previousBlock) {\n return false;\n }\n\n this.Editor.Caret.setToBlock(this.Editor.BlockManager.previousBlock, position, offset);\n return true;\n }\n\n /**\n * Sets caret to the next Block\n *\n * @param {string} position - position where to set caret\n * @param {number} offset - caret offset\n *\n * @return {boolean}\n */\n private setToNextBlock = (position: string = this.Editor.Caret.positions.DEFAULT, offset: number = 0): boolean => {\n if (!this.Editor.BlockManager.nextBlock) {\n return false;\n }\n\n this.Editor.Caret.setToBlock(this.Editor.BlockManager.nextBlock, position, offset);\n return true;\n }\n\n /**\n * Sets caret to the Block by passed index\n *\n * @param {number} index - index of Block where to set caret\n * @param {string} position - position where to set caret\n * @param {number} offset - caret offset\n *\n * @return {boolean}\n */\n private setToBlock = (\n index: number,\n position: string = this.Editor.Caret.positions.DEFAULT,\n offset: number = 0,\n ): boolean => {\n if (!this.Editor.BlockManager.blocks[index]) {\n return false;\n }\n\n this.Editor.Caret.setToBlock(this.Editor.BlockManager.blocks[index], position, offset);\n return true;\n }\n\n /**\n * Sets caret to the Editor\n *\n * @param {boolean} atEnd - if true, set Caret to the end of the Editor\n *\n * @return {boolean}\n */\n private focus = (atEnd: boolean = false) => {\n if (atEnd) {\n return this.setToLastBlock(this.Editor.Caret.positions.END);\n }\n\n return this.setToFirstBlock(this.Editor.Caret.positions.START);\n }\n}\n","import Module from '../../__module';\nimport {Events} from '../../../../types/api';\n\n/**\n * @class EventsAPI\n * provides with methods working with Toolbar\n */\nexport default class EventsAPI extends Module {\n /**\n * Available methods\n * @return {Events}\n */\n get methods(): Events {\n return {\n emit: (eventName: string, data: object) => this.emit(eventName, data),\n off: (eventName: string, callback: () => void) => this.off(eventName, callback),\n on: (eventName: string, callback: () => void) => this.on(eventName, callback),\n };\n }\n\n /**\n * Subscribe on Events\n * @param {String} eventName\n * @param {Function} callback\n */\n public on(eventName, callback): void {\n this.Editor.Events.on(eventName, callback);\n }\n\n /**\n * Emit event with data\n * @param {String} eventName\n * @param {Object} data\n */\n public emit(eventName, data): void {\n this.Editor.Events.emit(eventName, data);\n }\n\n /**\n * Unsubscribe from Event\n * @param {String} eventName\n * @param {Function} callback\n */\n public off(eventName, callback): void {\n this.Editor.Events.off(eventName, callback);\n }\n\n}\n","/**\n * @module API\n * @copyright 2018\n *\n * Each block has an Editor API instance to use provided public methods\n * if you cant to read more about how API works, please see docs\n */\nimport Module from '../../__module';\nimport {API as APIInterfaces} from '../../../../types';\n\n/**\n * @class API\n */\nexport default class API extends Module {\n public get methods(): APIInterfaces {\n return {\n blocks: this.Editor.BlocksAPI.methods,\n caret: this.Editor.CaretAPI.methods,\n events: this.Editor.EventsAPI.methods,\n listeners: this.Editor.ListenersAPI.methods,\n notifier: this.Editor.NotifierAPI.methods,\n sanitizer: this.Editor.SanitizerAPI.methods,\n saver: this.Editor.SaverAPI.methods,\n selection: this.Editor.SelectionAPI.methods,\n styles: this.Editor.StylesAPI.classes,\n toolbar: this.Editor.ToolbarAPI.methods,\n inlineToolbar: this.Editor.InlineToolbarAPI.methods,\n tooltip: this.Editor.TooltipAPI.methods,\n } as APIInterfaces;\n }\n}\n","import Module from '../../__module';\nimport { InlineToolbar } from '../../../../types/api/inline-toolbar';\n\n/**\n * @class InlineToolbarAPI\n * Provides methods for working with the Inline Toolbar\n */\nexport default class InlineToolbarAPI extends Module {\n /**\n * Available methods\n * @return {InlineToolbar}\n */\n get methods(): InlineToolbar {\n return {\n close: () => this.close(),\n open: () => this.open(),\n };\n }\n\n /**\n * Open Inline Toolbar\n */\n public open(): void {\n this.Editor.InlineToolbar.tryToShow();\n }\n\n /**\n * Close Inline Toolbar\n */\n public close(): void {\n this.Editor.InlineToolbar.close();\n }\n}\n","import Module from '../../__module';\nimport {Listeners} from '../../../../types/api';\n\n/**\n * @class ListenersAPI\n * Provides with methods working with DOM Listener\n */\nexport default class ListenersAPI extends Module {\n /**\n * Available methods\n * @return {Listeners}\n */\n get methods(): Listeners {\n return {\n on: (element: HTMLElement, eventType, handler, useCapture) => this.on(element, eventType, handler, useCapture),\n off: (element, eventType, handler) => this.off(element, eventType, handler),\n };\n }\n\n /**\n * adds DOM event listener\n *\n * @param {HTMLElement} element\n * @param {string} eventType\n * @param {() => void} handler\n * @param {boolean} useCapture\n */\n public on(element: HTMLElement, eventType: string, handler: () => void, useCapture?: boolean): void {\n this.Editor.Listeners.on(element, eventType, handler, useCapture);\n }\n\n /**\n * Removes DOM listener from element\n *\n * @param element\n * @param eventType\n * @param handler\n */\n public off(element, eventType, handler): void {\n this.Editor.Listeners.off(element, eventType, handler);\n }\n}\n","import Module from '../../__module';\nimport {Notifier} from '../../../../types/api';\nimport {ConfirmNotifierOptions, NotifierOptions, PromptNotifierOptions} from 'codex-notifier';\n\nexport default class NotifierAPI extends Module {\n\n /**\n * Available methods\n */\n get methods(): Notifier {\n return {\n show: (options: NotifierOptions | ConfirmNotifierOptions | PromptNotifierOptions) => this.show(options),\n };\n }\n\n public show(options: NotifierOptions | ConfirmNotifierOptions | PromptNotifierOptions) {\n return this.Editor.Notifier.show(options);\n }\n}\n","import Module from '../../__module';\nimport {Sanitizer} from '../../../../types/api';\n\n/**\n * @class SanitizerAPI\n * Provides Editor.js Sanitizer that allows developers to clean their HTML\n */\nexport default class SanitizerAPI extends Module {\n /**\n * Available methods\n * @return {Sanitizer}\n */\n get methods(): Sanitizer {\n return {\n clean: (taintString, config) => this.clean(taintString, config),\n };\n }\n\n public clean(taintString, config) {\n return this.Editor.Sanitizer.clean(taintString, config);\n }\n\n}\n","import Module from '../../__module';\nimport {Saver} from '../../../../types/api';\nimport {OutputData} from '../../../../types';\n\n/**\n * @class SaverAPI\n * provides with methods to save data\n */\nexport default class SaverAPI extends Module {\n /**\n * Available methods\n * @return {Saver}\n */\n get methods(): Saver {\n return {\n save: () => this.save(),\n };\n }\n\n /**\n * Return Editor's data\n */\n public save(): Promise {\n return this.Editor.Saver.save();\n }\n}\n","import Module from '../../__module';\nimport SelectionUtils from '../../selection';\nimport {Selection as SelectionAPIInterface} from '../../../../types/api';\n\n/**\n * @class SelectionAPI\n * Provides with methods working with SelectionUtils\n */\nexport default class SelectionAPI extends Module {\n /**\n * Available methods\n * @return {SelectionAPIInterface}\n */\n get methods(): SelectionAPIInterface {\n return {\n findParentTag: (tagName: string, className?: string) => this.findParentTag(tagName, className),\n expandToTag: (node: HTMLElement) => this.expandToTag(node),\n };\n }\n\n /**\n * Looks ahead from selection and find passed tag with class name\n * @param {string} tagName - tag to find\n * @param {string} className - tag's class name\n * @return {HTMLElement|null}\n */\n public findParentTag(tagName: string, className?: string): HTMLElement|null {\n return new SelectionUtils().findParentTag(tagName, className);\n }\n\n /**\n * Expand selection to passed tag\n * @param {HTMLElement} node - tag that should contain selection\n */\n public expandToTag(node: HTMLElement): void {\n new SelectionUtils().expandToTag(node);\n }\n\n}\n","import Module from '../../__module';\nimport {Styles} from '../../../../types/api';\n\n/**\n *\n */\nexport default class StylesAPI extends Module {\n get classes(): Styles {\n return {\n /**\n * Base Block styles\n */\n block: 'cdx-block',\n\n /**\n * Inline Tools styles\n */\n inlineToolButton: 'ce-inline-tool',\n inlineToolButtonActive: 'ce-inline-tool--active',\n\n /**\n * UI elements\n */\n input: 'cdx-input',\n loader: 'cdx-loader',\n button: 'cdx-button',\n\n /**\n * Settings styles\n */\n settingsButton: 'cdx-settings-button',\n settingsButtonActive: 'cdx-settings-button--active',\n };\n }\n}\n","import Module from '../../__module';\nimport {Toolbar} from '../../../../types/api';\n\n/**\n * @class ToolbarAPI\n * Provides methods for working with the Toolbar\n */\nexport default class ToolbarAPI extends Module {\n /**\n * Available methods\n * @return {Toolbar}\n */\n get methods(): Toolbar {\n return {\n close: () => this.close(),\n open: () => this.open(),\n };\n }\n\n /**\n * Open toolbar\n */\n public open(): void {\n this.Editor.Toolbar.open();\n }\n\n /**\n * Close toolbar and all included elements\n */\n public close(): void {\n this.Editor.Toolbar.close();\n }\n\n}\n","import Module from '../../__module';\nimport { Tooltip } from '../../../../types/api';\nimport {TooltipContent, TooltipOptions} from 'codex-tooltip';\n\n/**\n * @class TooltipAPI\n * @classdesc Tooltip API\n */\nexport default class TooltipAPI extends Module {\n /**\n * Available methods\n */\n get methods(): Tooltip {\n return {\n show: (element: HTMLElement,\n content: TooltipContent,\n options?: TooltipOptions,\n ) => this.show(element, content, options),\n hide: () => this.hide(),\n onHover: (element: HTMLElement,\n content: TooltipContent,\n options?: TooltipOptions,\n ) => this.onHover(element, content, options),\n };\n }\n\n /**\n * Method show tooltip on element with passed HTML content\n *\n * @param {HTMLElement} element\n * @param {TooltipContent} content\n * @param {TooltipOptions} options\n */\n public show(element: HTMLElement, content: TooltipContent, options?: TooltipOptions) {\n this.Editor.Tooltip.show(element, content, options);\n }\n\n /**\n * Method hides tooltip on HTML page\n */\n public hide() {\n this.Editor.Tooltip.hide();\n }\n\n /**\n * Decorator for showing Tooltip by mouseenter/mouseleave\n *\n * @param {HTMLElement} element\n * @param {TooltipContent} content\n * @param {TooltipOptions} options\n */\n public onHover(element: HTMLElement, content: TooltipContent, options?: TooltipOptions) {\n this.Editor.Tooltip.onHover(element, content, options);\n }\n}\n","/**\n * Contains keyboard and mouse events binded on each Block by Block Manager\n */\nimport Module from '../__module';\nimport * as _ from '../utils';\nimport SelectionUtils from '../selection';\nimport Flipper from '../flipper';\n\nexport default class BlockEvents extends Module {\n\n /**\n * All keydowns on Block\n * @param {KeyboardEvent} event - keydown\n */\n public keydown(event: KeyboardEvent): void {\n /**\n * Run common method for all keydown events\n */\n this.beforeKeydownProcessing(event);\n\n /**\n * Fire keydown processor by event.keyCode\n */\n switch (event.keyCode) {\n case _.keyCodes.BACKSPACE:\n this.backspace(event);\n break;\n\n case _.keyCodes.ENTER:\n this.enter(event);\n break;\n\n case _.keyCodes.DOWN:\n case _.keyCodes.RIGHT:\n this.arrowRightAndDown(event);\n break;\n\n case _.keyCodes.UP:\n case _.keyCodes.LEFT:\n this.arrowLeftAndUp(event);\n break;\n\n case _.keyCodes.TAB:\n this.tabPressed(event);\n break;\n\n case _.keyCodes.ESC:\n this.escapePressed(event);\n break;\n default:\n this.defaultHandler();\n break;\n }\n }\n\n /**\n * Fires on keydown before event processing\n * @param {KeyboardEvent} event - keydown\n */\n public beforeKeydownProcessing(event: KeyboardEvent): void {\n /**\n * Do not close Toolbox on Tabs or on Enter with opened Toolbox\n */\n if (!this.needToolbarClosing(event)) {\n return;\n }\n\n /**\n * When user type something:\n * - close Toolbar\n * - close Conversion Toolbar\n * - clear block highlighting\n */\n if (_.isPrintableKey(event.keyCode)) {\n this.Editor.Toolbar.close();\n this.Editor.ConversionToolbar.close();\n\n /**\n * Allow to use shortcuts with selected blocks\n * @type {boolean}\n */\n const isShortcut = event.ctrlKey || event.metaKey || event.altKey || event.shiftKey;\n\n if (!isShortcut) {\n this.Editor.BlockManager.clearFocused();\n this.Editor.BlockSelection.clearSelection(event);\n }\n }\n }\n\n /**\n * Key up on Block:\n * - shows Inline Toolbar if something selected\n * - shows conversion toolbar with 85% of block selection\n */\n public keyup(event): void {\n\n /**\n * If shift key was pressed some special shortcut is used (eg. cross block selection via shift + arrows)\n */\n if (event.shiftKey) {\n return;\n }\n\n /**\n * Check if editor is empty on each keyup and add special css class to wrapper\n */\n this.Editor.UI.checkEmptiness();\n }\n\n /**\n * Mouse up on Block:\n */\n public mouseUp(): void {\n }\n\n /**\n * Set up mouse selection handlers\n *\n * @param {MouseEvent} event\n */\n public mouseDown(event: MouseEvent): void {\n /**\n * Each mouse down on Block must disable selectAll state\n */\n if (!SelectionUtils.isCollapsed) {\n this.Editor.BlockSelection.clearSelection(event);\n }\n this.Editor.CrossBlockSelection.watchSelection(event);\n }\n\n /**\n * Open Toolbox to leaf Tools\n * @param {KeyboardEvent} event\n */\n public tabPressed(event): void {\n /**\n * Clear blocks selection by tab\n */\n this.Editor.BlockSelection.clearSelection(event);\n\n const { BlockManager, Tools, InlineToolbar, ConversionToolbar } = this.Editor;\n const currentBlock = BlockManager.currentBlock;\n\n if (!currentBlock) {\n return;\n }\n\n const canOpenToolbox = Tools.isInitial(currentBlock.tool) && currentBlock.isEmpty;\n const conversionToolbarOpened = !currentBlock.isEmpty && ConversionToolbar.opened;\n const inlineToolbarOpened = !currentBlock.isEmpty && !SelectionUtils.isCollapsed && InlineToolbar.opened;\n\n /**\n * For empty Blocks we show Plus button via Toolbox only for initial Blocks\n */\n if (canOpenToolbox) {\n this.activateToolbox();\n } else if (!conversionToolbarOpened && !inlineToolbarOpened) {\n this.activateBlockSettings();\n }\n }\n\n /**\n * Escape pressed\n * If some of Toolbar components are opened, then close it otherwise close Toolbar\n *\n * @param {Event} event\n */\n public escapePressed(event): void {\n /**\n * Clear blocks selection by ESC\n */\n this.Editor.BlockSelection.clearSelection(event);\n\n if (this.Editor.Toolbox.opened) {\n this.Editor.Toolbox.close();\n } else if (this.Editor.BlockSettings.opened) {\n this.Editor.BlockSettings.close();\n } else if (this.Editor.ConversionToolbar.opened) {\n this.Editor.ConversionToolbar.close();\n } else if (this.Editor.InlineToolbar.opened) {\n this.Editor.InlineToolbar.close();\n } else {\n this.Editor.Toolbar.close();\n }\n }\n\n /**\n * Add drop target styles\n *\n * @param {DragEvent} e\n */\n public dragOver(e: DragEvent) {\n const block = this.Editor.BlockManager.getBlockByChildNode(e.target as Node);\n\n block.dropTarget = true;\n }\n\n /**\n * Remove drop target style\n *\n * @param {DragEvent} e\n */\n public dragLeave(e: DragEvent) {\n const block = this.Editor.BlockManager.getBlockByChildNode(e.target as Node);\n\n block.dropTarget = false;\n }\n\n /**\n * Copying selected blocks\n * Before putting to the clipboard we sanitize all blocks and then copy to the clipboard\n *\n * @param event\n */\n public handleCommandC(event): void {\n const { BlockSelection } = this.Editor;\n\n if (!BlockSelection.anyBlockSelected) {\n return;\n }\n\n /**\n * Prevent default copy\n * Remove \"decline sound\" on macOS\n */\n event.preventDefault();\n\n // Copy Selected Blocks\n BlockSelection.copySelectedBlocks();\n }\n\n /**\n * Copy and Delete selected Blocks\n * @param event\n */\n public handleCommandX(event): void {\n const { BlockSelection, BlockManager, Caret } = this.Editor;\n\n if (!BlockSelection.anyBlockSelected) {\n return;\n }\n\n /**\n * Copy Blocks before removing\n *\n * Prevent default copy\n * Remove \"decline sound\" on macOS\n */\n event.preventDefault();\n\n BlockSelection.copySelectedBlocks();\n\n const selectionPositionIndex = BlockManager.removeSelectedBlocks();\n Caret.setToBlock(BlockManager.insertInitialBlockAtIndex(selectionPositionIndex, true), Caret.positions.START);\n\n /** Clear selection */\n BlockSelection.clearSelection(event);\n }\n\n /**\n * ENTER pressed on block\n * @param {KeyboardEvent} event - keydown\n */\n private enter(event: KeyboardEvent): void {\n const { BlockManager, Tools, UI } = this.Editor;\n const currentBlock = BlockManager.currentBlock;\n const tool = Tools.available[currentBlock.name];\n\n /**\n * Don't handle Enter keydowns when Tool sets enableLineBreaks to true.\n * Uses for Tools like where line breaks should be handled by default behaviour.\n */\n if (tool && tool[Tools.INTERNAL_SETTINGS.IS_ENABLED_LINE_BREAKS]) {\n return;\n }\n\n /**\n * Opened Toolbars uses Flipper with own Enter handling\n * Allow split block when no one button in Flipper is focused\n */\n if (UI.someToolbarOpened && UI.someFlipperButtonFocused) {\n return;\n }\n\n /**\n * Allow to create linebreaks by Shift+Enter\n */\n if (event.shiftKey) {\n return;\n }\n\n let newCurrent = this.Editor.BlockManager.currentBlock;\n\n /**\n * If enter has been pressed at the start of the text, just insert paragraph Block above\n */\n if (this.Editor.Caret.isAtStart && !this.Editor.BlockManager.currentBlock.hasMedia) {\n this.Editor.BlockManager.insertInitialBlockAtIndex(this.Editor.BlockManager.currentBlockIndex);\n } else {\n /**\n * Split the Current Block into two blocks\n * Renew local current node after split\n */\n newCurrent = this.Editor.BlockManager.split();\n }\n\n this.Editor.Caret.setToBlock(newCurrent);\n\n /**\n * If new Block is empty\n */\n if (this.Editor.Tools.isInitial(newCurrent.tool) && newCurrent.isEmpty) {\n /**\n * Show Toolbar\n */\n this.Editor.Toolbar.open(false);\n\n /**\n * Show Plus Button\n */\n this.Editor.Toolbar.plusButton.show();\n }\n\n event.preventDefault();\n }\n\n /**\n * Handle backspace keydown on Block\n * @param {KeyboardEvent} event - keydown\n */\n private backspace(event: KeyboardEvent): void {\n const { BlockManager, BlockSelection, Caret } = this.Editor;\n const currentBlock = BlockManager.currentBlock;\n const tool = this.Editor.Tools.available[currentBlock.name];\n\n /**\n * Check if Block should be removed by current Backspace keydown\n */\n if (currentBlock.selected || currentBlock.isEmpty && currentBlock.currentInput === currentBlock.firstInput) {\n event.preventDefault();\n\n const index = BlockManager.currentBlockIndex;\n\n if (BlockManager.previousBlock && BlockManager.previousBlock.inputs.length === 0) {\n /** If previous block doesn't contain inputs, remove it */\n BlockManager.removeBlock(index - 1);\n } else {\n /** If block is empty, just remove it */\n BlockManager.removeBlock();\n }\n\n Caret.setToBlock(\n BlockManager.currentBlock,\n index ? Caret.positions.END : Caret.positions.START,\n );\n\n /** Close Toolbar */\n this.Editor.Toolbar.close();\n\n /** Clear selection */\n BlockSelection.clearSelection(event);\n return;\n }\n\n /**\n * Don't handle Backspaces when Tool sets enableLineBreaks to true.\n * Uses for Tools like where line breaks should be handled by default behaviour.\n *\n * But if caret is at start of the block, we allow to remove it by backspaces\n */\n if (tool && tool[this.Editor.Tools.INTERNAL_SETTINGS.IS_ENABLED_LINE_BREAKS] && !Caret.isAtStart) {\n return;\n }\n\n // Don't let the codeblock mismerge with previous block\n if (currentBlock.name === 'code') {\n return;\n }\n\n if (currentBlock.name === 'simpleCode') {\n return;\n }\n\n const isFirstBlock = BlockManager.currentBlockIndex === 0;\n const canMergeBlocks = Caret.isAtStart &&\n SelectionUtils.isCollapsed &&\n currentBlock.currentInput === currentBlock.firstInput &&\n !isFirstBlock;\n\n if (canMergeBlocks) {\n /**\n * preventing browser default behaviour\n */\n event.preventDefault();\n\n /**\n * Merge Blocks\n */\n this.mergeBlocks();\n }\n }\n\n /**\n * Merge current and previous Blocks if they have the same type\n */\n private mergeBlocks() {\n const { BlockManager, Caret, Toolbar } = this.Editor;\n const targetBlock = BlockManager.previousBlock;\n const blockToMerge = BlockManager.currentBlock;\n\n /**\n * Blocks that can be merged:\n * 1) with the same Name\n * 2) Tool has 'merge' method\n *\n * other case will handle as usual ARROW LEFT behaviour\n */\n if (blockToMerge.name !== targetBlock.name || !targetBlock.mergeable) {\n /** If target Block doesn't contain inputs or empty, remove it */\n if (targetBlock.inputs.length === 0 || targetBlock.isEmpty) {\n BlockManager.removeBlock(BlockManager.currentBlockIndex - 1);\n\n Caret.setToBlock(BlockManager.currentBlock);\n Toolbar.close();\n return;\n }\n\n if (Caret.navigatePrevious()) {\n Toolbar.close();\n }\n\n return;\n }\n\n Caret.createShadow(targetBlock.pluginsContent);\n BlockManager.mergeBlocks(targetBlock, blockToMerge)\n .then( () => {\n /** Restore caret position after merge */\n Caret.restoreCaret(targetBlock.pluginsContent as HTMLElement);\n targetBlock.pluginsContent.normalize();\n Toolbar.close();\n });\n }\n\n /**\n * Handle right and down keyboard keys\n */\n private arrowRightAndDown(event: KeyboardEvent): void {\n const isFlipperCombination = Flipper.usedKeys.includes(event.keyCode) &&\n (!event.shiftKey || event.keyCode === _.keyCodes.TAB);\n\n /**\n * Arrows might be handled on toolbars by flipper\n * Check for Flipper.usedKeys to allow navigate by DOWN and disallow by RIGHT\n */\n if (this.Editor.UI.someToolbarOpened && isFlipperCombination) {\n return;\n }\n\n /**\n * Close Toolbar and highlighting when user moves cursor\n */\n this.Editor.BlockManager.clearFocused();\n this.Editor.Toolbar.close();\n\n const shouldEnableCBS = this.Editor.Caret.isAtEnd || this.Editor.BlockSelection.anyBlockSelected;\n\n if (event.shiftKey && event.keyCode === _.keyCodes.DOWN && shouldEnableCBS) {\n this.Editor.CrossBlockSelection.toggleBlockSelectedState();\n return;\n }\n\n if (this.Editor.Caret.navigateNext()) {\n /**\n * Default behaviour moves cursor by 1 character, we need to prevent it\n */\n event.preventDefault();\n } else {\n /**\n * After caret is set, update Block input index\n */\n _.delay(() => {\n /** Check currentBlock for case when user moves selection out of Editor */\n if (this.Editor.BlockManager.currentBlock) {\n this.Editor.BlockManager.currentBlock.updateCurrentInput();\n }\n }, 20)();\n }\n\n /**\n * Clear blocks selection by arrows\n */\n this.Editor.BlockSelection.clearSelection(event);\n }\n\n /**\n * Handle left and up keyboard keys\n */\n private arrowLeftAndUp(event: KeyboardEvent): void {\n /**\n * Arrows might be handled on toolbars by flipper\n * Check for Flipper.usedKeys to allow navigate by UP and disallow by LEFT\n */\n if (this.Editor.UI.someToolbarOpened) {\n if (Flipper.usedKeys.includes(event.keyCode) && (!event.shiftKey || event.keyCode === _.keyCodes.TAB)) {\n return;\n }\n\n this.Editor.UI.closeAllToolbars();\n }\n\n /**\n * Close Toolbar and highlighting when user moves cursor\n */\n this.Editor.BlockManager.clearFocused();\n this.Editor.Toolbar.close();\n\n const shouldEnableCBS = this.Editor.Caret.isAtStart || this.Editor.BlockSelection.anyBlockSelected;\n\n if (event.shiftKey && event.keyCode === _.keyCodes.UP && shouldEnableCBS) {\n this.Editor.CrossBlockSelection.toggleBlockSelectedState(false);\n return;\n }\n\n if (this.Editor.Caret.navigatePrevious()) {\n /**\n * Default behaviour moves cursor by 1 character, we need to prevent it\n */\n event.preventDefault();\n } else {\n /**\n * After caret is set, update Block input index\n */\n _.delay(() => {\n /** Check currentBlock for case when user ends selection out of Editor and then press arrow-key */\n if (this.Editor.BlockManager.currentBlock) {\n this.Editor.BlockManager.currentBlock.updateCurrentInput();\n }\n }, 20)();\n }\n\n /**\n * Clear blocks selection by arrows\n */\n this.Editor.BlockSelection.clearSelection(event);\n }\n\n /**\n * Default keydown handler\n */\n private defaultHandler(): void {}\n\n /**\n * Cases when we need to close Toolbar\n */\n private needToolbarClosing(event) {\n const toolboxItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.Toolbox.opened),\n blockSettingsItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.BlockSettings.opened),\n inlineToolbarItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.InlineToolbar.opened),\n conversionToolbarItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.ConversionToolbar.opened),\n flippingToolbarItems = event.keyCode === _.keyCodes.TAB;\n\n /**\n * Do not close Toolbar in cases:\n * 1. ShiftKey pressed (or combination with shiftKey)\n * 2. When Toolbar is opened and Tab leafs its Tools\n * 3. When Toolbar's component is opened and some its item selected\n */\n return !(event.shiftKey\n || flippingToolbarItems\n || toolboxItemSelected\n || blockSettingsItemSelected\n || inlineToolbarItemSelected\n || conversionToolbarItemSelected\n );\n }\n\n /**\n * If Toolbox is not open, then just open it and show plus button\n */\n private activateToolbox(): void {\n if (!this.Editor.Toolbar.opened) {\n this.Editor.Toolbar.open(false , false);\n this.Editor.Toolbar.plusButton.show();\n }\n\n this.Editor.Toolbox.open();\n }\n\n /**\n * Open Toolbar and show BlockSettings before flipping Tools\n */\n private activateBlockSettings(): void {\n if (!this.Editor.Toolbar.opened) {\n this.Editor.BlockManager.currentBlock.focused = true;\n this.Editor.Toolbar.open(true, false);\n this.Editor.Toolbar.plusButton.hide();\n }\n\n /**\n * If BlockSettings is not open, then open BlockSettings\n * Next Tab press will leaf Settings Buttons\n */\n if (!this.Editor.BlockSettings.opened) {\n this.Editor.BlockSettings.open();\n }\n }\n}\n","/**\n * @class BlockManager\n * @classdesc Manage editor`s blocks storage and appearance\n *\n * @module BlockManager\n *\n * @version 2.0.0\n */\nimport Block, {BlockToolAPI} from '../block';\nimport Module from '../__module';\nimport $ from '../dom';\nimport * as _ from '../utils';\nimport Blocks from '../blocks';\nimport {BlockTool, BlockToolConstructable, BlockToolData, PasteEvent, ToolConfig} from '../../../types';\n\n/**\n * @typedef {BlockManager} BlockManager\n * @property {Number} currentBlockIndex - Index of current working block\n * @property {Proxy} _blocks - Proxy for Blocks instance {@link Blocks}\n */\nexport default class BlockManager extends Module {\n\n /**\n * Returns current Block index\n * @return {number}\n */\n public get currentBlockIndex(): number {\n return this._currentBlockIndex;\n }\n\n /**\n * Set current Block index and fire Block lifecycle callbacks\n * @param newIndex\n */\n public set currentBlockIndex(newIndex: number) {\n if (this._blocks[this._currentBlockIndex]) {\n this._blocks[this._currentBlockIndex].willUnselect();\n }\n\n if (this._blocks[newIndex]) {\n this._blocks[newIndex].willSelect();\n }\n\n this._currentBlockIndex = newIndex;\n }\n\n /**\n * returns first Block\n * @return {Block}\n */\n public get firstBlock(): Block {\n return this._blocks[0];\n }\n\n /**\n * returns last Block\n * @return {Block}\n */\n public get lastBlock(): Block {\n return this._blocks[this._blocks.length - 1];\n }\n\n /**\n * Get current Block instance\n *\n * @return {Block}\n */\n public get currentBlock(): Block {\n return this._blocks[this.currentBlockIndex];\n }\n\n /**\n * Returns next Block instance\n * @return {Block|null}\n */\n public get nextBlock(): Block {\n const isLastBlock = this.currentBlockIndex === (this._blocks.length - 1);\n\n if (isLastBlock) {\n return null;\n }\n\n return this._blocks[this.currentBlockIndex + 1];\n }\n\n /**\n * Return first Block with inputs after current Block\n *\n * @returns {Block | undefined}\n */\n public get nextContentfulBlock(): Block {\n const nextBlocks = this.blocks.slice(this.currentBlockIndex + 1);\n\n return nextBlocks.find((block) => !!block.inputs.length);\n }\n\n /**\n * Return first Block with inputs before current Block\n *\n * @returns {Block | undefined}\n */\n public get previousContentfulBlock(): Block {\n const previousBlocks = this.blocks.slice(0, this.currentBlockIndex).reverse();\n\n return previousBlocks.find((block) => !!block.inputs.length);\n }\n\n /**\n * Returns previous Block instance\n * @return {Block|null}\n */\n public get previousBlock(): Block {\n const isFirstBlock = this.currentBlockIndex === 0;\n\n if (isFirstBlock) {\n return null;\n }\n\n return this._blocks[this.currentBlockIndex - 1];\n }\n\n /**\n * Get array of Block instances\n *\n * @returns {Block[]} {@link Blocks#array}\n */\n public get blocks(): Block[] {\n return this._blocks.array;\n }\n\n /**\n * Check if each Block is empty\n *\n * @returns {boolean}\n */\n public get isEditorEmpty(): boolean {\n return this.blocks.every((block) => block.isEmpty);\n }\n\n /**\n * Index of current working block\n *\n * @type {number}\n */\n private _currentBlockIndex: number = -1;\n\n /**\n * Proxy for Blocks instance {@link Blocks}\n *\n * @type {Proxy}\n * @private\n */\n private _blocks: Blocks = null;\n\n /**\n * Should be called after Editor.UI preparation\n * Define this._blocks property\n *\n * @returns {Promise}\n */\n public async prepare() {\n const blocks = new Blocks(this.Editor.UI.nodes.redactor);\n const { BlockEvents, Shortcuts } = this.Editor;\n\n /**\n * We need to use Proxy to overload set/get [] operator.\n * So we can use array-like syntax to access blocks\n *\n * @example\n * this._blocks[0] = new Block(...);\n *\n * block = this._blocks[0];\n *\n * @todo proxy the enumerate method\n *\n * @type {Proxy}\n * @private\n */\n this._blocks = new Proxy(blocks, {\n set: Blocks.set,\n get: Blocks.get,\n });\n\n /** Copy shortcut */\n Shortcuts.add({\n name: 'CMD+C',\n handler: (event) => {\n BlockEvents.handleCommandC(event);\n },\n });\n\n /** Copy and cut */\n Shortcuts.add({\n name: 'CMD+X',\n handler: (event) => {\n BlockEvents.handleCommandX(event);\n },\n });\n }\n\n /**\n * Creates Block instance by tool name\n *\n * @param {String} toolName - tools passed in editor config {@link EditorConfig#tools}\n * @param {Object} data - constructor params\n * @param {Object} settings - block settings\n *\n * @return {Block}\n */\n public composeBlock(toolName: string, data: BlockToolData = {}, settings: ToolConfig = {}): Block {\n const toolInstance = this.Editor.Tools.construct(toolName, data) as BlockTool;\n const toolClass = this.Editor.Tools.available[toolName] as BlockToolConstructable;\n const block = new Block(toolName, toolInstance, toolClass, settings, this.Editor.API.methods);\n\n this.bindEvents(block);\n\n return block;\n }\n\n /**\n * Insert new block into _blocks\n *\n * @param {String} toolName — plugin name, by default method inserts initial block type\n * @param {Object} data — plugin data\n * @param {Object} settings - default settings\n * @param {number} index - index where to insert new Block\n * @param {boolean} needToFocus - flag shows if needed to update current Block index\n *\n * @return {Block}\n */\n public insert(\n toolName: string = this.config.initialBlock,\n data: BlockToolData = {},\n settings: ToolConfig = {},\n index: number = this.currentBlockIndex + 1,\n needToFocus: boolean = true,\n ): Block {\n const block = this.composeBlock(toolName, data, settings);\n\n this._blocks[index] = block;\n\n if (needToFocus) {\n this.currentBlockIndex = index;\n }\n\n return block;\n }\n\n /**\n * Insert pasted content. Call onPaste callback after insert.\n *\n * @param {string} toolName\n * @param {PasteEvent} pasteEvent - pasted data\n * @param {boolean} replace - should replace current block\n */\n public paste(\n toolName: string,\n pasteEvent: PasteEvent,\n replace: boolean = false,\n ): Block {\n let block;\n\n if (replace) {\n block = this.replace(toolName);\n } else {\n block = this.insert(toolName);\n }\n\n try {\n block.call(BlockToolAPI.ON_PASTE, pasteEvent);\n } catch (e) {\n _.log(`${toolName}: onPaste callback call is failed`, 'error', e);\n }\n return block;\n }\n\n /**\n * Insert new initial block at passed index\n *\n * @param {number} index - index where Block should be inserted\n * @param {boolean} needToFocus - if true, updates current Block index\n *\n * TODO: Remove method and use insert() with index instead (?)\n *\n * @return {Block} inserted Block\n */\n public insertInitialBlockAtIndex(index: number, needToFocus: boolean = false) {\n const block = this.composeBlock(this.config.initialBlock, {}, {});\n\n this._blocks[index] = block;\n\n if (needToFocus) {\n this.currentBlockIndex = index;\n } else if (index <= this.currentBlockIndex) {\n this.currentBlockIndex++;\n }\n\n return block;\n }\n\n /**\n * Always inserts at the end\n * @return {Block}\n */\n public insertAtEnd(): Block {\n /**\n * Define new value for current block index\n */\n this.currentBlockIndex = this.blocks.length - 1;\n\n /**\n * Insert initial typed block\n */\n return this.insert();\n }\n\n /**\n * Merge two blocks\n * @param {Block} targetBlock - previous block will be append to this block\n * @param {Block} blockToMerge - block that will be merged with target block\n *\n * @return {Promise} - the sequence that can be continued\n */\n public async mergeBlocks(targetBlock: Block, blockToMerge: Block): Promise {\n const blockToMergeIndex = this._blocks.indexOf(blockToMerge);\n\n if (blockToMerge.isEmpty) {\n return;\n }\n\n const blockToMergeData = await blockToMerge.data;\n\n if (!_.isEmpty(blockToMergeData)) {\n await targetBlock.mergeWith(blockToMergeData);\n }\n\n this.removeBlock(blockToMergeIndex);\n this.currentBlockIndex = this._blocks.indexOf(targetBlock);\n }\n\n /**\n * Remove block with passed index or remove last\n * @param {Number|null} index\n */\n public removeBlock(index?: number): void {\n if (index === undefined) {\n index = this.currentBlockIndex;\n }\n this._blocks.remove(index);\n\n if (this.currentBlockIndex >= index) {\n this.currentBlockIndex--;\n }\n\n /**\n * If first Block was removed, insert new Initial Block and set focus on it`s first input\n */\n if (!this.blocks.length) {\n this.currentBlockIndex = -1;\n this.insert();\n return;\n } else if (index === 0) {\n this.currentBlockIndex = 0;\n }\n }\n\n /**\n * Remove only selected Blocks\n * and returns first Block index where started removing...\n * @return number|undefined\n */\n public removeSelectedBlocks(): number|undefined {\n let firstSelectedBlockIndex;\n\n /**\n * Remove selected Blocks from the end\n */\n for (let index = this.blocks.length - 1; index >= 0; index--) {\n if (!this.blocks[index].selected) {\n continue;\n }\n\n this.removeBlock(index);\n firstSelectedBlockIndex = index;\n }\n\n return firstSelectedBlockIndex;\n }\n\n /**\n * Attention!\n * After removing insert new initial typed Block and focus on it\n * Removes all blocks\n */\n public removeAllBlocks(): void {\n for (let index = this.blocks.length - 1; index >= 0; index--) {\n this._blocks.remove(index);\n }\n\n this.currentBlockIndex = -1;\n this.insert();\n this.currentBlock.firstInput.focus();\n }\n\n /**\n * Split current Block\n * 1. Extract content from Caret position to the Block`s end\n * 2. Insert a new Block below current one with extracted content\n *\n * @return {Block}\n */\n public split(): Block {\n const extractedFragment = this.Editor.Caret.extractFragmentFromCaretPosition();\n const wrapper = $.make('div');\n\n wrapper.appendChild(extractedFragment as DocumentFragment);\n\n /**\n * @todo make object in accordance with Tool\n */\n const data = {\n text: $.isEmpty(wrapper) ? '' : wrapper.innerHTML,\n };\n\n /**\n * Renew current Block\n * @type {Block}\n */\n return this.insert(this.config.initialBlock, data);\n }\n\n /**\n * Replace current working block\n *\n * @param {String} toolName — plugin name\n * @param {Object} data — plugin data\n *\n * @return {Block}\n */\n public replace(\n toolName: string = this.config.initialBlock,\n data: BlockToolData = {},\n ): Block {\n const block = this.composeBlock(toolName, data);\n\n this._blocks.insert(this.currentBlockIndex, block, true);\n\n return block;\n }\n\n /**\n * Returns Block by passed index\n * @param {Number} index\n * @return {Block}\n */\n public getBlockByIndex(index): Block {\n return this._blocks[index];\n }\n\n /**\n * Get Block instance by html element\n * @param {Node} element\n * @returns {Block}\n */\n public getBlock(element: HTMLElement): Block {\n if (!$.isElement(element) as boolean) {\n element = element.parentNode as HTMLElement;\n }\n\n const nodes = this._blocks.nodes,\n firstLevelBlock = element.closest(`.${Block.CSS.wrapper}`),\n index = nodes.indexOf(firstLevelBlock as HTMLElement);\n\n if (index >= 0) {\n return this._blocks[index];\n }\n }\n\n /**\n * Remove selection from all Blocks then highlight only Current Block\n */\n public highlightCurrentNode(): void {\n /**\n * Remove previous selected Block's state\n */\n this.clearFocused();\n\n /**\n * Mark current Block as selected\n * @type {boolean}\n */\n this.currentBlock.focused = true;\n }\n\n /**\n * Remove selection from all Blocks\n */\n public clearFocused(): void {\n this.blocks.forEach( (block) => block.focused = false);\n }\n\n /**\n * 1) Find first-level Block from passed child Node\n * 2) Mark it as current\n *\n * @param {Node} childNode - look ahead from this node.\n * @param {string} caretPosition - position where to set caret\n * @throws Error - when passed Node is not included at the Block\n */\n public setCurrentBlockByChildNode(childNode: Node): Block {\n /**\n * If node is Text TextNode\n */\n if (!$.isElement(childNode)) {\n childNode = childNode.parentNode;\n }\n\n const parentFirstLevelBlock = (childNode as HTMLElement).closest(`.${Block.CSS.wrapper}`);\n\n if (parentFirstLevelBlock) {\n /**\n * Update current Block's index\n * @type {number}\n */\n this.currentBlockIndex = this._blocks.nodes.indexOf(parentFirstLevelBlock as HTMLElement);\n return this.currentBlock;\n } else {\n throw new Error('Can not find a Block from this child Node');\n }\n }\n\n /**\n * Return block which contents passed node\n *\n * @param {Node} childNode\n * @return {Block}\n */\n public getBlockByChildNode(childNode: Node): Block {\n /**\n * If node is Text TextNode\n */\n if (!$.isElement(childNode)) {\n childNode = childNode.parentNode;\n }\n\n const firstLevelBlock = (childNode as HTMLElement).closest(`.${Block.CSS.wrapper}`);\n\n return this.blocks.find((block) => block.holder === firstLevelBlock);\n }\n\n /**\n * Swap Blocks Position\n * @param {Number} fromIndex\n * @param {Number} toIndex\n */\n public swap(fromIndex, toIndex): void {\n /** Move up current Block */\n this._blocks.swap(fromIndex, toIndex);\n\n /** Now actual block moved up so that current block index decreased */\n this.currentBlockIndex = toIndex;\n }\n\n /**\n * Sets current Block Index -1 which means unknown\n * and clear highlightings\n */\n public dropPointer(): void {\n this.currentBlockIndex = -1;\n this.clearFocused();\n }\n\n /**\n * Clears Editor\n * @param {boolean} needAddInitialBlock - 1) in internal calls (for example, in api.blocks.render)\n * we don't need to add empty initial block\n * 2) in api.blocks.clear we should add empty block\n */\n public clear(needAddInitialBlock: boolean = false): void {\n this._blocks.removeAll();\n this.dropPointer();\n\n if (needAddInitialBlock) {\n this.insert(this.config.initialBlock);\n }\n\n /**\n * Add empty modifier\n */\n this.Editor.UI.checkEmptiness();\n }\n\n /**\n * Bind Events\n * @param {Object} block\n */\n private bindEvents(block: Block): void {\n const {BlockEvents, Listeners} = this.Editor;\n\n Listeners.on(block.holder, 'keydown', (event) => BlockEvents.keydown(event as KeyboardEvent), true);\n Listeners.on(block.holder, 'mouseup', (event) => BlockEvents.mouseUp());\n Listeners.on(block.holder, 'mousedown', (event: MouseEvent) => BlockEvents.mouseDown(event));\n Listeners.on(block.holder, 'keyup', (event) => BlockEvents.keyup(event));\n Listeners.on(block.holder, 'dragover', (event) => BlockEvents.dragOver(event as DragEvent));\n Listeners.on(block.holder, 'dragleave', (event) => BlockEvents.dragLeave(event as DragEvent));\n }\n}\n","/**\n * @class BlockSelection\n * @classdesc Manages Block selection with shortcut CMD+A\n *\n * @module BlockSelection\n * @version 1.0.0\n */\nimport Module from '../__module';\nimport Block from '../block';\nimport * as _ from '../utils';\nimport $ from '../dom';\n\nimport SelectionUtils from '../selection';\n\nexport default class BlockSelection extends Module {\n\n /**\n * Sanitizer Config\n * @return {SanitizerConfig}\n */\n private get sanitizerConfig() {\n return {\n p: {},\n h1: {},\n h2: {},\n h3: {},\n h4: {},\n h5: {},\n h6: {},\n ol: {},\n ul: {},\n li: {},\n br: true,\n img: {\n src: true,\n width: true,\n height: true,\n },\n a: {\n href: true,\n },\n b: {},\n i: {},\n u: {},\n };\n }\n\n /**\n * Flag that identifies all Blocks selection\n * @return {boolean}\n */\n public get allBlocksSelected(): boolean {\n const {BlockManager} = this.Editor;\n\n return BlockManager.blocks.every((block) => block.selected === true);\n }\n\n /**\n * Set selected all blocks\n * @param {boolean} state\n */\n public set allBlocksSelected(state: boolean) {\n const {BlockManager} = this.Editor;\n\n BlockManager.blocks.forEach((block) => block.selected = state);\n }\n\n /**\n * Flag that identifies any Block selection\n * @return {boolean}\n */\n public get anyBlockSelected(): boolean {\n const {BlockManager} = this.Editor;\n\n return BlockManager.blocks.some((block) => block.selected === true);\n }\n\n /**\n * Return selected Blocks array\n * @return {Block[]}\n */\n public get selectedBlocks(): Block[] {\n return this.Editor.BlockManager.blocks.filter((block: Block) => block.selected);\n }\n\n /**\n * Flag used to define block selection\n * First CMD+A defines it as true and then second CMD+A selects all Blocks\n * @type {boolean}\n */\n private needToSelectAll: boolean = false;\n\n /**\n * Flag used to define native input selection\n * In this case we allow double CMD+A to select Block\n * @type {boolean}\n */\n private nativeInputSelected: boolean = false;\n\n /**\n * Flag identifies any input selection\n * That means we can select whole Block\n * @type {boolean}\n */\n private readyToBlockSelection: boolean = false;\n\n /**\n * SelectionUtils instance\n * @type {SelectionUtils}\n */\n private selection: SelectionUtils;\n\n /**\n * Module Preparation\n * Registers Shortcuts CMD+A and CMD+C\n * to select all and copy them\n */\n public prepare(): void {\n const {Shortcuts} = this.Editor;\n\n /** Selection shortcut */\n Shortcuts.add({\n name: 'CMD+A',\n handler: (event) => {\n const {BlockManager} = this.Editor;\n /**\n * When one page consist of two or more EditorJS instances\n * Shortcut module tries to handle all events. Thats why Editor's selection works inside the target Editor, but\n * for others error occurs because nothing to select.\n *\n * Prevent such actions if focus is not inside the Editor\n */\n if (!BlockManager.currentBlock) {\n return;\n }\n\n this.handleCommandA(event);\n },\n });\n\n this.selection = new SelectionUtils();\n }\n\n /**\n * Remove selection of Block\n * @param {number?} index - Block index according to the BlockManager's indexes\n */\n public unSelectBlockByIndex(index?) {\n const {BlockManager} = this.Editor;\n\n let block;\n\n if (isNaN(index)) {\n block = BlockManager.currentBlock;\n } else {\n block = BlockManager.getBlockByIndex(index);\n }\n\n block.selected = false;\n }\n\n /**\n * Clear selection from Blocks\n *\n * @param {Event} reason - event caused clear of selection\n * @param {boolean} restoreSelection - if true, restore saved selection\n */\n public clearSelection(reason?: Event, restoreSelection = false) {\n const {BlockManager, Caret, RectangleSelection} = this.Editor;\n\n this.needToSelectAll = false;\n this.nativeInputSelected = false;\n this.readyToBlockSelection = false;\n\n /**\n * If reason caused clear of the selection was printable key and any block is selected,\n * remove selected blocks and insert pressed key\n */\n if (this.anyBlockSelected && reason && reason instanceof KeyboardEvent && _.isPrintableKey(reason.keyCode)) {\n const indexToInsert = BlockManager.removeSelectedBlocks();\n\n BlockManager.insertInitialBlockAtIndex(indexToInsert, true);\n Caret.setToBlock(BlockManager.currentBlock);\n _.delay(() => {\n Caret.insertContentAtCaretPosition(reason.key);\n }, 20)();\n }\n\n this.Editor.CrossBlockSelection.clear(reason);\n\n if (!this.anyBlockSelected || RectangleSelection.isRectActivated()) {\n this.Editor.RectangleSelection.clearSelection();\n return;\n }\n\n /**\n * Restore selection when Block is already selected\n * but someone tries to write something.\n */\n if (restoreSelection) {\n this.selection.restore();\n }\n\n /** Now all blocks cleared */\n this.allBlocksSelected = false;\n }\n\n /**\n * Reduce each Block and copy its content\n */\n public copySelectedBlocks(): void {\n const fakeClipboard = $.make('div');\n\n this.selectedBlocks.forEach((block) => {\n /**\n * Make tag that holds clean HTML\n */\n const cleanHTML = this.Editor.Sanitizer.clean(block.holder.innerHTML, this.sanitizerConfig);\n const fragment = $.make('p');\n\n fragment.innerHTML = cleanHTML;\n fakeClipboard.appendChild(fragment);\n });\n\n _.copyTextToClipboard(fakeClipboard.innerHTML);\n }\n\n /**\n * select Block\n * @param {number?} index - Block index according to the BlockManager's indexes\n */\n public selectBlockByIndex(index?) {\n const {BlockManager} = this.Editor;\n\n /**\n * Remove previous focused Block's state\n */\n BlockManager.clearFocused();\n\n let block;\n\n if (isNaN(index)) {\n block = BlockManager.currentBlock;\n } else {\n block = BlockManager.getBlockByIndex(index);\n }\n\n /** Save selection */\n this.selection.save();\n SelectionUtils.get()\n .removeAllRanges();\n\n block.selected = true;\n\n /** close InlineToolbar when we selected any Block */\n this.Editor.InlineToolbar.close();\n }\n\n /**\n * First CMD+A selects all input content by native behaviour,\n * next CMD+A keypress selects all blocks\n *\n * @param {KeyboardEvent} event\n */\n private handleCommandA(event: KeyboardEvent): void {\n this.Editor.RectangleSelection.clearSelection();\n\n /** allow default selection on native inputs */\n if ($.isNativeInput(event.target) && !this.readyToBlockSelection) {\n this.readyToBlockSelection = true;\n return;\n }\n\n const workingBlock = this.Editor.BlockManager.getBlock(event.target as HTMLElement);\n const inputs = workingBlock.inputs;\n\n /**\n * If Block has more than one editable element allow native selection\n * Second cmd+a will select whole Block\n */\n if (inputs.length > 1 && !this.readyToBlockSelection) {\n this.readyToBlockSelection = true;\n return;\n }\n\n if (inputs.length === 1 && !this.needToSelectAll) {\n this.needToSelectAll = true;\n return;\n }\n\n if (this.needToSelectAll) {\n /**\n * Prevent default selection\n */\n event.preventDefault();\n\n this.selectAllBlocks();\n\n /**\n * Disable any selection after all Blocks selected\n */\n this.needToSelectAll = false;\n this.readyToBlockSelection = false;\n\n /**\n * Close ConversionToolbar when all Blocks selected\n */\n this.Editor.ConversionToolbar.close();\n } else if (this.readyToBlockSelection) {\n /**\n * prevent default selection when we use custom selection\n */\n event.preventDefault();\n\n /**\n * select working Block\n */\n this.selectBlockByIndex();\n\n /**\n * Enable all Blocks selection if current Block is selected\n */\n this.needToSelectAll = true;\n }\n }\n\n /**\n * Select All Blocks\n * Each Block has selected setter that makes Block copyable\n */\n private selectAllBlocks() {\n /**\n * Save selection\n * Will be restored when closeSelection fired\n */\n this.selection.save();\n\n /**\n * Remove Ranges from Selection\n */\n SelectionUtils.get()\n .removeAllRanges();\n\n this.allBlocksSelected = true;\n\n /** close InlineToolbar if we selected all Blocks */\n this.Editor.InlineToolbar.close();\n }\n}\n","/**\n * @class Caret\n * @classdesc Contains methods for working Caret\n *\n * Uses Range methods to manipulate with caret\n *\n * @module Caret\n *\n * @version 2.0.0\n */\n\nimport Selection from '../selection';\nimport Module from '../__module';\nimport Block from '../block';\nimport $ from '../dom';\nimport * as _ from '../utils';\n\n/**\n * @typedef {Caret} Caret\n */\nexport default class Caret extends Module {\n\n /**\n * Allowed caret positions in input\n *\n * @static\n * @returns {{START: string, END: string, DEFAULT: string}}\n */\n public get positions(): {START: string, END: string, DEFAULT: string} {\n return {\n START: 'start',\n END: 'end',\n DEFAULT: 'default',\n };\n }\n\n /**\n * Elements styles that can be useful for Caret Module\n */\n private static get CSS(): {shadowCaret: string} {\n return {\n shadowCaret: 'cdx-shadow-caret',\n };\n }\n\n /**\n * Get's deepest first node and checks if offset is zero\n * @return {boolean}\n */\n public get isAtStart(): boolean {\n const selection = Selection.get();\n const firstNode = $.getDeepestNode(this.Editor.BlockManager.currentBlock.currentInput);\n let focusNode = selection.focusNode;\n\n /** In case lastNode is native input */\n if ($.isNativeInput(firstNode)) {\n return (firstNode as HTMLInputElement).selectionEnd === 0;\n }\n\n /** Case when selection have been cleared programmatically, for example after CBS */\n if (!selection.anchorNode) {\n return false;\n }\n\n /**\n * Workaround case when caret in the text like \" |Hello!\"\n * selection.anchorOffset is 1, but real caret visible position is 0\n * @type {number}\n */\n\n let firstLetterPosition = focusNode.textContent.search(/\\S/);\n\n if (firstLetterPosition === -1) { // empty text\n firstLetterPosition = 0;\n }\n\n /**\n * If caret was set by external code, it might be set to text node wrapper.\n *
|hello
<---- Selection references to instead of text node\n *\n * In this case, anchor node has ELEMENT_NODE node type.\n * Anchor offset shows amount of children between start of the element and caret position.\n *\n * So we use child with focusOffset index as new anchorNode.\n */\n let focusOffset = selection.focusOffset;\n if (focusNode.nodeType !== Node.TEXT_NODE && focusNode.childNodes.length) {\n if (focusNode.childNodes[focusOffset]) {\n focusNode = focusNode.childNodes[focusOffset];\n focusOffset = 0;\n } else {\n focusNode = focusNode.childNodes[focusOffset - 1];\n focusOffset = focusNode.textContent.length;\n }\n }\n\n /**\n * In case of\n *
\n *
<-- first (and deepest) node is
\n * |adaddad <-- focus node\n *
\n */\n if ($.isLineBreakTag(firstNode as HTMLElement) || $.isEmpty(firstNode)) {\n const leftSiblings = this.getHigherLevelSiblings(focusNode as HTMLElement, 'left');\n const nothingAtLeft = leftSiblings.every((node) => {\n /**\n * Workaround case when block starts with several
's (created by SHIFT+ENTER)\n * @see https://github.com/codex-team/editor.js/issues/726\n * We need to allow to delete such linebreaks, so in this case caret IS NOT AT START\n */\n const regularLineBreak = $.isLineBreakTag(node);\n /**\n * Workaround SHIFT+ENTER in Safari, that creates
instead of
\n */\n const lineBreakInSafari = node.children.length === 1 && $.isLineBreakTag(node.children[0] as HTMLElement);\n const isLineBreak = regularLineBreak || lineBreakInSafari;\n\n return $.isEmpty(node) && !isLineBreak;\n });\n\n if (nothingAtLeft && focusOffset === firstLetterPosition) {\n return true;\n }\n }\n\n /**\n * We use <= comparison for case:\n * \"| Hello\" <--- selection.anchorOffset is 0, but firstLetterPosition is 1\n */\n return firstNode === null || focusNode === firstNode && focusOffset <= firstLetterPosition;\n }\n\n /**\n * Get's deepest last node and checks if offset is last node text length\n * @return {boolean}\n */\n public get isAtEnd(): boolean {\n const selection = Selection.get();\n let focusNode = selection.focusNode;\n\n const lastNode = $.getDeepestNode(this.Editor.BlockManager.currentBlock.currentInput, true);\n\n /** In case lastNode is native input */\n if ($.isNativeInput(lastNode)) {\n return (lastNode as HTMLInputElement).selectionEnd === (lastNode as HTMLInputElement).value.length;\n }\n\n /** Case when selection have been cleared programmatically, for example after CBS */\n if (!selection.focusNode) {\n return false;\n }\n\n /**\n * If caret was set by external code, it might be set to text node wrapper.\n *
hello|
<---- Selection references to
instead of text node\n *\n * In this case, anchor node has ELEMENT_NODE node type.\n * Anchor offset shows amount of children between start of the element and caret position.\n *\n * So we use child with anchofocusOffset - 1 as new focusNode.\n */\n let focusOffset = selection.focusOffset;\n if (focusNode.nodeType !== Node.TEXT_NODE && focusNode.childNodes.length) {\n if (focusNode.childNodes[focusOffset - 1]) {\n focusNode = focusNode.childNodes[focusOffset - 1];\n focusOffset = focusNode.textContent.length;\n } else {\n focusNode = focusNode.childNodes[0];\n focusOffset = 0;\n }\n }\n\n /**\n * In case of\n *
\n * adaddad| <-- anchor node\n *
<-- first (and deepest) node is
\n *
\n */\n if ($.isLineBreakTag(lastNode as HTMLElement) || $.isEmpty(lastNode)) {\n const rightSiblings = this.getHigherLevelSiblings(focusNode as HTMLElement, 'right');\n const nothingAtRight = rightSiblings.every((node, i) => {\n /**\n * If last right sibling is BR isEmpty returns false, but there actually nothing at right\n */\n const isLastBR = i === rightSiblings.length - 1 && $.isLineBreakTag(node as HTMLElement);\n\n return (isLastBR) || $.isEmpty(node) && !$.isLineBreakTag(node);\n });\n\n if (nothingAtRight && focusOffset === focusNode.textContent.length) {\n return true;\n }\n }\n\n /**\n * Workaround case:\n * hello | <--- anchorOffset will be 5, but textContent.length will be 6.\n * Why not regular .trim():\n * in case of ' hello |' trim() will also remove space at the beginning, so length will be lower than anchorOffset\n */\n const rightTrimmedText = lastNode.textContent.replace(/\\s+$/, '');\n\n /**\n * We use >= comparison for case:\n * \"Hello |\" <--- selection.anchorOffset is 7, but rightTrimmedText is 6\n */\n return focusNode === lastNode && focusOffset >= rightTrimmedText.length;\n }\n\n /**\n * Method gets Block instance and puts caret to the text node with offset\n * There two ways that method applies caret position:\n * - first found text node: sets at the beginning, but you can pass an offset\n * - last found text node: sets at the end of the node. Also, you can customize the behaviour\n *\n * @param {Block} block - Block class\n * @param {String} position - position where to set caret.\n * If default - leave default behaviour and apply offset if it's passed\n * @param {Number} offset - caret offset regarding to the text node\n */\n public setToBlock(block: Block, position: string = this.positions.DEFAULT, offset: number = 0): void {\n const {BlockManager} = this.Editor;\n let element;\n\n switch (position) {\n case this.positions.START:\n element = block.firstInput;\n break;\n case this.positions.END:\n element = block.lastInput;\n break;\n default:\n element = block.currentInput;\n }\n\n if (!element) {\n return;\n }\n\n const nodeToSet = $.getDeepestNode(element, position === this.positions.END);\n const contentLength = $.getContentLength(nodeToSet);\n\n switch (true) {\n case position === this.positions.START:\n offset = 0;\n break;\n case position === this.positions.END:\n case offset > contentLength:\n offset = contentLength;\n break;\n }\n\n /**\n * @todo try to fix via Promises or use querySelectorAll to not to use timeout\n */\n _.delay( () => {\n this.set(nodeToSet as HTMLElement, offset);\n }, 20)();\n\n BlockManager.setCurrentBlockByChildNode(block.holder);\n BlockManager.currentBlock.currentInput = element;\n }\n\n /**\n * Set caret to the current input of current Block.\n *\n * @param {HTMLElement} input - input where caret should be set\n * @param {String} position - position of the caret.\n * If default - leave default behaviour and apply offset if it's passed\n * @param {number} offset - caret offset regarding to the text node\n */\n public setToInput(input: HTMLElement, position: string = this.positions.DEFAULT, offset: number = 0): void {\n const {currentBlock} = this.Editor.BlockManager;\n const nodeToSet = $.getDeepestNode(input);\n\n switch (position) {\n case this.positions.START:\n this.set(nodeToSet as HTMLElement, 0);\n break;\n\n case this.positions.END:\n const contentLength = $.getContentLength(nodeToSet);\n\n this.set(nodeToSet as HTMLElement, contentLength);\n break;\n\n default:\n if (offset) {\n this.set(nodeToSet as HTMLElement, offset);\n }\n }\n\n currentBlock.currentInput = input;\n }\n\n /**\n * Creates Document Range and sets caret to the element with offset\n * @param {HTMLElement} element - target node.\n * @param {Number} offset - offset\n */\n public set(element: HTMLElement, offset: number = 0): void {\n const range = document.createRange(),\n selection = Selection.get();\n\n /** if found deepest node is native input */\n if ($.isNativeInput(element)) {\n if (!$.canSetCaret(element)) {\n return;\n }\n\n element.focus();\n (element as HTMLInputElement).selectionStart = (element as HTMLInputElement).selectionEnd = offset;\n return;\n }\n\n range.setStart(element, offset);\n range.setEnd(element, offset);\n\n selection.removeAllRanges();\n selection.addRange(range);\n\n /** If new cursor position is not visible, scroll to it */\n const {top, bottom} = element.nodeType === Node.ELEMENT_NODE\n ? element.getBoundingClientRect()\n : range.getBoundingClientRect();\n const {innerHeight} = window;\n\n if (top < 0) { window.scrollBy(0, top); }\n if (bottom > innerHeight) { window.scrollBy(0, bottom - innerHeight); }\n }\n /**\n * Set Caret to the last Block\n * If last block is not empty, append another empty block\n */\n public setToTheLastBlock(): void {\n const lastBlock = this.Editor.BlockManager.lastBlock;\n\n if (!lastBlock) {\n return;\n }\n\n /**\n * If last block is empty and it is an initialBlock, set to that.\n * Otherwise, append new empty block and set to that\n */\n if (this.Editor.Tools.isInitial(lastBlock.tool) && lastBlock.isEmpty) {\n this.setToBlock(lastBlock);\n } else {\n const newBlock = this.Editor.BlockManager.insertAtEnd();\n\n this.setToBlock(newBlock);\n }\n }\n\n /**\n * Extract content fragment of current Block from Caret position to the end of the Block\n */\n public extractFragmentFromCaretPosition(): void|DocumentFragment {\n const selection = Selection.get();\n\n if (selection.rangeCount) {\n const selectRange = selection.getRangeAt(0);\n const currentBlockInput = this.Editor.BlockManager.currentBlock.currentInput;\n\n selectRange.deleteContents();\n\n if (currentBlockInput) {\n const range = selectRange.cloneRange();\n\n range.selectNodeContents(currentBlockInput);\n range.setStart(selectRange.endContainer, selectRange.endOffset);\n return range.extractContents();\n }\n }\n }\n\n /**\n * Set's caret to the next Block or Tool`s input\n * Before moving caret, we should check if caret position is at the end of Plugins node\n * Using {@link Dom#getDeepestNode} to get a last node and match with current selection\n *\n * @param {Boolean} force - force navigation even if caret is not at the end\n *\n * @return {Boolean}\n */\n public navigateNext(force: boolean = false): boolean {\n const {currentBlock, nextContentfulBlock} = this.Editor.BlockManager;\n const {nextInput} = currentBlock;\n\n if (!nextContentfulBlock && !nextInput) {\n return false;\n }\n\n if (force || this.isAtEnd) {\n /** If next Tool`s input exists, focus on it. Otherwise set caret to the next Block */\n if (!nextInput) {\n this.setToBlock(nextContentfulBlock, this.positions.START);\n } else {\n this.setToInput(nextInput, this.positions.START);\n }\n\n return true;\n }\n\n return false;\n }\n\n /**\n * Set's caret to the previous Tool`s input or Block\n * Before moving caret, we should check if caret position is start of the Plugins node\n * Using {@link Dom#getDeepestNode} to get a last node and match with current selection\n *\n * @param {Boolean} force - force navigation even if caret is not at the start\n *\n * @return {Boolean}\n */\n public navigatePrevious(force: boolean = false): boolean {\n const {currentBlock, previousContentfulBlock} = this.Editor.BlockManager;\n\n if (!currentBlock) {\n return false;\n }\n\n const {previousInput} = currentBlock;\n\n if (!previousContentfulBlock && !previousInput) {\n return false;\n }\n\n if (force || this.isAtStart) {\n /** If previous Tool`s input exists, focus on it. Otherwise set caret to the previous Block */\n if (!previousInput) {\n if (currentBlock.name === 'simpleCode') {\n return false;\n }\n this.setToBlock( previousContentfulBlock, this.positions.END );\n } else {\n this.setToInput(previousInput, this.positions.END);\n }\n return true;\n }\n\n return false;\n }\n\n /**\n * Inserts shadow element after passed element where caret can be placed\n * @param {Node} element\n */\n public createShadow(element): void {\n const shadowCaret = document.createElement('span');\n\n shadowCaret.classList.add(Caret.CSS.shadowCaret);\n element.insertAdjacentElement('beforeEnd', shadowCaret);\n }\n\n /**\n * Restores caret position\n * @param {HTMLElement} element\n */\n public restoreCaret(element: HTMLElement): void {\n const shadowCaret = element.querySelector(`.${Caret.CSS.shadowCaret}`);\n\n if (!shadowCaret) {\n return;\n }\n\n /**\n * After we set the caret to the required place\n * we need to clear shadow caret\n *\n * - make new range\n * - select shadowed span\n * - use extractContent to remove it from DOM\n */\n const sel = new Selection();\n\n sel.expandToTag(shadowCaret as HTMLElement);\n\n setTimeout(() => {\n const newRange = document.createRange();\n\n newRange.selectNode(shadowCaret);\n newRange.extractContents();\n }, 50);\n }\n\n /**\n * Inserts passed content at caret position\n *\n * @param {string} content - content to insert\n */\n public insertContentAtCaretPosition(content: string): void {\n const fragment = document.createDocumentFragment();\n const wrapper = document.createElement('div');\n const selection = Selection.get();\n const range = Selection.range;\n\n wrapper.innerHTML = content;\n\n Array.from(wrapper.childNodes).forEach((child: Node) => fragment.appendChild(child));\n\n const lastChild = fragment.lastChild;\n\n range.deleteContents();\n range.insertNode(fragment);\n\n /** Cross-browser caret insertion */\n const newRange = document.createRange();\n\n newRange.setStart(lastChild, lastChild.textContent.length);\n\n selection.removeAllRanges();\n selection.addRange(newRange);\n }\n\n /**\n * Get all first-level (first child of [contenteditabel]) siblings from passed node\n * Then you can check it for emptiness\n *\n * @example\n *
\n *\n * @return {Element[]}\n */\n private getHigherLevelSiblings(from: HTMLElement, direction?: string): HTMLElement[] {\n let current = from;\n const siblings = [];\n\n /**\n * Find passed node's firs-level parent (in example - blockquote)\n */\n while (current.parentNode && (current.parentNode as HTMLElement).contentEditable !== 'true') {\n current = current.parentNode as HTMLElement;\n }\n\n const sibling = direction === 'left' ? 'previousSibling' : 'nextSibling';\n\n /**\n * Find all left/right siblings\n */\n while (current[sibling]) {\n current = current[sibling] as HTMLElement;\n siblings.push(current);\n }\n\n return siblings;\n }\n}\n","import Module from '../__module';\nimport Block from '../block';\nimport SelectionUtils from '../selection';\nimport * as _ from '../utils';\n\nexport default class CrossBlockSelection extends Module {\n /**\n * Block where selection is started\n */\n private firstSelectedBlock: Block;\n\n /**\n * Last selected Block\n */\n private lastSelectedBlock: Block;\n\n /**\n * Sets up listeners\n *\n * @param {MouseEvent} event - mouse down event\n */\n public watchSelection(event: MouseEvent): void {\n if (event.button !== _.mouseButtons.LEFT) {\n return;\n }\n\n const {BlockManager, UI, Listeners} = this.Editor;\n\n this.firstSelectedBlock = BlockManager.getBlock(event.target as HTMLElement);\n this.lastSelectedBlock = this.firstSelectedBlock;\n\n Listeners.on(document, 'mouseover', this.onMouseOver);\n Listeners.on(document, 'mouseup', this.onMouseUp);\n }\n\n /**\n * return boolean is cross block selection started\n */\n public get isCrossBlockSelectionStarted(): boolean {\n return !!this.firstSelectedBlock\n && !!this.lastSelectedBlock;\n }\n\n /**\n * Change selection state of the next Block\n * Used for CBS via Shift + arrow keys\n *\n * @param {boolean} next - if true, toggle next block. Previous otherwise\n */\n public toggleBlockSelectedState(next: boolean = true): void {\n const {BlockManager} = this.Editor;\n\n if (!this.lastSelectedBlock) {\n this.lastSelectedBlock = this.firstSelectedBlock = BlockManager.currentBlock;\n }\n\n if (this.firstSelectedBlock === this.lastSelectedBlock) {\n this.firstSelectedBlock.selected = true;\n SelectionUtils.get().removeAllRanges();\n }\n\n const nextBlockIndex = BlockManager.blocks.indexOf(this.lastSelectedBlock) + (next ? 1 : -1);\n const nextBlock = BlockManager.blocks[nextBlockIndex];\n\n if (!nextBlock) {\n return;\n }\n\n if (this.lastSelectedBlock.selected !== nextBlock.selected) {\n nextBlock.selected = true;\n } else {\n this.lastSelectedBlock.selected = false;\n }\n\n this.lastSelectedBlock = nextBlock;\n\n /** close InlineToolbar when Blocks selected */\n this.Editor.InlineToolbar.close();\n }\n\n /**\n * Clear saved state\n *\n * @param {Event} reason - event caused clear of selection\n */\n public clear(reason?: Event) {\n const {BlockManager, BlockSelection, Caret} = this.Editor;\n const fIndex = BlockManager.blocks.indexOf(this.firstSelectedBlock);\n const lIndex = BlockManager.blocks.indexOf(this.lastSelectedBlock);\n\n if (BlockSelection.anyBlockSelected && fIndex > -1 && lIndex > -1) {\n if (reason && reason instanceof KeyboardEvent) {\n /**\n * Set caret depending on pressed key if pressed key is an arrow.\n */\n switch (reason.keyCode) {\n case _.keyCodes.DOWN:\n case _.keyCodes.RIGHT:\n Caret.setToBlock(BlockManager.blocks[Math.max(fIndex, lIndex)], Caret.positions.END);\n break;\n\n case _.keyCodes.UP:\n case _.keyCodes.LEFT:\n Caret.setToBlock(BlockManager.blocks[Math.min(fIndex, lIndex)], Caret.positions.START);\n break;\n default:\n Caret.setToBlock(BlockManager.blocks[Math.max(fIndex, lIndex)], Caret.positions.END);\n }\n } else {\n /**\n * By default set caret at the end of the last selected block\n */\n Caret.setToBlock(BlockManager.blocks[Math.max(fIndex, lIndex)], Caret.positions.END);\n }\n }\n\n this.firstSelectedBlock = this.lastSelectedBlock = null;\n }\n\n /**\n * Mouse up event handler.\n * Removes the listeners\n */\n private onMouseUp = (): void => {\n const {Listeners} = this.Editor;\n\n Listeners.off(document, 'mouseover', this.onMouseOver);\n Listeners.off(document, 'mouseup', this.onMouseUp);\n }\n\n /**\n * Mouse over event handler\n * Gets target and related blocks and change selected state for blocks in between\n *\n * @param {MouseEvent} event\n */\n private onMouseOver = (event: MouseEvent): void => {\n const {BlockManager} = this.Editor;\n\n const relatedBlock = BlockManager.getBlockByChildNode(event.relatedTarget as Node) || this.lastSelectedBlock;\n const targetBlock = BlockManager.getBlockByChildNode(event.target as Node);\n\n if (!relatedBlock || !targetBlock) {\n return;\n }\n\n if (targetBlock === relatedBlock) {\n return;\n }\n\n if (relatedBlock === this.firstSelectedBlock) {\n SelectionUtils.get().removeAllRanges();\n\n relatedBlock.selected = true;\n targetBlock.selected = true;\n return;\n }\n\n if (targetBlock === this.firstSelectedBlock) {\n relatedBlock.selected = false;\n targetBlock.selected = false;\n return;\n }\n\n this.Editor.InlineToolbar.close();\n\n this.toggleBlocksSelectedState(relatedBlock, targetBlock);\n this.lastSelectedBlock = targetBlock;\n }\n\n /**\n * Change blocks selection state between passed two blocks.\n *\n * @param {Block} firstBlock\n * @param {Block} lastBlock\n */\n private toggleBlocksSelectedState(firstBlock: Block, lastBlock: Block): void {\n const {BlockManager} = this.Editor;\n const fIndex = BlockManager.blocks.indexOf(firstBlock);\n const lIndex = BlockManager.blocks.indexOf(lastBlock);\n\n /**\n * If first and last block have the different selection state\n * it means we should't toggle selection of the first selected block.\n * In the other case we shouldn't toggle the last selected block.\n */\n const shouldntSelectFirstBlock = firstBlock.selected !== lastBlock.selected;\n\n for (let i = Math.min(fIndex, lIndex); i <= Math.max(fIndex, lIndex); i++) {\n const block = BlockManager.blocks[i];\n\n if (\n block !== this.firstSelectedBlock &&\n block !== (shouldntSelectFirstBlock ? firstBlock : lastBlock)\n ) {\n BlockManager.blocks[i].selected = !BlockManager.blocks[i].selected;\n }\n }\n }\n}\n","import SelectionUtils from '../selection';\n\nimport Module from '../__module';\nexport default class DragNDrop extends Module {\n\n /**\n * If drag has been started at editor, we save it\n *\n * @type Boolean\n * @private\n */\n private isStartedAtEditor = false;\n\n /**\n * Bind events\n *\n * @private\n */\n public prepare(): void {\n this.bindEvents();\n }\n\n /**\n * Add drag events listeners to editor zone\n * @private\n */\n private bindEvents(): void {\n this.Editor.Listeners.on(this.Editor.UI.nodes.holder, 'drop', this.processDrop, true);\n\n this.Editor.Listeners.on(this.Editor.UI.nodes.holder, 'dragstart', (dragEvent: DragEvent) => {\n\n if (SelectionUtils.isAtEditor && !SelectionUtils.isCollapsed) {\n this.isStartedAtEditor = true;\n }\n\n this.Editor.InlineToolbar.close();\n });\n\n /* Prevent default browser behavior to allow drop on non-contenteditable elements */\n this.Editor.Listeners.on(this.Editor.UI.nodes.holder, 'dragover', (e) => e.preventDefault(), true);\n }\n\n /**\n * Handle drop event\n *\n * @param {DragEvent} dropEvent\n */\n private processDrop = async (dropEvent: DragEvent): Promise
=> {\n const {\n BlockManager,\n Caret,\n Paste,\n } = this.Editor;\n\n dropEvent.preventDefault();\n\n BlockManager.blocks.forEach((block) => block.dropTarget = false);\n\n if (SelectionUtils.isAtEditor && !SelectionUtils.isCollapsed && this.isStartedAtEditor) {\n document.execCommand('delete');\n }\n\n this.isStartedAtEditor = false;\n\n /**\n * Try to set current block by drop target.\n * If drop target (error will be thrown) is not part of the Block, set last Block as current.\n */\n try {\n const targetBlock = BlockManager.setCurrentBlockByChildNode(dropEvent.target as Node);\n\n this.Editor.Caret.setToBlock(targetBlock, Caret.positions.END);\n } catch (e) {\n const targetBlock = BlockManager.setCurrentBlockByChildNode(BlockManager.lastBlock.holder);\n\n this.Editor.Caret.setToBlock(targetBlock, Caret.positions.END);\n }\n\n Paste.processDataTransfer(dropEvent.dataTransfer, true);\n }\n}\n","import Module from '../__module';\n\n/**\n * @module eventDispatcher\n *\n * Has two important methods:\n * - {Function} on - appends subscriber to the event. If event doesn't exist - creates new one\n * - {Function} emit - fires all subscribers with data\n * - {Function off - unsubsribes callback\n *\n * @version 1.0.0\n *\n * @typedef {Events} Events\n * @property {Object} subscribers - all subscribers grouped by event name\n */\nexport default class Events extends Module {\n\n /**\n * Object with events` names as key and array of callback functions as value\n * @type {{}}\n */\n private subscribers: {[name: string]: Array<(data?: any) => any>} = {};\n\n /**\n * Subscribe any event on callback\n *\n * @param {String} eventName - event name\n * @param {Function} callback - subscriber\n */\n public on(eventName: string, callback: (data: any) => any) {\n if (!(eventName in this.subscribers)) {\n this.subscribers[eventName] = [];\n }\n\n // group by events\n this.subscribers[eventName].push(callback);\n }\n\n /**\n * Subscribe any event on callback. Callback will be called once and be removed from subscribers array after call.\n *\n * @param {String} eventName - event name\n * @param {Function} callback - subscriber\n */\n public once(eventName: string, callback: (data: any) => any) {\n if (!(eventName in this.subscribers)) {\n this.subscribers[eventName] = [];\n }\n\n const wrappedCallback = (data: any) => {\n const result = callback(data);\n\n const indexOfHandler = this.subscribers[eventName].indexOf(wrappedCallback);\n\n if (indexOfHandler !== -1) {\n this.subscribers[eventName].splice(indexOfHandler, 1);\n }\n\n return result;\n };\n\n // group by events\n this.subscribers[eventName].push(wrappedCallback);\n }\n\n /**\n * Emit callbacks with passed data\n *\n * @param {String} eventName - event name\n * @param {Object} data - subscribers get this data when they were fired\n */\n public emit(eventName: string, data?: any): void {\n if (!this.subscribers[eventName]) {\n return;\n }\n\n this.subscribers[eventName].reduce((previousData, currentHandler) => {\n const newData = currentHandler(previousData);\n\n return newData ? newData : previousData;\n }, data);\n }\n\n /**\n * Unsubsribe callback from event\n *\n * @param eventName\n * @param callback\n */\n public off(eventName: string, callback: (data: any) => void): void {\n for (let i = 0; i < this.subscribers[eventName].length; i++) {\n if (this.subscribers[eventName][i] === callback) {\n delete this.subscribers[eventName][i];\n break;\n }\n }\n }\n\n /**\n * Destroyer\n * clears subsribers list\n */\n public destroy(): void {\n this.subscribers = null;\n }\n}\n","import Module from '../__module';\n\n/**\n * Event listener information\n */\nexport interface ListenerData {\n /**\n * Element where to listen to dispatched events\n */\n element: EventTarget;\n\n /**\n * Event to listen\n */\n eventType: string;\n\n /**\n * Event handler\n *\n * @param {Event} event\n */\n handler: (event: Event) => void;\n\n /**\n * @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener\n */\n options: boolean | AddEventListenerOptions;\n}\n\n/**\n * Editor.js Listeners module\n *\n * @module Listeners\n *\n * Module-decorator for event listeners assignment\n *\n * @author Codex Team\n * @version 2.0.0\n */\n\n/**\n * @typedef {Listeners} Listeners\n * @property {Array} allListeners\n */\nexport default class Listeners extends Module {\n\n /**\n * Stores all listeners data to find/remove/process it\n * @type {ListenerData[]}\n */\n private allListeners: ListenerData[] = [];\n\n /**\n * Assigns event listener on element\n *\n * @param {EventTarget} element - DOM element that needs to be listened\n * @param {String} eventType - event type\n * @param {Function} handler - method that will be fired on event\n * @param {Boolean|AddEventListenerOptions} options - useCapture or {capture, passive, once}\n */\n public on(\n element: EventTarget,\n eventType: string,\n handler: (event: Event) => void,\n options: boolean | AddEventListenerOptions = false,\n ): void {\n const assignedEventData = {\n element,\n eventType,\n handler,\n options,\n };\n\n const alreadyExist = this.findOne(element, eventType, handler);\n\n if (alreadyExist) { return; }\n\n this.allListeners.push(assignedEventData);\n element.addEventListener(eventType, handler, options);\n }\n\n /**\n * Removes event listener from element\n *\n * @param {EventTarget} element - DOM element that we removing listener\n * @param {String} eventType - event type\n * @param {Function} handler - remove handler, if element listens several handlers on the same event type\n * @param {Boolean|AddEventListenerOptions} options - useCapture or {capture, passive, once}\n */\n public off(\n element: EventTarget,\n eventType: string,\n handler?: (event: Event) => void,\n options?: boolean | AddEventListenerOptions,\n ): void {\n const existingListeners = this.findAll(element, eventType, handler);\n\n existingListeners.forEach((listener, i) => {\n const index = this.allListeners.indexOf(existingListeners[i]);\n\n if (index > 0) {\n this.allListeners.splice(index, 1);\n\n listener.element.removeEventListener(listener.eventType, listener.handler, listener.options);\n }\n });\n\n }\n\n /**\n * @param {EventTarget} element\n * @param {String} eventType\n * @param {Function} handler\n * @return {ListenerData|null}\n */\n public findOne(element: EventTarget, eventType?: string, handler?: (event: Event) => void): ListenerData {\n const foundListeners = this.findAll(element, eventType, handler);\n\n return foundListeners.length > 0 ? foundListeners[0] : null;\n }\n\n /**\n * @param {EventTarget} element\n * @param {String} eventType\n * @param {Function} handler\n * @return {ListenerData[]}\n */\n public findAll(element: EventTarget, eventType?: string, handler?: (event: Event) => void): ListenerData[] {\n let found;\n const foundByEventTargets = element ? this.findByEventTarget(element) : [];\n\n if (element && eventType && handler) {\n found = foundByEventTargets.filter( (event) => event.eventType === eventType && event.handler === handler );\n } else if (element && eventType) {\n found = foundByEventTargets.filter( (event) => event.eventType === eventType);\n } else {\n found = foundByEventTargets;\n }\n\n return found;\n }\n\n /**\n * Removes all listeners\n */\n public removeAll(): void {\n this.allListeners.map( (current) => {\n current.element.removeEventListener(current.eventType, current.handler, current.options);\n });\n\n this.allListeners = [];\n }\n\n /**\n * Search method: looks for listener by passed element\n * @param {EventTarget} element - searching element\n * @returns {Array} listeners that found on element\n */\n private findByEventTarget(element: EventTarget): ListenerData[] {\n return this.allListeners.filter((listener) => {\n if (listener.element === element) {\n return listener;\n }\n });\n }\n\n /**\n * Search method: looks for listener by passed event type\n * @param {String} eventType\n * @return {Array} listeners that found on element\n */\n private findByType(eventType: string): ListenerData[] {\n return this.allListeners.filter((listener) => {\n if (listener.eventType === eventType) {\n return listener;\n }\n });\n }\n\n /**\n * Search method: looks for listener by passed handler\n * @param {Function} handler\n * @return {Array} listeners that found on element\n */\n private findByHandler(handler: (event: Event) => void): ListenerData[] {\n return this.allListeners.filter((listener) => {\n if (listener.handler === handler) {\n return listener;\n }\n });\n }\n}\n","/**\n * @module ModificationsObserver\n *\n * Handles any mutations\n * and gives opportunity to handle outside\n */\n\nimport Module from '../__module';\nimport * as _ from '../utils';\nimport Block from '../block';\n\nexport default class ModificationsObserver extends Module {\n\n /**\n * Debounce Timer\n * @type {number}\n */\n public static readonly DebounceTimer = 450;\n\n /**\n * MutationObserver instance\n */\n private observer: MutationObserver;\n\n /**\n * Allows to temporary disable mutations handling\n */\n private disabled: boolean;\n\n /**\n * Used to prevent several mutation callback execution\n * @type {Function}\n */\n private mutationDebouncer = _.debounce( () => {\n this.updateNativeInputs();\n this.config.onChange();\n }, ModificationsObserver.DebounceTimer);\n\n /**\n * Array of native inputs in Blocks.\n * Changes in native inputs are not handled by modification observer, so we need to set change event listeners on them\n */\n private nativeInputs: HTMLElement[] = [];\n\n /**\n * Clear timeout and set null to mutationDebouncer property\n */\n public destroy() {\n this.mutationDebouncer = null;\n if (this.observer) {\n this.observer.disconnect();\n }\n this.observer = null;\n this.nativeInputs.forEach((input) => this.Editor.Listeners.off(input, 'input', this.mutationDebouncer));\n }\n\n /**\n * Preparation method\n * @return {Promise}\n */\n public async prepare(): Promise {\n /**\n * wait till Browser render Editor's Blocks\n */\n window.setTimeout( () => {\n this.setObserver();\n }, 1000);\n }\n\n /**\n * Allows to disable observer,\n * for example when Editor wants to stealthy mutate DOM\n */\n public disable() {\n this.disabled = true;\n }\n\n /**\n * Enables mutation handling\n * Should be called after .disable()\n */\n public enable() {\n this.disabled = false;\n }\n\n /**\n * setObserver\n *\n * sets 'DOMSubtreeModified' listener on Editor's UI.nodes.redactor\n * so that User can handle outside from API\n */\n private setObserver(): void {\n const {UI} = this.Editor;\n const observerOptions = {\n childList: true,\n attributes: true,\n subtree: true,\n characterData: true,\n characterDataOldValue: true,\n };\n\n this.observer = new MutationObserver((mutationList, observer) => {\n this.mutationHandler(mutationList, observer);\n });\n this.observer.observe(UI.nodes.redactor, observerOptions);\n }\n\n /**\n * MutationObserver events handler\n * @param mutationList\n * @param observer\n */\n private mutationHandler(mutationList, observer) {\n /**\n * Skip mutations in stealth mode\n */\n if (this.disabled) {\n return;\n }\n\n /**\n * We divide two Mutation types:\n * 1) mutations that concerns client changes: settings changes, symbol added, deletion, insertions and so on\n * 2) functional changes. On each client actions we set functional identifiers to interact with user\n */\n let contentMutated = false;\n\n mutationList.forEach((mutation) => {\n switch (mutation.type) {\n case 'childList':\n case 'subtree':\n case 'characterData':\n case 'characterDataOldValue':\n contentMutated = true;\n break;\n case 'attributes':\n const mutatedTarget = mutation.target as Element;\n\n /**\n * Changes on Element.ce-block usually is functional\n */\n if (!mutatedTarget.classList.contains(Block.CSS.wrapper)) {\n contentMutated = true;\n return;\n }\n break;\n }\n });\n\n /** call once */\n if (contentMutated) {\n this.mutationDebouncer();\n }\n }\n\n /**\n * Gets native inputs and set oninput event handler\n */\n private updateNativeInputs(): void {\n if (this.nativeInputs) {\n this.nativeInputs.forEach((input) => {\n this.Editor.Listeners.off(input, 'input');\n });\n }\n\n this.nativeInputs = Array.from(this.Editor.UI.nodes.redactor.querySelectorAll('textarea, input, select'));\n\n this.nativeInputs.forEach((input) => this.Editor.Listeners.on(input, 'input', this.mutationDebouncer));\n }\n}\n","import Module from '../__module';\n\n/**\n * Use external package module for notifications\n *\n * @see https://github.com/codex-team/js-notifier\n */\nimport notifier, {ConfirmNotifierOptions, NotifierOptions, PromptNotifierOptions} from 'codex-notifier';\n\n/**\n * Notifier module\n */\nexport default class Notifier extends Module {\n\n /**\n * Show web notification\n *\n * @param {NotifierOptions | ConfirmNotifierOptions | PromptNotifierOptions} options\n */\n public show(options: NotifierOptions | ConfirmNotifierOptions | PromptNotifierOptions) {\n notifier.show(options);\n }\n}\n","import Module from '../__module';\nimport $ from '../dom';\nimport * as _ from '../utils';\nimport {\n BlockTool,\n BlockToolConstructable,\n PasteConfig,\n PasteEvent,\n PasteEventDetail,\n} from '../../../types';\nimport Block from '../block';\n\n/**\n * Tag substitute object.\n */\ninterface TagSubstitute {\n /**\n * Name of related Tool\n * @type {string}\n */\n tool: string;\n}\n\n/**\n * Pattern substitute object.\n */\ninterface PatternSubstitute {\n /**\n * Pattern`s key\n * @type {string}\n */\n key: string;\n\n /**\n * Pattern regexp\n * @type {RegExp}\n */\n pattern: RegExp;\n\n /**\n * Name of related Tool\n * @type {string}\n */\n tool: string;\n}\n\n/**\n * Files` types substitutions object.\n */\ninterface FilesSubstitution {\n /**\n * Array of file extensions Tool can handle\n * @type {string[]}\n */\n extensions: string[];\n\n /**\n * Array of MIME types Tool can handle\n * @type {string[]}\n */\n mimeTypes: string[];\n}\n\n/**\n * Processed paste data object.\n */\ninterface PasteData {\n /**\n * Name of related Tool\n * @type {string}\n */\n tool: string;\n\n /**\n * Pasted data. Processed and wrapped to HTML element\n * @type {HTMLElement}\n */\n content: HTMLElement;\n\n /**\n * Pasted data\n */\n event: PasteEvent;\n\n /**\n * True if content should be inserted as new Block\n * @type {boolean}\n */\n isBlock: boolean;\n}\n\n/**\n * @class Paste\n * @classdesc Contains methods to handle paste on editor\n *\n * @module Paste\n *\n * @version 2.0.0\n */\nexport default class Paste extends Module {\n\n /** If string`s length is greater than this number we don't check paste patterns */\n public static readonly PATTERN_PROCESSING_MAX_LENGTH = 450;\n\n /**\n * Tags` substitutions parameters\n */\n private toolsTags: {[tag: string]: TagSubstitute} = {};\n\n /**\n * Store tags to substitute by tool name\n */\n private tagsByTool: {[tools: string]: string[]} = {};\n\n /** Patterns` substitutions parameters */\n private toolsPatterns: PatternSubstitute[] = [];\n\n /** Files` substitutions parameters */\n private toolsFiles: {\n [tool: string]: FilesSubstitution,\n } = {};\n\n /**\n * List of tools which do not need a paste handling\n */\n private exceptionList: string[] = [];\n\n /**\n * Set onPaste callback and collect tools` paste configurations\n *\n * @public\n */\n public async prepare(): Promise {\n this.setCallback();\n this.processTools();\n }\n\n /**\n * Handle pasted or dropped data transfer object\n *\n * @param {DataTransfer} dataTransfer - pasted or dropped data transfer object\n * @param {boolean} isDragNDrop\n */\n public async processDataTransfer(dataTransfer: DataTransfer, isDragNDrop = false): Promise {\n const { Sanitizer } = this.Editor;\n\n const types = dataTransfer.types;\n\n /**\n * In Microsoft Edge types is DOMStringList. So 'contains' is used to check if 'Files' type included\n */\n const includesFiles = types.includes ? types.includes('Files') : (types as any).contains('Files');\n\n if (includesFiles) {\n await this.processFiles(dataTransfer.files);\n return;\n }\n\n const plainData = dataTransfer.getData('text/plain');\n let htmlData = dataTransfer.getData('text/html');\n\n /**\n * If text was drag'n'dropped, wrap content with P tag to insert it as the new Block\n */\n if (isDragNDrop && plainData.trim() && htmlData.trim()) {\n htmlData = '' + ( htmlData.trim() ? htmlData : plainData ) + '
';\n }\n\n /** Add all tags that can be substituted to sanitizer configuration */\n const toolsTags = Object.keys(this.toolsTags).reduce((result, tag) => {\n result[tag.toLowerCase()] = true;\n\n return result;\n }, {});\n\n const customConfig = Object.assign({}, toolsTags, Sanitizer.getAllInlineToolsConfig(), {br: {}});\n\n const cleanData = Sanitizer.clean(htmlData, customConfig);\n\n /** If there is no HTML or HTML string is equal to plain one, process it as plain text */\n if (!cleanData.trim() || cleanData.trim() === plainData || !$.isHTMLString(cleanData)) {\n await this.processText(plainData);\n } else {\n await this.processText(cleanData, true);\n }\n }\n\n /**\n * Process pasted text and divide them into Blocks\n *\n * @param {string} data - text to process. Can be HTML or plain.\n * @param {boolean} isHTML - if passed string is HTML, this parameter should be true\n */\n public async processText(data: string, isHTML: boolean = false) {\n const {Caret, BlockManager, Tools} = this.Editor;\n const dataToInsert = isHTML ? this.processHTML(data) : this.processPlain(data);\n\n if (!dataToInsert.length) {\n return;\n }\n\n if (dataToInsert.length === 1) {\n if (!dataToInsert[0].isBlock) {\n this.processInlinePaste(dataToInsert.pop());\n } else {\n this.processSingleBlock(dataToInsert.pop());\n }\n return;\n }\n\n const isCurrentBlockInitial = BlockManager.currentBlock && Tools.isInitial(BlockManager.currentBlock.tool);\n const needToReplaceCurrentBlock = isCurrentBlockInitial && BlockManager.currentBlock.isEmpty;\n\n await Promise.all(dataToInsert.map(\n async (content, i) => await this.insertBlock(content, i === 0 && needToReplaceCurrentBlock),\n ));\n\n if (BlockManager.currentBlock) {\n Caret.setToBlock(BlockManager.currentBlock, Caret.positions.END);\n }\n }\n\n /**\n * Set onPaste callback handler\n */\n private setCallback(): void {\n const {Listeners} = this.Editor;\n\n Listeners.on(document, 'paste', this.handlePasteEvent);\n }\n\n /**\n * Get and process tool`s paste configs\n */\n private processTools(): void {\n const tools = this.Editor.Tools.blockTools;\n\n Object.entries(tools).forEach(this.processTool);\n }\n\n /**\n * Process paste config for each tool\n *\n * @param {string} name\n * @param {Tool} tool\n */\n private processTool = ([name, tool]: [string, BlockToolConstructable]): void => {\n try {\n const toolInstance = new this.Editor.Tools.blockTools[name]({\n api: this.Editor.API.methods,\n config: {},\n data: {},\n }) as BlockTool;\n\n if (tool.pasteConfig === false) {\n this.exceptionList.push(name);\n return;\n }\n\n if (typeof toolInstance.onPaste !== 'function') {\n return;\n }\n\n const toolPasteConfig = tool.pasteConfig || {};\n\n this.getTagsConfig(name, toolPasteConfig);\n this.getFilesConfig(name, toolPasteConfig);\n this.getPatternsConfig(name, toolPasteConfig);\n } catch (e) {\n _.log(\n `Paste handling for «${name}» Tool hasn't been set up because of the error`,\n 'warn',\n e,\n );\n }\n }\n\n /**\n * Get tags to substitute by Tool\n *\n * @param {string} name - Tool name\n * @param {PasteConfig} toolPasteConfig - Tool onPaste configuration\n */\n private getTagsConfig(name: string, toolPasteConfig: PasteConfig): void {\n const tags = toolPasteConfig.tags || [];\n\n tags.forEach((tag) => {\n if (this.toolsTags.hasOwnProperty(tag)) {\n _.log(\n `Paste handler for «${name}» Tool on «${tag}» tag is skipped ` +\n `because it is already used by «${this.toolsTags[tag].tool}» Tool.`,\n 'warn',\n );\n return;\n }\n\n this.toolsTags[tag.toUpperCase()] = {\n tool: name,\n };\n });\n\n this.tagsByTool[name] = tags.map((t) => t.toUpperCase());\n }\n\n /**\n * Get files` types and extensions to substitute by Tool\n *\n * @param {string} name - Tool name\n * @param {PasteConfig} toolPasteConfig - Tool onPaste configuration\n */\n private getFilesConfig(name: string, toolPasteConfig: PasteConfig): void {\n\n const {files = {}} = toolPasteConfig;\n let {extensions, mimeTypes} = files;\n\n if (!extensions && !mimeTypes) {\n return;\n }\n\n if (extensions && !Array.isArray(extensions)) {\n _.log(`«extensions» property of the onDrop config for «${name}» Tool should be an array`);\n extensions = [];\n }\n\n if (mimeTypes && !Array.isArray(mimeTypes)) {\n _.log(`«mimeTypes» property of the onDrop config for «${name}» Tool should be an array`);\n mimeTypes = [];\n }\n\n if (mimeTypes) {\n mimeTypes = mimeTypes.filter((type) => {\n if (!_.isValidMimeType(type)) {\n _.log(`MIME type value «${type}» for the «${name}» Tool is not a valid MIME type`, 'warn');\n return false;\n }\n\n return true;\n });\n }\n\n this.toolsFiles[name] = {\n extensions: extensions || [],\n mimeTypes: mimeTypes || [],\n };\n }\n\n /**\n * Get RegExp patterns to substitute by Tool\n *\n * @param {string} name - Tool name\n * @param {PasteConfig} toolPasteConfig - Tool onPaste configuration\n */\n private getPatternsConfig(name: string, toolPasteConfig: PasteConfig): void {\n if (!toolPasteConfig.patterns || _.isEmpty(toolPasteConfig.patterns)) {\n return;\n }\n\n Object.entries(toolPasteConfig.patterns).forEach(([key, pattern]: [string, RegExp]) => {\n /** Still need to validate pattern as it provided by user */\n if (!(pattern instanceof RegExp)) {\n _.log(\n `Pattern ${pattern} for «${name}» Tool is skipped because it should be a Regexp instance.`,\n 'warn',\n );\n }\n\n this.toolsPatterns.push({\n key,\n pattern,\n tool: name,\n });\n });\n }\n\n /**\n * Check if browser behavior suits better\n *\n * @param {EventTarget} element - element where content has been pasted\n * @returns {boolean}\n */\n private isNativeBehaviour(element: EventTarget): boolean {\n return $.isNativeInput(element);\n }\n\n /**\n * Check if Editor should process pasted data and pass data transfer object to handler\n *\n * @param {ClipboardEvent} event\n */\n private handlePasteEvent = async (event: ClipboardEvent): Promise => {\n const {BlockManager, Toolbar} = this.Editor;\n\n /** If target is native input or is not Block, use browser behaviour */\n if (\n !BlockManager.currentBlock ||\n this.isNativeBehaviour(event.target) && !event.clipboardData.types.includes('Files')\n ) {\n return;\n }\n\n /**\n * If Tools is in list of exceptions, skip processing of paste event\n */\n if (BlockManager.currentBlock && this.exceptionList.includes(BlockManager.currentBlock.name)) {\n return;\n }\n\n event.preventDefault();\n this.processDataTransfer(event.clipboardData);\n\n BlockManager.clearFocused();\n Toolbar.close();\n }\n\n /**\n * Get files from data transfer object and insert related Tools\n *\n * @param {FileList} items - pasted or dropped items\n */\n private async processFiles(items: FileList) {\n const {BlockManager, Tools} = this.Editor;\n\n let dataToInsert: Array<{type: string, event: PasteEvent}>;\n\n dataToInsert = await Promise.all(\n Array\n .from(items)\n .map((item) => this.processFile(item)),\n );\n dataToInsert = dataToInsert.filter((data) => !!data);\n\n const isCurrentBlockInitial = Tools.isInitial(BlockManager.currentBlock.tool);\n const needToReplaceCurrentBlock = isCurrentBlockInitial && BlockManager.currentBlock.isEmpty;\n\n dataToInsert.forEach(\n (data, i) => {\n BlockManager.paste(data.type, data.event, i === 0 && needToReplaceCurrentBlock);\n },\n );\n }\n\n /**\n * Get information about file and find Tool to handle it\n *\n * @param {File} file\n */\n private async processFile(file: File) {\n const extension = _.getFileExtension(file);\n\n const foundConfig = Object\n .entries(this.toolsFiles)\n .find(([toolName, {mimeTypes, extensions}]) => {\n const [fileType, fileSubtype] = file.type.split('/');\n\n const foundExt = extensions.find((ext) => ext.toLowerCase() === extension.toLowerCase());\n const foundMimeType = mimeTypes.find((mime) => {\n const [type, subtype] = mime.split('/');\n\n return type === fileType && (subtype === fileSubtype || subtype === '*');\n });\n\n return !!foundExt || !!foundMimeType;\n });\n\n if (!foundConfig) {\n return;\n }\n\n const [tool] = foundConfig;\n const pasteEvent = this.composePasteEvent('file', {\n file,\n });\n\n return {\n event: pasteEvent,\n type: tool,\n };\n }\n\n /**\n * Split HTML string to blocks and return it as array of Block data\n *\n * @param {string} innerHTML\n * @returns {PasteData[]}\n */\n private processHTML(innerHTML: string): PasteData[] {\n const {Tools, Sanitizer} = this.Editor;\n const initialTool = this.config.initialBlock;\n const wrapper = $.make('DIV');\n\n wrapper.innerHTML = innerHTML;\n\n const nodes = this.getNodes(wrapper);\n\n return nodes\n .map((node) => {\n let content, tool = initialTool, isBlock = false;\n\n switch (node.nodeType) {\n /** If node is a document fragment, use temp wrapper to get innerHTML */\n case Node.DOCUMENT_FRAGMENT_NODE:\n content = $.make('div');\n content.appendChild(node);\n break;\n\n /** If node is an element, then there might be a substitution */\n case Node.ELEMENT_NODE:\n content = node as HTMLElement;\n isBlock = true;\n\n if (this.toolsTags[content.tagName]) {\n tool = this.toolsTags[content.tagName].tool;\n }\n break;\n }\n\n const {tags} = Tools.blockTools[tool].pasteConfig as PasteConfig;\n\n const toolTags = tags.reduce((result, tag) => {\n result[tag.toLowerCase()] = {};\n\n return result;\n }, {});\n const customConfig = Object.assign({}, toolTags, Sanitizer.getInlineToolsConfig(tool));\n\n content.innerHTML = Sanitizer.clean(content.innerHTML, customConfig);\n\n const event = this.composePasteEvent('tag', {\n data: content,\n });\n\n return {content, isBlock, tool, event};\n })\n .filter((data) => !$.isNodeEmpty(data.content) || $.isSingleTag(data.content));\n }\n\n /**\n * Split plain text by new line symbols and return it as array of Block data\n *\n * @param {string} plain\n * @returns {PasteData[]}\n */\n private processPlain(plain: string): PasteData[] {\n const {initialBlock} = this.config as {initialBlock: string},\n {Tools} = this.Editor;\n\n if (!plain) {\n return [];\n }\n\n const tool = initialBlock;\n\n return plain\n .split(/\\r?\\n/)\n .filter((text) => text.trim())\n .map((text) => {\n const content = $.make('div');\n\n content.innerHTML = text;\n\n const event = this.composePasteEvent('tag', {\n data: content,\n });\n\n return {content, tool, isBlock: false, event};\n });\n }\n\n /**\n * Process paste of single Block tool content\n *\n * @param {PasteData} dataToInsert\n */\n private async processSingleBlock(dataToInsert: PasteData): Promise {\n const {Caret, BlockManager, Tools} = this.Editor;\n const {currentBlock} = BlockManager;\n\n /**\n * If pasted tool isn`t equal current Block or if pasted content contains block elements, insert it as new Block\n */\n if (\n !currentBlock ||\n dataToInsert.tool !== currentBlock.name ||\n !$.containsOnlyInlineElements(dataToInsert.content.innerHTML)\n ) {\n this.insertBlock(dataToInsert, currentBlock && Tools.isInitial(currentBlock.tool) && currentBlock.isEmpty);\n return;\n }\n\n Caret.insertContentAtCaretPosition(dataToInsert.content.innerHTML);\n }\n\n /**\n * Process paste to single Block:\n * 1. Find patterns` matches\n * 2. Insert new block if it is not the same type as current one\n * 3. Just insert text if there is no substitutions\n *\n * @param {PasteData} dataToInsert\n */\n private async processInlinePaste(dataToInsert: PasteData): Promise {\n const {BlockManager, Caret, Sanitizer, Tools} = this.Editor;\n const {content, tool} = dataToInsert;\n\n const currentBlockIsInitial = BlockManager.currentBlock && Tools.isInitial(BlockManager.currentBlock.tool);\n\n if (currentBlockIsInitial && content.textContent.length < Paste.PATTERN_PROCESSING_MAX_LENGTH) {\n const blockData = await this.processPattern(content.textContent);\n\n if (blockData) {\n let insertedBlock;\n\n const needToReplaceCurrentBlock = BlockManager.currentBlock\n && Tools.isInitial(BlockManager.currentBlock.tool)\n && BlockManager.currentBlock.isEmpty;\n\n insertedBlock = BlockManager.paste(blockData.tool, blockData.event, needToReplaceCurrentBlock);\n\n Caret.setToBlock(insertedBlock, Caret.positions.END);\n return;\n }\n }\n\n /** If there is no pattern substitute - insert string as it is */\n if (BlockManager.currentBlock && BlockManager.currentBlock.currentInput) {\n const currentToolSanitizeConfig = Sanitizer.getInlineToolsConfig(BlockManager.currentBlock.name);\n\n document.execCommand('insertHTML', false, Sanitizer.clean(content.innerHTML, currentToolSanitizeConfig));\n } else {\n this.insertBlock(dataToInsert);\n }\n }\n\n /**\n * Get patterns` matches\n *\n * @param {string} text\n * @returns Promise<{data: BlockToolData, tool: string}>\n */\n private async processPattern(text: string): Promise<{event: PasteEvent, tool: string}> {\n const pattern = this.toolsPatterns.find((substitute) => {\n const execResult = substitute.pattern.exec(text);\n\n if (!execResult) {\n return false;\n }\n\n return text === execResult.shift();\n });\n\n if (!pattern) {\n return;\n }\n\n const event = this.composePasteEvent('pattern', {\n key: pattern.key,\n data: text,\n });\n\n return {\n event,\n tool: pattern.tool,\n };\n }\n\n /**\n *\n * @param {PasteData} data\n * @param {Boolean} canReplaceCurrentBlock - if true and is current Block is empty, will replace current Block\n * @returns {Promise}\n */\n private async insertBlock(data: PasteData, canReplaceCurrentBlock: boolean = false): Promise {\n const {BlockManager, Caret} = this.Editor;\n const {currentBlock} = BlockManager;\n let block: Block;\n\n if (canReplaceCurrentBlock && currentBlock && currentBlock.isEmpty) {\n block = BlockManager.paste(data.tool, data.event, true);\n Caret.setToBlock(block, Caret.positions.END);\n return;\n }\n\n block = BlockManager.paste(data.tool, data.event);\n\n Caret.setToBlock(block, Caret.positions.END);\n }\n\n /**\n * Recursively divide HTML string to two types of nodes:\n * 1. Block element\n * 2. Document Fragments contained text and markup tags like a, b, i etc.\n *\n * @param {Node} wrapper\n * @returns {Node[]}\n */\n private getNodes(wrapper: Node): Node[] {\n const children = Array.from(wrapper.childNodes),\n tags = Object.keys(this.toolsTags);\n\n const reducer = (nodes: Node[], node: Node): Node[] => {\n if ($.isEmpty(node) && !$.isSingleTag(node as HTMLElement)) {\n return nodes;\n }\n\n const lastNode = nodes[nodes.length - 1];\n\n let destNode: Node = new DocumentFragment();\n\n if (lastNode && $.isFragment(lastNode)) {\n destNode = nodes.pop();\n }\n\n switch (node.nodeType) {\n /**\n * If node is HTML element:\n * 1. Check if it is inline element\n * 2. Check if it contains another block or substitutable elements\n */\n case Node.ELEMENT_NODE:\n const element = node as HTMLElement;\n\n if (element.tagName === 'BR') {\n return [...nodes, destNode, new DocumentFragment()];\n }\n\n const {tool = ''} = this.toolsTags[element.tagName] || {};\n const toolTags = this.tagsByTool[tool] || [];\n\n const isSubstitutable = tags.includes(element.tagName);\n const isBlockElement = $.blockElements.includes(element.tagName.toLowerCase());\n const containsAnotherToolTags = Array\n .from(element.children)\n .some(\n ({tagName}) => tags.includes(tagName) && !toolTags.includes(tagName),\n );\n\n const containsBlockElements = Array.from(element.children).some(\n ({tagName}) => $.blockElements.includes(tagName.toLowerCase()),\n );\n\n /** Append inline elements to previous fragment */\n if (!isBlockElement && !isSubstitutable && !containsAnotherToolTags) {\n destNode.appendChild(element);\n return [...nodes, destNode];\n }\n\n if (\n (isSubstitutable && !containsAnotherToolTags) ||\n (isBlockElement && !containsBlockElements && !containsAnotherToolTags )\n ) {\n return [...nodes, destNode, element];\n }\n break;\n\n /**\n * If node is text node, wrap it with DocumentFragment\n */\n case Node.TEXT_NODE:\n destNode.appendChild(node);\n return [...nodes, destNode];\n\n default:\n return [...nodes, destNode];\n }\n\n return [...nodes, ...Array.from(node.childNodes).reduce(reducer, [])];\n };\n\n return children.reduce(reducer, []);\n }\n\n /**\n * Compose paste event with passed type and detail\n *\n * @param {string} type\n * @param {PasteEventDetail} detail\n */\n private composePasteEvent(type: string, detail: PasteEventDetail): PasteEvent {\n return new CustomEvent(type, {\n detail,\n }) as PasteEvent;\n }\n}\n","/**\n * @class RectangleSelection\n * @classdesc Manages Block selection with mouse\n *\n * @module RectangleSelection\n * @version 1.0.0\n */\nimport Module from '../__module';\nimport $ from '../dom';\n\nimport SelectionUtils from '../selection';\nimport Block from '../block';\n\nexport default class RectangleSelection extends Module {\n /**\n * CSS classes for the Block\n * @return {{wrapper: string, content: string}}\n */\n static get CSS() {\n return {\n overlay: 'codex-editor-overlay',\n overlayContainer: 'codex-editor-overlay__container',\n rect: 'codex-editor-overlay__rectangle',\n topScrollZone: 'codex-editor-overlay__scroll-zone--top',\n bottomScrollZone: 'codex-editor-overlay__scroll-zone--bottom',\n };\n }\n\n /**\n * Using the selection rectangle\n * @type {boolean}\n */\n private isRectSelectionActivated: boolean = false;\n\n /**\n * Speed of Scrolling\n */\n private readonly SCROLL_SPEED: number = 3;\n\n /**\n * Height of scroll zone on boundary of screen\n */\n private readonly HEIGHT_OF_SCROLL_ZONE = 40;\n\n /**\n * Scroll zone type indicators\n */\n private readonly BOTTOM_SCROLL_ZONE = 1;\n private readonly TOP_SCROLL_ZONE = 2;\n\n /**\n * Id of main button for event.button\n */\n private readonly MAIN_MOUSE_BUTTON = 0;\n\n /**\n * Mouse is clamped\n */\n private mousedown: boolean = false;\n\n /**\n * Is scrolling now\n */\n private isScrolling: boolean = false;\n\n /**\n * Mouse is in scroll zone\n */\n private inScrollZone: number | null = null;\n\n /**\n * Coords of rect\n */\n private startX: number = 0;\n private startY: number = 0;\n private mouseX: number = 0;\n private mouseY: number = 0;\n\n /**\n * Selected blocks\n */\n private stackOfSelected: number[] = [];\n\n /**\n * Does the rectangle intersect blocks\n */\n private rectCrossesBlocks: boolean;\n\n /**\n * Selection rectangle\n */\n private overlayRectangle: HTMLDivElement;\n\n /**\n * Module Preparation\n * Creating rect and hang handlers\n */\n public prepare(): void {\n const {Listeners} = this.Editor;\n const {container} = this.genHTML();\n\n Listeners.on(container, 'mousedown', (event: MouseEvent) => {\n if (event.button !== this.MAIN_MOUSE_BUTTON) {\n return;\n }\n this.startSelection(event.pageX, event.pageY);\n }, false);\n\n Listeners.on(document.body, 'mousemove', (event: MouseEvent) => {\n this.changingRectangle(event);\n this.scrollByZones(event.clientY);\n }, false);\n\n Listeners.on(document.body, 'mouseleave', () => {\n this.clearSelection();\n this.endSelection();\n });\n\n Listeners.on(window, 'scroll', (event) => {\n this.changingRectangle(event);\n }, false);\n\n Listeners.on(document.body, 'mouseup', () => {\n this.endSelection();\n }, false);\n }\n\n /**\n * Init rect params\n * @param {number} pageX - X coord of mouse\n * @param {number} pageY - Y coord of mouse\n */\n public startSelection(pageX, pageY) {\n this.Editor.BlockSelection.allBlocksSelected = false;\n this.clearSelection();\n this.stackOfSelected = [];\n\n const elemWhereSelectionStart = document.elementFromPoint(pageX - window.pageXOffset, pageY - window.pageYOffset);\n\n const selectorsToAvoid = [\n `.${Block.CSS.content}`,\n `.${this.Editor.Toolbar.CSS.toolbar}`,\n `.${this.Editor.InlineToolbar.CSS.inlineToolbar}`,\n ];\n\n const startsInsideEditor = elemWhereSelectionStart.closest('.' + this.Editor.UI.CSS.editorWrapper);\n const startsInSelectorToAvoid = selectorsToAvoid.some(((selector) => !!elemWhereSelectionStart.closest(selector)));\n\n /**\n * If selection starts outside of the editor or inside the blocks or on Editor UI elements, do not handle it\n */\n if (!startsInsideEditor || startsInSelectorToAvoid) {\n return;\n }\n\n this.mousedown = true;\n this.startX = pageX;\n this.startY = pageY;\n }\n\n /**\n * Clear all params to end selection\n */\n public endSelection() {\n this.mousedown = false;\n this.startX = 0;\n this.startY = 0;\n this.overlayRectangle.style.display = 'none';\n }\n\n /**\n * is RectSelection Activated\n */\n public isRectActivated() {\n return this.isRectSelectionActivated;\n }\n\n /**\n * Mark that selection is end\n */\n public clearSelection() {\n this.isRectSelectionActivated = false;\n }\n\n /**\n * Scroll If mouse in scroll zone\n * @param {number} clientY - Y coord of mouse\n */\n private scrollByZones(clientY) {\n this.inScrollZone = null;\n if (clientY <= this.HEIGHT_OF_SCROLL_ZONE) {\n this.inScrollZone = this.TOP_SCROLL_ZONE;\n }\n if (document.documentElement.clientHeight - clientY <= this.HEIGHT_OF_SCROLL_ZONE) {\n this.inScrollZone = this.BOTTOM_SCROLL_ZONE;\n }\n\n if (!this.inScrollZone) {\n this.isScrolling = false;\n return;\n }\n\n if (!this.isScrolling) {\n this.scrollVertical(this.inScrollZone === this.TOP_SCROLL_ZONE ? -this.SCROLL_SPEED : this.SCROLL_SPEED);\n this.isScrolling = true;\n }\n }\n\n private genHTML() {\n const {UI} = this.Editor;\n\n const container = UI.nodes.holder.querySelector('.' + UI.CSS.editorWrapper);\n const overlay = $.make('div', RectangleSelection.CSS.overlay, {});\n const overlayContainer = $.make('div', RectangleSelection.CSS.overlayContainer, {});\n const overlayRectangle = $.make('div', RectangleSelection.CSS.rect, {});\n\n overlayContainer.appendChild(overlayRectangle);\n overlay.appendChild(overlayContainer);\n container.appendChild(overlay);\n\n this.overlayRectangle = overlayRectangle as HTMLDivElement;\n return {\n container,\n overlay,\n };\n }\n\n /**\n * Activates scrolling if blockSelection is active and mouse is in scroll zone\n * @param {number} speed - speed of scrolling\n */\n private scrollVertical(speed) {\n if (!(this.inScrollZone && this.mousedown)) {\n return;\n }\n const lastOffset = window.pageYOffset;\n window.scrollBy(0, speed);\n this.mouseY += window.pageYOffset - lastOffset;\n setTimeout(() => {\n this.scrollVertical(speed);\n }, 0);\n }\n\n /**\n * Handles the change in the rectangle and its effect\n * @param {MouseEvent} event\n */\n private changingRectangle(event) {\n if (!this.mousedown) {\n return;\n }\n\n if (event.pageY !== undefined) {\n this.mouseX = event.pageX;\n this.mouseY = event.pageY;\n }\n\n const {rightPos, leftPos, index} = this.genInfoForMouseSelection();\n // There is not new block in selection\n\n const rectIsOnRighSideOfredactor = this.startX > rightPos && this.mouseX > rightPos;\n const rectISOnLeftSideOfRedactor = this.startX < leftPos && this.mouseX < leftPos;\n this.rectCrossesBlocks = !(rectIsOnRighSideOfredactor || rectISOnLeftSideOfRedactor);\n\n if (!this.isRectSelectionActivated) {\n this.rectCrossesBlocks = false;\n this.isRectSelectionActivated = true;\n this.shrinkRectangleToPoint();\n this.overlayRectangle.style.display = 'block';\n }\n\n this.updateRectangleSize();\n\n if (index === undefined) {\n return;\n }\n\n this.trySelectNextBlock(index);\n // For case, when rect is out from blocks\n this.inverseSelection();\n\n SelectionUtils.get().removeAllRanges();\n event.preventDefault();\n }\n\n /**\n * Shrink rect to singular point\n */\n private shrinkRectangleToPoint() {\n this.overlayRectangle.style.left = `${this.startX - window.pageXOffset}px`;\n this.overlayRectangle.style.top = `${this.startY - window.pageYOffset}px`;\n this.overlayRectangle.style.bottom = `calc(100% - ${this.startY - window.pageYOffset}px`;\n this.overlayRectangle.style.right = `calc(100% - ${this.startX - window.pageXOffset}px`;\n }\n\n /**\n * Select or unselect all of blocks in array if rect is out or in selectable area\n */\n private inverseSelection() {\n const firstBlockInStack = this.Editor.BlockManager.getBlockByIndex(this.stackOfSelected[0]);\n const isSelecteMode = firstBlockInStack.selected;\n\n if (this.rectCrossesBlocks && !isSelecteMode) {\n for (const it of this.stackOfSelected) {\n this.Editor.BlockSelection.selectBlockByIndex(it);\n }\n }\n\n if (!this.rectCrossesBlocks && isSelecteMode) {\n for (const it of this.stackOfSelected) {\n this.Editor.BlockSelection.unSelectBlockByIndex(it);\n }\n }\n }\n\n /**\n * Updates size of rectangle\n */\n private updateRectangleSize() {\n // Depending on the position of the mouse relative to the starting point,\n // change this.e distance from the desired edge of the screen*/\n if (this.mouseY >= this.startY) {\n this.overlayRectangle.style.top = `${this.startY - window.pageYOffset}px`;\n this.overlayRectangle.style.bottom = `calc(100% - ${this.mouseY - window.pageYOffset}px`;\n } else {\n this.overlayRectangle.style.bottom = `calc(100% - ${this.startY - window.pageYOffset}px`;\n this.overlayRectangle.style.top = `${this.mouseY - window.pageYOffset}px`;\n }\n\n if (this.mouseX >= this.startX) {\n this.overlayRectangle.style.left = `${this.startX - window.pageXOffset}px`;\n this.overlayRectangle.style.right = `calc(100% - ${this.mouseX - window.pageXOffset}px`;\n } else {\n this.overlayRectangle.style.right = `calc(100% - ${this.startX - window.pageXOffset}px`;\n this.overlayRectangle.style.left = `${this.mouseX - window.pageXOffset}px`;\n }\n }\n\n /**\n * Collects information needed to determine the behavior of the rectangle\n * @return {number} index - index next Block, leftPos - start of left border of Block, rightPos - right border\n */\n private genInfoForMouseSelection() {\n const widthOfRedactor = document.body.offsetWidth;\n const centerOfRedactor = widthOfRedactor / 2;\n const Y = this.mouseY - window.pageYOffset;\n const elementUnderMouse = document.elementFromPoint(centerOfRedactor, Y);\n const blockInCurrentPos = this.Editor.BlockManager.getBlockByChildNode(elementUnderMouse);\n let index;\n if (blockInCurrentPos !== undefined) {\n index = this.Editor.BlockManager.blocks.findIndex((block) => block.holder === blockInCurrentPos.holder);\n }\n const contentElement = this.Editor.BlockManager.lastBlock.holder.querySelector('.' + Block.CSS.content);\n const centerOfBlock = Number.parseInt(window.getComputedStyle(contentElement).width, 10) / 2;\n const leftPos = centerOfRedactor - centerOfBlock;\n const rightPos = centerOfRedactor + centerOfBlock;\n\n return {\n index,\n leftPos,\n rightPos,\n };\n }\n\n /**\n * Select block with index index\n * @param index - index of block in redactor\n */\n private addBlockInSelection(index) {\n if (this.rectCrossesBlocks) {\n this.Editor.BlockSelection.selectBlockByIndex(index);\n }\n this.stackOfSelected.push(index);\n }\n\n /**\n * Adds a block to the selection and determines which blocks should be selected\n * @param {object} index - index of new block in the reactor\n */\n private trySelectNextBlock(index) {\n const sameBlock = this.stackOfSelected[this.stackOfSelected.length - 1] === index;\n const sizeStack = this.stackOfSelected.length;\n const down = 1, up = -1, undef = 0;\n\n if (sameBlock) {\n return;\n }\n\n const blockNumbersIncrease = this.stackOfSelected[sizeStack - 1] - this.stackOfSelected[sizeStack - 2] > 0;\n const direction = sizeStack <= 1 ? undef : blockNumbersIncrease ? down : up;\n const selectionInDownDurection = index > this.stackOfSelected[sizeStack - 1] && direction === down;\n const selectionInUpDirection = index < this.stackOfSelected[sizeStack - 1] && direction === up;\n const generalSelection = selectionInDownDurection || selectionInUpDirection || direction === undef;\n const reduction = !generalSelection;\n\n // When the selection is too fast, some blocks do not have time to be noticed. Fix it.\n if (!reduction && (index > this.stackOfSelected[sizeStack - 1] ||\n this.stackOfSelected[sizeStack - 1] === undefined)) {\n let ind = this.stackOfSelected[sizeStack - 1] + 1 || index;\n\n for (ind; ind <= index; ind++) {\n this.addBlockInSelection(ind);\n }\n return;\n }\n\n // for both directions\n if (!reduction && (index < this.stackOfSelected[sizeStack - 1])) {\n for (let ind = this.stackOfSelected[sizeStack - 1] - 1; ind >= index; ind--) {\n this.addBlockInSelection(ind);\n }\n return;\n }\n\n if (!reduction) {\n return;\n }\n\n let i = sizeStack - 1;\n let cmp;\n\n // cmp for different directions\n if (index > this.stackOfSelected[sizeStack - 1]) {\n cmp = () => index > this.stackOfSelected[i];\n } else {\n cmp = () => index < this.stackOfSelected[i];\n }\n\n // Remove blocks missed due to speed.\n // cmp checks if we have removed all the necessary blocks\n while (cmp()) {\n if (this.rectCrossesBlocks) {\n this.Editor.BlockSelection.unSelectBlockByIndex(this.stackOfSelected[i]);\n }\n this.stackOfSelected.pop();\n i--;\n }\n return;\n }\n}\n","import Module from '../__module';\nimport * as _ from '../utils';\nimport {ChainData} from '../utils';\nimport {BlockToolData} from '../../../types';\nimport {BlockToolConstructable} from '../../../types/tools';\n\n/**\n * Editor.js Renderer Module\n *\n * @module Renderer\n * @author CodeX Team\n *\n * @version 2.0.0\n */\nexport default class Renderer extends Module {\n /**\n * @typedef {Object} RendererBlocks\n * @property {String} type - tool name\n * @property {Object} data - tool data\n */\n\n /**\n * @example\n *\n * blocks: [\n * {\n * type : 'paragraph',\n * data : {\n * text : 'Hello from Codex!'\n * }\n * },\n * {\n * type : 'paragraph',\n * data : {\n * text : 'Leave feedback if you like it!'\n * }\n * },\n * ]\n *\n */\n\n /**\n * Make plugin blocks from array of plugin`s data\n * @param {RendererBlocks[]} blocks\n */\n public async render(blocks: BlockToolData[]): Promise {\n const chainData = blocks.map((block) => ({function: () => this.insertBlock(block)}));\n\n const sequence = await _.sequence(chainData as ChainData[]);\n\n this.Editor.UI.checkEmptiness();\n\n return sequence;\n }\n\n /**\n * Get plugin instance\n * Add plugin instance to BlockManager\n * Insert block to working zone\n *\n * @param {Object} item\n * @returns {Promise}\n * @private\n */\n public async insertBlock(item): Promise {\n const { Tools, BlockManager } = this.Editor;\n const tool = item.type;\n const data = item.data;\n const settings = item.settings;\n\n if (tool in Tools.available) {\n try {\n BlockManager.insert(tool, data, settings);\n } catch (error) {\n _.log(`Block «${tool}» skipped because of plugins error`, 'warn', data);\n throw Error(error);\n }\n } else {\n\n /** If Tool is unavailable, create stub Block for it */\n const stubData = {\n savedData: {\n type: tool,\n data,\n },\n title: tool,\n };\n\n if (tool in Tools.unavailable) {\n const toolToolboxSettings = (Tools.unavailable[tool] as BlockToolConstructable).toolbox;\n const userToolboxSettings = Tools.getToolSettings(tool).toolbox;\n\n stubData.title = toolToolboxSettings.title || userToolboxSettings.title || stubData.title;\n }\n\n const stub = BlockManager.insert(Tools.stubTool, stubData, settings);\n\n stub.stretched = true;\n\n _.log(`Tool «${tool}» is not found. Check 'tools' property at your initial Editor.js config.`, 'warn');\n }\n }\n}\n","/**\n * CodeX Sanitizer\n *\n * @module Sanitizer\n * Clears HTML from taint tags\n *\n * @version 2.0.0\n *\n * @example\n * Module can be used within two ways:\n * 1) When you have an instance\n * - this.Editor.Sanitizer.clean(yourTaintString);\n * 2) As static method\n * - EditorJS.Sanitizer.clean(yourTaintString, yourCustomConfiguration);\n *\n * {@link SanitizerConfig}\n */\n\nimport Module from '../__module';\nimport * as _ from '../utils';\n\n/**\n * @typedef {Object} SanitizerConfig\n * @property {Object} tags - define tags restrictions\n *\n * @example\n *\n * tags : {\n * p: true,\n * a: {\n * href: true,\n * rel: \"nofollow\",\n * target: \"_blank\"\n * }\n * }\n */\n\nimport HTMLJanitor from 'html-janitor';\nimport {BlockToolData, InlineToolConstructable, SanitizerConfig} from '../../../types';\n\nexport default class Sanitizer extends Module {\n /**\n * Memoize tools config\n */\n private configCache: {[toolName: string]: SanitizerConfig} = {};\n\n /**\n * Cached inline tools config\n */\n private inlineToolsConfigCache: SanitizerConfig | null = null;\n\n /**\n * Sanitize Blocks\n *\n * Enumerate blocks and clean data\n *\n * @param {{tool, data: BlockToolData}[]} blocksData[]\n */\n public sanitizeBlocks(\n blocksData: Array<{tool: string, data: BlockToolData}>,\n ): Array<{tool: string, data: BlockToolData}> {\n\n return blocksData.map((block) => {\n const toolConfig = this.composeToolConfig(block.tool);\n\n if (_.isEmpty(toolConfig)) {\n return block;\n }\n\n block.data = this.deepSanitize(block.data, toolConfig);\n\n return block;\n });\n }\n\n /**\n * Method recursively reduces Block's data and cleans with passed rules\n *\n * @param {BlockToolData|object|*} dataToSanitize - taint string or object/array that contains taint string\n * @param {SanitizerConfig} rules - object with sanitizer rules\n */\n public deepSanitize(dataToSanitize: any, rules: SanitizerConfig): any {\n /**\n * BlockData It may contain 3 types:\n * - Array\n * - Object\n * - Primitive\n */\n if (Array.isArray(dataToSanitize)) {\n /**\n * Array: call sanitize for each item\n */\n return this.cleanArray(dataToSanitize, rules);\n } else if (typeof dataToSanitize === 'object') {\n /**\n * Objects: just clean object deeper.\n */\n return this.cleanObject(dataToSanitize, rules);\n } else {\n /**\n * Primitives (number|string|boolean): clean this item\n *\n * Clean only strings\n */\n if (typeof dataToSanitize === 'string') {\n return this.cleanOneItem(dataToSanitize, rules);\n }\n return dataToSanitize;\n }\n }\n\n /**\n * Cleans string from unwanted tags\n * Method allows to use default config\n *\n * @param {string} taintString - taint string\n * @param {SanitizerConfig} customConfig - allowed tags\n *\n * @return {string} clean HTML\n */\n public clean(taintString: string, customConfig: SanitizerConfig = {} as SanitizerConfig): string {\n\n const sanitizerConfig = {\n tags: customConfig,\n };\n\n /**\n * API client can use custom config to manage sanitize process\n */\n const sanitizerInstance = this.createHTMLJanitorInstance(sanitizerConfig);\n return sanitizerInstance.clean(taintString);\n }\n\n /**\n * Merge with inline tool config\n *\n * @param {string} toolName\n * @param {SanitizerConfig} toolRules\n * @return {SanitizerConfig}\n */\n public composeToolConfig(toolName: string): SanitizerConfig {\n /**\n * If cache is empty, then compose tool config and put it to the cache object\n */\n if (this.configCache[toolName]) {\n return this.configCache[toolName];\n }\n\n const sanitizeGetter = this.Editor.Tools.INTERNAL_SETTINGS.SANITIZE_CONFIG;\n const toolClass = this.Editor.Tools.available[toolName];\n const baseConfig = this.getInlineToolsConfig(toolName);\n\n /**\n * If Tools doesn't provide sanitizer config or it is empty\n */\n if (!toolClass.sanitize || (toolClass[sanitizeGetter] && _.isEmpty(toolClass[sanitizeGetter]))) {\n return baseConfig;\n }\n\n const toolRules = toolClass.sanitize;\n\n const toolConfig = {} as SanitizerConfig;\n for (const fieldName in toolRules) {\n if (toolRules.hasOwnProperty(fieldName)) {\n const rule = toolRules[fieldName];\n if (typeof rule === 'object') {\n toolConfig[fieldName] = Object.assign({}, baseConfig, rule);\n } else {\n toolConfig[fieldName] = rule;\n }\n }\n }\n this.configCache[toolName] = toolConfig;\n\n return toolConfig;\n }\n\n /**\n * Returns Sanitizer config\n * When Tool's \"inlineToolbar\" value is True, get all sanitizer rules from all tools,\n * otherwise get only enabled\n */\n public getInlineToolsConfig(name: string): SanitizerConfig {\n const {Tools} = this.Editor;\n const toolsConfig = Tools.getToolSettings(name);\n const enableInlineTools = toolsConfig.inlineToolbar || [];\n\n let config = {} as SanitizerConfig;\n\n if (typeof enableInlineTools === 'boolean' && enableInlineTools) {\n /**\n * getting all tools sanitizer rule\n */\n config = this.getAllInlineToolsConfig();\n } else {\n /**\n * getting only enabled\n */\n (enableInlineTools as string[]).map( (inlineToolName) => {\n config = Object.assign(\n config,\n Tools.inline[inlineToolName][Tools.INTERNAL_SETTINGS.SANITIZE_CONFIG],\n ) as SanitizerConfig;\n });\n }\n\n return config;\n }\n\n /**\n * Return general config for all inline tools\n */\n public getAllInlineToolsConfig(): SanitizerConfig {\n const {Tools} = this.Editor;\n\n if (this.inlineToolsConfigCache) {\n return this.inlineToolsConfigCache;\n }\n\n const config: SanitizerConfig = {} as SanitizerConfig;\n\n Object.entries(Tools.inline)\n .forEach( ([name, inlineTool]: [string, InlineToolConstructable]) => {\n Object.assign(config, inlineTool[Tools.INTERNAL_SETTINGS.SANITIZE_CONFIG]);\n });\n\n this.inlineToolsConfigCache = config;\n\n return this.inlineToolsConfigCache;\n }\n\n /**\n * Clean array\n * @param {array} array - [1, 2, {}, []]\n * @param {object} ruleForItem\n */\n private cleanArray(array: any[], ruleForItem: SanitizerConfig): any[] {\n return array.map( (arrayItem) => this.deepSanitize(arrayItem, ruleForItem));\n }\n\n /**\n * Clean object\n * @param {object} object - {level: 0, text: 'adada', items: [1,2,3]}}\n * @param {object} rules - { b: true } or true|false\n * @return {object}\n */\n private cleanObject(object: any, rules: SanitizerConfig|{[field: string]: SanitizerConfig}): any {\n const cleanData = {};\n\n for (const fieldName in object) {\n if (!object.hasOwnProperty(fieldName)) {\n continue;\n }\n\n const currentIterationItem = object[fieldName];\n\n /**\n * Get object from config by field name\n * - if it is a HTML Janitor rule, call with this rule\n * - otherwise, call with parent's config\n */\n const ruleForItem = this.isRule(rules[fieldName] as SanitizerConfig) ? rules[fieldName] : rules;\n\n cleanData[fieldName] = this.deepSanitize(currentIterationItem, ruleForItem as SanitizerConfig);\n }\n return cleanData;\n }\n\n /**\n * @param {string} taintString\n * @param {SanitizerConfig|boolean} rule\n * @return {string}\n */\n private cleanOneItem(taintString: string, rule: SanitizerConfig|boolean): string {\n if (typeof rule === 'object') {\n return this.clean(taintString, rule);\n } else if (rule === false) {\n return this.clean(taintString, {} as SanitizerConfig);\n } else {\n return taintString;\n }\n }\n\n /**\n * Check if passed item is a HTML Janitor rule:\n * { a : true }, {}, false, true, function(){} — correct rules\n * undefined, null, 0, 1, 2 — not a rules\n * @param config\n */\n private isRule(config: SanitizerConfig): boolean {\n return typeof config === 'object' || typeof config === 'boolean' || typeof config === 'function';\n }\n\n /**\n * If developer uses editor's API, then he can customize sanitize restrictions.\n * Or, sanitizing config can be defined globally in editors initialization. That config will be used everywhere\n * At least, if there is no config overrides, that API uses Default configuration\n *\n * @uses https://www.npmjs.com/package/html-janitor\n * @license https://github.com/guardian/html-janitor/blob/master/LICENSE\n *\n * @param {SanitizerConfig} config - sanitizer extension\n */\n private createHTMLJanitorInstance(config: {tags: SanitizerConfig}): HTMLJanitor|null {\n if (config) {\n return new HTMLJanitor(config);\n }\n return null;\n }\n}\n","/**\n * Editor.js Saver\n *\n * @module Saver\n * @author Codex Team\n * @version 2.0.0\n */\nimport Module from '../__module';\nimport {OutputData} from '../../../types';\nimport {ValidatedData} from '../../types-internal/block-data';\nimport Block from '../block';\nimport * as _ from '../utils';\n\ndeclare const VERSION: string;\n\n/**\n * @classdesc This method reduces all Blocks asyncronically and calls Block's save method to extract data\n *\n * @typedef {Saver} Saver\n * @property {Element} html - Editor HTML content\n * @property {String} json - Editor JSON output\n */\nexport default class Saver extends Module {\n /**\n * Composes new chain of Promises to fire them alternatelly\n * @return {OutputData}\n */\n public async save(): Promise {\n const {BlockManager, Sanitizer, ModificationsObserver} = this.Editor;\n const blocks = BlockManager.blocks,\n chainData = [];\n\n /**\n * Disable modifications observe while saving\n */\n ModificationsObserver.disable();\n\n blocks.forEach((block: Block) => {\n chainData.push(this.getSavedData(block));\n });\n\n const extractedData = await Promise.all(chainData);\n const sanitizedData = await Sanitizer.sanitizeBlocks(extractedData);\n\n ModificationsObserver.enable();\n\n return this.makeOutput(sanitizedData);\n }\n\n /**\n * Saves and validates\n * @param {Block} block - Editor's Tool\n * @return {ValidatedData} - Tool's validated data\n */\n private async getSavedData(block: Block): Promise {\n const blockData = await block.save();\n const isValid = blockData && await block.validate(blockData.data);\n\n return {...blockData, isValid};\n }\n\n /**\n * Creates output object with saved data, time and version of editor\n * @param {ValidatedData} allExtractedData\n * @return {OutputData}\n */\n private makeOutput(allExtractedData): OutputData {\n let totalTime = 0;\n const blocks = [];\n\n _.log('[Editor.js saving]:', 'groupCollapsed');\n\n allExtractedData.forEach(({tool, data, time, isValid}) => {\n totalTime += time;\n\n /**\n * Capitalize Tool name\n */\n _.log(`${tool.charAt(0).toUpperCase() + tool.slice(1)}`, 'group');\n\n if (isValid) {\n /** Group process info */\n _.log(data);\n _.log(undefined, 'groupEnd');\n } else {\n _.log(`Block «${tool}» skipped because saved data is invalid`);\n _.log(undefined, 'groupEnd');\n return;\n }\n\n /** If it was stub Block, get original data */\n if (tool === this.Editor.Tools.stubTool) {\n blocks.push(data);\n return;\n }\n\n blocks.push({\n type: tool,\n data,\n });\n });\n\n _.log('Total', 'log', totalTime);\n _.log(undefined, 'groupEnd');\n\n return {\n time: +new Date(),\n blocks,\n version: VERSION,\n };\n }\n}\n","import Shortcut from '@codexteam/shortcuts';\n\n/**\n * ShortcutData interface\n * Each shortcut must have name and handler\n * `name` is a shortcut, like 'CMD+K', 'CMD+B' etc\n * `handler` is a callback\n */\nexport interface ShortcutData {\n\n /**\n * Shortcut name\n * Ex. CMD+I, CMD+B ....\n */\n name: string;\n\n /**\n * Shortcut handler\n */\n handler(event): void;\n}\n\n/**\n * Contains keyboard and mouse events binded on each Block by Block Manager\n */\nimport Module from '../__module';\n\n/**\n * @class Shortcut\n * @classdesc Allows to register new shortcut\n *\n * Internal Shortcuts Module\n */\nexport default class Shortcuts extends Module {\n /**\n * All registered shortcuts\n * @type {Shortcut[]}\n */\n private registeredShortcuts: Shortcut[] = [];\n\n /**\n * Register shortcut\n * @param {ShortcutData} shortcut\n */\n public add(shortcut: ShortcutData): void {\n const newShortcut = new Shortcut({\n name: shortcut.name,\n on: document, // UI.nodes.redactor\n callback: shortcut.handler,\n });\n\n this.registeredShortcuts.push(newShortcut);\n }\n\n /**\n * Remove shortcut\n * @param {ShortcutData} shortcut\n */\n public remove(shortcut: string): void {\n const index = this.registeredShortcuts.findIndex((shc) => shc.name === shortcut);\n\n this.registeredShortcuts[index].remove();\n this.registeredShortcuts.splice(index, 1);\n }\n}\n","import Module from '../../__module';\nimport $ from '../../dom';\nimport Flipper, {FlipperOptions} from '../../flipper';\nimport * as _ from '../../utils';\n\n/**\n * Block Settings\n *\n * ____ Settings Panel ____\n * | ...................... |\n * | . Tool Settings . |\n * | ...................... |\n * | . Default Settings . |\n * | ...................... |\n * |________________________|\n */\nexport default class BlockSettings extends Module {\n\n /**\n * Module Events\n * @return {{opened: string, closed: string}}\n */\n public get events(): {opened: string, closed: string} {\n return {\n opened: 'block-settings-opened',\n closed: 'block-settings-closed',\n };\n }\n\n /**\n * Block Settings CSS\n * @return {{wrapper, wrapperOpened, toolSettings, defaultSettings, button}}\n */\n public get CSS() {\n return {\n // Settings Panel\n wrapper: 'ce-settings',\n wrapperOpened: 'ce-settings--opened',\n toolSettings: 'ce-settings__plugin-zone',\n defaultSettings: 'ce-settings__default-zone',\n\n button: 'ce-settings__button',\n\n focusedButton : 'ce-settings__button--focused',\n focusedButtonAnimated: 'ce-settings__button--focused-animated',\n };\n }\n\n /**\n * Is Block Settings opened or not\n * @returns {boolean}\n */\n public get opened(): boolean {\n return this.nodes.wrapper.classList.contains(this.CSS.wrapperOpened);\n }\n\n /**\n * Block settings UI HTML elements\n */\n public nodes: {[key: string]: HTMLElement} = {\n wrapper: null,\n toolSettings: null,\n defaultSettings: null,\n };\n\n /**\n * List of buttons\n */\n private buttons: HTMLElement[] = [];\n\n /**\n * Instance of class that responses for leafing buttons by arrows/tab\n * @type {Flipper|null}\n */\n private flipper: Flipper = null;\n\n /**\n * Panel with block settings with 2 sections:\n * - Tool's Settings\n * - Default Settings [Move, Remove, etc]\n *\n * @return {Element}\n */\n public make(): void {\n this.nodes.wrapper = $.make('div', this.CSS.wrapper);\n\n this.nodes.toolSettings = $.make('div', this.CSS.toolSettings);\n this.nodes.defaultSettings = $.make('div', this.CSS.defaultSettings);\n\n $.append(this.nodes.wrapper, [this.nodes.toolSettings, this.nodes.defaultSettings]);\n\n /**\n * Active leafing by arrows/tab\n * Buttons will be filled on opening\n */\n this.enableFlipper();\n }\n\n /**\n * Open Block Settings pane\n */\n public open(): void {\n this.nodes.wrapper.classList.add(this.CSS.wrapperOpened);\n\n /**\n * Fill Tool's settings\n */\n this.addToolSettings();\n\n /**\n * Add default settings that presents for all Blocks\n */\n this.addDefaultSettings();\n\n /** Tell to subscribers that block settings is opened */\n this.Editor.Events.emit(this.events.opened);\n\n this.flipper.activate(this.blockTunesButtons);\n }\n\n /**\n * Close Block Settings pane\n */\n public close(): void {\n this.nodes.wrapper.classList.remove(this.CSS.wrapperOpened);\n\n /** Clear settings */\n this.nodes.toolSettings.innerHTML = '';\n this.nodes.defaultSettings.innerHTML = '';\n\n /** Tell to subscribers that block settings is closed */\n this.Editor.Events.emit(this.events.closed);\n\n /** Clear cached buttons */\n this.buttons = [];\n\n /** Clear focus on active button */\n this.flipper.deactivate();\n }\n\n /**\n * Returns Tools Settings and Default Settings\n * @return {HTMLElement[]}\n */\n public get blockTunesButtons(): HTMLElement[] {\n /**\n * Return from cache\n * if exists\n */\n if (this.buttons.length !== 0) {\n return this.buttons;\n }\n\n const toolSettings = this.nodes.toolSettings.querySelectorAll(`.${this.Editor.StylesAPI.classes.settingsButton}`);\n const defaultSettings = this.nodes.defaultSettings.querySelectorAll(`.${this.CSS.button}`);\n\n toolSettings.forEach((item) => {\n this.buttons.push((item as HTMLElement));\n });\n\n defaultSettings.forEach((item) => {\n this.buttons.push((item as HTMLElement));\n });\n\n return this.buttons;\n }\n\n /**\n * Add Tool's settings\n */\n private addToolSettings(): void {\n if (typeof this.Editor.BlockManager.currentBlock.tool.renderSettings === 'function') {\n $.append(this.nodes.toolSettings, this.Editor.BlockManager.currentBlock.tool.renderSettings());\n }\n }\n\n /**\n * Add default settings\n */\n private addDefaultSettings(): void {\n $.append(this.nodes.defaultSettings, this.Editor.BlockManager.currentBlock.renderTunes());\n }\n\n /**\n * Active leafing by arrows/tab\n * Buttons will be filled on opening\n */\n private enableFlipper(): void {\n this.flipper = new Flipper({\n focusedItemClass: this.CSS.focusedButton,\n activateCallback: () => {\n /**\n * Restoring focus on current Block after settings clicked.\n * For example, when H3 changed to H2 — DOM Elements replaced, so we need to focus a new one\n */\n _.delay( () => {\n this.Editor.Caret.setToBlock(this.Editor.BlockManager.currentBlock);\n }, 10)();\n },\n } as FlipperOptions);\n }\n}\n","import Module from '../../__module';\nimport $ from '../../dom';\nimport {BlockToolConstructable} from '../../../../types';\nimport * as _ from '../../utils';\nimport {SavedData} from '../../../types-internal/block-data';\nimport Block from '../../block';\nimport Flipper from '../../flipper';\n\n/**\n * Block Converter\n */\nexport default class ConversionToolbar extends Module {\n /**\n * CSS getter\n */\n public static get CSS(): { [key: string]: string } {\n return {\n conversionToolbarWrapper: 'ce-conversion-toolbar',\n conversionToolbarShowed: 'ce-conversion-toolbar--showed',\n conversionToolbarTools: 'ce-conversion-toolbar__tools',\n conversionToolbarLabel: 'ce-conversion-toolbar__label',\n conversionTool: 'ce-conversion-tool',\n conversionToolHidden: 'ce-conversion-tool--hidden',\n conversionToolIcon: 'ce-conversion-tool__icon',\n\n conversionToolFocused : 'ce-conversion-tool--focused',\n conversionToolActive : 'ce-conversion-tool--active',\n };\n }\n\n /**\n * HTML Elements used for UI\n */\n public nodes: { [key: string]: HTMLElement } = {\n wrapper: null,\n tools: null,\n };\n\n /**\n * Conversion Toolbar open/close state\n * @type {boolean}\n */\n public opened: boolean = false;\n\n /**\n * Available tools\n */\n private tools: { [key: string]: HTMLElement } = {};\n\n /**\n * Instance of class that responses for leafing buttons by arrows/tab\n * @type {Flipper|null}\n */\n private flipper: Flipper = null;\n\n /**\n * Callback that fill be fired on open/close and accepts an opening state\n */\n private togglingCallback = null;\n\n /**\n * Create UI of Conversion Toolbar\n */\n public make(): HTMLElement {\n this.nodes.wrapper = $.make('div', ConversionToolbar.CSS.conversionToolbarWrapper);\n this.nodes.tools = $.make('div', ConversionToolbar.CSS.conversionToolbarTools);\n\n const label = $.make('div', ConversionToolbar.CSS.conversionToolbarLabel, {\n textContent: 'Convert to',\n });\n\n /**\n * Add Tools that has 'import' method\n */\n this.addTools();\n\n /**\n * Prepare Flipper to be able to leaf tools by arrows/tab\n */\n this.enableFlipper();\n\n $.append(this.nodes.wrapper, label);\n $.append(this.nodes.wrapper, this.nodes.tools);\n\n return this.nodes.wrapper;\n }\n\n /**\n * Toggle conversion dropdown visibility\n * @param {function} [togglingCallback] — callback that will accept opening state\n */\n public toggle(togglingCallback?: (openedState: boolean) => void): void {\n if (!this.opened) {\n this.open();\n } else {\n this.close();\n }\n\n if (typeof togglingCallback === 'function') {\n this.togglingCallback = togglingCallback;\n\n this.togglingCallback(this.opened);\n }\n }\n\n /**\n * Shows Conversion Toolbar\n */\n public open(): void {\n this.filterTools();\n\n this.opened = true;\n this.nodes.wrapper.classList.add(ConversionToolbar.CSS.conversionToolbarShowed);\n\n /**\n * We use timeout to prevent bubbling Enter keydown on first dropdown item\n * Conversion flipper will be activated after dropdown will open\n */\n setTimeout(() => {\n this.flipper.activate(Object.values(this.tools).filter((button) => {\n return !button.classList.contains(ConversionToolbar.CSS.conversionToolHidden);\n }));\n this.flipper.focusFirst();\n\n if (typeof this.togglingCallback === 'function') {\n this.togglingCallback(true);\n }\n }, 50);\n }\n\n /**\n * Closes Conversion Toolbar\n */\n public close(): void {\n this.opened = false;\n this.flipper.deactivate();\n this.nodes.wrapper.classList.remove(ConversionToolbar.CSS.conversionToolbarShowed);\n\n if (typeof this.togglingCallback === 'function') {\n this.togglingCallback(false);\n }\n }\n\n /**\n * Returns true if it has more than one tool available for convert in\n */\n public hasTools(): boolean {\n const tools = Object.keys(this.tools); // available tools in array representation\n\n return !(tools.length === 1 && tools.shift() === this.config.initialBlock);\n }\n\n /**\n * Replaces one Block with another\n * For that Tools must provide import/export methods\n *\n * @param {string} replacingToolName\n */\n public async replaceWithBlock(replacingToolName: string): Promise {\n /**\n * At first, we get current Block data\n * @type {BlockToolConstructable}\n */\n const currentBlockClass = this.Editor.BlockManager.currentBlock.class;\n const currentBlockName = this.Editor.BlockManager.currentBlock.name;\n const savedBlock = await this.Editor.BlockManager.currentBlock.save() as SavedData;\n const { INTERNAL_SETTINGS } = this.Editor.Tools;\n const blockData = savedBlock.data;\n\n /**\n * When current Block name is equals to the replacing tool Name,\n * than convert this Block back to the initial Block\n */\n if (currentBlockName === replacingToolName) {\n replacingToolName = this.config.initialBlock;\n }\n\n /**\n * Getting a class of replacing Tool\n * @type {BlockToolConstructable}\n */\n const replacingTool = this.Editor.Tools.toolsClasses[replacingToolName] as BlockToolConstructable;\n\n /**\n * Export property can be:\n * 1) Function — Tool defines which data to return\n * 2) String — the name of saved property\n *\n * In both cases returning value must be a string\n */\n let exportData: string = '';\n const exportProp = currentBlockClass[INTERNAL_SETTINGS.CONVERSION_CONFIG].export;\n\n if (typeof exportProp === 'function') {\n exportData = exportProp(blockData);\n } else if (typeof exportProp === 'string') {\n exportData = blockData[exportProp];\n } else {\n _.log('Conversion «export» property must be a string or function. ' +\n 'String means key of saved data object to export. Function should export processed string to export.');\n return;\n }\n\n /**\n * Clean exported data with replacing sanitizer config\n */\n const cleaned: string = this.Editor.Sanitizer.clean(\n exportData,\n replacingTool.sanitize,\n );\n\n /**\n * «import» property can be Function or String\n * function — accept imported string and compose tool data object\n * string — the name of data field to import\n */\n let newBlockData = {};\n const importProp = replacingTool[INTERNAL_SETTINGS.CONVERSION_CONFIG].import;\n\n if (typeof importProp === 'function') {\n newBlockData = importProp(cleaned);\n } else if (typeof importProp === 'string') {\n newBlockData[importProp] = cleaned;\n } else {\n _.log('Conversion «import» property must be a string or function. ' +\n 'String means key of tool data to import. Function accepts a imported string and return composed tool data.');\n return;\n }\n\n this.Editor.BlockManager.replace(replacingToolName, newBlockData);\n this.Editor.BlockSelection.clearSelection();\n\n this.close();\n this.Editor.InlineToolbar.close();\n\n _.delay(() => {\n this.Editor.Caret.setToBlock(this.Editor.BlockManager.currentBlock);\n }, 10)();\n }\n\n /**\n * Iterates existing Tools and inserts to the ConversionToolbar\n * if tools have ability to import\n */\n private addTools(): void {\n const tools = this.Editor.Tools.blockTools;\n\n for (const toolName in tools) {\n if (!tools.hasOwnProperty(toolName)) {\n continue;\n }\n\n const internalSettings = this.Editor.Tools.INTERNAL_SETTINGS;\n const toolClass = tools[toolName] as BlockToolConstructable;\n const toolToolboxSettings = toolClass[internalSettings.TOOLBOX];\n const conversionConfig = toolClass[internalSettings.CONVERSION_CONFIG];\n\n /**\n * Skip tools that don't pass 'toolbox' property\n */\n if (_.isEmpty(toolToolboxSettings) || !toolToolboxSettings.icon) {\n continue;\n }\n\n /**\n * Skip tools without «import» rule specified\n */\n if (!conversionConfig || !conversionConfig.import) {\n continue;\n }\n\n this.addTool(toolName, toolToolboxSettings.icon, toolToolboxSettings.title);\n }\n }\n\n /**\n * Add tool to the Conversion Toolbar\n */\n private addTool(toolName: string, toolIcon: string, title: string): void {\n const tool = $.make('div', [ ConversionToolbar.CSS.conversionTool ]);\n const icon = $.make('div', [ ConversionToolbar.CSS.conversionToolIcon ]);\n\n tool.dataset.tool = toolName;\n icon.innerHTML = toolIcon;\n\n $.append(tool, icon);\n $.append(tool, $.text(title || _.capitalize(toolName)));\n\n $.append(this.nodes.tools, tool);\n this.tools[toolName] = tool;\n\n this.Editor.Listeners.on(tool, 'click', async () => {\n await this.replaceWithBlock(toolName);\n });\n }\n\n /**\n * Hide current Tool and show others\n */\n private filterTools(): void {\n const { currentBlock } = this.Editor.BlockManager;\n\n /**\n * Show previously hided\n */\n Object.entries(this.tools).forEach(([name, button]) => {\n button.hidden = false;\n button.classList.toggle(ConversionToolbar.CSS.conversionToolHidden, name === currentBlock.name);\n });\n }\n\n /**\n * Prepare Flipper to be able to leaf tools by arrows/tab\n */\n private enableFlipper(): void {\n this.flipper = new Flipper({\n focusedItemClass: ConversionToolbar.CSS.conversionToolFocused,\n });\n }\n}\n","import Module from '../../__module';\nimport $ from '../../dom';\nimport * as _ from '../../utils';\n\n/**\n *\n * «Toolbar» is the node that moves up/down over current block\n *\n * ______________________________________ Toolbar ____________________________________________\n * | |\n * | ..................... Content .................... ......... Block Actions .......... |\n * | . . . . |\n * | . . . [Open Settings] . |\n * | . [Plus Button] [Toolbox: {Tool1}, {Tool2}] . . . |\n * | . . . [Settings Panel] . |\n * | .................................................. .................................. |\n * | |\n * |___________________________________________________________________________________________|\n *\n *\n * Toolbox — its an Element contains tools buttons. Can be shown by Plus Button.\n *\n * _______________ Toolbox _______________\n * | |\n * | [Header] [Image] [List] [Quote] ... |\n * |_______________________________________|\n *\n *\n * Settings Panel — is an Element with block settings:\n *\n * ____ Settings Panel ____\n * | ...................... |\n * | . Tool Settings . |\n * | ...................... |\n * | . Default Settings . |\n * | ...................... |\n * |________________________|\n *\n *\n * @class\n * @classdesc Toolbar module\n *\n * @typedef {Toolbar} Toolbar\n * @property {Object} nodes\n * @property {Element} nodes.wrapper - Toolbar main element\n * @property {Element} nodes.content - Zone with Plus button and toolbox.\n * @property {Element} nodes.actions - Zone with Block Settings and Remove Button\n * @property {Element} nodes.blockActionsButtons - Zone with Block Buttons: [Settings]\n * @property {Element} nodes.plusButton - Button that opens or closes Toolbox\n * @property {Element} nodes.toolbox - Container for tools\n * @property {Element} nodes.settingsToggler - open/close Settings Panel button\n * @property {Element} nodes.settings - Settings Panel\n * @property {Element} nodes.pluginSettings - Plugin Settings section of Settings Panel\n * @property {Element} nodes.defaultSettings - Default Settings section of Settings Panel\n */\nexport default class Toolbar extends Module {\n /**\n * HTML Elements used for Toolbar UI\n */\n public nodes: {[key: string]: HTMLElement} = {\n wrapper : null,\n content : null,\n actions : null,\n\n // Content Zone\n plusButton : null,\n\n // Actions Zone\n blockActionsButtons: null,\n settingsToggler : null,\n };\n\n /**\n * CSS styles\n * @return {Object}\n */\n public get CSS() {\n return {\n toolbar: 'ce-toolbar',\n content: 'ce-toolbar__content',\n actions: 'ce-toolbar__actions',\n actionsOpened: 'ce-toolbar__actions--opened',\n\n toolbarOpened: 'ce-toolbar--opened',\n\n // Content Zone\n plusButton: 'ce-toolbar__plus',\n plusButtonShortcut: 'ce-toolbar__plus-shortcut',\n plusButtonHidden: 'ce-toolbar__plus--hidden',\n\n // Actions Zone\n blockActionsButtons: 'ce-toolbar__actions-buttons',\n settingsToggler: 'ce-toolbar__settings-btn',\n };\n }\n\n /**\n * Makes toolbar\n */\n public make(): void {\n this.nodes.wrapper = $.make('div', this.CSS.toolbar);\n\n /**\n * Make Content Zone and Actions Zone\n */\n ['content', 'actions'].forEach( (el) => {\n this.nodes[el] = $.make('div', this.CSS[el]);\n $.append(this.nodes.wrapper, this.nodes[el]);\n });\n\n /**\n * Fill Content Zone:\n * - Plus Button\n * - Toolbox\n */\n this.nodes.plusButton = $.make('div', this.CSS.plusButton);\n $.append(this.nodes.plusButton, $.svg('plus', 14, 14));\n $.append(this.nodes.content, this.nodes.plusButton);\n\n this.Editor.Listeners.on(this.nodes.plusButton, 'click', () => this.plusButtonClicked(), false);\n\n /**\n * Add events to show/hide tooltip for plus button\n */\n const tooltipContent = $.make('div');\n\n tooltipContent.appendChild(document.createTextNode('Add'));\n tooltipContent.appendChild($.make('div', this.CSS.plusButtonShortcut, {\n textContent: '⇥ Tab',\n }));\n\n this.Editor.Tooltip.onHover(this.nodes.plusButton, tooltipContent);\n\n /**\n * Make a Toolbox\n */\n this.Editor.Toolbox.make();\n\n /**\n * Fill Actions Zone:\n * - Settings Toggler\n * - Remove Block Button\n * - Settings Panel\n */\n this.nodes.blockActionsButtons = $.make('div', this.CSS.blockActionsButtons);\n this.nodes.settingsToggler = $.make('span', this.CSS.settingsToggler);\n const settingsIcon = $.svg('dots', 18, 4);\n\n $.append(this.nodes.settingsToggler, settingsIcon);\n $.append(this.nodes.blockActionsButtons, this.nodes.settingsToggler);\n $.append(this.nodes.actions, this.nodes.blockActionsButtons);\n\n this.Editor.Tooltip.onHover(this.nodes.settingsToggler, 'Click to tune', {\n placement: 'top',\n });\n\n /**\n * Make and append Settings Panel\n */\n this.Editor.BlockSettings.make();\n $.append(this.nodes.actions, this.Editor.BlockSettings.nodes.wrapper);\n\n /**\n * Append toolbar to the Editor\n */\n $.append(this.Editor.UI.nodes.wrapper, this.nodes.wrapper);\n\n /**\n * Bind events on the Toolbar elements\n */\n this.bindEvents();\n }\n\n /**\n * Move Toolbar to the Current Block\n * @param {Boolean} forceClose - force close Toolbar Settings and Toolbar\n */\n public move(forceClose: boolean = true): void {\n if (forceClose) {\n /** Close Toolbox when we move toolbar */\n this.Editor.Toolbox.close();\n this.Editor.BlockSettings.close();\n }\n\n const currentBlock = this.Editor.BlockManager.currentBlock.holder;\n\n /**\n * If no one Block selected as a Current\n */\n if (!currentBlock) {\n return;\n }\n\n const { isMobile } = this.Editor.UI;\n const blockHeight = currentBlock.offsetHeight;\n let toolbarY = currentBlock.offsetTop;\n\n /**\n * 1) On desktop — Toolbar at the top of Block, Plus/Toolbox moved the center of Block\n * 2) On mobile — Toolbar at the bottom of Block\n */\n if (!isMobile) {\n const contentOffset = Math.floor(blockHeight / 2);\n\n this.nodes.plusButton.style.transform = `translate3d(0, calc(${contentOffset}px - 50%), 0)`;\n this.Editor.Toolbox.nodes.toolbox.style.transform = `translate3d(0, calc(${contentOffset}px - 50%), 0)`;\n } else {\n toolbarY += blockHeight;\n }\n\n /**\n * Move Toolbar to the Top coordinate of Block\n */\n this.nodes.wrapper.style.transform = `translate3D(0, ${Math.floor(toolbarY)}px, 0)`;\n }\n\n /**\n * Open Toolbar with Plus Button and Actions\n * @param {boolean} withBlockActions - by default, Toolbar opens with Block Actions.\n * This flag allows to open Toolbar without Actions.\n * @param {boolean} needToCloseToolbox - by default, Toolbar will be moved with opening\n * (by click on Block, or by enter)\n * with closing Toolbox and Block Settings\n * This flag allows to open Toolbar with Toolbox\n */\n public open(withBlockActions: boolean = true, needToCloseToolbox: boolean = true): void {\n _.delay(() => {\n this.move(needToCloseToolbox);\n this.nodes.wrapper.classList.add(this.CSS.toolbarOpened);\n\n if (withBlockActions) {\n this.blockActions.show();\n } else {\n this.blockActions.hide();\n }\n }, 50)();\n }\n\n /**\n * returns toolbar opened state\n * @return {Boolean}\n */\n public get opened(): boolean {\n return this.nodes.wrapper.classList.contains(this.CSS.toolbarOpened);\n }\n\n /**\n * Close the Toolbar\n */\n public close(): void {\n this.nodes.wrapper.classList.remove(this.CSS.toolbarOpened);\n\n /** Close components */\n this.blockActions.hide();\n this.Editor.Toolbox.close();\n this.Editor.BlockSettings.close();\n }\n\n /**\n * Plus Button public methods\n * @return {{hide: function(): void, show: function(): void}}\n */\n public get plusButton(): {hide: () => void, show: () => void} {\n return {\n hide: () => this.nodes.plusButton.classList.add(this.CSS.plusButtonHidden),\n show: () => {\n if (this.Editor.Toolbox.isEmpty) {\n return;\n }\n this.nodes.plusButton.classList.remove(this.CSS.plusButtonHidden);\n },\n };\n }\n\n /**\n * Block actions appearance manipulations\n * @return {{hide: function(): void, show: function(): void}}\n */\n private get blockActions(): {hide: () => void, show: () => void} {\n return {\n hide: () => {\n this.nodes.actions.classList.remove(this.CSS.actionsOpened);\n },\n show : () => {\n this.nodes.actions.classList.add(this.CSS.actionsOpened);\n },\n };\n }\n\n /**\n * Handler for Plus Button\n * @param {MouseEvent} event\n */\n private plusButtonClicked(): void {\n this.Editor.Toolbox.toggle();\n }\n\n /**\n * Bind events on the Toolbar Elements:\n * - Block Settings\n */\n private bindEvents(): void {\n /**\n * Settings toggler\n */\n this.Editor.Listeners.on(this.nodes.settingsToggler, 'click', () => this.settingsTogglerClicked());\n }\n\n /**\n * Clicks on the Block Settings toggler\n */\n private settingsTogglerClicked(): void {\n if (this.Editor.BlockSettings.opened) {\n this.Editor.BlockSettings.close();\n } else {\n this.Editor.BlockSettings.open();\n }\n }\n}\n","import Module from '../../__module';\nimport $ from '../../dom';\n\nimport SelectionUtils from '../../selection';\nimport * as _ from '../../utils';\nimport {InlineTool, InlineToolConstructable, ToolConstructable, ToolSettings} from '../../../../types';\nimport Flipper from '../../flipper';\n\n/**\n * Inline toolbar with actions that modifies selected text fragment\n *\n * |¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯|\n * | B i [link] [mark] |\n * |________________________|\n */\nexport default class InlineToolbar extends Module {\n\n /**\n * CSS styles\n */\n public CSS = {\n inlineToolbar: 'ce-inline-toolbar',\n inlineToolbarShowed: 'ce-inline-toolbar--showed',\n inlineToolbarLeftOriented: 'ce-inline-toolbar--left-oriented',\n inlineToolbarRightOriented: 'ce-inline-toolbar--right-oriented',\n inlineToolbarShortcut: 'ce-inline-toolbar__shortcut',\n buttonsWrapper: 'ce-inline-toolbar__buttons',\n actionsWrapper: 'ce-inline-toolbar__actions',\n inlineToolButton: 'ce-inline-tool',\n inlineToolButtonLast: 'ce-inline-tool--last',\n inputField: 'cdx-input',\n focusedButton: 'ce-inline-tool--focused',\n conversionToggler: 'ce-inline-toolbar__dropdown',\n conversionTogglerHidden: 'ce-inline-toolbar__dropdown--hidden',\n conversionTogglerContent: 'ce-inline-toolbar__dropdown-content',\n };\n\n /**\n * State of inline toolbar\n * @type {boolean}\n */\n public opened: boolean = false;\n\n /**\n * Inline Toolbar elements\n */\n private nodes: {\n wrapper: HTMLElement,\n buttons: HTMLElement,\n conversionToggler: HTMLElement,\n conversionTogglerContent: HTMLElement,\n actions: HTMLElement,\n } = {\n wrapper: null,\n buttons: null,\n conversionToggler: null,\n conversionTogglerContent: null,\n /**\n * Zone below the buttons where Tools can create additional actions by 'renderActions()' method\n * For example, input for the 'link' tool or textarea for the 'comment' tool\n */\n actions: null,\n };\n\n /**\n * Margin above/below the Toolbar\n */\n private readonly toolbarVerticalMargin: number = 5;\n\n /**\n * Tools instances\n */\n private toolsInstances: Map;\n\n /**\n * Buttons List\n * @type {NodeList}\n */\n private buttonsList: NodeList = null;\n\n /**\n * Cache for Inline Toolbar width\n * @type {number}\n */\n private width: number = 0;\n\n /**\n * Instance of class that responses for leafing buttons by arrows/tab\n */\n private flipper: Flipper = null;\n\n /**\n * Inline Toolbar Tools\n *\n * @returns Map\n */\n get tools(): Map {\n if (!this.toolsInstances || this.toolsInstances.size === 0) {\n const allTools = this.inlineTools;\n\n this.toolsInstances = new Map();\n for (const tool in allTools) {\n if (allTools.hasOwnProperty(tool)) {\n this.toolsInstances.set(tool, allTools[tool]);\n }\n }\n }\n\n return this.toolsInstances;\n }\n\n /**\n * Making DOM\n */\n public make() {\n this.nodes.wrapper = $.make('div', this.CSS.inlineToolbar);\n this.nodes.buttons = $.make('div', this.CSS.buttonsWrapper);\n this.nodes.actions = $.make('div', this.CSS.actionsWrapper);\n\n // To prevent reset of a selection when click on the wrapper\n this.Editor.Listeners.on(this.nodes.wrapper, 'mousedown', (event) => {\n const isClickedOnActionsWrapper = (event.target as Element).closest(`.${this.CSS.actionsWrapper}`);\n\n // If click is on actions wrapper,\n // do not prevent default behaviour because actions might include interactive elements\n if (!isClickedOnActionsWrapper) {\n event.preventDefault();\n }\n });\n\n /**\n * Append Inline Toolbar to the Editor\n */\n $.append(this.nodes.wrapper, [this.nodes.buttons, this.nodes.actions]);\n $.append(this.Editor.UI.nodes.wrapper, this.nodes.wrapper);\n\n /**\n * Add button that will allow switching block type\n */\n this.addConversionToggler();\n\n /**\n * Append Inline Toolbar Tools\n */\n this.addTools();\n\n /**\n * Prepare conversion toolbar.\n * If it has any conversion tool then it will be enabled in the Inline Toolbar\n */\n this.prepareConversionToolbar();\n\n /**\n * Recalculate initial width with all buttons\n */\n this.recalculateWidth();\n\n /**\n * Allow to leaf buttons by arrows / tab\n * Buttons will be filled on opening\n */\n this.enableFlipper();\n }\n\n /**\n * Moving / appearance\n * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n */\n\n /**\n * Shows Inline Toolbar if something is selected\n * @param {boolean} [needToClose] - pass true to close toolbar if it is not allowed.\n * Avoid to use it just for closing IT, better call .close() clearly.\n */\n public tryToShow(needToClose: boolean = false): void {\n if (!this.allowedToShow()) {\n if (needToClose) {\n this.close();\n }\n return;\n }\n\n this.move();\n this.open();\n this.Editor.Toolbar.close();\n\n /** Check Tools state for selected fragment */\n this.checkToolsState();\n }\n\n /**\n * Move Toolbar to the selected text\n */\n public move(): void {\n const selectionRect = SelectionUtils.rect as DOMRect;\n const wrapperOffset = this.Editor.UI.nodes.wrapper.getBoundingClientRect();\n const newCoords = {\n x: selectionRect.x - wrapperOffset.left,\n y: selectionRect.y\n + selectionRect.height\n // + window.scrollY\n - wrapperOffset.top\n + this.toolbarVerticalMargin,\n };\n\n /**\n * If we know selections width, place InlineToolbar to center\n */\n if (selectionRect.width) {\n newCoords.x += Math.floor(selectionRect.width / 2);\n }\n\n /**\n * Inline Toolbar has -50% translateX, so we need to check real coords to prevent overflowing\n */\n const realLeftCoord = newCoords.x - this.width / 2;\n const realRightCoord = newCoords.x + this.width / 2;\n\n /**\n * By default, Inline Toolbar has top-corner at the center\n * We are adding a modifiers for to move corner to the left or right\n */\n this.nodes.wrapper.classList.toggle(\n this.CSS.inlineToolbarLeftOriented,\n realLeftCoord < this.Editor.UI.contentRect.left,\n );\n\n this.nodes.wrapper.classList.toggle(\n this.CSS.inlineToolbarRightOriented,\n realRightCoord > this.Editor.UI.contentRect.right,\n );\n\n this.nodes.wrapper.style.left = Math.floor(newCoords.x) + 'px';\n this.nodes.wrapper.style.top = Math.floor(newCoords.y) + 'px';\n }\n\n /**\n * Hides Inline Toolbar\n */\n public close(): void {\n this.nodes.wrapper.classList.remove(this.CSS.inlineToolbarShowed);\n this.tools.forEach((toolInstance) => {\n if (typeof toolInstance.clear === 'function') {\n toolInstance.clear();\n }\n });\n\n this.opened = false;\n\n this.flipper.deactivate();\n this.Editor.ConversionToolbar.close();\n }\n\n /**\n * Shows Inline Toolbar\n */\n public open(): void {\n /**\n * Filter inline-tools and show only allowed by Block's Tool\n */\n this.filterTools();\n\n /**\n * Show Inline Toolbar\n */\n this.nodes.wrapper.classList.add(this.CSS.inlineToolbarShowed);\n\n /**\n * Call 'clear' method for Inline Tools (for example, 'link' want to clear input)\n */\n this.tools.forEach((toolInstance: InlineTool) => {\n if (typeof toolInstance.clear === 'function') {\n toolInstance.clear();\n }\n });\n\n this.buttonsList = this.nodes.buttons.querySelectorAll(`.${this.CSS.inlineToolButton}`);\n this.opened = true;\n\n if (this.Editor.ConversionToolbar.hasTools()) {\n /**\n * Change Conversion Dropdown content for current tool\n */\n this.setConversionTogglerContent();\n } else {\n /**\n * hide Conversion Dropdown with there are no tools\n */\n this.nodes.conversionToggler.hidden = true;\n }\n\n /**\n * Get currently visible buttons to pass it to the Flipper\n */\n let visibleTools = Array.from(this.buttonsList);\n\n visibleTools.unshift(this.nodes.conversionToggler);\n visibleTools = visibleTools.filter((tool) => !(tool as HTMLElement).hidden);\n\n this.flipper.activate(visibleTools as HTMLElement[]);\n }\n\n /**\n * Check if node is contained by Inline Toolbar\n *\n * @param {Node} node — node to chcek\n */\n public containsNode(node: Node): boolean {\n return this.nodes.wrapper.contains(node);\n }\n\n /**\n * Need to show Inline Toolbar or not\n */\n private allowedToShow(): boolean {\n /**\n * Tags conflicts with window.selection function.\n * Ex. IMG tag returns null (Firefox) or Redactors wrapper (Chrome)\n */\n const tagsConflictsWithSelection = ['IMG', 'INPUT'];\n const currentSelection = SelectionUtils.get();\n const selectedText = SelectionUtils.text;\n\n // old browsers\n if (!currentSelection || !currentSelection.anchorNode) {\n return false;\n }\n\n // empty selection\n if (currentSelection.isCollapsed || selectedText.length < 1) {\n return false;\n }\n\n const target = !$.isElement(currentSelection.anchorNode )\n ? currentSelection.anchorNode.parentElement\n : currentSelection.anchorNode;\n\n if (currentSelection && tagsConflictsWithSelection.includes(target.tagName)) {\n return false;\n }\n\n // The selection of the element only in contenteditable\n const contenteditable = target.closest('[contenteditable=\"true\"]');\n\n if (contenteditable === null) {\n return false;\n }\n\n // is enabled by current Block's Tool\n const currentBlock = this.Editor.BlockManager.getBlock(currentSelection.anchorNode as HTMLElement);\n\n if (!currentBlock) {\n return false;\n }\n\n const toolSettings = this.Editor.Tools.getToolSettings(currentBlock.name);\n\n return toolSettings && toolSettings[this.Editor.Tools.USER_SETTINGS.ENABLED_INLINE_TOOLS];\n }\n\n /**\n * Show only allowed Tools\n */\n private filterTools(): void {\n const currentSelection = SelectionUtils.get(),\n currentBlock = this.Editor.BlockManager.getBlock(currentSelection.anchorNode as HTMLElement);\n\n const toolSettings = this.Editor.Tools.getToolSettings(currentBlock.name),\n inlineToolbarSettings = toolSettings && toolSettings[this.Editor.Tools.USER_SETTINGS.ENABLED_INLINE_TOOLS];\n\n /**\n * All Inline Toolbar buttons\n * @type {HTMLElement[]}\n */\n const buttons = Array.from(this.nodes.buttons.querySelectorAll(`.${this.CSS.inlineToolButton}`)) as HTMLElement[];\n\n /**\n * Show previously hided\n */\n buttons.forEach((button) => {\n button.hidden = false;\n button.classList.remove(this.CSS.inlineToolButtonLast);\n });\n\n /**\n * Filter buttons if Block Tool pass config like inlineToolbar=['link']\n */\n if (Array.isArray(inlineToolbarSettings)) {\n buttons.forEach((button) => {\n button.hidden = !inlineToolbarSettings.includes(button.dataset.tool);\n });\n }\n\n /**\n * Tick for removing right-margin from last visible button.\n * Current generation of CSS does not allow to filter :visible elements\n */\n const lastVisibleButton = buttons.filter((button) => !button.hidden).pop();\n\n if (lastVisibleButton) {\n lastVisibleButton.classList.add(this.CSS.inlineToolButtonLast);\n }\n\n /**\n * Recalculate width because some buttons can be hidden\n */\n this.recalculateWidth();\n }\n\n /**\n * Recalculate inline toolbar width\n */\n private recalculateWidth(): void {\n this.width = this.nodes.wrapper.offsetWidth;\n }\n\n /**\n * Create a toggler for Conversion Dropdown\n * and prepend it to the buttons list\n */\n private addConversionToggler(): void {\n this.nodes.conversionToggler = $.make('div', this.CSS.conversionToggler);\n this.nodes.conversionTogglerContent = $.make('div', this.CSS.conversionTogglerContent);\n\n const icon = $.svg('toggler-down', 13, 13);\n\n this.nodes.conversionToggler.appendChild(this.nodes.conversionTogglerContent);\n this.nodes.conversionToggler.appendChild(icon);\n\n this.nodes.buttons.appendChild(this.nodes.conversionToggler);\n\n this.Editor.Listeners.on(this.nodes.conversionToggler, 'click', () => {\n this.Editor.ConversionToolbar.toggle((conversionToolbarOpened) => {\n if (conversionToolbarOpened) {\n this.flipper.deactivate();\n } else {\n this.flipper.activate();\n }\n });\n });\n\n this.Editor.Tooltip.onHover(this.nodes.conversionToggler, 'Convert to', {\n placement: 'top',\n hidingDelay: 100,\n });\n }\n\n /**\n * Changes Conversion Dropdown content for current block's Tool\n */\n private setConversionTogglerContent(): void {\n const {BlockManager, Tools} = this.Editor;\n const toolName = BlockManager.currentBlock.name;\n\n /**\n * If tool does not provide 'export' rule, hide conversion dropdown\n */\n const conversionConfig = Tools.available[toolName][Tools.INTERNAL_SETTINGS.CONVERSION_CONFIG] || {};\n const exportRuleDefined = conversionConfig && conversionConfig.export;\n\n this.nodes.conversionToggler.hidden = !exportRuleDefined;\n this.nodes.conversionToggler.classList.toggle(this.CSS.conversionTogglerHidden, !exportRuleDefined);\n\n /**\n * Get icon or title for dropdown\n */\n const toolSettings = Tools.getToolSettings(toolName);\n const toolboxSettings = Tools.available[toolName][Tools.INTERNAL_SETTINGS.TOOLBOX] || {};\n const userToolboxSettings = toolSettings.toolbox || {};\n\n this.nodes.conversionTogglerContent.innerHTML =\n userToolboxSettings.icon\n || toolboxSettings.icon\n || userToolboxSettings.title\n || toolboxSettings.title\n || _.capitalize(toolName);\n }\n\n /**\n * Makes the Conversion Dropdown\n */\n private prepareConversionToolbar(): void {\n const ct = this.Editor.ConversionToolbar.make();\n\n $.append(this.nodes.wrapper, ct);\n }\n\n /**\n * Working with Tools\n * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n */\n\n /**\n * Fill Inline Toolbar with Tools\n */\n private addTools(): void {\n this.tools.forEach((toolInstance, toolName) => {\n this.addTool(toolName, toolInstance);\n });\n }\n\n /**\n * Add tool button and activate clicks\n */\n private addTool(toolName: string, tool: InlineTool): void {\n const {\n Listeners,\n Tools,\n Tooltip,\n } = this.Editor;\n\n const button = tool.render();\n\n if (!button) {\n _.log('Render method must return an instance of Node', 'warn', toolName);\n return;\n }\n\n button.dataset.tool = toolName;\n this.nodes.buttons.appendChild(button);\n\n if (typeof tool.renderActions === 'function') {\n const actions = tool.renderActions();\n this.nodes.actions.appendChild(actions);\n }\n\n Listeners.on(button, 'click', (event) => {\n this.toolClicked(tool);\n event.preventDefault();\n });\n\n /**\n * Enable shortcuts\n * Ignore tool that doesn't have shortcut or empty string\n */\n const toolSettings = Tools.getToolSettings(toolName);\n\n let shortcut = null;\n\n /**\n * Get internal inline tools\n */\n const internalTools: string[] = Object\n .entries(Tools.internalTools)\n .filter(([name, toolClass]: [string, ToolConstructable | ToolSettings]) => {\n if (_.isFunction(toolClass)) {\n return toolClass[Tools.INTERNAL_SETTINGS.IS_INLINE];\n }\n\n return (toolClass as ToolSettings).class[Tools.INTERNAL_SETTINGS.IS_INLINE];\n })\n .map(([name]: [string, InlineToolConstructable | ToolSettings]) => name);\n\n /**\n * 1) For internal tools, check public getter 'shortcut'\n * 2) For external tools, check tool's settings\n */\n if (internalTools.includes(toolName)) {\n shortcut = this.inlineTools[toolName][Tools.INTERNAL_SETTINGS.SHORTCUT];\n } else if (toolSettings && toolSettings[Tools.USER_SETTINGS.SHORTCUT]) {\n shortcut = toolSettings[Tools.USER_SETTINGS.SHORTCUT];\n }\n\n if (shortcut) {\n this.enableShortcuts(tool, shortcut);\n }\n\n /**\n * Enable tooltip module on button\n */\n const tooltipContent = $.make('div');\n const toolTitle = Tools.toolsClasses[toolName][Tools.INTERNAL_SETTINGS.TITLE] || _.capitalize(toolName);\n\n tooltipContent.appendChild($.text(toolTitle));\n\n if (shortcut) {\n tooltipContent.appendChild($.make('div', this.CSS.inlineToolbarShortcut, {\n textContent: _.beautifyShortcut(shortcut),\n }));\n }\n\n Tooltip.onHover(button, tooltipContent, {\n placement: 'top',\n hidingDelay: 100,\n });\n\n }\n\n /**\n * Enable Tool shortcut with Editor Shortcuts Module\n * @param {InlineTool} tool - Tool instance\n * @param {string} shortcut - shortcut according to the ShortcutData Module format\n */\n private enableShortcuts(tool: InlineTool, shortcut: string): void {\n this.Editor.Shortcuts.add({\n name: shortcut,\n handler: (event) => {\n const {currentBlock} = this.Editor.BlockManager;\n\n /**\n * Editor is not focused\n */\n if (!currentBlock) {\n return;\n }\n\n /**\n * We allow to fire shortcut with empty selection (isCollapsed=true)\n * it can be used by tools like «Mention» that works without selection:\n * Example: by SHIFT+@ show dropdown and insert selected username\n */\n // if (SelectionUtils.isCollapsed) return;\n\n const toolSettings = this.Editor.Tools.getToolSettings(currentBlock.name);\n\n if (!toolSettings || !toolSettings[this.Editor.Tools.USER_SETTINGS.ENABLED_INLINE_TOOLS]) {\n return;\n }\n\n event.preventDefault();\n this.toolClicked(tool);\n },\n });\n }\n\n /**\n * Inline Tool button clicks\n * @param {InlineTool} tool - Tool's instance\n */\n private toolClicked(tool: InlineTool): void {\n const range = SelectionUtils.range;\n\n tool.surround(range);\n this.checkToolsState();\n }\n\n /**\n * Check Tools` state by selection\n */\n private checkToolsState(): void {\n this.tools.forEach((toolInstance) => {\n toolInstance.checkState(SelectionUtils.get());\n });\n }\n\n /**\n * Get inline tools tools\n * Tools that has isInline is true\n */\n private get inlineTools(): { [name: string]: InlineTool } {\n const result = {};\n\n for (const tool in this.Editor.Tools.inline) {\n if (this.Editor.Tools.inline.hasOwnProperty(tool)) {\n const toolSettings = this.Editor.Tools.getToolSettings(tool);\n\n result[tool] = this.Editor.Tools.constructInline(this.Editor.Tools.inline[tool], toolSettings);\n }\n }\n\n return result;\n }\n\n /**\n * Allow to leaf buttons by arrows / tab\n * Buttons will be filled on opening\n */\n private enableFlipper(): void {\n this.flipper = new Flipper({\n focusedItemClass: this.CSS.focusedButton,\n allowArrows: false,\n });\n }\n}\n","import Module from '../../__module';\nimport $ from '../../dom';\nimport * as _ from '../../utils';\nimport {BlockToolConstructable} from '../../../../types';\nimport Flipper from '../../flipper';\nimport {BlockToolAPI} from '../../block';\n\n/**\n * @class Toolbox\n * @classdesc Holder for Tools\n *\n * @typedef {Toolbox} Toolbox\n * @property {Boolean} opened - opening state\n * @property {Object} nodes - Toolbox nodes\n * @property {Object} CSS - CSS class names\n *\n */\nexport default class Toolbox extends Module {\n\n /**\n * CSS styles\n * @return {{toolbox: string, toolboxButton string, toolboxButtonActive: string,\n * toolboxButtonApistackActive: string,\n * toolboxOpened: string, tooltip: string, tooltipShown: string, tooltipShortcut: string}}\n */\n get CSS() {\n return {\n toolbox: 'ce-toolbox',\n toolboxButton: 'ce-toolbox__button',\n toolboxButtonApistack: 'editor-toolbar-button',\n toolboxButtonApistackActive : 'editor-toolbar-button-active',\n toolboxButtonActive : 'ce-toolbox__button--active',\n toolboxOpened: 'ce-toolbox--opened',\n openedToolbarHolderModifier: 'codex-editor--toolbox-opened',\n buttonTooltip: 'ce-toolbox-button-tooltip',\n buttonShortcut: 'ce-toolbox-button-tooltip__shortcut',\n };\n }\n\n /**\n * Returns True if Toolbox is Empty and nothing to show\n * @return {boolean}\n */\n public get isEmpty(): boolean {\n return this.displayedToolsCount === 0;\n }\n\n /**\n * Opening state\n * @type {boolean}\n */\n public opened: boolean = false;\n\n /**\n * HTMLElements used for Toolbox UI\n */\n public nodes: {\n toolbox: HTMLElement,\n buttons: HTMLElement[],\n } = {\n toolbox: null,\n buttons: [],\n };\n\n /**\n * How many tools displayed in Toolbox\n * @type {number}\n */\n private displayedToolsCount: number = 0;\n\n /**\n * Instance of class that responses for leafing buttons by arrows/tab\n * @type {Flipper|null}\n */\n private flipper: Flipper = null;\n\n /**\n * Makes the Toolbox\n */\n public make(): void {\n this.nodes.toolbox = $.make('div', this.CSS.toolbox);\n $.append(this.Editor.Toolbar.nodes.content, this.nodes.toolbox);\n\n this.addTools();\n this.enableFlipper();\n }\n\n /**\n * Toolbox Tool's button click handler\n *\n * @param {MouseEvent|KeyboardEvent} event\n * @param {string} toolName\n */\n public toolButtonActivate(event: MouseEvent|KeyboardEvent, toolName: string): void {\n const tool = this.Editor.Tools.toolsClasses[toolName] as BlockToolConstructable;\n\n this.insertNewBlock(tool, toolName);\n }\n\n /**\n * Open Toolbox with Tools\n */\n public open(): void {\n if (this.isEmpty) {\n return;\n }\n\n this.Editor.UI.nodes.wrapper.classList.add(this.CSS.openedToolbarHolderModifier);\n this.nodes.toolbox.classList.add(this.CSS.toolboxOpened);\n\n this.opened = true;\n this.flipper.activate();\n }\n\n /**\n * Close Toolbox\n */\n public close(): void {\n this.nodes.toolbox.classList.remove(this.CSS.toolboxOpened);\n this.Editor.UI.nodes.wrapper.classList.remove(this.CSS.openedToolbarHolderModifier);\n\n this.opened = false;\n this.flipper.deactivate();\n }\n\n /**\n * Close Toolbox\n */\n public toggle(): void {\n if (!this.opened) {\n this.open();\n } else {\n this.close();\n }\n }\n\n /**\n * Iterates available tools and appends them to the Toolbox\n */\n private addTools(): void {\n const tools = this.Editor.Tools.available;\n\n for (const toolName in tools) {\n if (tools.hasOwnProperty(toolName)) {\n this.addTool(toolName, tools[toolName] as BlockToolConstructable);\n }\n }\n }\n\n private makeapistackToolboxButton(button: HTMLElement, buttonContent: any): HTMLElement {\n button.innerHTML += `${buttonContent.icon}
\n ${buttonContent.name}`;\n return button;\n }\n\n /**\n * Append Tool to the Toolbox\n *\n * @param {string} toolName - tool name\n * @param {BlockToolConstructable} tool - tool class\n */\n private addTool(toolName: string, tool: BlockToolConstructable): void {\n const internalSettings = this.Editor.Tools.INTERNAL_SETTINGS;\n const userSettings = this.Editor.Tools.USER_SETTINGS;\n\n const toolToolboxSettings = tool[internalSettings.TOOLBOX];\n\n /**\n * Skip tools that don't pass 'toolbox' property\n */\n if (_.isEmpty(toolToolboxSettings)) {\n return;\n }\n\n if (toolToolboxSettings && !toolToolboxSettings.icon) {\n _.log('Toolbar icon is missed. Tool %o skipped', 'warn', toolName);\n return;\n }\n\n /**\n * @todo Add checkup for the render method\n */\n // if (typeof tool.render !== 'function') {\n // _.log('render method missed. Tool %o skipped', 'warn', tool);\n // return;\n // }\n\n const userToolboxSettings = this.Editor.Tools.getToolSettings(toolName)[userSettings.TOOLBOX] || {};\n\n const button = this.makeapistackToolboxButton($.make('div', [ this.CSS.toolboxButtonApistack ]), {\n name: toolName,\n icon: userToolboxSettings.icon || toolToolboxSettings.icon,\n });\n\n button.dataset.tool = toolName;\n\n $.append(this.nodes.toolbox, button);\n\n this.nodes.toolbox.appendChild(button);\n this.nodes.buttons.push(button);\n\n /**\n * Add click listener\n */\n this.Editor.Listeners.on(button, 'click', (event: KeyboardEvent|MouseEvent) => {\n this.toolButtonActivate(event, toolName);\n });\n\n /**\n * Add listeners to show/hide toolbox tooltip\n */\n const tooltipContent = this.drawTooltip(toolName);\n\n this.Editor.Tooltip.onHover(button, tooltipContent, {\n placement: 'bottom',\n hidingDelay: 200,\n });\n\n /**\n * Enable shortcut\n */\n const toolSettings = this.Editor.Tools.getToolSettings(toolName);\n\n if (toolSettings && toolSettings[this.Editor.Tools.USER_SETTINGS.SHORTCUT]) {\n this.enableShortcut(tool, toolName, toolSettings[this.Editor.Tools.USER_SETTINGS.SHORTCUT]);\n }\n\n /** Increment Tools count */\n this.displayedToolsCount++;\n }\n\n /**\n * Draw tooltip for toolbox tools\n *\n * @param {String} toolName - toolbox tool name\n * @return { HTMLElement }\n */\n private drawTooltip(toolName: string): HTMLElement {\n const toolSettings = this.Editor.Tools.getToolSettings(toolName);\n const toolboxSettings = this.Editor.Tools.available[toolName][this.Editor.Tools.INTERNAL_SETTINGS.TOOLBOX] || {};\n const userToolboxSettings = toolSettings.toolbox || {};\n const name = userToolboxSettings.title || toolboxSettings.title || toolName;\n\n let shortcut = toolSettings[this.Editor.Tools.USER_SETTINGS.SHORTCUT];\n\n const tooltip = $.make('div', this.CSS.buttonTooltip);\n const hint = document.createTextNode(_.capitalize(name));\n\n tooltip.appendChild(hint);\n\n if (shortcut) {\n shortcut = _.beautifyShortcut(shortcut);\n\n tooltip.appendChild($.make('div', this.CSS.buttonShortcut, {\n textContent: shortcut,\n }));\n }\n\n return tooltip;\n }\n\n /**\n * Enable shortcut Block Tool implemented shortcut\n * @param {BlockToolConstructable} tool - Tool class\n * @param {String} toolName - Tool name\n * @param {String} shortcut - shortcut according to the ShortcutData Module format\n */\n private enableShortcut(tool: BlockToolConstructable, toolName: string, shortcut: string) {\n this.Editor.Shortcuts.add({\n name: shortcut,\n handler: (event: KeyboardEvent) => {\n event.preventDefault();\n this.insertNewBlock(tool, toolName);\n },\n });\n }\n\n /**\n * Creates Flipper instance to be able to leaf tools\n */\n private enableFlipper(): void {\n const tools = Array.from(this.nodes.toolbox.childNodes) as HTMLElement[];\n this.flipper = new Flipper({\n items: tools,\n focusedItemClass: this.CSS.toolboxButtonApistackActive,\n });\n }\n\n /**\n * Inserts new block\n * Can be called when button clicked on Toolbox or by ShortcutData\n *\n * @param {BlockToolConstructable} tool - Tool Class\n * @param {String} toolName - Tool name\n */\n private insertNewBlock(tool: BlockToolConstructable, toolName: string) {\n const {BlockManager, Caret} = this.Editor;\n /**\n * @type {Block}\n */\n const {currentBlock} = BlockManager;\n\n let newBlock;\n\n if (currentBlock.isEmpty) {\n newBlock = BlockManager.replace(toolName);\n } else {\n newBlock = BlockManager.insert(toolName);\n }\n\n /**\n * Apply callback before inserting html\n */\n newBlock.call(BlockToolAPI.APPEND_CALLBACK);\n\n this.Editor.Caret.setToBlock(newBlock);\n\n /** If new block doesn't contain inpus, insert new paragraph above */\n if (newBlock.inputs.length === 0) {\n if (newBlock === BlockManager.lastBlock) {\n BlockManager.insertAtEnd();\n Caret.setToBlock(BlockManager.lastBlock);\n } else {\n Caret.setToBlock(BlockManager.nextBlock);\n }\n }\n\n /**\n * close toolbar when node is changed\n */\n this.Editor.Toolbar.close();\n }\n}\n","import Paragraph from '../tools/paragraph/dist/bundle';\nimport Module from '../__module';\nimport * as _ from '../utils';\nimport {\n BlockToolConstructable,\n InlineTool,\n InlineToolConstructable, Tool,\n ToolConfig,\n ToolConstructable,\n ToolSettings,\n} from '../../../types';\nimport BoldInlineTool from '../inline-tools/inline-tool-bold';\nimport ItalicInlineTool from '../inline-tools/inline-tool-italic';\nimport LinkInlineTool from '../inline-tools/inline-tool-link';\nimport Stub from '../tools/stub';\n\n/**\n * @module Editor.js Tools Submodule\n *\n * Creates Instances from Plugins and binds external config to the instances\n */\n\n/**\n * Class properties:\n *\n * @typedef {Tools} Tools\n * @property {Tools[]} toolsAvailable - available Tools\n * @property {Tools[]} toolsUnavailable - unavailable Tools\n * @property {object} toolsClasses - all classes\n * @property {object} toolsSettings - Tools settings\n * @property {EditorConfig} config - Editor config\n */\nexport default class Tools extends Module {\n\n /**\n * Name of Stub Tool\n * Stub Tool is used to substitute unavailable block Tools and store their data\n * @type {string}\n */\n public stubTool = 'stub';\n\n /**\n * Returns available Tools\n * @return {Tool[]}\n */\n public get available(): {[name: string]: ToolConstructable} {\n return this.toolsAvailable;\n }\n\n /**\n * Returns unavailable Tools\n * @return {Tool[]}\n */\n public get unavailable(): {[name: string]: ToolConstructable} {\n return this.toolsUnavailable;\n }\n\n /**\n * Return Tools for the Inline Toolbar\n * @return {Object} - object of Inline Tool's classes\n */\n public get inline(): {[name: string]: ToolConstructable} {\n if (this._inlineTools) {\n return this._inlineTools;\n }\n\n const tools = Object.entries(this.available).filter( ([name, tool]) => {\n if (!tool[this.INTERNAL_SETTINGS.IS_INLINE]) {\n return false;\n }\n\n /**\n * Some Tools validation\n */\n const inlineToolRequiredMethods = ['render', 'surround', 'checkState'];\n const notImplementedMethods = inlineToolRequiredMethods.filter( (method) => !this.constructInline(tool)[method]);\n\n if (notImplementedMethods.length) {\n _.log(\n `Incorrect Inline Tool: ${tool.name}. Some of required methods is not implemented %o`,\n 'warn',\n notImplementedMethods,\n );\n return false;\n }\n\n return true;\n });\n\n /**\n * collected inline tools with key of tool name\n */\n const result = {};\n\n tools.forEach(([name, tool]) => result[name] = tool);\n\n /**\n * Cache prepared Tools\n */\n this._inlineTools = result;\n\n return this._inlineTools;\n }\n\n /**\n * Return editor block tools\n */\n public get blockTools(): {[name: string]: BlockToolConstructable} {\n // eslint-disable-next-line no-unused-vars\n const tools = Object.entries(this.available).filter( ([name, tool]) => {\n return !tool[this.INTERNAL_SETTINGS.IS_INLINE];\n });\n\n /**\n * collected block tools with key of tool name\n */\n const result = {};\n\n tools.forEach(([name, tool]) => result[name] = tool);\n\n return result;\n }\n\n /**\n * Constant for available Tools internal settings provided by Tool developer\n *\n * @return {object}\n */\n public get INTERNAL_SETTINGS() {\n return {\n IS_ENABLED_LINE_BREAKS: 'enableLineBreaks',\n IS_INLINE: 'isInline',\n TITLE: 'title', // for Inline Tools. Block Tools can pass title along with icon through the 'toolbox' static prop.\n SHORTCUT: 'shortcut',\n TOOLBOX: 'toolbox',\n SANITIZE_CONFIG: 'sanitize',\n CONVERSION_CONFIG: 'conversionConfig',\n };\n }\n\n /**\n * Constant for available Tools settings provided by user\n *\n * return {object}\n */\n public get USER_SETTINGS() {\n return {\n SHORTCUT: 'shortcut',\n TOOLBOX: 'toolbox',\n ENABLED_INLINE_TOOLS: 'inlineToolbar',\n CONFIG: 'config',\n };\n }\n\n /**\n * Map {name: Class, ...} where:\n * name — block type name in JSON. Got from EditorConfig.tools keys\n * @type {Object}\n */\n public readonly toolsClasses: {[name: string]: ToolConstructable} = {};\n\n /**\n * Tools` classes available to use\n */\n private readonly toolsAvailable: {[name: string]: ToolConstructable} = {};\n\n /**\n * Tools` classes not available to use because of preparation failure\n */\n private readonly toolsUnavailable: {[name: string]: ToolConstructable} = {};\n\n /**\n * Tools settings in a map {name: settings, ...}\n * @type {Object}\n */\n private readonly toolsSettings: {[name: string]: ToolSettings} = {};\n\n /**\n * Cache for the prepared inline tools\n * @type {null|object}\n * @private\n */\n private _inlineTools: {[name: string]: ToolConstructable} = {};\n\n /**\n * @constructor\n *\n * @param {EditorConfig} config\n */\n constructor({config}) {\n super({config});\n\n this.toolsClasses = {};\n\n this.toolsSettings = {};\n\n /**\n * Available tools list\n * {name: Class, ...}\n * @type {Object}\n */\n this.toolsAvailable = {};\n\n /**\n * Tools that rejected a prepare method\n * {name: Class, ... }\n * @type {Object}\n */\n this.toolsUnavailable = {};\n\n this._inlineTools = null;\n }\n\n /**\n * Creates instances via passed or default configuration\n * @return {Promise}\n */\n public prepare() {\n this.validateTools();\n\n /**\n * Assign internal tools\n */\n this.config.tools = _.deepMerge({}, this.internalTools, this.config.tools);\n\n if (!this.config.hasOwnProperty('tools') || Object.keys(this.config.tools).length === 0) {\n throw Error('Can\\'t start without tools');\n }\n\n /**\n * Save Tools settings to a map\n */\n for (const toolName in this.config.tools) {\n /**\n * If Tool is an object not a Tool's class then\n * save class and settings separately\n */\n if (typeof this.config.tools[toolName] === 'object') {\n /**\n * Save Tool's class from 'class' field\n * @type {Tool}\n */\n this.toolsClasses[toolName] = (this.config.tools[toolName] as ToolSettings).class;\n\n /**\n * Save Tool's settings\n * @type {ToolSettings}\n */\n this.toolsSettings[toolName] = this.config.tools[toolName] as ToolSettings;\n\n /**\n * Remove Tool's class from settings\n */\n delete this.toolsSettings[toolName].class;\n } else {\n /**\n * Save Tool's class\n * @type {Tool}\n */\n this.toolsClasses[toolName] = this.config.tools[toolName] as ToolConstructable;\n\n /**\n * Set empty settings for Block by default\n * @type {{}}\n */\n this.toolsSettings[toolName] = {class: this.config.tools[toolName] as ToolConstructable};\n }\n }\n\n /**\n * getting classes that has prepare method\n */\n const sequenceData = this.getListOfPrepareFunctions();\n\n /**\n * if sequence data contains nothing then resolve current chain and run other module prepare\n */\n if (sequenceData.length === 0) {\n return Promise.resolve();\n }\n\n /**\n * to see how it works {@link Util#sequence}\n */\n return _.sequence(sequenceData, (data: any) => {\n this.success(data);\n }, (data) => {\n this.fallback(data);\n });\n }\n\n /**\n * @param {ChainData.data} data - append tool to available list\n */\n public success(data) {\n this.toolsAvailable[data.toolName] = this.toolsClasses[data.toolName];\n }\n\n /**\n * @param {ChainData.data} data - append tool to unavailable list\n */\n public fallback(data) {\n this.toolsUnavailable[data.toolName] = this.toolsClasses[data.toolName];\n }\n\n /**\n * Return Tool`s instance\n *\n * @param {String} tool — tool name\n * @param {BlockToolData} data — initial data\n * @return {BlockTool}\n */\n public construct(tool, data) {\n const plugin = this.toolsClasses[tool];\n\n /**\n * Configuration to be passed to the Tool's constructor\n */\n const config = this.toolsSettings[tool][this.USER_SETTINGS.CONFIG] || {};\n\n // Pass placeholder to initial Block config\n if (tool === this.config.initialBlock && !config.placeholder) {\n config.placeholder = this.config.placeholder;\n }\n\n /**\n * @type {{api: API, config: ({}), data: BlockToolData}}\n */\n const constructorOptions = {\n api: this.Editor.API.methods,\n config,\n data,\n };\n\n return new plugin(constructorOptions);\n }\n\n /**\n * Return Inline Tool's instance\n *\n * @param {InlineTool} tool\n * @param {ToolSettings} toolSettings\n * @return {InlineTool} — instance\n */\n public constructInline(tool: InlineToolConstructable, toolSettings: ToolSettings = {} as ToolSettings): InlineTool {\n /**\n * @type {{api: API}}\n */\n const constructorOptions = {\n api: this.Editor.API.methods,\n config: (toolSettings[this.USER_SETTINGS.CONFIG] || {}) as ToolSettings,\n };\n\n return new tool(constructorOptions) as InlineTool;\n }\n\n /**\n * Check if passed Tool is an instance of Initial Block Tool\n * @param {Tool} tool - Tool to check\n * @return {Boolean}\n */\n public isInitial(tool) {\n return tool instanceof this.available[this.config.initialBlock];\n }\n\n /**\n * Return Tool's config by name\n * @param {string} toolName\n * @return {ToolSettings}\n */\n public getToolSettings(toolName): ToolSettings {\n return this.toolsSettings[toolName];\n }\n\n /**\n * Binds prepare function of plugins with user or default config\n * @return {Array} list of functions that needs to be fired sequentially\n */\n private getListOfPrepareFunctions(): Array<{\n function: (data: {toolName: string, config: ToolConfig}) => void,\n data: {toolName: string, config: ToolConfig},\n }> {\n const toolPreparationList: Array<{\n function: (data: {toolName: string, config: ToolConfig}) => void,\n data: {toolName: string, config: ToolConfig}}\n > = [];\n\n for (const toolName in this.toolsClasses) {\n if (this.toolsClasses.hasOwnProperty(toolName)) {\n const toolClass = this.toolsClasses[toolName];\n\n if (typeof toolClass.prepare === 'function') {\n toolPreparationList.push({\n function: toolClass.prepare,\n data: {\n toolName,\n config: this.toolsSettings[toolName][this.USER_SETTINGS.CONFIG],\n },\n });\n } else {\n /**\n * If Tool hasn't a prepare method, mark it as available\n */\n this.toolsAvailable[toolName] = toolClass;\n }\n }\n }\n\n return toolPreparationList;\n }\n\n /**\n * Validate Tools configuration objects and throw Error for user if it is invalid\n */\n private validateTools() {\n /**\n * Check Tools for a class containing\n */\n for (const toolName in this.config.tools) {\n if (this.config.tools.hasOwnProperty(toolName)) {\n if (toolName in this.internalTools) {\n return;\n }\n\n const tool = this.config.tools[toolName];\n\n if (!_.isFunction(tool) && !_.isFunction((tool as ToolSettings).class)) {\n throw Error(\n `Tool «${toolName}» must be a constructor function or an object with function in the «class» property`,\n );\n }\n }\n }\n }\n\n /**\n * Returns internal tools\n * Includes Bold, Italic, Link and Paragraph\n */\n get internalTools() {\n return {\n bold: {class: BoldInlineTool},\n italic: {class: ItalicInlineTool},\n link: {class: LinkInlineTool},\n paragraph: {\n class: Paragraph,\n inlineToolbar: true,\n },\n stub: {class: Stub},\n };\n }\n}\n","import Module from '../__module';\n\n/**\n * Use external module CodeX Tooltip\n */\nimport CodeXTooltips, { TooltipContent, TooltipOptions } from 'codex-tooltip';\nimport {ModuleConfig} from '../../types-internal/module-config';\n\n/**\n * Tooltip\n *\n * Decorates any tooltip module like adapter\n */\nexport default class Tooltip extends Module {\n\n /**\n * Tooltips lib: CodeX Tooltips\n * @see https://github.com/codex-team/codex.tooltips\n */\n private lib: CodeXTooltips = new CodeXTooltips();\n\n /**\n * @constructor\n * @param {EditorConfig}\n */\n constructor({config}: ModuleConfig) {\n super({config});\n }\n\n /**\n * Shows tooltip on element with passed HTML content\n *\n * @param {HTMLElement} element - any HTML element in DOM\n * @param {TooltipContent} content - tooltip's content\n * @param {TooltipOptions} options - showing settings\n */\n public show(element: HTMLElement, content: TooltipContent, options?: TooltipOptions): void {\n this.lib.show(element, content, options);\n }\n\n /**\n * Hides tooltip\n */\n public hide(): void {\n this.lib.hide();\n }\n\n /**\n * Binds 'mouseenter' and 'mouseleave' events that shows/hides the Tooltip\n *\n * @param {HTMLElement} element - any HTML element in DOM\n * @param {TooltipContent} content - tooltip's content\n * @param {TooltipOptions} options - showing settings\n */\n public onHover(element: HTMLElement, content: TooltipContent, options?: TooltipOptions): void {\n this.lib.onHover(element, content, options);\n }\n}\n","/**\n * Prebuilded sprite of SVG icons\n */\nimport sprite from '../../../dist/sprite.svg';\n\n/**\n * Module UI\n *\n * @type {UI}\n */\nimport Module from '../__module';\nimport $ from '../dom';\nimport * as _ from '../utils';\n\nimport Selection from '../selection';\nimport Block from '../block';\nimport Flipper from '../flipper';\n\n/**\n * @class\n *\n * @classdesc Makes Editor.js UI:\n * \n * \n * \n * \n * \n *\n * @typedef {UI} UI\n * @property {EditorConfig} config - editor configuration {@link EditorJS#configuration}\n * @property {Object} Editor - available editor modules {@link EditorJS#moduleInstances}\n * @property {Object} nodes -\n * @property {Element} nodes.holder - element where we need to append redactor\n * @property {Element} nodes.wrapper - \n * @property {Element} nodes.redactor - \n */\nexport default class UI extends Module {\n\n /**\n * Editor.js UI CSS class names\n * @return {{editorWrapper: string, editorZone: string}}\n */\n public get CSS(): {\n editorWrapper: string, editorWrapperNarrow: string, editorZone: string, editorZoneHidden: string,\n editorLoader: string, editorEmpty: string,\n } {\n return {\n editorWrapper : 'codex-editor',\n editorWrapperNarrow : 'codex-editor--narrow',\n editorZone : 'codex-editor__redactor',\n editorZoneHidden : 'codex-editor__redactor--hidden',\n editorLoader : 'codex-editor__loader',\n editorEmpty : 'codex-editor--empty',\n };\n }\n\n /**\n * Return Width of center column of Editor\n * @return {DOMRect}\n */\n public get contentRect(): DOMRect {\n if (this.contentRectCache) {\n return this.contentRectCache;\n }\n\n const someBlock = this.nodes.wrapper.querySelector(`.${Block.CSS.content}`);\n\n /**\n * When Editor is not ready, there is no Blocks, so return the default value\n */\n if (!someBlock) {\n return {\n width: 650,\n left: 0,\n right: 0,\n } as DOMRect;\n }\n\n this.contentRectCache = someBlock.getBoundingClientRect() as DOMRect;\n\n return this.contentRectCache;\n }\n\n /**\n * Flag that became true on mobile viewport\n * @type {boolean}\n */\n public isMobile: boolean = false;\n\n /**\n * HTML Elements used for UI\n */\n public nodes: { [key: string]: HTMLElement } = {\n holder: null,\n wrapper: null,\n redactor: null,\n };\n\n /**\n * Cache for center column rectangle info\n * Invalidates on window resize\n * @type {DOMRect}\n */\n private contentRectCache: DOMRect = undefined;\n\n /**\n * Handle window resize only when it finished\n * @type {() => void}\n */\n private resizeDebouncer: () => void = _.debounce(() => {\n this.windowResize();\n }, 200);\n\n /**\n * Adds loader to editor while content is not ready\n */\n public addLoader(): void {\n this.nodes.loader = $.make('div', this.CSS.editorLoader);\n this.nodes.wrapper.prepend(this.nodes.loader);\n this.nodes.redactor.classList.add(this.CSS.editorZoneHidden);\n }\n\n /**\n * Removes loader when content has loaded\n */\n public removeLoader(): void {\n this.nodes.loader.remove();\n this.nodes.redactor.classList.remove(this.CSS.editorZoneHidden);\n }\n\n /**\n * Making main interface\n */\n public async prepare(): Promise {\n /**\n * Detect mobile version\n */\n this.checkIsMobile();\n\n /**\n * Make main UI elements\n */\n await this.make();\n\n /**\n * Loader for rendering process\n */\n this.addLoader();\n\n /**\n * Append SVG sprite\n */\n await this.appendSVGSprite();\n\n /**\n * Make toolbar\n */\n await this.Editor.Toolbar.make();\n\n /**\n * Make the Inline toolbar\n */\n await this.Editor.InlineToolbar.make();\n\n /**\n * Load and append CSS\n */\n await this.loadStyles();\n\n /**\n * Bind events for the UI elements\n */\n await this.bindEvents();\n }\n\n /**\n * Check if Editor is empty and set CSS class to wrapper\n */\n public checkEmptiness(): void {\n const {BlockManager} = this.Editor;\n\n this.nodes.wrapper.classList.toggle(this.CSS.editorEmpty, BlockManager.isEditorEmpty);\n }\n\n /**\n * Check if one of Toolbar is opened\n * Used to prevent global keydowns (for example, Enter) conflicts with Enter-on-toolbar\n * @return {boolean}\n */\n public get someToolbarOpened(): boolean {\n const { Toolbox, BlockSettings, InlineToolbar, ConversionToolbar } = this.Editor;\n\n return BlockSettings.opened || InlineToolbar.opened || ConversionToolbar.opened || Toolbox.opened;\n }\n\n /**\n * Check for some Flipper-buttons is under focus\n */\n public get someFlipperButtonFocused(): boolean {\n return Object.entries(this.Editor).filter(([moduleName, moduleClass]) => {\n return moduleClass.flipper instanceof Flipper;\n }).some(([moduleName, moduleClass]) => {\n return moduleClass.flipper.currentItem;\n });\n }\n\n /**\n * Clean editor`s UI\n */\n public destroy(): void {\n this.nodes.holder.innerHTML = '';\n }\n\n /**\n * Close all Editor's toolbars\n */\n public closeAllToolbars(): void {\n const { Toolbox, BlockSettings, InlineToolbar, ConversionToolbar } = this.Editor;\n\n BlockSettings.close();\n InlineToolbar.close();\n ConversionToolbar.close();\n Toolbox.close();\n }\n\n /**\n * Check for mobile mode and cache a result\n */\n private checkIsMobile() {\n this.isMobile = window.innerWidth < 650;\n }\n\n /**\n * Makes Editor.js interface\n * @return {Promise}\n */\n private async make(): Promise {\n /**\n * Element where we need to append Editor.js\n * @type {Element}\n */\n this.nodes.holder = $.getHolder(this.config.holder);\n\n /**\n * Create and save main UI elements\n */\n this.nodes.wrapper = $.make('div', this.CSS.editorWrapper);\n this.nodes.redactor = $.make('div', this.CSS.editorZone);\n\n /**\n * If Editor has injected into the narrow container, enable Narrow Mode\n */\n if (this.nodes.holder.offsetWidth < this.contentRect.width) {\n this.nodes.wrapper.classList.add(this.CSS.editorWrapperNarrow);\n }\n\n /**\n * Set customizable bottom zone height\n */\n this.nodes.redactor.style.paddingBottom = this.config.minHeight + 'px';\n\n this.nodes.wrapper.appendChild(this.nodes.redactor);\n this.nodes.holder.appendChild(this.nodes.wrapper);\n\n }\n\n /**\n * Appends CSS\n */\n private loadStyles(): void {\n /**\n * Load CSS\n */\n const styles = require('../../styles/main.css');\n\n /**\n * Make tag\n */\n const tag = $.make('style', null, {\n textContent: styles.toString(),\n });\n\n /**\n * Append styles at the top of HEAD tag\n */\n $.prepend(document.head, tag);\n }\n\n /**\n * Bind events on the Editor.js interface\n */\n private bindEvents(): void {\n this.Editor.Listeners.on(\n this.nodes.redactor,\n 'click',\n (event) => this.redactorClicked(event as MouseEvent),\n false,\n );\n this.Editor.Listeners.on(this.nodes.redactor,\n 'mousedown',\n (event) => this.documentTouched(event as MouseEvent),\n true,\n );\n this.Editor.Listeners.on(this.nodes.redactor,\n 'touchstart',\n (event) => this.documentTouched(event as MouseEvent),\n true,\n );\n\n this.Editor.Listeners.on(document, 'keydown', (event) => this.documentKeydown(event as KeyboardEvent), true);\n this.Editor.Listeners.on(document, 'click', (event) => this.documentClicked(event as MouseEvent), true);\n\n /**\n * Handle selection change to manipulate Inline Toolbar appearance\n */\n this.Editor.Listeners.on(document, 'selectionchange', (event: Event) => {\n this.selectionChanged(event);\n }, true);\n\n this.Editor.Listeners.on(window, 'resize', () => {\n this.resizeDebouncer();\n }, {\n passive: true,\n });\n }\n\n /**\n * Resize window handler\n */\n private windowResize(): void {\n /**\n * Invalidate content zone size cached, because it may be changed\n */\n this.contentRectCache = null;\n\n /**\n * Detect mobile version\n */\n this.checkIsMobile();\n }\n\n /**\n * All keydowns on document\n * @param {Event} event\n */\n private documentKeydown(event: KeyboardEvent): void {\n switch (event.keyCode) {\n case _.keyCodes.ENTER:\n this.enterPressed(event);\n break;\n case _.keyCodes.BACKSPACE:\n this.backspacePressed(event);\n break;\n default:\n this.defaultBehaviour(event);\n break;\n }\n }\n\n /**\n * Ignore all other document's keydown events\n * @param {KeyboardEvent} event\n */\n private defaultBehaviour(event: KeyboardEvent): void {\n const keyDownOnEditor = (event.target as HTMLElement).closest(`.${this.CSS.editorWrapper}`);\n const {currentBlock} = this.Editor.BlockManager;\n const isMetaKey = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;\n\n /**\n * Ignore keydowns on editor and meta keys\n */\n if (keyDownOnEditor || (currentBlock && isMetaKey)) {\n return;\n }\n\n /**\n * Remove all highlights and remove caret\n */\n this.Editor.BlockManager.dropPointer();\n\n /**\n * Close Toolbar\n */\n this.Editor.Toolbar.close();\n }\n\n /**\n * @param {KeyboardEvent} event\n */\n private backspacePressed(event: KeyboardEvent): void {\n const {BlockManager, BlockSelection, Caret} = this.Editor;\n\n if (BlockSelection.anyBlockSelected) {\n const selectionPositionIndex = BlockManager.removeSelectedBlocks();\n Caret.setToBlock(BlockManager.insertInitialBlockAtIndex(selectionPositionIndex, true), Caret.positions.START);\n\n /** Clear selection */\n BlockSelection.clearSelection(event);\n\n /**\n * Stop propagations\n * Manipulation with BlockSelections is handled in global backspacePress because they may occur\n * with CMD+A or RectangleSelection and they can be handled on document event\n */\n event.preventDefault();\n event.stopPropagation();\n event.stopImmediatePropagation();\n }\n }\n\n /**\n * Enter pressed on document\n * @param event\n */\n private enterPressed(event: KeyboardEvent): void {\n const { BlockManager, BlockSelection, Caret } = this.Editor;\n const hasPointerToBlock = BlockManager.currentBlockIndex >= 0;\n\n if (BlockSelection.anyBlockSelected) {\n const selectionPositionIndex = BlockManager.removeSelectedBlocks();\n Caret.setToBlock(BlockManager.insertInitialBlockAtIndex(selectionPositionIndex, true), Caret.positions.START);\n\n /** Clear selection */\n BlockSelection.clearSelection(event);\n\n /**\n * Stop propagations\n * Manipulation with BlockSelections is handled in global enterPress because they may occur\n * with CMD+A or RectangleSelection\n */\n event.preventDefault();\n event.stopImmediatePropagation();\n event.stopPropagation();\n return;\n }\n\n /**\n * If Caret is not set anywhere, event target on Enter is always Element that we handle\n * In our case it is document.body\n *\n * So, BlockManager points some Block and Enter press is on Body\n * We can create a new block\n */\n if (!this.someToolbarOpened && hasPointerToBlock && (event.target as HTMLElement).tagName === 'BODY') {\n /**\n * Insert initial typed Block\n */\n const newBlock = this.Editor.BlockManager.insert();\n\n this.Editor.Caret.setToBlock(newBlock);\n\n /**\n * And highlight\n */\n this.Editor.BlockManager.highlightCurrentNode();\n\n /**\n * Move toolbar and show plus button because new Block is empty\n */\n this.Editor.Toolbar.move();\n this.Editor.Toolbar.plusButton.show();\n }\n\n this.Editor.BlockSelection.clearSelection(event);\n }\n\n /**\n * All clicks on document\n * @param {MouseEvent} event - Click\n */\n private documentClicked(event: MouseEvent): void {\n /**\n * Sometimes we emulate click on some UI elements, for example by Enter on Block Settings button\n * We don't need to handle such events, because they handled in other place.\n */\n if (!event.isTrusted) {\n return;\n }\n /**\n * Close Inline Toolbar when nothing selected\n * Do not fire check on clicks at the Inline Toolbar buttons\n */\n const target = event.target as HTMLElement;\n const clickedInsideOfEditor = this.nodes.holder.contains(target) || Selection.isAtEditor;\n\n if (!clickedInsideOfEditor) {\n /**\n * Clear highlightings and pointer on BlockManager\n *\n * Current page might contain several instances\n * Click between instances MUST clear focus, pointers and close toolbars\n */\n this.Editor.BlockManager.dropPointer();\n this.Editor.InlineToolbar.close();\n this.Editor.Toolbar.close();\n this.Editor.ConversionToolbar.close();\n }\n\n /**\n * Clear Selection if user clicked somewhere\n */\n if (!this.Editor.CrossBlockSelection.isCrossBlockSelectionStarted) {\n this.Editor.BlockSelection.clearSelection(event);\n }\n\n /**\n * Clear Selection if user clicked somewhere\n */\n if (!this.Editor.CrossBlockSelection.isCrossBlockSelectionStarted) {\n this.Editor.BlockSelection.clearSelection(event);\n }\n }\n\n /**\n * First touch on editor\n * Fired before click\n *\n * Used to change current block — we need to do it before 'selectionChange' event.\n * Also:\n * - Move and show the Toolbar\n * - Set a Caret\n */\n private documentTouched(event: MouseEvent | TouchEvent): void {\n let clickedNode = event.target as HTMLElement;\n\n /**\n * If click was fired is on Editor`s wrapper, try to get clicked node by elementFromPoint method\n */\n if (clickedNode === this.nodes.redactor) {\n const clientX = event instanceof MouseEvent ? event.clientX : event.touches[0].clientX;\n const clientY = event instanceof MouseEvent ? event.clientY : event.touches[0].clientY;\n\n clickedNode = document.elementFromPoint(clientX, clientY) as HTMLElement;\n }\n\n /**\n * Select clicked Block as Current\n */\n try {\n /**\n * Renew Current Block\n */\n this.Editor.BlockManager.setCurrentBlockByChildNode(clickedNode);\n\n /**\n * Highlight Current Node\n */\n this.Editor.BlockManager.highlightCurrentNode();\n } catch (e) {\n /**\n * If clicked outside first-level Blocks and it is not RectSelection, set Caret to the last empty Block\n */\n if (!this.Editor.RectangleSelection.isRectActivated()) {\n this.Editor.Caret.setToTheLastBlock();\n }\n }\n\n /**\n * Move and open toolbar\n */\n this.Editor.Toolbar.open();\n\n /**\n * Hide the Plus Button\n */\n this.Editor.Toolbar.plusButton.hide();\n }\n\n /**\n * All clicks on the redactor zone\n *\n * @param {MouseEvent} event\n *\n * @description\n * - By clicks on the Editor's bottom zone:\n * - if last Block is empty, set a Caret to this\n * - otherwise, add a new empty Block and set a Caret to that\n */\n private redactorClicked(event: MouseEvent): void {\n if (!Selection.isCollapsed) {\n return;\n }\n\n // event.stopImmediatePropagation();\n // event.stopPropagation();\n\n if (!this.Editor.BlockManager.currentBlock) {\n this.Editor.BlockManager.insert();\n }\n\n /**\n * Show the Plus Button if:\n * - Block is an initial-block (Text)\n * - Block is empty\n */\n const isInitialBlock = this.Editor.Tools.isInitial(this.Editor.BlockManager.currentBlock.tool);\n\n if (isInitialBlock) {\n /**\n * Check isEmpty only for paragraphs to prevent unnecessary tree-walking on Tools with many nodes (for ex. Table)\n */\n const isEmptyBlock = this.Editor.BlockManager.currentBlock.isEmpty;\n\n if (isEmptyBlock) {\n this.Editor.Toolbar.plusButton.show();\n }\n }\n }\n\n /**\n * Handle selection changes on mobile devices\n * Uses for showing the Inline Toolbar\n * @param {Event} event\n */\n private selectionChanged(event: Event): void {\n const focusedElement = Selection.anchorElement as Element;\n\n /**\n * Event can be fired on clicks at the Editor elements, for example, at the Inline Toolbar\n * We need to skip such firings\n */\n if (!focusedElement || !focusedElement.closest(`.${Block.CSS.content}`)) {\n\n /**\n * If new selection is not on Inline Toolbar, we need to close it\n */\n if (!this.Editor.InlineToolbar.containsNode(focusedElement)) {\n this.Editor.InlineToolbar.close();\n }\n\n return;\n }\n\n this.Editor.InlineToolbar.tryToShow(true);\n }\n\n /**\n * Append prebuilt sprite with SVG icons\n */\n private appendSVGSprite(): void {\n const spriteHolder = $.make('div');\n\n spriteHolder.hidden = true;\n spriteHolder.style.display = 'none';\n spriteHolder.innerHTML = sprite;\n\n $.append(this.nodes.wrapper, spriteHolder);\n }\n}\n","'use strict';\n\n/**\n * Extend Element interface to include prefixed and experimental properties\n */\ninterface Element {\n matchesSelector: (selector: string) => boolean;\n mozMatchesSelector: (selector: string) => boolean;\n msMatchesSelector: (selector: string) => boolean;\n oMatchesSelector: (selector: string) => boolean;\n\n prepend: (...nodes: Array) => void;\n append: (...nodes: Array) => void;\n}\n\n/**\n * The Element.matches() method returns true if the element\n * would be selected by the specified selector string;\n * otherwise, returns false.\n *\n * {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/matches#Polyfill}\n */\nif (!Element.prototype.matches) {\n Element.prototype.matches = Element.prototype.matchesSelector ||\n Element.prototype.mozMatchesSelector ||\n Element.prototype.msMatchesSelector ||\n Element.prototype.oMatchesSelector ||\n Element.prototype.webkitMatchesSelector ||\n function(s) {\n const matches = (this.document || this.ownerDocument).querySelectorAll(s);\n let i = matches.length;\n\n while (--i >= 0 && matches.item(i) !== this) {\n }\n\n return i > -1;\n };\n}\n\n/**\n * The Element.closest() method returns the closest ancestor\n * of the current element (or the current element itself) which\n * matches the selectors given in parameter.\n * If there isn't such an ancestor, it returns null.\n *\n * {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill}\n */\nif (!Element.prototype.closest) {\n Element.prototype.closest = function(s) {\n let el = this;\n\n if (!document.documentElement.contains(el)) {\n return null;\n }\n\n do {\n if (el.matches(s)) {\n return el;\n }\n\n el = el.parentElement || el.parentNode;\n } while (el !== null);\n\n return null;\n };\n}\n\n/**\n * The ParentNode.prepend method inserts a set of Node objects\n * or DOMString objects before the first child of the ParentNode.\n * DOMString objects are inserted as equivalent Text nodes.\n *\n * {@link https://developer.mozilla.org/en-US/docs/Web/API/ParentNode/prepend#Polyfill}\n */\nif (!Element.prototype.prepend) {\n Element.prototype.prepend = function prepend(nodes: Node|Node[]|any) {\n const docFrag = document.createDocumentFragment();\n\n if (!Array.isArray(nodes)) {\n nodes = [ nodes ];\n }\n\n nodes.forEach((node: Node|any) => {\n const isNode = node instanceof Node;\n\n docFrag.appendChild(isNode ? node : document.createTextNode(String(node)));\n });\n\n this.insertBefore(docFrag, this.firstChild);\n };\n}\n","/**\n * TextRange interface fot IE9-\n */\nimport * as _ from './utils';\nimport $ from './dom';\n\ninterface TextRange {\n boundingTop: number;\n boundingLeft: number;\n boundingBottom: number;\n boundingRight: number;\n boundingHeight: number;\n boundingWidth: number;\n}\n\n/**\n * Interface for object returned by document.selection in IE9-\n */\ninterface MSSelection {\n createRange: () => TextRange;\n type: string;\n}\n\n/**\n * Extends Document interface for IE9-\n */\ninterface Document {\n selection?: MSSelection;\n}\n\n/**\n * Working with selection\n * @typedef {SelectionUtils} SelectionUtils\n */\nexport default class SelectionUtils {\n\n /**\n * Editor styles\n * @return {{editorWrapper: string, editorZone: string}}\n */\n static get CSS(): { editorWrapper: string, editorZone: string } {\n return {\n editorWrapper: 'codex-editor',\n editorZone: 'codex-editor__redactor',\n };\n }\n\n /**\n * Returns selected anchor\n * {@link https://developer.mozilla.org/ru/docs/Web/API/Selection/anchorNode}\n * @return {Node|null}\n */\n static get anchorNode(): Node | null {\n const selection = window.getSelection();\n\n return selection ? selection.anchorNode : null;\n }\n\n /**\n * Returns selected anchor element\n * @return {Element|null}\n */\n static get anchorElement(): Element | null {\n const selection = window.getSelection();\n\n if (!selection) {\n return null;\n }\n\n const anchorNode = selection.anchorNode;\n\n if (!anchorNode) {\n return null;\n }\n\n if (!$.isElement(anchorNode)) {\n return anchorNode.parentElement;\n } else {\n return anchorNode;\n }\n }\n\n /**\n * Returns selection offset according to the anchor node\n * {@link https://developer.mozilla.org/ru/docs/Web/API/Selection/anchorOffset}\n * @return {Number|null}\n */\n static get anchorOffset(): number | null {\n const selection = window.getSelection();\n\n return selection ? selection.anchorOffset : null;\n }\n\n /**\n * Is current selection range collapsed\n * @return {boolean|null}\n */\n static get isCollapsed(): boolean | null {\n const selection = window.getSelection();\n\n return selection ? selection.isCollapsed : null;\n }\n\n /**\n * Check current selection if it is at Editor's zone\n * @return {boolean}\n */\n static get isAtEditor(): boolean {\n const selection = SelectionUtils.get();\n\n /**\n * Something selected on document\n */\n let selectedNode = (selection.anchorNode || selection.focusNode) as HTMLElement;\n\n if (selectedNode && selectedNode.nodeType === Node.TEXT_NODE) {\n selectedNode = selectedNode.parentNode as HTMLElement;\n }\n\n let editorZone = null;\n if (selectedNode) {\n editorZone = selectedNode.closest(`.${SelectionUtils.CSS.editorZone}`);\n }\n\n /**\n * SelectionUtils is not out of Editor because Editor's wrapper was found\n */\n return editorZone && editorZone.nodeType === Node.ELEMENT_NODE;\n }\n\n /**\n * Return first range\n * @return {Range|null}\n */\n static get range(): Range {\n const selection = window.getSelection();\n\n return selection && selection.rangeCount ? selection.getRangeAt(0) : null;\n }\n\n /**\n * Calculates position and size of selected text\n * @return {{x, y, width, height, top?, left?, bottom?, right?}}\n */\n static get rect(): DOMRect | ClientRect {\n let sel: Selection | MSSelection = (document as Document).selection,\n range: TextRange | Range;\n\n let rect = {\n x: 0,\n y: 0,\n width: 0,\n height: 0,\n } as DOMRect;\n\n if (sel && sel.type !== 'Control') {\n sel = sel as MSSelection;\n range = sel.createRange() as TextRange;\n rect.x = range.boundingLeft;\n rect.y = range.boundingTop;\n rect.width = range.boundingWidth;\n rect.height = range.boundingHeight;\n\n return rect;\n }\n\n if (!window.getSelection) {\n _.log('Method window.getSelection is not supported', 'warn');\n return rect;\n }\n\n sel = window.getSelection();\n\n if (sel.rangeCount === null || isNaN(sel.rangeCount)) {\n _.log('Method SelectionUtils.rangeCount is not supported', 'warn');\n return rect;\n }\n\n if (sel.rangeCount === 0) {\n return rect;\n }\n\n range = sel.getRangeAt(0).cloneRange() as Range;\n\n if (range.getBoundingClientRect) {\n rect = range.getBoundingClientRect() as DOMRect;\n }\n // Fall back to inserting a temporary element\n if (rect.x === 0 && rect.y === 0) {\n const span = document.createElement('span');\n\n if (span.getBoundingClientRect) {\n // Ensure span has dimensions and position by\n // adding a zero-width space character\n span.appendChild(document.createTextNode('\\u200b'));\n range.insertNode(span);\n rect = span.getBoundingClientRect() as DOMRect;\n\n const spanParent = span.parentNode;\n\n spanParent.removeChild(span);\n\n // Glue any broken text nodes back together\n spanParent.normalize();\n }\n }\n\n return rect;\n }\n\n /**\n * Returns selected text as String\n * @returns {string}\n */\n static get text(): string {\n return window.getSelection ? window.getSelection().toString() : '';\n }\n\n /**\n * Returns window SelectionUtils\n * {@link https://developer.mozilla.org/ru/docs/Web/API/Window/getSelection}\n * @return {Selection}\n */\n public static get(): Selection {\n return window.getSelection();\n }\n\n public instance: Selection = null;\n public selection: Selection = null;\n\n /**\n * This property can store SelectionUtils's range for restoring later\n * @type {Range|null}\n */\n public savedSelectionRange: Range = null;\n\n /**\n * Fake background is active\n *\n * @return {boolean}\n */\n public isFakeBackgroundEnabled = false;\n\n /**\n * Native Document's commands for fake background\n */\n private readonly commandBackground: string = 'backColor';\n private readonly commandRemoveFormat: string = 'removeFormat';\n\n /**\n * Removes fake background\n */\n public removeFakeBackground() {\n if (!this.isFakeBackgroundEnabled) {\n return;\n }\n\n this.isFakeBackgroundEnabled = false;\n document.execCommand(this.commandRemoveFormat);\n }\n\n /**\n * Sets fake background\n */\n public setFakeBackground() {\n document.execCommand(this.commandBackground, false, '#a8d6ff');\n\n this.isFakeBackgroundEnabled = true;\n }\n\n /**\n * Save SelectionUtils's range\n */\n public save(): void {\n this.savedSelectionRange = SelectionUtils.range;\n }\n\n /**\n * Restore saved SelectionUtils's range\n */\n public restore(): void {\n if (!this.savedSelectionRange) {\n return;\n }\n\n const sel = window.getSelection();\n\n sel.removeAllRanges();\n sel.addRange(this.savedSelectionRange);\n }\n\n /**\n * Clears saved selection\n */\n public clearSaved(): void {\n this.savedSelectionRange = null;\n }\n\n /**\n * Collapse current selection\n */\n public collapseToEnd(): void {\n const sel = window.getSelection();\n const range = document.createRange();\n\n range.selectNodeContents(sel.focusNode);\n range.collapse(false);\n sel.removeAllRanges();\n sel.addRange(range);\n }\n\n /**\n * Looks ahead to find passed tag from current selection\n *\n * @param {String} tagName - tag to found\n * @param {String} [className] - tag's class name\n * @param {Number} [searchDepth] - count of tags that can be included. For better performance.\n * @return {HTMLElement|null}\n */\n public findParentTag(tagName: string, className?: string, searchDepth = 10): HTMLElement | null {\n const selection = window.getSelection();\n let parentTag = null;\n\n /**\n * If selection is missing or no anchorNode or focusNode were found then return null\n */\n if (!selection || !selection.anchorNode || !selection.focusNode) {\n return null;\n }\n\n /**\n * Define Nodes for start and end of selection\n */\n const boundNodes = [\n /** the Node in which the selection begins */\n selection.anchorNode as HTMLElement,\n /** the Node in which the selection ends */\n selection.focusNode as HTMLElement,\n ];\n\n /**\n * For each selection parent Nodes we try to find target tag [with target class name]\n * It would be saved in parentTag variable\n */\n boundNodes.forEach((parent) => {\n /** Reset tags limit */\n let searchDepthIterable = searchDepth;\n\n while (searchDepthIterable > 0 && parent.parentNode) {\n /**\n * Check tag's name\n */\n if (parent.tagName === tagName) {\n /**\n * Save the result\n */\n parentTag = parent;\n\n /**\n * Optional additional check for class-name mismatching\n */\n if (className && parent.classList && !parent.classList.contains(className)) {\n parentTag = null;\n }\n\n /**\n * If we have found required tag with class then go out from the cycle\n */\n if (parentTag) {\n break;\n }\n }\n\n /**\n * Target tag was not found. Go up to the parent and check it\n */\n parent = parent.parentNode as HTMLElement;\n searchDepthIterable--;\n }\n });\n\n /**\n * Return found tag or null\n */\n return parentTag;\n }\n\n /**\n * Expands selection range to the passed parent node\n *\n * @param {HTMLElement} element\n */\n public expandToTag(element: HTMLElement): void {\n const selection = window.getSelection();\n\n selection.removeAllRanges();\n const range = document.createRange();\n\n range.selectNodeContents(element);\n selection.addRange(range);\n }\n}\n","!function(e,t){\"object\"==typeof exports&&\"object\"==typeof module?module.exports=t():\"function\"==typeof define&&define.amd?define([],t):\"object\"==typeof exports?exports.Paragraph=t():e.Paragraph=t()}(window,function(){return function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){\"undefined\"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:\"Module\"}),Object.defineProperty(e,\"__esModule\",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&\"object\"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,\"default\",{enumerable:!0,value:e}),2&t&&\"string\"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,\"a\",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p=\"/\",n(n.s=0)}([function(e,t,n){function r(e,t){for(var n=0;n=0&&f.splice(t,1)}function y(e){var t=document.createElement(\"style\");return void 0===e.attrs.type&&(e.attrs.type=\"text/css\"),b(t,e.attrs),h(e,t),t}function b(e,t){Object.keys(t).forEach(function(n){e.setAttribute(n,t[n])})}function m(e,t){var n,r,o,i;if(t.transform&&e.css){if(!(i=t.transform(e.css)))return function(){};e.css=i}if(t.singleton){var a=u++;n=c||(c=y(t)),r=w.bind(null,n,a,!1),o=w.bind(null,n,a,!0)}else e.sourceMap&&\"function\"==typeof URL&&\"function\"==typeof URL.createObjectURL&&\"function\"==typeof URL.revokeObjectURL&&\"function\"==typeof Blob&&\"function\"==typeof btoa?(n=function(e){var t=document.createElement(\"link\");return void 0===e.attrs.type&&(e.attrs.type=\"text/css\"),e.attrs.rel=\"stylesheet\",b(t,e.attrs),h(e,t),t}(t),r=function(e,t,n){var r=n.css,o=n.sourceMap,i=void 0===t.convertToAbsoluteUrls&&o;(t.convertToAbsoluteUrls||i)&&(r=l(r));o&&(r+=\"\\n/*# sourceMappingURL=data:application/json;base64,\"+btoa(unescape(encodeURIComponent(JSON.stringify(o))))+\" */\");var a=new Blob([r],{type:\"text/css\"}),s=e.href;e.href=URL.createObjectURL(a),s&&URL.revokeObjectURL(s)}.bind(null,n,t),o=function(){v(n),n.href&&URL.revokeObjectURL(n.href)}):(n=y(t),r=function(e,t){var n=t.css,r=t.media;r&&e.setAttribute(\"media\",r);if(e.styleSheet)e.styleSheet.cssText=n;else{for(;e.firstChild;)e.removeChild(e.firstChild);e.appendChild(document.createTextNode(n))}}.bind(null,n),o=function(){v(n)});return r(e),function(t){if(t){if(t.css===e.css&&t.media===e.media&&t.sourceMap===e.sourceMap)return;r(e=t)}else o()}}e.exports=function(e,t){if(\"undefined\"!=typeof DEBUG&&DEBUG&&\"object\"!=typeof document)throw new Error(\"The style-loader cannot be used in a non-browser environment\");(t=t||{}).attrs=\"object\"==typeof t.attrs?t.attrs:{},t.singleton||\"boolean\"==typeof t.singleton||(t.singleton=a()),t.insertInto||(t.insertInto=\"head\"),t.insertAt||(t.insertAt=\"bottom\");var n=d(e,t);return p(n,t),function(e){for(var r=[],o=0;o\\n \\n\\n'}])});","import $ from '../../dom';\nimport {BlockTool, BlockToolData} from '../../../../types';\n\nexport default class Stub implements BlockTool {\n /**\n * Stub styles\n * @type {{wrapper: string; info: string; title: string; subtitle: string}}\n */\n private CSS = {\n wrapper: 'ce-stub',\n info: 'ce-stub__info',\n title: 'ce-stub__title',\n subtitle: 'ce-stub__subtitle',\n };\n\n /**\n * Main stub wrapper\n */\n private readonly wrapper: HTMLElement;\n\n /**\n * Stub title — tool name\n */\n private readonly title: string;\n\n /**\n * Stub hint\n */\n private readonly subtitle: string;\n\n /**\n * Original Tool data\n */\n private readonly savedData: BlockToolData;\n\n constructor({data, config, api}) {\n this.title = data.title || 'Error';\n this.subtitle = 'The block can not be displayed correctly.';\n this.savedData = data.savedData;\n\n this.wrapper = this.make();\n }\n\n /**\n * Returns stub holder\n * @return {HTMLElement}\n */\n public render(): HTMLElement {\n return this.wrapper;\n }\n\n /**\n * Return original Tool data\n * @return {BlockToolData}\n */\n public save(): BlockToolData {\n return this.savedData;\n }\n\n /**\n * Create Tool html markup\n * @return {HTMLElement}\n */\n private make(): HTMLElement {\n const wrapper = $.make('div', this.CSS.wrapper);\n const icon = $.svg('sad-face', 52, 52);\n const infoContainer = $.make('div', this.CSS.info);\n const title = $.make('div', this.CSS.title, {\n textContent: this.title,\n });\n const subtitle = $.make('div', this.CSS.subtitle, {\n textContent: this.subtitle,\n });\n\n wrapper.appendChild(icon);\n\n infoContainer.appendChild(title);\n infoContainer.appendChild(subtitle);\n\n wrapper.appendChild(infoContainer);\n\n return wrapper;\n }\n}\n","/**\n * Class Util\n */\n\nimport Dom from './dom';\n\n/**\n * Possible log levels\n */\nexport enum LogLevels {\n VERBOSE = 'VERBOSE',\n INFO = 'INFO',\n WARN = 'WARN',\n ERROR = 'ERROR',\n}\n\n/**\n * Allow to use global VERSION, that will be overwritten by Webpack\n */\ndeclare const VERSION: string;\n\n/**\n * @typedef {Object} ChainData\n * @property {Object} data - data that will be passed to the success or fallback\n * @property {Function} function - function's that must be called asynchronically\n */\nexport interface ChainData {\n data?: any;\n function: (...args: any[]) => any;\n}\n\n/**\n * Editor.js utils\n */\n\n/**\n * Returns basic keycodes as constants\n * @return {{}}\n */\nexport const keyCodes = {\n BACKSPACE: 8,\n TAB: 9,\n ENTER: 13,\n SHIFT: 16,\n CTRL: 17,\n ALT: 18,\n ESC: 27,\n SPACE: 32,\n LEFT: 37,\n UP: 38,\n DOWN: 40,\n RIGHT: 39,\n DELETE: 46,\n META: 91,\n};\n\n/**\n * Return mouse buttons codes\n */\nexport const mouseButtons = {\n LEFT: 0,\n WHEEL: 1,\n RIGHT: 2,\n BACKWARD: 3,\n FORWARD: 4,\n};\n\n/**\n * Custom logger\n *\n * @param {boolean} labeled — if true, Editor.js label is shown\n * @param {string} msg - message\n * @param {string} type - logging type 'log'|'warn'|'error'|'info'\n * @param {*} [args] - argument to log with a message\n * @param {string} style - additional styling to message\n * @param labeled\n */\nfunction _log(\n labeled: boolean,\n msg: string,\n type: string = 'log',\n args?: any,\n style: string = 'color: inherit',\n): void {\n\n if ( !('console' in window) || !window.console[ type ] ) {\n return;\n }\n\n const isSimpleType = ['info', 'log', 'warn', 'error'].includes(type);\n const argsToPass = [];\n\n switch (_log.logLevel) {\n case LogLevels.ERROR:\n if (type !== 'error') {\n return;\n }\n break;\n\n case LogLevels.WARN:\n if (!['error', 'warn'].includes(type)) {\n return;\n }\n break;\n\n case LogLevels.INFO:\n if (!isSimpleType || labeled) {\n return;\n }\n break;\n }\n\n if (args) {\n argsToPass.push(args);\n }\n\n const editorLabelText = `Editor.js ${VERSION}`;\n const editorLabelStyle = `line-height: 1em;\n color: #006FEA;\n display: inline-block;\n font-size: 11px;\n line-height: 1em;\n background-color: #fff;\n padding: 4px 9px;\n border-radius: 30px;\n border: 1px solid rgba(56, 138, 229, 0.16);\n margin: 4px 5px 4px 0;`;\n\n if (labeled) {\n if (isSimpleType) {\n argsToPass.unshift(editorLabelStyle, style);\n msg = `%c${editorLabelText}%c ${msg}`;\n } else {\n msg = `( ${editorLabelText} )${msg}`;\n }\n }\n\n try {\n if (!isSimpleType) {\n console[type](msg);\n } else if (args) {\n console[type](`${msg} %o`, ...argsToPass);\n } else {\n console[type](msg, ...argsToPass);\n }\n } catch (ignored) {}\n}\n\n/**\n * Current log level\n */\n_log.logLevel = LogLevels.VERBOSE;\n\n/**\n * Set current log level\n *\n * @param {LogLevels} logLevel - log level to set\n */\nexport function setLogLevel(logLevel: LogLevels) {\n _log.logLevel = logLevel;\n}\n\n/**\n * _log method proxy without Editor.js label\n */\nexport const log = _log.bind(window, false);\n\n/**\n * _log method proxy with Editor.js label\n */\nexport const logLabeled = _log.bind(window, true);\n\n/**\n * Returns true if passed key code is printable (a-Z, 0-9, etc) character.\n * @param {number} keyCode\n * @return {boolean}\n */\nexport function isPrintableKey( keyCode: number ): boolean {\n return (keyCode > 47 && keyCode < 58) || // number keys\n keyCode === 32 || keyCode === 13 || // Spacebar & return key(s)\n (keyCode > 64 && keyCode < 91) || // letter keys\n (keyCode > 95 && keyCode < 112) || // Numpad keys\n (keyCode > 185 && keyCode < 193) || // ;=,-./` (in order)\n (keyCode > 218 && keyCode < 223); // [\\]' (in order)\n}\n\n/**\n * Fires a promise sequence asyncronically\n *\n * @param {ChainData[]} chains - list or ChainData's\n * @param {Function} success - success callback\n * @param {Function} fallback - callback that fires in case of errors\n *\n * @return {Promise}\n */\nexport async function sequence(\n chains: ChainData[],\n success: (data: any) => void = () => {},\n fallback: (data: any) => void = () => {},\n): Promise {\n /**\n * Decorator\n *\n * @param {ChainData} chainData\n *\n * @param {Function} successCallback\n * @param {Function} fallbackCallback\n *\n * @return {Promise}\n */\n async function waitNextBlock(\n chainData: ChainData,\n successCallback: (data: any) => void,\n fallbackCallback: (data: any) => void,\n ): Promise {\n try {\n await chainData.function(chainData.data);\n await successCallback(typeof chainData.data !== 'undefined' ? chainData.data : {});\n } catch (e) {\n fallbackCallback(typeof chainData.data !== 'undefined' ? chainData.data : {});\n }\n }\n\n /**\n * pluck each element from queue\n * First, send resolved Promise as previous value\n * Each plugins \"prepare\" method returns a Promise, that's why\n * reduce current element will not be able to continue while can't get\n * a resolved Promise\n */\n return await chains.reduce(async (previousValue, currentValue) => {\n await previousValue;\n return waitNextBlock(currentValue, success, fallback);\n }, Promise.resolve());\n}\n\n/**\n * Make array from array-like collection\n *\n * @param {ArrayLike} collection\n *\n * @return {Array}\n */\nexport function array(collection: ArrayLike): any[] {\n return Array.prototype.slice.call(collection);\n}\n\n/**\n * Check if passed variable is a function\n * @param {*} fn\n * @return {boolean}\n */\nexport function isFunction(fn: any): boolean {\n return typeof fn === 'function';\n}\n\n/**\n * Check if passed function is a class\n * @param {function} fn\n * @return {boolean}\n */\nexport function isClass(fn: any): boolean {\n return typeof fn === 'function' && /^\\s*class\\s+/.test(fn.toString());\n}\n\n/**\n * Checks if object is empty\n *\n * @param {Object} object\n * @return {boolean}\n */\nexport function isEmpty(object: object): boolean {\n if (!object) {\n return true;\n }\n\n return Object.keys(object).length === 0 && object.constructor === Object;\n}\n\n/**\n * Check if passed object is a Promise\n * @param {*} object - object to check\n * @return {Boolean}\n */\nexport function isPromise(object: any): boolean {\n return Promise.resolve(object) === object;\n}\n\n/**\n * Delays method execution\n *\n * @param {Function} method\n * @param {Number} timeout\n */\nexport function delay(method: (...args: any[]) => any, timeout: number) {\n return function() {\n const context = this,\n args = arguments;\n\n window.setTimeout(() => method.apply(context, args), timeout);\n };\n}\n\n/**\n * Get file extension\n *\n * @param {File} file\n * @return string\n */\nexport function getFileExtension(file: File): string {\n return file.name.split('.').pop();\n}\n\n/**\n * Check if string is MIME type\n *\n * @param {string} type\n * @return boolean\n */\nexport function isValidMimeType(type: string): boolean {\n return /^[-\\w]+\\/([-+\\w]+|\\*)$/.test(type);\n}\n\n/**\n * Debouncing method\n * Call method after passed time\n *\n * Note that this method returns Function and declared variable need to be called\n *\n * @param {Function} func - function that we're throttling\n * @param {Number} wait - time in milliseconds\n * @param {Boolean} immediate - call now\n * @return {Function}\n */\nexport function debounce(func: () => void, wait?: number , immediate?: boolean): () => void {\n let timeout;\n\n return () => {\n const context = this,\n args = arguments;\n\n const later = () => {\n timeout = null;\n if (!immediate) {\n func.apply(context, args);\n }\n };\n\n const callNow = immediate && !timeout;\n\n window.clearTimeout(timeout);\n timeout = window.setTimeout(later, wait);\n if (callNow) {\n func.apply(context, args);\n }\n };\n}\n\n/**\n * Copies passed text to the clipboard\n * @param text\n */\nexport function copyTextToClipboard(text) {\n const el = Dom.make('div', 'codex-editor-clipboard', {\n innerHTML: text,\n });\n\n document.body.appendChild(el);\n\n const selection = window.getSelection();\n const range = document.createRange();\n range.selectNode(el);\n\n window.getSelection().removeAllRanges();\n selection.addRange(range);\n\n document.execCommand('copy');\n document.body.removeChild(el);\n}\n\n/**\n * Returns object with os name as key and boolean as value. Shows current user OS\n *\n * @return {[key: string]: boolean}\n */\nexport function getUserOS(): {[key: string]: boolean} {\n const OS = {\n win: false,\n mac: false,\n x11: false,\n linux: false,\n };\n\n const userOS = Object.keys(OS).find((os: string) => navigator.appVersion.toLowerCase().indexOf(os) !== -1);\n\n if (userOS) {\n OS[userOS] = true;\n return OS;\n }\n\n return OS;\n}\n\n/**\n * Capitalizes first letter of the string\n * @param {string} text\n * @return {string}\n */\nexport function capitalize(text: string): string {\n return text[0].toUpperCase() + text.slice(1);\n}\n\n/**\n * Merge to objects recursively\n * @param {object} target\n * @param {object[]} sources\n * @return {object}\n */\nexport function deepMerge(target, ...sources) {\n const isObject = (item) => item && typeOf(item) === 'object';\n\n if (!sources.length) { return target; }\n const source = sources.shift();\n\n if (isObject(target) && isObject(source)) {\n for (const key in source) {\n if (isObject(source[key])) {\n if (!target[key]) {\n Object.assign(target, { [key]: {} });\n }\n\n deepMerge(target[key], source[key]);\n } else {\n Object.assign(target, { [key]: source[key] });\n }\n }\n }\n\n return deepMerge(target, ...sources);\n}\n\n/**\n * Return true if current device supports touch events\n *\n * Note! This is a simple solution, it can give false-positive results.\n * To detect touch devices more carefully, use 'touchstart' event listener\n * @see http://www.stucox.com/blog/you-cant-detect-a-touchscreen/\n *\n * @return {boolean}\n */\nexport const isTouchSupported: boolean = 'ontouchstart' in document.documentElement;\n\n/**\n * Return string representation of the object type\n *\n * @param {any} object\n */\nexport function typeOf(object: any): string {\n return Object.prototype.toString.call(object).match(/\\s([a-zA-Z]+)/)[1].toLowerCase();\n}\n\n/**\n * Make shortcut command more human-readable\n * @param {string} shortcut — string like 'CMD+B'\n */\nexport function beautifyShortcut(shortcut: string): string {\n const OS = getUserOS();\n\n shortcut = shortcut\n .replace(/shift/gi, '⇧')\n .replace(/backspace/gi, '⌫')\n .replace(/enter/gi, '⏎')\n .replace(/up/gi, '↑')\n .replace(/left/gi, '→')\n .replace(/down/gi, '↓')\n .replace(/right/gi, '←')\n .replace(/escape/gi, '⎋')\n .replace(/insert/gi, 'Ins')\n .replace(/delete/gi, '␡')\n .replace(/\\+/gi, ' + ');\n\n if (OS.mac) {\n shortcut = shortcut.replace(/ctrl|cmd/gi, '⌘').replace(/alt/gi, '⌥');\n } else {\n shortcut = shortcut.replace(/cmd/gi, 'Ctrl').replace(/windows/gi, 'WIN');\n }\n\n return shortcut;\n}\n","module.exports = \".codex-editor{position:relative;-webkit-box-sizing:border-box;box-sizing:border-box;z-index:1}.codex-editor .hide,.codex-editor__redactor--hidden{display:none}.codex-editor__redactor [contenteditable]:empty:after{content:\\\"\\\\feff \\\"}@media (min-width:651px){.codex-editor--narrow .codex-editor__redactor{margin-right:50px}}@media (min-width:651px){.codex-editor--narrow .ce-toolbar__actions{right:-5px}}.codex-editor__loader{position:relative;height:30vh}.codex-editor__loader:before{content:\\\"\\\";position:absolute;left:50%;top:50%;width:30px;height:30px;margin-top:-15px;margin-left:-15px;border-radius:50%;border:2px solid rgba(201,201,204,.48);border-top-color:transparent;-webkit-box-sizing:border-box;box-sizing:border-box;-webkit-animation:editor-loader-spin .8s linear infinite;animation:editor-loader-spin .8s linear infinite;will-change:transform}.codex-editor-copyable{position:absolute;height:1px;width:1px;top:-400%;opacity:.001}.codex-editor-overlay{position:fixed;top:0;left:0;right:0;bottom:0;z-index:999;pointer-events:none;overflow:hidden}.codex-editor-overlay__container{position:relative;pointer-events:auto;z-index:0}.codex-editor-overlay__rectangle{position:absolute;pointer-events:none;background-color:rgba(46,170,220,.2);border:1px solid transparent}.codex-editor svg{fill:currentColor;vertical-align:middle;max-height:100%}::-moz-selection{background-color:#d4ecff}::selection{background-color:#d4ecff}.codex-editor--toolbox-opened [contentEditable=true][data-placeholder]:focus:before{opacity:0!important}@-webkit-keyframes editor-loader-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes editor-loader-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.ce-toolbar{position:absolute;left:0;right:0;top:0;-webkit-transition:opacity .1s ease;transition:opacity .1s ease;will-change:opacity,transform;display:none}@media (max-width:650px){.ce-toolbar{position:absolute;background-color:#fff;border:1px solid #eaeaea;-webkit-box-shadow:0 3px 15px -3px rgba(13,20,33,.13);box-shadow:0 3px 15px -3px rgba(13,20,33,.13);border-radius:4px;z-index:2}}@media (max-width:650px) and (max-width:650px){.ce-toolbar{-webkit-box-shadow:0 13px 7px -5px rgba(26,38,49,.09),6px 15px 34px -6px rgba(33,48,73,.29);box-shadow:0 13px 7px -5px rgba(26,38,49,.09),6px 15px 34px -6px rgba(33,48,73,.29);border-bottom-color:#d5d7db}}@media (max-width:650px){.ce-toolbar{padding:3px;margin-top:5px}.ce-toolbar--left-oriented:before{left:15px;margin-left:0}.ce-toolbar--right-oriented:before{left:auto;right:15px;margin-left:0}}.ce-toolbar--opened{display:block}@media (max-width:650px){.ce-toolbar--opened{display:-webkit-box;display:-ms-flexbox;display:flex}}.ce-toolbar__content{max-width:650px;margin:0 auto;position:relative}@media (max-width:650px){.ce-toolbar__content{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-line-pack:center;align-content:center;margin:0;max-width:calc(100% - 40px)}}.ce-toolbar__plus{color:#707684;cursor:pointer;width:34px;height:34px;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;position:absolute;left:-34px;-ms-flex-negative:0;flex-shrink:0}.ce-toolbar__plus--active,.ce-toolbar__plus:hover{color:#388ae5}.ce-toolbar__plus--active{-webkit-animation:bounceIn .75s 1;animation:bounceIn .75s 1;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}.ce-toolbar__plus-shortcut{opacity:.6;word-spacing:-2px;margin-top:5px}.ce-toolbar__plus--hidden{display:none}@media (max-width:650px){.ce-toolbar__plus{display:-webkit-inline-box!important;display:-ms-inline-flexbox!important;display:inline-flex!important;position:static;-webkit-transform:none!important;transform:none!important}}.ce-toolbar .ce-toolbox,.ce-toolbar__plus{top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%)}.ce-toolbar__actions{position:absolute;right:0;top:10px;padding-right:16px;opacity:0}@media (max-width:650px){.ce-toolbar__actions{position:static;margin-left:auto;padding-right:10px;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}}.ce-toolbar__actions--opened{opacity:1}.ce-toolbar__actions-buttons{text-align:right}.ce-toolbar__settings-btn{display:inline-block;width:24px;height:24px;color:#707684;cursor:pointer}@media (min-width:651px){.codex-editor--narrow .ce-toolbar__plus{left:5px}}.ce-toolbox{max-width:150px;width:100%;position:absolute;visibility:hidden;-webkit-transition:opacity .1s ease;transition:opacity .1s ease;will-change:opacity;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-ms-flex-wrap:wrap;flex-wrap:wrap}@media (max-width:650px){.ce-toolbox{position:static;-webkit-transform:none!important;transform:none!important;-webkit-box-align:center;-ms-flex-align:center;align-items:center;overflow-x:auto}}.ce-toolbox--opened{opacity:1;visibility:visible;-webkit-transform:translateZ(0)!important;transform:translateZ(0)!important}.ce-toolbox__button-apistack{color:#707684;cursor:pointer;width:50%;height:60px;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;list-style-type:none;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;text-align:center;border:1px solid #000;-ms-flex-negative:0;flex-shrink:0}.ce-toolbox__button-apistack__name{font-weight:700;font-size:12px;text-transform:capitalize}.ce-toolbox__button-apistack__icon{text-align:center}.ce-toolbox__button-apistack__icon>svg{width:12px;height:14px}.ce-toolbox__button-apistack--active,.ce-toolbox__button-apistack:hover{color:#388ae5}.ce-toolbox__button-apistack--active{-webkit-animation:bounceIn .25s 1;animation:bounceIn .25s 1;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}.ce-toolbox-button-tooltip__shortcut{opacity:.6;word-spacing:-3px;margin-top:3px}@media (min-width:651px){.codex-editor--narrow .ce-toolbox{background:#fff;z-index:2}}.ce-inline-toolbar{position:absolute;background-color:#fff;border:1px solid #eaeaea;-webkit-box-shadow:0 3px 15px -3px rgba(13,20,33,.13);box-shadow:0 3px 15px -3px rgba(13,20,33,.13);border-radius:4px;z-index:2}@media (max-width:650px){.ce-inline-toolbar{-webkit-box-shadow:0 13px 7px -5px rgba(26,38,49,.09),6px 15px 34px -6px rgba(33,48,73,.29);box-shadow:0 13px 7px -5px rgba(26,38,49,.09),6px 15px 34px -6px rgba(33,48,73,.29);border-bottom-color:#d5d7db}}.ce-inline-toolbar{-webkit-transform:translateX(-50%) translateY(8px) scale(.9);transform:translateX(-50%) translateY(8px) scale(.9);opacity:0;visibility:hidden;-webkit-transition:opacity .25s ease,-webkit-transform .15s ease;transition:opacity .25s ease,-webkit-transform .15s ease;transition:transform .15s ease,opacity .25s ease;transition:transform .15s ease,opacity .25s ease,-webkit-transform .15s ease;will-change:transform,opacity;top:0;left:0}.ce-inline-toolbar--left-oriented:before{left:15px;margin-left:0}.ce-inline-toolbar--right-oriented:before{left:auto;right:15px;margin-left:0}.ce-inline-toolbar--showed{opacity:1;visibility:visible;-webkit-transform:translateX(-50%);transform:translateX(-50%)}.ce-inline-toolbar--left-oriented{-webkit-transform:translateX(-23px) translateY(8px) scale(.9);transform:translateX(-23px) translateY(8px) scale(.9)}.ce-inline-toolbar--left-oriented.ce-inline-toolbar--showed{-webkit-transform:translateX(-23px);transform:translateX(-23px)}.ce-inline-toolbar--right-oriented{-webkit-transform:translateX(-100%) translateY(8px) scale(.9);transform:translateX(-100%) translateY(8px) scale(.9);margin-left:23px}.ce-inline-toolbar--right-oriented.ce-inline-toolbar--showed{-webkit-transform:translateX(-100%);transform:translateX(-100%)}.ce-inline-toolbar [hidden]{display:none!important}.ce-inline-toolbar__buttons{display:-webkit-box;display:-ms-flexbox;display:flex;padding:0 6px}.ce-inline-toolbar__dropdown{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;height:34px;padding:0 9px 0 10px;margin:0 6px 0 -6px;-webkit-box-align:center;-ms-flex-align:center;align-items:center;cursor:pointer;border-right:1px solid rgba(201,201,204,.48)}.ce-inline-toolbar__dropdown:hover{background:#eff2f5}.ce-inline-toolbar__dropdown--hidden{display:none}.ce-inline-toolbar__dropdown-content{display:-webkit-box;display:-ms-flexbox;display:flex;font-weight:500;font-size:14px}.ce-inline-toolbar__dropdown-content svg{height:12px}.ce-inline-toolbar__dropdown .icon--toggler-down{margin-left:4px}.ce-inline-toolbar__shortcut{opacity:.6;word-spacing:-3px;margin-top:3px}.ce-inline-tool{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;width:34px;height:34px;line-height:34px;padding:0!important;text-align:center;border-radius:3px;cursor:pointer;border:0;outline:none;background-color:transparent;vertical-align:bottom;color:#000;margin:0}.ce-inline-tool:hover{background-color:#eff2f5}.ce-inline-tool{border-radius:0;line-height:normal;width:auto;padding:0 5px!important;min-width:24px}.ce-inline-tool .icon,.ce-inline-tool>svg{margin:auto}.ce-inline-tool--active{color:#388ae5}.ce-inline-tool--focused{-webkit-box-shadow:inset 0 0 0 1px rgba(7,161,227,.08);box-shadow:inset 0 0 0 1px rgba(7,161,227,.08);background:rgba(34,186,255,.08)!important}.ce-inline-tool--focused-animated{-webkit-animation-name:buttonClicked;animation-name:buttonClicked;-webkit-animation-duration:.25s;animation-duration:.25s}.ce-inline-tool:not(:last-of-type){margin-right:2px}.ce-inline-tool .icon{height:12px}.ce-inline-tool--last{margin-right:0!important}.ce-inline-tool--link .icon--unlink,.ce-inline-tool--unlink .icon--link{display:none}.ce-inline-tool--unlink .icon--unlink{display:inline-block;margin-bottom:-1px}.ce-inline-tool-input{outline:none;border:0;border-radius:0 0 4px 4px;margin:0;font-size:13px;padding:10px;width:100%;-webkit-box-sizing:border-box;box-sizing:border-box;display:none;font-weight:500;border-top:1px solid rgba(201,201,204,.48)}.ce-inline-tool-input::-webkit-input-placeholder{color:#707684}.ce-inline-tool-input::-moz-placeholder{color:#707684}.ce-inline-tool-input:-ms-input-placeholder{color:#707684}.ce-inline-tool-input::-ms-input-placeholder{color:#707684}.ce-inline-tool-input::placeholder{color:#707684}.ce-inline-tool-input--showed{display:block}.ce-conversion-toolbar{position:absolute;background-color:#fff;border:1px solid #eaeaea;-webkit-box-shadow:0 3px 15px -3px rgba(13,20,33,.13);box-shadow:0 3px 15px -3px rgba(13,20,33,.13);border-radius:4px;z-index:2}@media (max-width:650px){.ce-conversion-toolbar{-webkit-box-shadow:0 13px 7px -5px rgba(26,38,49,.09),6px 15px 34px -6px rgba(33,48,73,.29);box-shadow:0 13px 7px -5px rgba(26,38,49,.09),6px 15px 34px -6px rgba(33,48,73,.29);border-bottom-color:#d5d7db}}.ce-conversion-toolbar{opacity:0;visibility:hidden;will-change:transform,opacity;-webkit-transition:opacity .1s ease,-webkit-transform .1s ease;transition:opacity .1s ease,-webkit-transform .1s ease;transition:transform .1s ease,opacity .1s ease;transition:transform .1s ease,opacity .1s ease,-webkit-transform .1s ease;-webkit-transform:translateY(-8px);transform:translateY(-8px);left:-1px;width:150px;margin-top:5px;-webkit-box-sizing:content-box;box-sizing:content-box}.ce-conversion-toolbar--left-oriented:before{left:15px;margin-left:0}.ce-conversion-toolbar--right-oriented:before{left:auto;right:15px;margin-left:0}.ce-conversion-toolbar--showed{opacity:1;visibility:visible;-webkit-transform:none;transform:none}.ce-conversion-toolbar [hidden]{display:none!important}.ce-conversion-toolbar__buttons{display:-webkit-box;display:-ms-flexbox;display:flex}.ce-conversion-toolbar__label{color:#707684;font-size:11px;font-weight:500;letter-spacing:.33px;padding:10px 10px 5px;text-transform:uppercase}.ce-conversion-tool{display:-webkit-box;display:-ms-flexbox;display:flex;padding:5px 10px;font-size:14px;line-height:20px;font-weight:500;cursor:pointer;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.ce-conversion-tool--hidden{display:none}.ce-conversion-tool--focused{-webkit-box-shadow:inset 0 0 0 1px rgba(7,161,227,.08);box-shadow:inset 0 0 0 1px rgba(7,161,227,.08);background:rgba(34,186,255,.08)!important}.ce-conversion-tool--focused-animated{-webkit-animation-name:buttonClicked;animation-name:buttonClicked;-webkit-animation-duration:.25s;animation-duration:.25s}.ce-conversion-tool:hover{background:#eff2f5}.ce-conversion-tool__icon{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;width:20px;height:20px;border:1px solid rgba(201,201,204,.48);border-radius:3px;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;margin-right:10px;background:#fff}.ce-conversion-tool__icon svg{width:11px;height:11px}.ce-conversion-tool--last{margin-right:0!important}.ce-conversion-tool--active{color:#388ae5!important;-webkit-animation:bounceIn .75s 1;animation:bounceIn .75s 1;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}.ce-settings{position:absolute;background-color:#fff;border:1px solid #eaeaea;-webkit-box-shadow:0 3px 15px -3px rgba(13,20,33,.13);box-shadow:0 3px 15px -3px rgba(13,20,33,.13);border-radius:4px;z-index:2}@media (max-width:650px){.ce-settings{-webkit-box-shadow:0 13px 7px -5px rgba(26,38,49,.09),6px 15px 34px -6px rgba(33,48,73,.29);box-shadow:0 13px 7px -5px rgba(26,38,49,.09),6px 15px 34px -6px rgba(33,48,73,.29);border-bottom-color:#d5d7db}}.ce-settings{right:5px;top:35px;min-width:114px}.ce-settings--left-oriented:before{left:15px;margin-left:0}.ce-settings--right-oriented:before{left:auto;right:15px;margin-left:0}@media (max-width:650px){.ce-settings{bottom:50px;top:auto}}.ce-settings:before{left:auto;right:12px}@media (max-width:650px){.ce-settings:before{bottom:-5px;top:auto}}.ce-settings{display:none}.ce-settings--opened{display:block;-webkit-animation-duration:.1s;animation-duration:.1s;-webkit-animation-name:panelShowing;animation-name:panelShowing}.ce-settings__plugin-zone:not(:empty){padding:3px 3px 0}.ce-settings__default-zone:not(:empty){padding:3px}.ce-settings__button{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;width:34px;height:34px;line-height:34px;padding:0!important;text-align:center;border-radius:3px;cursor:pointer;border:0;outline:none;background-color:transparent;vertical-align:bottom;color:#000;margin:0}.ce-settings__button:hover{background-color:#eff2f5}.ce-settings__button .icon,.ce-settings__button>svg{margin:auto}.ce-settings__button--active{color:#388ae5}.ce-settings__button--focused{-webkit-box-shadow:inset 0 0 0 1px rgba(7,161,227,.08);box-shadow:inset 0 0 0 1px rgba(7,161,227,.08);background:rgba(34,186,255,.08)!important}.ce-settings__button--focused-animated{-webkit-animation-name:buttonClicked;animation-name:buttonClicked;-webkit-animation-duration:.25s;animation-duration:.25s}.ce-settings__button:not(:nth-child(3n+3)){margin-right:3px}.ce-settings__button:nth-child(n+4){margin-top:3px}.ce-settings__button{line-height:32px}.ce-settings__button--disabled{cursor:not-allowed!important;opacity:.3}.ce-settings__button--selected{color:#388ae5}.ce-settings__button--delete{-webkit-transition:background-color .3s ease;transition:background-color .3s ease;will-change:background-color}.ce-settings__button--delete .icon{-webkit-transition:-webkit-transform .2s ease-out;transition:-webkit-transform .2s ease-out;transition:transform .2s ease-out;transition:transform .2s ease-out,-webkit-transform .2s ease-out;will-change:transform}.ce-settings__button--confirm{background-color:#e24a4a!important;color:#fff}.ce-settings__button--confirm:hover{background-color:#d54a4a!important}.ce-settings__button--confirm .icon{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.ce-block:first-of-type{margin-top:0}.ce-block--focused{background-image:linear-gradient(17deg,rgba(243,248,255,.03) 63.45%,rgba(207,214,229,.27) 98%);border-radius:3px}@media (max-width:650px){.ce-block--focused{background-image:none;background-color:rgba(200,199,219,.17);margin:0 -10px;padding:0 10px}}.ce-block--selected .ce-block__content{background:#e1f2ff}.ce-block--selected .ce-block__content [contenteditable]{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ce-block--selected .ce-block__content .ce-stub,.ce-block--selected .ce-block__content img{opacity:.55}.ce-block--stretched .ce-block__content{max-width:none}.ce-block__content{position:relative;max-width:650px;margin:0 auto;-webkit-transition:background-color .15s ease;transition:background-color .15s ease}.ce-block--drop-target .ce-block__content:before{content:\\\"\\\";position:absolute;top:100%;left:-20px;margin-top:-1px;height:8px;width:8px;border:solid #388ae5;border-width:1px 1px 0 0;-webkit-transform-origin:right;transform-origin:right;-webkit-transform:rotate(45deg);transform:rotate(45deg)}.ce-block--drop-target .ce-block__content:after{content:\\\"\\\";position:absolute;top:100%;height:1px;width:100%;color:#388ae5;background:repeating-linear-gradient(90deg,#388ae5,#388ae5 1px,#fff 0,#fff 6px)}.ce-block a{cursor:pointer;text-decoration:underline}.ce-block b{font-weight:700}.ce-block i{font-style:italic}@media (min-width:651px){.codex-editor--narrow .ce-block--focused{margin-right:-50px;padding-right:50px}}.wobble{-webkit-animation-name:wobble;animation-name:wobble;-webkit-animation-duration:.4s;animation-duration:.4s}@-webkit-keyframes wobble{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}15%{-webkit-transform:translate3d(-5%,0,0) rotate(-5deg);transform:translate3d(-5%,0,0) rotate(-5deg)}30%{-webkit-transform:translate3d(2%,0,0) rotate(3deg);transform:translate3d(2%,0,0) rotate(3deg)}45%{-webkit-transform:translate3d(-3%,0,0) rotate(-3deg);transform:translate3d(-3%,0,0) rotate(-3deg)}60%{-webkit-transform:translate3d(2%,0,0) rotate(2deg);transform:translate3d(2%,0,0) rotate(2deg)}75%{-webkit-transform:translate3d(-1%,0,0) rotate(-1deg);transform:translate3d(-1%,0,0) rotate(-1deg)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes wobble{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}15%{-webkit-transform:translate3d(-5%,0,0) rotate(-5deg);transform:translate3d(-5%,0,0) rotate(-5deg)}30%{-webkit-transform:translate3d(2%,0,0) rotate(3deg);transform:translate3d(2%,0,0) rotate(3deg)}45%{-webkit-transform:translate3d(-3%,0,0) rotate(-3deg);transform:translate3d(-3%,0,0) rotate(-3deg)}60%{-webkit-transform:translate3d(2%,0,0) rotate(2deg);transform:translate3d(2%,0,0) rotate(2deg)}75%{-webkit-transform:translate3d(-1%,0,0) rotate(-1deg);transform:translate3d(-1%,0,0) rotate(-1deg)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@-webkit-keyframes bounceIn{0%,20%,40%,60%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}20%{-webkit-transform:scale3d(1.03,1.03,1.03);transform:scale3d(1.03,1.03,1.03)}60%{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes bounceIn{0%,20%,40%,60%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}20%{-webkit-transform:scale3d(1.03,1.03,1.03);transform:scale3d(1.03,1.03,1.03)}60%{-webkit-transform:scaleX(1);transform:scaleX(1)}}@-webkit-keyframes selectionBounce{0%,20%,40%,60%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}50%{-webkit-transform:scale3d(1.01,1.01,1.01);transform:scale3d(1.01,1.01,1.01)}70%{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes selectionBounce{0%,20%,40%,60%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}50%{-webkit-transform:scale3d(1.01,1.01,1.01);transform:scale3d(1.01,1.01,1.01)}70%{-webkit-transform:scaleX(1);transform:scaleX(1)}}@-webkit-keyframes buttonClicked{0%,20%,40%,60%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{-webkit-transform:scale3d(.95,.95,.95);transform:scale3d(.95,.95,.95)}60%{-webkit-transform:scale3d(1.02,1.02,1.02);transform:scale3d(1.02,1.02,1.02)}80%{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes buttonClicked{0%,20%,40%,60%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{-webkit-transform:scale3d(.95,.95,.95);transform:scale3d(.95,.95,.95)}60%{-webkit-transform:scale3d(1.02,1.02,1.02);transform:scale3d(1.02,1.02,1.02)}80%{-webkit-transform:scaleX(1);transform:scaleX(1)}}@-webkit-keyframes panelShowing{0%{opacity:0;-webkit-transform:translateY(-8px) scale(.9);transform:translateY(-8px) scale(.9)}70%{opacity:1;-webkit-transform:translateY(2px);transform:translateY(2px)}to{-webkit-transform:translateY(0);transform:translateY(0)}}@keyframes panelShowing{0%{opacity:0;-webkit-transform:translateY(-8px) scale(.9);transform:translateY(-8px) scale(.9)}70%{opacity:1;-webkit-transform:translateY(2px);transform:translateY(2px)}to{-webkit-transform:translateY(0);transform:translateY(0)}}.cdx-block{padding:.4em 0}.cdx-input{border:1px solid rgba(201,201,204,.48);-webkit-box-shadow:inset 0 1px 2px 0 rgba(35,44,72,.06);box-shadow:inset 0 1px 2px 0 rgba(35,44,72,.06);border-radius:3px;padding:10px 12px;outline:none;width:100%;-webkit-box-sizing:border-box;box-sizing:border-box}.cdx-input[data-placeholder]:before{position:static!important;display:inline-block;width:0;white-space:nowrap;pointer-events:none}.cdx-settings-button{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;width:34px;height:34px;line-height:34px;padding:0!important;text-align:center;border-radius:3px;cursor:pointer;border:0;outline:none;background-color:transparent;vertical-align:bottom;color:#000;margin:0}.cdx-settings-button:hover{background-color:#eff2f5}.cdx-settings-button .icon,.cdx-settings-button>svg{margin:auto}.cdx-settings-button--focused{-webkit-box-shadow:inset 0 0 0 1px rgba(7,161,227,.08);box-shadow:inset 0 0 0 1px rgba(7,161,227,.08);background:rgba(34,186,255,.08)!important}.cdx-settings-button--focused-animated{-webkit-animation-name:buttonClicked;animation-name:buttonClicked;-webkit-animation-duration:.25s;animation-duration:.25s}.cdx-settings-button:not(:nth-child(3n+3)){margin-right:3px}.cdx-settings-button:nth-child(n+4){margin-top:3px}.cdx-settings-button--active{color:#388ae5}.cdx-loader{position:relative;border:1px solid rgba(201,201,204,.48)}.cdx-loader:before{content:\\\"\\\";position:absolute;left:50%;top:50%;width:18px;height:18px;margin:-11px 0 0 -11px;border:2px solid rgba(201,201,204,.48);border-left-color:#388ae5;border-radius:50%;-webkit-animation:cdxRotation 1.2s linear infinite;animation:cdxRotation 1.2s linear infinite}@-webkit-keyframes cdxRotation{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes cdxRotation{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.cdx-button{padding:13px;border-radius:3px;border:1px solid rgba(201,201,204,.48);font-size:14.9px;background:#fff;-webkit-box-shadow:0 2px 2px 0 rgba(18,30,57,.04);box-shadow:0 2px 2px 0 rgba(18,30,57,.04);color:#707684;text-align:center;cursor:pointer}.cdx-button:hover{background:#fbfcfe;-webkit-box-shadow:0 1px 3px 0 rgba(18,30,57,.08);box-shadow:0 1px 3px 0 rgba(18,30,57,.08)}.cdx-button svg{height:20px;margin-right:.2em;margin-top:-2px}.ce-stub{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;width:100%;padding:3.5em 0;margin:17px 0;border-radius:3px;background:#fcf7f7;color:#b46262}.ce-stub__info{margin-left:20px}.ce-stub__title{margin-bottom:3px;font-weight:600;font-size:18px;text-transform:capitalize}.ce-stub__subtitle{font-size:16px}\""],"sourceRoot":""}
\ No newline at end of file
diff --git a/src/components/modules/blockEvents.ts b/src/components/modules/blockEvents.ts
index 36c1e490b..8e8b5bec7 100644
--- a/src/components/modules/blockEvents.ts
+++ b/src/components/modules/blockEvents.ts
@@ -141,11 +141,15 @@ export default class BlockEvents extends Module {
const { BlockManager, Tools, InlineToolbar, ConversionToolbar } = this.Editor;
const currentBlock = BlockManager.currentBlock;
-
+ const byPassTabInPlugins = ['code', 'simpleCode', 'table'];
if (!currentBlock) {
return;
}
+ for (const plugin of byPassTabInPlugins) {
+ if (plugin === currentBlock.name) { return; }
+ }
+
const canOpenToolbox = Tools.isInitial(currentBlock.tool) && currentBlock.isEmpty;
const conversionToolbarOpened = !currentBlock.isEmpty && ConversionToolbar.opened;
const inlineToolbarOpened = !currentBlock.isEmpty && !SelectionUtils.isCollapsed && InlineToolbar.opened;
@@ -373,6 +377,15 @@ export default class BlockEvents extends Module {
return;
}
+ // Don't let the codeblock mismerge with previous block
+ if (currentBlock.name === 'code') {
+ return;
+ }
+
+ if (currentBlock.name === 'simpleCode') {
+ return;
+ }
+
const isFirstBlock = BlockManager.currentBlockIndex === 0;
const canMergeBlocks = Caret.isAtStart &&
SelectionUtils.isCollapsed &&
diff --git a/src/components/modules/caret.ts b/src/components/modules/caret.ts
index 4584f0af9..e77aeed7c 100644
--- a/src/components/modules/caret.ts
+++ b/src/components/modules/caret.ts
@@ -432,6 +432,9 @@ export default class Caret extends Module {
if (force || this.isAtStart) {
/** If previous Tool`s input exists, focus on it. Otherwise set caret to the previous Block */
if (!previousInput) {
+ if (currentBlock.name === 'simpleCode') {
+ return false;
+ }
this.setToBlock( previousContentfulBlock, this.positions.END );
} else {
this.setToInput(previousInput, this.positions.END);
diff --git a/src/components/modules/toolbar/toolbox.ts b/src/components/modules/toolbar/toolbox.ts
index 13a939462..5b905d259 100644
--- a/src/components/modules/toolbar/toolbox.ts
+++ b/src/components/modules/toolbar/toolbox.ts
@@ -20,16 +20,18 @@ export default class Toolbox extends Module {
/**
* CSS styles
* @return {{toolbox: string, toolboxButton string, toolboxButtonActive: string,
+ * toolboxButtonApistackActive: string,
* toolboxOpened: string, tooltip: string, tooltipShown: string, tooltipShortcut: string}}
*/
get CSS() {
return {
toolbox: 'ce-toolbox',
toolboxButton: 'ce-toolbox__button',
+ toolboxButtonApistack: 'editor-toolbar-button',
+ toolboxButtonApistackActive : 'editor-toolbar-button-active',
toolboxButtonActive : 'ce-toolbox__button--active',
toolboxOpened: 'ce-toolbox--opened',
openedToolbarHolderModifier: 'codex-editor--toolbox-opened',
-
buttonTooltip: 'ce-toolbox-button-tooltip',
buttonShortcut: 'ce-toolbox-button-tooltip__shortcut',
};
@@ -145,6 +147,12 @@ export default class Toolbox extends Module {
}
}
+ private makeapistackToolboxButton(button: HTMLElement, buttonContent: any): HTMLElement {
+ button.innerHTML += `${buttonContent.icon}
+ ${buttonContent.customButtonName || buttonContent.name}`;
+ return button;
+ }
+
/**
* Append Tool to the Toolbox
*
@@ -179,10 +187,13 @@ export default class Toolbox extends Module {
const userToolboxSettings = this.Editor.Tools.getToolSettings(toolName)[userSettings.TOOLBOX] || {};
- const button = $.make('li', [ this.CSS.toolboxButton ]);
+ const button = this.makeapistackToolboxButton($.make('div', [ this.CSS.toolboxButtonApistack ]), {
+ name: toolName,
+ icon: userToolboxSettings.icon || toolToolboxSettings.icon,
+ customButtonName: userToolboxSettings.customButtonName || toolToolboxSettings.customButtonName || null,
+ });
button.dataset.tool = toolName;
- button.innerHTML = userToolboxSettings.icon || toolToolboxSettings.icon;
$.append(this.nodes.toolbox, button);
@@ -272,7 +283,7 @@ export default class Toolbox extends Module {
const tools = Array.from(this.nodes.toolbox.childNodes) as HTMLElement[];
this.flipper = new Flipper({
items: tools,
- focusedItemClass: this.CSS.toolboxButtonActive,
+ focusedItemClass: this.CSS.toolboxButtonApistackActive,
});
}
diff --git a/src/components/modules/ui.ts b/src/components/modules/ui.ts
index 5222a2ddf..afdadd0a6 100644
--- a/src/components/modules/ui.ts
+++ b/src/components/modules/ui.ts
@@ -581,8 +581,8 @@ export default class UI extends Module {
return;
}
- event.stopImmediatePropagation();
- event.stopPropagation();
+ // event.stopImmediatePropagation();
+ // event.stopPropagation();
if (!this.Editor.BlockManager.currentBlock) {
this.Editor.BlockManager.insert();
diff --git a/src/styles/toolbar.css b/src/styles/toolbar.css
index 9ce620faa..8f10538ef 100644
--- a/src/styles/toolbar.css
+++ b/src/styles/toolbar.css
@@ -60,8 +60,7 @@
}
}
- &__plus,
- .ce-toolbox {
+ &__plus, .ce-toolbox{
top: 50%;
transform: translateY(-50%);
}
diff --git a/src/styles/toolbox.css b/src/styles/toolbox.css
index ab4c11915..34040588a 100644
--- a/src/styles/toolbox.css
+++ b/src/styles/toolbox.css
@@ -1,10 +1,15 @@
.ce-toolbox {
+ /* apistack override start*/
+ max-width: 150px;
+ width: 100%;
+ /* apistack override end*/
position: absolute;
visibility: hidden;
transition: opacity 100ms ease;
will-change: opacity;
display: flex;
flex-direction: row;
+ flex-wrap: wrap;
@media (--mobile){
position: static;
@@ -16,12 +21,18 @@
&--opened {
opacity: 1;
visibility: visible;
+ transform: translate3d(0px, 0, 0px)!important;
}
- &__button {
- @apply --toolbox-button;
+ &__button-apistack {
+ @apply --toolbox-button-apistack;
flex-shrink: 0;
}
+
+ /* &__button {
+ @apply --toolbox-button;
+ flex-shrink: 0;
+ } */
}
.ce-toolbox-button-tooltip {
diff --git a/src/styles/variables.css b/src/styles/variables.css
index 2b7fe4e61..860ccac85 100644
--- a/src/styles/variables.css
+++ b/src/styles/variables.css
@@ -49,6 +49,11 @@
*/
--toolbox-buttons-size: 34px;
+ /**
+ * Apistack-Override : Toolbar Plus Button and Toolbox buttons height and width
+ */
+ --toolbox-buttons-size-apistack: 60px;
+
/**
* Confirm deletion bg
*/
@@ -107,6 +112,47 @@
};
+ --toolbox-button-apistack: {
+ color: var(--grayText);
+ cursor: pointer;
+ width: 50%;
+ /* width: var(--toolbox-buttons-size-apistack); */
+ height: var(--toolbox-buttons-size-apistack);
+ display: inline-flex;
+ flex-direction: column;
+ list-style-type: none;
+ justify-content: center;
+ align-items: center;
+ text-align: center;
+ border: 1px solid #000;
+
+ /* apistack overrides start*/
+ &__name {
+ font-weight: bold;
+ font-size: 12px;
+ text-transform: capitalize;
+ }
+ &__icon {
+ text-align: center;
+ }
+ &__icon > svg {
+ width: 12px;
+ height: 14px;
+ }
+ /* apistack overrides end*/
+
+ &:hover,
+ &--active {
+ color: var(--color-active-icon);
+ }
+
+ &--active{
+ animation: bounceIn 0.25s 1;
+ animation-fill-mode: forwards;
+ }
+
+ };
+
/**
* Styles for Settings Button in Toolbar
*/
diff --git a/test/index.html b/test/index.html
new file mode 100644
index 000000000..e650bc115
--- /dev/null
+++ b/test/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+ Document
+
+
+
+
+
+
+
+
\ No newline at end of file