1window.TinyMCERails = { 2 configuration: { 3 default: {} 4 }, 5 6 initialize: function(config, options) { 7 if (typeof tinyMCE != 'undefined') { 8 // Merge the custom options with the given configuration 9 var configuration = TinyMCERails.configuration[config || 'default']; 10 configuration = TinyMCERails._merge(configuration, options); 11 12 tinymce.init(configuration); 13 } else { 14 // Wait until TinyMCE is loaded 15 setTimeout(function() { 16 TinyMCERails.initialize(config, options); 17 }, 50); 18 } 19 }, 20 21 setupTurbolinks: function() { 22 // Remove all TinyMCE instances before rendering 23 document.addEventListener('turbolinks:before-render', function() { 24 tinymce.remove(); 25 }); 26 }, 27 28 _merge: function() { 29 var result = {}; 30 31 for (var i = 0; i < arguments.length; ++i) { 32 var source = arguments[i]; 33 34 for (var key in source) { 35 if (Object.prototype.hasOwnProperty.call(source, key)) { 36 if (Object.prototype.toString.call(source[key]) === '[object Object]') { 37 result[key] = TinyMCERails._merge(result[key], source[key]); 38 } else { 39 result[key] = source[key]; 40 } 41 } 42 } 43 } 44 45 return result; 46 } 47}; 48 49if (typeof Turbolinks != 'undefined' && Turbolinks.supported) { 50 TinyMCERails.setupTurbolinks(); 51}; 52window.tinymce = window.tinymce || { 53 base: '/g4h_assets/tinymce', 54 suffix: '' 55}; 56/** 57 * TinyMCE version 8.4.0 (2026-03-31
vendor: 20,506 bytes, lines 57-631
57) 58 */ 59 60(function () { 61 'use strict'; 62 63 var typeOf$1 = function (x) { 64 if (x === null) { 65 return 'null'; 66 } 67 if (x === undefined) { 68 return 'undefined'; 69 } 70 var t = typeof x; 71 if (t === 'object' && (Array.prototype.isPrototypeOf(x) || x.constructor && x.constructor.name === 'Array')) { 72 return 'array'; 73 } 74 if (t === 'object' && (String.prototype.isPrototypeOf(x) || x.constructor && x.constructor.name === 'String')) { 75 return 'string'; 76 } 77 return t; 78 }; 79 var isEquatableType = function (x) { 80 return ['undefined', 'boolean', 'number', 'string', 'function', 'xml', 'null'].indexOf(x) !== -1; 81 }; 82 83 var sort$1 = function (xs, compareFn) { 84 var clone = Array.prototype.slice.call(xs); 85 return clone.sort(compareFn); 86 }; 87 88 var contramap = function (eqa, f) { 89 return eq$2(function (x, y) { return eqa.eq(f(x), f(y)); }); 90 }; 91 var eq$2 = function (f) { 92 return ({ eq: f }); 93 }; 94 var tripleEq = eq$2(function (x, y) { return x === y; }); 95 var eqString = tripleEq; 96 var eqArray = function (eqa) { return eq$2(function (x, y) { 97 if (x.length !== y.length) { 98 return false; 99 } 100 var len = x.length; 101 for (var i = 0; i < len; i++) { 102 if (!eqa.eq(x[i], y[i])) { 103 return false; 104 } 105 } 106 return true; 107 }); }; 108 // TODO: Make an Ord typeclass 109 var eqSortedArray = function (eqa, compareFn) { 110 return contramap(eqArray(eqa), function (xs) { return sort$1(xs, compareFn); }); 111 }; 112 var eqRecord = function (eqa) { return eq$2(function (x, y) { 113 var kx = Object.keys(x); 114 var ky = Object.keys(y); 115 if (!eqSortedArray(eqString).eq(kx, ky)) { 116 return false; 117 } 118 var len = kx.length; 119 for (var i = 0; i < len; i++) { 120 var q = kx[i]; 121 if (!eqa.eq(x[q], y[q])) { 122 return false; 123 } 124 } 125 return true; 126 }); }; 127 var eqAny = eq$2(function (x, y) { 128 if (x === y) { 129 return true; 130 } 131 var tx = typeOf$1(x); 132 var ty = typeOf$1(y); 133 if (tx !== ty) { 134 return false; 135 } 136 if (isEquatableType(tx)) { 137 return x === y; 138 } 139 else if (tx === 'array') { 140 return eqArray(eqAny).eq(x, y); 141 } 142 else if (tx === 'object') { 143 return eqRecord(eqAny).eq(x, y); 144 } 145 return false; 146 }); 147 148 /* eslint-disable @typescript-eslint/no-wrapper-object-types */ 149 const getPrototypeOf$2 = Object.getPrototypeOf; 150 const hasProto = (v, constructor, predicate) => { 151 if (predicate(v, constructor.prototype)) { 152 return true; 153 } 154 else { 155 // String-based fallback time 156 return v.constructor?.name === constructor.name; 157 } 158 }; 159 const typeOf = (x) => { 160 const t = typeof x; 161 if (x === null) { 162 return 'null'; 163 } 164 else if (t === 'object' && Array.isArray(x)) { 165 return 'array'; 166 } 167 else if (t === 'object' && hasProto(x, String, (o, proto) => proto.isPrototypeOf(o))) { 168 return 'string'; 169 } 170 else { 171 return t; 172 } 173 }; 174 const isType$1 = (type) => (value) => typeOf(value) === type; 175 const isSimpleType = (type) => (value) => typeof value === type; 176 const eq$1 = (t) => (a) => t === a; 177 const is$5 = (value, constructor) => isObject(value) && hasProto(value, constructor, (o, proto) => getPrototypeOf$2(o) === proto); 178 const isString = isType$1('string'); 179 const isObject = isType$1('object'); 180 const isPlainObject = (value) => is$5(value, Object); 181 const isArray$1 = isType$1('array'); 182 const isNull = eq$1(null); 183 const isBoolean = isSimpleType('boolean'); 184 const isUndefined = eq$1(undefined); 185 const isNullable = (a) => a === null || a === undefined; 186 const isNonNullable = (a) => !isNullable(a); 187 const isFunction = isSimpleType('function'); 188 const isNumber = isSimpleType('number'); 189 const isArrayOf = (value, pred) => { 190 if (isArray$1(value)) { 191 for (let i = 0, len = value.length; i < len; ++i) { 192 if (!(pred(value[i]))) { 193 return false; 194 } 195 } 196 return true; 197 } 198 return false; 199 }; 200 201 const noop = () => { }; 202 /** Compose a unary function with an n-ary function */ 203 const compose = (fa, fb) => { 204 return (...args) => { 205 return fa(fb.apply(null, args)); 206 }; 207 }; 208 /** Compose two unary functions. Similar to compose, but avoids using Function.prototype.apply. */ 209 const compose1 = (fbc, fab) => (a) => fbc(fab(a)); 210 const constant = (value) => { 211 return () => { 212 return value; 213 }; 214 }; 215 const identity = (x) => { 216 return x; 217 }; 218 const tripleEquals = (a, b) => { 219 return a === b; 220 }; 221 function curry(fn, ...initialArgs) { 222 return (...restArgs) => { 223 const all = initialArgs.concat(restArgs); 224 return fn.apply(null, all); 225 }; 226 } 227 const not = (f) => (t) => !f(t); 228 const die = (msg) => { 229 return () => { 230 throw new Error(msg); 231 }; 232 }; 233 const apply$1 = (f) => { 234 return f(); 235 }; 236 const call = (f) => { 237 f(); 238 }; 239 const never = constant(false); 240 const always = constant(true); 241 242 /** 243 * The `Optional` type represents a value (of any type) that potentially does 244 * not exist. Any `Optional<T>` can either be a `Some<T>` (in which case the 245 * value does exist) or a `None` (in which case the value does not exist). This 246 * module defines a whole lot of FP-inspired utility functions for dealing with 247 * `Optional` objects. 248 * 249 * Comparison with null or undefined: 250 * - We don't get fancy null coalescing operators with `Optional` 251 * - We do get fancy helper functions with `Optional` 252 * - `Optional` support nesting, and allow for the type to still be nullable (or 253 * another `Optional`) 254 * - There is no option to turn off strict-optional-checks like there is for 255 * strict-null-checks 256 */ 257 class Optional { 258 tag; 259 value; 260 // Sneaky optimisation: every instance of Optional.none is identical, so just 261 // reuse the same object 262 static singletonNone = new Optional(false); 263 // The internal representation has a `tag` and a `value`, but both are 264 // private: able to be console.logged, but not able to be accessed by code 265 constructor(tag, value) { 266 this.tag = tag; 267 this.value = value; 268 } 269 // --- Identities --- 270 /** 271 * Creates a new `Optional<T>` that **does** contain a value. 272 */ 273 static some(value) { 274 return new Optional(true, value); 275 } 276 /** 277 * Create a new `Optional<T>` that **does not** contain a value. `T` can be 278 * any type because we don't actually have a `T`. 279 */ 280 static none() { 281 return Optional.singletonNone; 282 } 283 /** 284 * Perform a transform on an `Optional` type. Regardless of whether this 285 * `Optional` contains a value or not, `fold` will return a value of type `U`. 286 * If this `Optional` does not contain a value, the `U` will be created by 287 * calling `onNone`. If this `Optional` does contain a value, the `U` will be 288 * created by calling `onSome`. 289 * 290 * For the FP enthusiasts in the room, this function: 291 * 1. Could be used to implement all of the functions below 292 * 2. Forms a catamorphism 293 */ 294 fold(onNone, onSome) { 295 if (this.tag) { 296 return onSome(this.value); 297 } 298 else { 299 return onNone(); 300 } 301 } 302 /** 303 * Determine if this `Optional` object contains a value. 304 */ 305 isSome() { 306 return this.tag; 307 } 308 /** 309 * Determine if this `Optional` object **does not** contain a value. 310 */ 311 isNone() { 312 return !this.tag; 313 } 314 // --- Functor (name stolen from Haskell / maths) --- 315 /** 316 * Perform a transform on an `Optional` object, **if** there is a value. If 317 * you provide a function to turn a T into a U, this is the function you use 318 * to turn an `Optional<T>` into an `Optional<U>`. If this **does** contain 319 * a value then the output will also contain a value (that value being the 320 * output of `mapper(this.value)`), and if this **does not** contain a value 321 * then neither will the output. 322 */ 323 map(mapper) { 324 if (this.tag) { 325 return Optional.some(mapper(this.value)); 326 } 327 else { 328 return Optional.none(); 329 } 330 } 331 // --- Monad (name stolen from Haskell / maths) --- 332 /** 333 * Perform a transform on an `Optional` object, **if** there is a value. 334 * Unlike `map`, here the transform itself also returns an `Optional`. 335 */ 336 bind(binder) { 337 if (this.tag) { 338 return binder(this.value); 339 } 340 else { 341 return Optional.none(); 342 } 343 } 344 // --- Traversable (name stolen from Haskell / maths) --- 345 /** 346 * For a given predicate, this function finds out if there **exists** a value 347 * inside this `Optional` object that meets the predicate. In practice, this 348 * means that for `Optional`s that do not contain a value it returns false (as 349 * no predicate-meeting value exists). 350 */ 351 exists(predicate) { 352 return this.tag && predicate(this.value); 353 } 354 /** 355 * For a given predicate, this function finds out if **all** the values inside 356 * this `Optional` object meet the predicate. In practice, this means that 357 * for `Optional`s that do not contain a value it returns true (as all 0 358 * objects do meet the predicate). 359 */ 360 forall(predicate) { 361 return !this.tag || predicate(this.value); 362 } 363 filter(predicate) { 364 if (!this.tag || predicate(this.value)) { 365 return this; 366 } 367 else { 368 return Optional.none(); 369 } 370 } 371 // --- Getters --- 372 /** 373 * Get the value out of the inside of the `Optional` object, using a default 374 * `replacement` value if the provided `Optional` object does not contain a 375 * value. 376 */ 377 getOr(replacement) { 378 return this.tag ? this.value : replacement; 379 } 380 /** 381 * Get the value out of the inside of the `Optional` object, using a default 382 * `replacement` value if the provided `Optional` object does not contain a 383 * value. Unlike `getOr`, in this method the `replacement` object is also 384 * `Optional` - meaning that this method will always return an `Optional`. 385 */ 386 or(replacement) { 387 return this.tag ? this : replacement; 388 } 389 /** 390 * Get the value out of the inside of the `Optional` object, using a default 391 * `replacement` value if the provided `Optional` object does not contain a 392 * value. Unlike `getOr`, in this method the `replacement` value is 393 * "thunked" - that is to say that you don't pass a value to `getOrThunk`, you 394 * pass a function which (if called) will **return** the `value` you want to 395 * use. 396 */ 397 getOrThunk(thunk) { 398 return this.tag ? this.value : thunk(); 399 } 400 /** 401 * Get the value out of the inside of the `Optional` object, using a default 402 * `replacement` value if the provided Optional object does not contain a 403 * value. 404 * 405 * Unlike `or`, in this method the `replacement` value is "thunked" - that is 406 * to say that you don't pass a value to `orThunk`, you pass a function which 407 * (if called) will **return** the `value` you want to use. 408 * 409 * Unlike `getOrThunk`, in this method the `replacement` value is also 410 * `Optional`, meaning that this method will always return an `Optional`. 411 */ 412 orThunk(thunk) { 413 return this.tag ? this : thunk(); 414 } 415 /** 416 * Get the value out of the inside of the `Optional` object, throwing an 417 * exception if the provided `Optional` object does not contain a value. 418 * 419 * WARNING: 420 * You should only be using this function if you know that the `Optional` 421 * object **is not** empty (otherwise you're throwing exceptions in production 422 * code, which is bad). 423 * 424 * In tests this is more acceptable. 425 * 426 * Prefer other methods to this, such as `.each`. 427 */ 428 getOrDie(message) { 429 if (!this.tag) { 430 throw new Error(message ?? 'Called getOrDie on None'); 431 } 432 else { 433 return this.value; 434 } 435 } 436 // --- Interop with null and undefined --- 437 /** 438 * Creates an `Optional` value from a nullable (or undefined-able) input. 439 * Null, or undefined, is converted to `None`, and anything else is converted 440 * to `Some`. 441 */ 442 static from(value) { 443 return isNonNullable(value) ? Optional.some(value) : Optional.none(); 444 } 445 /** 446 * Converts an `Optional` to a nullable type, by getting the value if it 447 * exists, or returning `null` if it does not. 448 */ 449 getOrNull() { 450 return this.tag ? this.value : null; 451 } 452 /** 453 * Converts an `Optional` to an undefined-able type, by getting the value if 454 * it exists, or returning `undefined` if it does not. 455 */ 456 getOrUndefined() { 457 return this.value; 458 } 459 // --- Utilities --- 460 /** 461 * If the `Optional` contains a value, perform an action on that value. 462 * Unlike the rest of the methods on this type, `.each` has side-effects. If 463 * you want to transform an `Optional<T>` **into** something, then this is not 464 * the method for you. If you want to use an `Optional<T>` to **do** 465 * something, then this is the method for you - provided you're okay with not 466 * doing anything in the case where the `Optional` doesn't have a value inside 467 * it. If you're not sure whether your use-case fits into transforming 468 * **into** something or **doing** something, check whether it has a return 469 * value. If it does, you should be performing a transform. 470 */ 471 each(worker) { 472 if (this.tag) { 473 worker(this.value); 474 } 475 } 476 /** 477 * Turn the `Optional` object into an array that contains all of the values 478 * stored inside the `Optional`. In practice, this means the output will have 479 * either 0 or 1 elements. 480 */ 481 toArray() { 482 return this.tag ? [this.value] : []; 483 } 484 /** 485 * Turn the `Optional` object into a string for debugging or printing. Not 486 * recommended for production code, but good for debugging. Also note that 487 * these days an `Optional` object can be logged to the console directly, and 488 * its inner value (if it exists) will be visible. 489 */ 490 toString() { 491 return this.tag ? `some(${this.value})` : 'none()'; 492 } 493 } 494 495 const nativeSlice = Array.prototype.slice; 496 const nativeIndexOf = Array.prototype.indexOf; 497 const nativePush = Array.prototype.push; 498 const rawIndexOf = (ts, t) => nativeIndexOf.call(ts, t); 499 const indexOf$1 = (xs, x) => { 500 // The rawIndexOf method does not wrap up in an option. This is for performance reasons. 501 const r = rawIndexOf(xs, x); 502 return r === -1 ? Optional.none() : Optional.some(r); 503 }; 504 const contains$2 = (xs, x) => rawIndexOf(xs, x) > -1; 505 const exists = (xs, pred) => { 506 for (let i = 0, len = xs.length; i < len; i++) { 507 const x = xs[i]; 508 if (pred(x, i)) { 509 return true; 510 } 511 } 512 return false; 513 }; 514 const map$3 = (xs, f) => { 515 // pre-allocating array size when it's guaranteed to be known 516 // http://jsperf.com/push-allocated-vs-dynamic/22 517 const len = xs.length; 518 const r = new Array(len); 519 for (let i = 0; i < len; i++) { 520 const x = xs[i]; 521 r[i] = f(x, i); 522 } 523 return r; 524 }; 525 // Unwound implementing other functions in terms of each. 526 // The code size is roughly the same, and it should allow for better optimisation. 527 // const each = function<T, U>(xs: T[], f: (x: T, i?: number, xs?: T[]) => void): void { 528 const each$e = (xs, f) => { 529 for (let i = 0, len = xs.length; i < len; i++) { 530 const x = xs[i]; 531 f(x, i); 532 } 533 }; 534 const eachr = (xs, f) => { 535 for (let i = xs.length - 1; i >= 0; i--) { 536 const x = xs[i]; 537 f(x, i); 538 } 539 }; 540 const partition$2 = (xs, pred) => { 541 const pass = []; 542 const fail = []; 543 for (let i = 0, len = xs.length; i < len; i++) { 544 const x = xs[i]; 545 const arr = pred(x, i) ? pass : fail; 546 arr.push(x); 547 } 548 return { pass, fail }; 549 }; 550 const filter$5 = (xs, pred) => { 551 const r = []; 552 for (let i = 0, len = xs.length; i < len; i++) { 553 const x = xs[i]; 554 if (pred(x, i)) { 555 r.push(x); 556 } 557 } 558 return r; 559 }; 560 /* 561 * Groups an array into contiguous arrays of like elements. Whether an element is like or not depends on f. 562 * 563 * f is a function that derives a value from an element - e.g. true or false, or a string. 564 * Elements are like if this function generates the same value for them (according to ===). 565 * 566 * 567 * Order of the elements is preserved. Arr.flatten() on the result will return the original list, as with Haskell groupBy function. 568 * For a good explanation, see the group function (which is a special case of groupBy) 569 * http://hackage.haskell.org/package/base-4.7.0.0/docs/Data-List.html#v:group 570 */ 571 const groupBy = (xs, f) => { 572 if (xs.length === 0) { 573 return []; 574 } 575 else { 576 let wasType = f(xs[0]); // initial case for matching 577 const r = []; 578 let group = []; 579 for (let i = 0, len = xs.length; i < len; i++) { 580 const x = xs[i]; 581 const type = f(x); 582 if (type !== wasType) { 583 r.push(group); 584 group = []; 585 } 586 wasType = type; 587 group.push(x); 588 } 589 if (group.length !== 0) { 590 r.push(group); 591 } 592 return r; 593 } 594 }; 595 const foldr = (xs, f, acc) => { 596 eachr(xs, (x, i) => { 597 acc = f(acc, x, i); 598 }); 599 return acc; 600 }; 601 const foldl = (xs, f, acc) => { 602 each$e(xs, (x, i) => { 603 acc = f(acc, x, i); 604 }); 605 return acc; 606 }; 607 const findUntil$1 = (xs, pred, until) => { 608 for (let i = 0, len = xs.length; i < len; i++) { 609 const x = xs[i]; 610 if (pred(x, i)) { 611 return Optional.some(x); 612 } 613 else if (until(x, i)) { 614 break; 615 } 616 } 617 return Optional.none(); 618 }; 619 const find$2 = (xs, pred) => { 620 return findUntil$1(xs, pred, never); 621 }; 622 const findIndex$2 = (xs, pred) => { 623 for (let i = 0, len = xs.length; i < len; i++) { 624 const x = xs[i]; 625 if (pred(x, i)) { 626 return Optional.some(i); 627 } 628 } 629 return Optional.none(); 630 }; 631 const findLast
631Index = (arr, pred) => { 632 for (let i = arr.length - 1; i >= 0; i--) { 633 if (pred(arr[i], i)) { 634 return Optional.some(i); 635 } 636 } 637 return Optional.none(); 638 }; 639 const flatten$1 = (xs) => { 640 // Note, this is possible because push supports multiple arguments: 641 // http://jsperf.com/concat-push/6 642 // Note that in the past, concat() would silently work (very slowly) for array-like objects.
vendor: 3,862 bytes, lines 643-767
643 // With this change it will throw an error. 644 const r = []; 645 for (let i = 0, len = xs.length; i < len; ++i) { 646 // Ensure that each value is an array itself 647 if (!isArray$1(xs[i])) { 648 throw new Error('Arr.flatten item ' + i + ' was not an array, input: ' + xs); 649 } 650 nativePush.apply(r, xs[i]); 651 } 652 return r; 653 }; 654 const bind$3 = (xs, f) => flatten$1(map$3(xs, f)); 655 const forall = (xs, pred) => { 656 for (let i = 0, len = xs.length; i < len; ++i) { 657 const x = xs[i]; 658 if (pred(x, i) !== true) { 659 return false; 660 } 661 } 662 return true; 663 }; 664 const reverse = (xs) => { 665 const r = nativeSlice.call(xs, 0); 666 r.reverse(); 667 return r; 668 }; 669 const difference = (a1, a2) => filter$5(a1, (x) => !contains$2(a2, x)); 670 const mapToObject = (xs, f) => { 671 const r = {}; 672 for (let i = 0, len = xs.length; i < len; i++) { 673 const x = xs[i]; 674 r[String(x)] = f(x, i); 675 } 676 return r; 677 }; 678 const sort = (xs, comparator) => { 679 const copy = nativeSlice.call(xs, 0); 680 copy.sort(comparator); 681 return copy; 682 }; 683 const get$b = (xs, i) => i >= 0 && i < xs.length ? Optional.some(xs[i]) : Optional.none(); 684 const head = (xs) => get$b(xs, 0); 685 const last$2 = (xs) => get$b(xs, xs.length - 1); 686 const from = isFunction(Array.from) ? Array.from : (x) => nativeSlice.call(x); 687 const findMap = (arr, f) => { 688 for (let i = 0; i < arr.length; i++) { 689 const r = f(arr[i], i); 690 if (r.isSome()) { 691 return r; 692 } 693 } 694 return Optional.none(); 695 }; 696 const unique$1 = (xs, comparator) => { 697 const r = []; 698 const isDuplicated = isFunction(comparator) ? 699 (x) => exists(r, (i) => comparator(i, x)) : 700 (x) => contains$2(r, x); 701 for (let i = 0, len = xs.length; i < len; i++) { 702 const x = xs[i]; 703 if (!isDuplicated(x)) { 704 r.push(x); 705 } 706 } 707 return r; 708 }; 709 710 // There are many variations of Object iteration that are faster than the 'for-in' style: 711 // http://jsperf.com/object-keys-iteration/107 712 // 713 // Use the native keys if it is available (IE9+), otherwise fall back to manually filtering 714 const keys = Object.keys; 715 const hasOwnProperty$1 = Object.hasOwnProperty; 716 const each$d = (obj, f) => { 717 const props = keys(obj); 718 for (let k = 0, len = props.length; k < len; k++) { 719 const i = props[k]; 720 const x = obj[i]; 721 f(x, i); 722 } 723 }; 724 const map$2 = (obj, f) => { 725 return tupleMap(obj, (x, i) => ({ 726 k: i, 727 v: f(x, i) 728 })); 729 }; 730 const tupleMap = (obj, f) => { 731 const r = {}; 732 each$d(obj, (x, i) => { 733 const tuple = f(x, i); 734 r[tuple.k] = tuple.v; 735 }); 736 return r; 737 }; 738 const objAcc = (r) => (x, i) => { 739 r[i] = x; 740 }; 741 const internalFilter = (obj, pred, onTrue, onFalse) => { 742 each$d(obj, (x, i) => { 743 (pred(x, i) ? onTrue : onFalse)(x, i); 744 }); 745 }; 746 const bifilter = (obj, pred) => { 747 const t = {}; 748 const f = {}; 749 internalFilter(obj, pred, objAcc(t), objAcc(f)); 750 return { t, f }; 751 }; 752 const filter$4 = (obj, pred) => { 753 const t = {}; 754 internalFilter(obj, pred, objAcc(t), noop); 755 return t; 756 }; 757 const mapToArray = (obj, f) => { 758 const r = []; 759 each$d(obj, (value, name) => { 760 r.push(f(value, name)); 761 }); 762 return r; 763 }; 764 const values = (obj) => { 765 return mapToArray(obj, identity); 766 }; 767 const get$a
vendor: 53,531 bytes, lines 767-2256
767 = (obj, key) => { 768 return has$2(obj, key) ? Optional.from(obj[key]) : Optional.none(); 769 }; 770 const has$2 = (obj, key) => hasOwnProperty$1.call(obj, key); 771 const hasNonNullableKey = (obj, key) => has$2(obj, key) && obj[key] !== undefined && obj[key] !== null; 772 const equal$1 = (a1, a2, eq = eqAny) => eqRecord(eq).eq(a1, a2); 773 774 /* 775 * Generates a church encoded ADT (https://en.wikipedia.org/wiki/Church_encoding) 776 * For syntax and use, look at the test code. 777 */ 778 const generate$1 = (cases) => { 779 // validation 780 if (!isArray$1(cases)) { 781 throw new Error('cases must be an array'); 782 } 783 if (cases.length === 0) { 784 throw new Error('there must be at least one case'); 785 } 786 const constructors = []; 787 // adt is mutated to add the individual cases 788 const adt = {}; 789 each$e(cases, (acase, count) => { 790 const keys$1 = keys(acase); 791 // validation 792 if (keys$1.length !== 1) { 793 throw new Error('one and only one name per case'); 794 } 795 const key = keys$1[0]; 796 const value = acase[key]; 797 // validation 798 if (adt[key] !== undefined) { 799 throw new Error('duplicate key detected:' + key); 800 } 801 else if (key === 'cata') { 802 throw new Error('cannot have a case named cata (sorry)'); 803 } 804 else if (!isArray$1(value)) { 805 // this implicitly checks if acase is an object 806 throw new Error('case arguments must be an array'); 807 } 808 constructors.push(key); 809 // 810 // constructor for key 811 // 812 adt[key] = (...args) => { 813 const argLength = args.length; 814 // validation 815 if (argLength !== value.length) { 816 throw new Error('Wrong number of arguments to case ' + key + '. Expected ' + value.length + ' (' + value + '), got ' + argLength); 817 } 818 const match = (branches) => { 819 const branchKeys = keys(branches); 820 if (constructors.length !== branchKeys.length) { 821 throw new Error('Wrong number of arguments to match. Expected: ' + constructors.join(',') + '\nActual: ' + branchKeys.join(',')); 822 } 823 const allReqd = forall(constructors, (reqKey) => { 824 return contains$2(branchKeys, reqKey); 825 }); 826 if (!allReqd) { 827 throw new Error('Not all branches were specified when using match. Specified: ' + branchKeys.join(', ') + '\nRequired: ' + constructors.join(', ')); 828 } 829 return branches[key].apply(null, args); 830 }; 831 // 832 // the fold function for key 833 // 834 return { 835 fold: (...foldArgs) => { 836 // runtime validation 837 if (foldArgs.length !== cases.length) { 838 throw new Error('Wrong number of arguments to fold. Expected ' + cases.length + ', got ' + foldArgs.length); 839 } 840 const target = foldArgs[count]; 841 return target.apply(null, args); 842 }, 843 match, 844 // NOTE: Only for debugging. 845 log: (label) => { 846 // eslint-disable-next-line no-console 847 console.log(label, { 848 constructors, 849 constructor: key, 850 params: args 851 }); 852 } 853 }; 854 }; 855 }); 856 return adt; 857 }; 858 const Adt = { 859 generate: generate$1 860 }; 861 862 const Cell = (initial) => { 863 let value = initial; 864 const get = () => { 865 return value; 866 }; 867 const set = (v) => { 868 value = v; 869 }; 870 return { 871 get, 872 set 873 }; 874 }; 875 876 /** 877 * Creates a new `Result<T, E>` that **does** contain a value. 878 */ 879 const value$2 = (value) => { 880 const applyHelper = (fn) => fn(value); 881 const constHelper = constant(value); 882 const outputHelper = () => output; 883 const output = { 884 // Debug info 885 tag: true, 886 inner: value, 887 // Actual Result methods 888 fold: (_onError, onValue) => onValue(value), 889 isValue: always, 890 isError: never, 891 map: (mapper) => Result.value(mapper(value)), 892 mapError: outputHelper, 893 bind: applyHelper, 894 exists: applyHelper, 895 forall: applyHelper, 896 getOr: constHelper, 897 or: outputHelper, 898 getOrThunk: constHelper, 899 orThunk: outputHelper, 900 getOrDie: constHelper, 901 each: (fn) => { 902 // Can't write the function inline because we don't want to return something by mistake 903 fn(value); 904 }, 905 toOptional: () => Optional.some(value), 906 }; 907 return output; 908 }; 909 /** 910 * Creates a new `Result<T, E>` that **does not** contain a value, and therefore 911 * contains an error. 912 */ 913 const error = (error) => { 914 const outputHelper = () => output; 915 const output = { 916 // Debug info 917 tag: false, 918 inner: error, 919 // Actual Result methods 920 fold: (onError, _onValue) => onError(error), 921 isValue: never, 922 isError: always, 923 map: outputHelper, 924 mapError: (mapper) => Result.error(mapper(error)), 925 bind: outputHelper, 926 exists: never, 927 forall: always, 928 getOr: identity, 929 or: identity, 930 getOrThunk: apply$1, 931 orThunk: apply$1, 932 getOrDie: die(String(error)), 933 each: noop, 934 toOptional: Optional.none, 935 }; 936 return output; 937 }; 938 /** 939 * Creates a new `Result<T, E>` from an `Optional<T>` and an `E`. If the 940 * `Optional` contains a value, so will the outputted `Result`. If it does not, 941 * the outputted `Result` will contain an error (and that error will be the 942 * error passed in). 943 */ 944 const fromOption = (optional, err) => optional.fold(() => error(err), value$2); 945 const Result = { 946 value: value$2, 947 error, 948 fromOption 949 }; 950 951 // Use window object as the global if it's available since CSP will block script evals 952 // eslint-disable-next-line @typescript-eslint/no-implied-eval 953 const Global = typeof window !== 'undefined' ? window : Function('return this;')(); 954 955 /* eslint-disable no-bitwise */ 956 const uuidV4Bytes = () => { 957 const bytes = window.crypto.getRandomValues(new Uint8Array(16)); 958 // https://tools.ietf.org/html/rfc4122#section-4.1.3 959 // This will first bit mask away the most significant 4 bits (version octet) 960 // then mask in the v4 number we only care about v4 random version at this point so (byte & 0b00001111 | 0b01000000) 961 bytes[6] = bytes[6] & 15 | 64; 962 // https://tools.ietf.org/html/rfc4122#section-4.1.1 963 // This will first bit mask away the highest two bits then masks in the highest bit so (byte & 0b00111111 | 0b10000000) 964 // So it will set the Msb0=1 & Msb1=0 described by the "The variant specified in this document." row in the table 965 bytes[8] = bytes[8] & 63 | 128; 966 return bytes; 967 }; 968 const uuidV4String = () => { 969 const uuid = uuidV4Bytes(); 970 const getHexRange = (startIndex, endIndex) => { 971 let buff = ''; 972 for (let i = startIndex; i <= endIndex; ++i) { 973 const hexByte = uuid[i].toString(16).padStart(2, '0'); 974 buff += hexByte; 975 } 976 return buff; 977 }; 978 // RFC 4122 UUID format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 979 return `${getHexRange(0, 3)}-${getHexRange(4, 5)}-${getHexRange(6, 7)}-${getHexRange(8, 9)}-${getHexRange(10, 15)}`; 980 }; 981 982 /** 983 * Adds two numbers, and wrap to a range. 984 * If the result overflows to the right, snap to the left. 985 * If the result overflows to the left, snap to the right. 986 */ 987 // ASSUMPTION: Max will always be larger than min 988 const clamp$2 = (value, min, max) => Math.min(Math.max(value, min), max); 989 // the division is meant to get a number between 0 and 1 for more information check this discussion: https://stackoverflow.com/questions/58285941/how-to-replace-math-random-with-crypto-getrandomvalues-and-keep-same-result 990 const random = () => window.crypto.getRandomValues(new Uint32Array(1))[0] / 4294967295; 991 992 /** 993 * Generate a unique identifier. 994 * 995 * The unique portion of the identifier only contains an underscore 996 * and digits, so that it may safely be used within HTML attributes. 997 * 998 * The chance of generating a non-unique identifier has been minimized 999 * by combining the current time, a random number and a one-up counter. 1000 * 1001 * generate :: String -> String 1002 */ 1003 let unique = 0; 1004 const generate = (prefix) => { 1005 const date = new Date(); 1006 const time = date.getTime(); 1007 const random$1 = Math.floor(random() * 1000000000); 1008 unique++; 1009 return prefix + '_' + random$1 + unique + String(time); 1010 }; 1011 /** 1012 * Generate a uuidv4 string 1013 * In accordance with RFC 4122 (https://datatracker.ietf.org/doc/html/rfc4122) 1014 */ 1015 const uuidV4 = () => { 1016 if (window.isSecureContext) { 1017 return window.crypto.randomUUID(); 1018 } 1019 else { 1020 return uuidV4String(); 1021 } 1022 }; 1023 1024 const shallow$1 = (old, nu) => { 1025 return nu; 1026 }; 1027 const deep$1 = (old, nu) => { 1028 const bothObjects = isPlainObject(old) && isPlainObject(nu); 1029 return bothObjects ? deepMerge(old, nu) : nu; 1030 }; 1031 const baseMerge = (merger) => { 1032 return (...objects) => { 1033 if (objects.length === 0) { 1034 throw new Error(`Can't merge zero objects`); 1035 } 1036 const ret = {}; 1037 for (let j = 0; j < objects.length; j++) { 1038 const curObject = objects[j]; 1039 for (const key in curObject) { 1040 if (has$2(curObject, key)) { 1041 ret[key] = merger(ret[key], curObject[key]); 1042 } 1043 } 1044 } 1045 return ret; 1046 }; 1047 }; 1048 const deepMerge = baseMerge(deep$1); 1049 const merge$1 = baseMerge(shallow$1); 1050 1051 /** 1052 * **Is** the value stored inside this Optional object equal to `rhs`? 1053 */ 1054 const is$4 = (lhs, rhs, comparator = tripleEquals) => lhs.exists((left) => comparator(left, rhs)); 1055 /** 1056 * Are these two Optional objects equal? Equality here means either they're both 1057 * `Some` (and the values are equal under the comparator) or they're both `None`. 1058 */ 1059 const equals = (lhs, rhs, comparator = tripleEquals) => lift2(lhs, rhs, comparator).getOr(lhs.isNone() && rhs.isNone()); 1060 const cat = (arr) => { 1061 const r = []; 1062 const push = (x) => { 1063 r.push(x); 1064 }; 1065 for (let i = 0; i < arr.length; i++) { 1066 arr[i].each(push); 1067 } 1068 return r; 1069 }; 1070 /* 1071 Notes on the lift functions: 1072 - We used to have a generic liftN, but we were concerned about its type-safety, and the below variants were faster in microbenchmarks. 1073 - The getOrDie calls are partial functions, but are checked beforehand. This is faster and more convenient (but less safe) than folds. 1074 - && is used instead of a loop for simplicity and performance. 1075 */ 1076 const lift2 = (oa, ob, f) => oa.isSome() && ob.isSome() ? Optional.some(f(oa.getOrDie(), ob.getOrDie())) : Optional.none(); 1077 const lift3 = (oa, ob, oc, f) => oa.isSome() && ob.isSome() && oc.isSome() ? Optional.some(f(oa.getOrDie(), ob.getOrDie(), oc.getOrDie())) : Optional.none(); 1078 const flatten = (oot) => oot.bind(identity); 1079 // This can help with type inference, by specifying the type param on the none case, so the caller doesn't have to. 1080 const someIf = (b, a) => b ? Optional.some(a) : Optional.none(); 1081 1082 /** path :: ([String], JsObj?) -> JsObj */ 1083 const path = (parts, scope) => { 1084 let o = scope !== undefined && scope !== null ? scope : Global; 1085 for (let i = 0; i < parts.length && o !== undefined && o !== null; ++i) { 1086 o = o[parts[i]]; 1087 } 1088 return o; 1089 }; 1090 /** resolve :: (String, JsObj?) -> JsObj */ 1091 const resolve$3 = (p, scope) => { 1092 const parts = p.split('.'); 1093 return path(parts, scope); 1094 }; 1095 1096 Adt.generate([ 1097 { bothErrors: ['error1', 'error2'] }, 1098 { firstError: ['error1', 'value2'] }, 1099 { secondError: ['value1', 'error2'] }, 1100 { bothValues: ['value1', 'value2'] } 1101 ]); 1102 /** partition :: [Result a] -> { errors: [String], values: [a] } */ 1103 const partition$1 = (results) => { 1104 const errors = []; 1105 const values = []; 1106 each$e(results, (result) => { 1107 result.fold((err) => { 1108 errors.push(err); 1109 }, (value) => { 1110 values.push(value); 1111 }); 1112 }); 1113 return { errors, values }; 1114 }; 1115 1116 const singleton = (doRevoke) => { 1117 const subject = Cell(Optional.none()); 1118 const revoke = () => subject.get().each(doRevoke); 1119 const clear = () => { 1120 revoke(); 1121 subject.set(Optional.none()); 1122 }; 1123 const isSet = () => subject.get().isSome(); 1124 const get = () => subject.get(); 1125 const set = (s) => { 1126 revoke(); 1127 subject.set(Optional.some(s)); 1128 }; 1129 return { 1130 clear, 1131 isSet, 1132 get, 1133 set 1134 }; 1135 }; 1136 const repeatable = (delay) => { 1137 const intervalId = Cell(Optional.none()); 1138 const revoke = () => intervalId.get().each((id) => clearInterval(id)); 1139 const clear = () => { 1140 revoke(); 1141 intervalId.set(Optional.none()); 1142 }; 1143 const isSet = () => intervalId.get().isSome(); 1144 const get = () => intervalId.get(); 1145 const set = (functionToRepeat) => { 1146 revoke(); 1147 intervalId.set(Optional.some(setInterval(functionToRepeat, delay))); 1148 }; 1149 return { 1150 clear, 1151 isSet, 1152 get, 1153 set, 1154 }; 1155 }; 1156 const value$1 = () => { 1157 const subject = singleton(noop); 1158 const on = (f) => subject.get().each(f); 1159 return { 1160 ...subject, 1161 on 1162 }; 1163 }; 1164 1165 const removeFromStart = (str, numChars) => { 1166 return str.substring(numChars); 1167 }; 1168 1169 const checkRange = (str, substr, start) => substr === '' || str.length >= substr.length && str.substr(start, start + substr.length) === substr; 1170 const removeLeading = (str, prefix) => { 1171 return startsWith(str, prefix) ? removeFromStart(str, prefix.length) : str; 1172 }; 1173 const contains$1 = (str, substr, start = 0, end) => { 1174 const idx = str.indexOf(substr, start); 1175 if (idx !== -1) { 1176 return isUndefined(end) ? true : idx + substr.length <= end; 1177 } 1178 else { 1179 return false; 1180 } 1181 }; 1182 /** Does 'str' start with 'prefix'? 1183 * Note: all strings start with the empty string. 1184 * More formally, for all strings x, startsWith(x, ""). 1185 * This is so that for all strings x and y, startsWith(y + x, y) 1186 */ 1187 const startsWith = (str, prefix) => { 1188 return checkRange(str, prefix, 0); 1189 }; 1190 /** Does 'str' end with 'suffix'? 1191 * Note: all strings end with the empty string. 1192 * More formally, for all strings x, endsWith(x, ""). 1193 * This is so that for all strings x and y, endsWith(x + y, y) 1194 */ 1195 const endsWith = (str, suffix) => { 1196 return checkRange(str, suffix, str.length - suffix.length); 1197 }; 1198 const blank = (r) => (s) => s.replace(r, ''); 1199 /** removes all leading and trailing spaces */ 1200 const trim$4 = blank(/^\s+|\s+$/g); 1201 const lTrim = blank(/^\s+/g); 1202 const rTrim = blank(/\s+$/g); 1203 const isNotEmpty = (s) => s.length > 0; 1204 const isEmpty$5 = (s) => !isNotEmpty(s); 1205 const repeat = (s, count) => count <= 0 ? '' : new Array(count + 1).join(s); 1206 const toInt = (value, radix = 10) => { 1207 const num = parseInt(value, radix); 1208 return isNaN(num) ? Optional.none() : Optional.some(num); 1209 }; 1210 1211 // Run a function fn after rate ms. If another invocation occurs 1212 // during the time it is waiting, ignore it completely. 1213 const first$1 = (fn, rate) => { 1214 let timer = null; 1215 const cancel = () => { 1216 if (!isNull(timer)) { 1217 clearTimeout(timer); 1218 timer = null; 1219 } 1220 }; 1221 const throttle = (...args) => { 1222 if (isNull(timer)) { 1223 timer = setTimeout(() => { 1224 timer = null; 1225 fn.apply(null, args); 1226 }, rate); 1227 } 1228 }; 1229 return { 1230 cancel, 1231 throttle 1232 }; 1233 }; 1234 // Run a function fn after rate ms. If another invocation occurs 1235 // during the time it is waiting, reschedule the function again 1236 // with the new arguments. 1237 const last$1 = (fn, rate) => { 1238 let timer = null; 1239 const cancel = () => { 1240 if (!isNull(timer)) { 1241 clearTimeout(timer); 1242 timer = null; 1243 } 1244 }; 1245 const throttle = (...args) => { 1246 cancel(); 1247 timer = setTimeout(() => { 1248 timer = null; 1249 fn.apply(null, args); 1250 }, rate); 1251 }; 1252 return { 1253 cancel, 1254 throttle 1255 }; 1256 }; 1257 1258 const cached = (f) => { 1259 let called = false; 1260 let r; 1261 return (...args) => { 1262 if (!called) { 1263 called = true; 1264 r = f.apply(null, args); 1265 } 1266 return r; 1267 }; 1268 }; 1269 1270 const zeroWidth = '\uFEFF'; 1271 const nbsp = '\u00A0'; 1272 const ellipsis = '\u2026'; 1273 const isZwsp$2 = (char) => char === zeroWidth; 1274 const removeZwsp = (s) => s.replace(/\uFEFF/g, ''); 1275 1276 const stringArray = (a) => { 1277 const all = {}; 1278 each$e(a, (key) => { 1279 all[key] = {}; 1280 }); 1281 return keys(all); 1282 }; 1283 1284 const isArrayLike = (o) => o.length !== undefined; 1285 const isArray = Array.isArray; 1286 const toArray$1 = (obj) => { 1287 if (!isArray(obj)) { 1288 const array = []; 1289 for (let i = 0, l = obj.length; i < l; i++) { 1290 array[i] = obj[i]; 1291 } 1292 return array; 1293 } 1294 else { 1295 return obj; 1296 } 1297 }; 1298 const each$c = (o, cb, s) => { 1299 if (!o) { 1300 return false; 1301 } 1302 s = s || o; 1303 if (isArrayLike(o)) { 1304 // Indexed arrays, needed for Safari 1305 for (let n = 0, l = o.length; n < l; n++) { 1306 if (cb.call(s, o[n], n, o) === false) { 1307 return false; 1308 } 1309 } 1310 } 1311 else { 1312 // Hashtables 1313 for (const n in o) { 1314 if (has$2(o, n)) { 1315 if (cb.call(s, o[n], n, o) === false) { 1316 return false; 1317 } 1318 } 1319 } 1320 } 1321 return true; 1322 }; 1323 const map$1 = (array, callback) => { 1324 const out = []; 1325 each$c(array, (item, index) => { 1326 out.push(callback(item, index, array)); 1327 }); 1328 return out; 1329 }; 1330 const filter$3 = (a, f) => { 1331 const o = []; 1332 each$c(a, (v, index) => { 1333 if (!f || f(v, index, a)) { 1334 o.push(v); 1335 } 1336 }); 1337 return o; 1338 }; 1339 const indexOf = (a, v) => { 1340 if (a) { 1341 for (let i = 0, l = a.length; i < l; i++) { 1342 if (a[i] === v) { 1343 return i; 1344 } 1345 } 1346 } 1347 return -1; 1348 }; 1349 const reduce = (collection, iteratee, accumulator, thisArg) => { 1350 let acc = isUndefined(accumulator) ? collection[0] : accumulator; 1351 for (let i = 0; i < collection.length; i++) { 1352 acc = iteratee.call(thisArg, acc, collection[i], i); 1353 } 1354 return acc; 1355 }; 1356 const findIndex$1 = (array, predicate, thisArg) => { 1357 for (let i = 0, l = array.length; i < l; i++) { 1358 if (predicate.call(thisArg, array[i], i, array)) { 1359 return i; 1360 } 1361 } 1362 return -1; 1363 }; 1364 const last = (collection) => collection[collection.length - 1]; 1365 1366 const DeviceType = (os, browser, userAgent, mediaMatch) => { 1367 const isiPad = os.isiOS() && /ipad/i.test(userAgent) === true; 1368 const isiPhone = os.isiOS() && !isiPad; 1369 const isMobile = os.isiOS() || os.isAndroid(); 1370 const isTouch = isMobile || mediaMatch('(pointer:coarse)'); 1371 const isTablet = isiPad || !isiPhone && isMobile && mediaMatch('(min-device-width:768px)'); 1372 const isPhone = isiPhone || isMobile && !isTablet; 1373 const iOSwebview = browser.isSafari() && os.isiOS() && /safari/i.test(userAgent) === false; 1374 const isDesktop = !isPhone && !isTablet && !iOSwebview; 1375 return { 1376 isiPad: constant(isiPad), 1377 isiPhone: constant(isiPhone), 1378 isTablet: constant(isTablet), 1379 isPhone: constant(isPhone), 1380 isTouch: constant(isTouch), 1381 isAndroid: os.isAndroid, 1382 isiOS: os.isiOS, 1383 isWebView: constant(iOSwebview), 1384 isDesktop: constant(isDesktop) 1385 }; 1386 }; 1387 1388 const firstMatch = (regexes, s) => { 1389 for (let i = 0; i < regexes.length; i++) { 1390 const x = regexes[i]; 1391 if (x.test(s)) { 1392 return x; 1393 } 1394 } 1395 return undefined; 1396 }; 1397 const find$1 = (regexes, agent) => { 1398 const r = firstMatch(regexes, agent); 1399 if (!r) { 1400 return { major: 0, minor: 0 }; 1401 } 1402 const group = (i) => { 1403 return Number(agent.replace(r, '$' + i)); 1404 }; 1405 return nu$3(group(1), group(2)); 1406 }; 1407 const detect$4 = (versionRegexes, agent) => { 1408 const cleanedAgent = String(agent).toLowerCase(); 1409 if (versionRegexes.length === 0) { 1410 return unknown$2(); 1411 } 1412 return find$1(versionRegexes, cleanedAgent); 1413 }; 1414 const unknown$2 = () => { 1415 return nu$3(0, 0); 1416 }; 1417 const nu$3 = (major, minor) => { 1418 return { major, minor }; 1419 }; 1420 const Version = { 1421 nu: nu$3, 1422 detect: detect$4, 1423 unknown: unknown$2 1424 }; 1425 1426 const detectBrowser$1 = (browsers, userAgentData) => { 1427 return findMap(userAgentData.brands, (uaBrand) => { 1428 const lcBrand = uaBrand.brand.toLowerCase(); 1429 return find$2(browsers, (browser) => lcBrand === browser.brand?.toLowerCase()) 1430 .map((info) => ({ 1431 current: info.name, 1432 version: Version.nu(parseInt(uaBrand.version, 10), 0) 1433 })); 1434 }); 1435 }; 1436 1437 const detect$3 = (candidates, userAgent) => { 1438 const agent = String(userAgent).toLowerCase(); 1439 return find$2(candidates, (candidate) => { 1440 return candidate.search(agent); 1441 }); 1442 }; 1443 // They (browser and os) are the same at the moment, but they might 1444 // not stay that way. 1445 const detectBrowser = (browsers, userAgent) => { 1446 return detect$3(browsers, userAgent).map((browser) => { 1447 const version = Version.detect(browser.versionRegexes, userAgent); 1448 return { 1449 current: browser.name, 1450 version 1451 }; 1452 }); 1453 }; 1454 const detectOs = (oses, userAgent) => { 1455 return detect$3(oses, userAgent).map((os) => { 1456 const version = Version.detect(os.versionRegexes, userAgent); 1457 return { 1458 current: os.name, 1459 version 1460 }; 1461 }); 1462 }; 1463 1464 const normalVersionRegex = /.*?version\/\ ?([0-9]+)\.([0-9]+).*/; 1465 const checkContains = (target) => { 1466 return (uastring) => { 1467 return contains$1(uastring, target); 1468 }; 1469 }; 1470 const browsers = [ 1471 // This is legacy Edge 1472 { 1473 name: 'Edge', 1474 versionRegexes: [/.*?edge\/ ?([0-9]+)\.([0-9]+)$/], 1475 search: (uastring) => { 1476 return contains$1(uastring, 'edge/') && contains$1(uastring, 'chrome') && contains$1(uastring, 'safari') && contains$1(uastring, 'applewebkit'); 1477 } 1478 }, 1479 // This is Google Chrome and Chromium Edge 1480 { 1481 name: 'Chromium', 1482 brand: 'Chromium', 1483 versionRegexes: [/.*?chrome\/([0-9]+)\.([0-9]+).*/, normalVersionRegex], 1484 search: (uastring) => { 1485 return contains$1(uastring, 'chrome') && !contains$1(uastring, 'chromeframe'); 1486 } 1487 }, 1488 { 1489 name: 'IE', 1490 versionRegexes: [/.*?msie\ ?([0-9]+)\.([0-9]+).*/, /.*?rv:([0-9]+)\.([0-9]+).*/], 1491 search: (uastring) => { 1492 return contains$1(uastring, 'msie') || contains$1(uastring, 'trident'); 1493 } 1494 }, 1495 // INVESTIGATE: Is this still the Opera user agent? 1496 { 1497 name: 'Opera', 1498 versionRegexes: [normalVersionRegex, /.*?opera\/([0-9]+)\.([0-9]+).*/], 1499 search: checkContains('opera') 1500 }, 1501 { 1502 name: 'Firefox', 1503 versionRegexes: [/.*?firefox\/\ ?([0-9]+)\.([0-9]+).*/], 1504 search: checkContains('firefox') 1505 }, 1506 { 1507 name: 'Safari', 1508 versionRegexes: [normalVersionRegex, /.*?cpu os ([0-9]+)_([0-9]+).*/], 1509 search: (uastring) => { 1510 return (contains$1(uastring, 'safari') || contains$1(uastring, 'mobile/')) && contains$1(uastring, 'applewebkit'); 1511 } 1512 } 1513 ]; 1514 const oses = [ 1515 { 1516 name: 'Windows', 1517 search: checkContains('win'), 1518 versionRegexes: [/.*?windows\ nt\ ?([0-9]+)\.([0-9]+).*/] 1519 }, 1520 { 1521 name: 'iOS', 1522 search: (uastring) => { 1523 return contains$1(uastring, 'iphone') || contains$1(uastring, 'ipad'); 1524 }, 1525 versionRegexes: [/.*?version\/\ ?([0-9]+)\.([0-9]+).*/, /.*cpu os ([0-9]+)_([0-9]+).*/, /.*cpu iphone os ([0-9]+)_([0-9]+).*/] 1526 }, 1527 { 1528 name: 'Android', 1529 search: checkContains('android'), 1530 versionRegexes: [/.*?android\ ?([0-9]+)\.([0-9]+).*/] 1531 }, 1532 { 1533 name: 'macOS', 1534 search: checkContains('mac os x'), 1535 versionRegexes: [/.*?mac\ os\ x\ ?([0-9]+)_([0-9]+).*/] 1536 }, 1537 { 1538 name: 'Linux', 1539 search: checkContains('linux'), 1540 versionRegexes: [] 1541 }, 1542 { name: 'Solaris', 1543 search: checkContains('sunos'), 1544 versionRegexes: [] 1545 }, 1546 { 1547 name: 'FreeBSD', 1548 search: checkContains('freebsd'), 1549 versionRegexes: [] 1550 }, 1551 { 1552 name: 'ChromeOS', 1553 search: checkContains('cros'), 1554 versionRegexes: [/.*?chrome\/([0-9]+)\.([0-9]+).*/] 1555 } 1556 ]; 1557 const PlatformInfo = { 1558 browsers: constant(browsers), 1559 oses: constant(oses) 1560 }; 1561 1562 const edge = 'Edge'; 1563 const chromium = 'Chromium'; 1564 const ie = 'IE'; 1565 const opera = 'Opera'; 1566 const firefox = 'Firefox'; 1567 const safari = 'Safari'; 1568 const unknown$1 = () => { 1569 return nu$2({ 1570 current: undefined, 1571 version: Version.unknown() 1572 }); 1573 }; 1574 const nu$2 = (info) => { 1575 const current = info.current; 1576 const version = info.version; 1577 const isBrowser = (name) => () => current === name; 1578 return { 1579 current, 1580 version, 1581 isEdge: isBrowser(edge), 1582 isChromium: isBrowser(chromium), 1583 // NOTE: isIe just looks too weird 1584 isIE: isBrowser(ie), 1585 isOpera: isBrowser(opera), 1586 isFirefox: isBrowser(firefox), 1587 isSafari: isBrowser(safari) 1588 }; 1589 }; 1590 const Browser = { 1591 unknown: unknown$1, 1592 nu: nu$2, 1593 edge: constant(edge), 1594 chromium: constant(chromium), 1595 ie: constant(ie), 1596 opera: constant(opera), 1597 firefox: constant(firefox), 1598 safari: constant(safari) 1599 }; 1600 1601 const windows = 'Windows'; 1602 const ios = 'iOS'; 1603 const android = 'Android'; 1604 const linux = 'Linux'; 1605 const macos = 'macOS'; 1606 const solaris = 'Solaris'; 1607 const freebsd = 'FreeBSD'; 1608 const chromeos = 'ChromeOS'; 1609 // Though there is a bit of dupe with this and Browser, trying to 1610 // reuse code makes it much harder to follow and change. 1611 const unknown = () => { 1612 return nu$1({ 1613 current: undefined, 1614 version: Version.unknown() 1615 }); 1616 }; 1617 const nu$1 = (info) => { 1618 const current = info.current; 1619 const version = info.version; 1620 const isOS = (name) => () => current === name; 1621 return { 1622 current, 1623 version, 1624 isWindows: isOS(windows), 1625 // TODO: Fix capitalisation 1626 isiOS: isOS(ios), 1627 isAndroid: isOS(android), 1628 isMacOS: isOS(macos), 1629 isLinux: isOS(linux), 1630 isSolaris: isOS(solaris), 1631 isFreeBSD: isOS(freebsd), 1632 isChromeOS: isOS(chromeos) 1633 }; 1634 }; 1635 const OperatingSystem = { 1636 unknown, 1637 nu: nu$1, 1638 windows: constant(windows), 1639 ios: constant(ios), 1640 android: constant(android), 1641 linux: constant(linux), 1642 macos: constant(macos), 1643 solaris: constant(solaris), 1644 freebsd: constant(freebsd), 1645 chromeos: constant(chromeos) 1646 }; 1647 1648 const detect$2 = (userAgent, userAgentDataOpt, mediaMatch) => { 1649 const browsers = PlatformInfo.browsers(); 1650 const oses = PlatformInfo.oses(); 1651 const browser = userAgentDataOpt.bind((userAgentData) => detectBrowser$1(browsers, userAgentData)) 1652 .orThunk(() => detectBrowser(browsers, userAgent)) 1653 .fold(Browser.unknown, Browser.nu); 1654 const os = detectOs(oses, userAgent).fold(OperatingSystem.unknown, OperatingSystem.nu); 1655 const deviceType = DeviceType(os, browser, userAgent, mediaMatch); 1656 return { 1657 browser, 1658 os, 1659 deviceType 1660 }; 1661 }; 1662 const PlatformDetection = { 1663 detect: detect$2 1664 }; 1665 1666 const mediaMatch = (query) => window.matchMedia(query).matches; 1667 // IMPORTANT: Must be in a thunk, otherwise rollup thinks calling this immediately 1668 // causes side effects and won't tree shake this away 1669 // Note: navigator.userAgentData is not part of the native typescript types yet 1670 let platform$4 = cached(() => PlatformDetection.detect(window.navigator.userAgent, Optional.from((window.navigator.userAgentData)), mediaMatch)); 1671 const detect$1 = () => platform$4(); 1672 1673 const unsafe = (name, scope) => { 1674 return resolve$3(name, scope); 1675 }; 1676 const getOrDie = (name, scope) => { 1677 const actual = unsafe(name, scope); 1678 if (actual === undefined || actual === null) { 1679 throw new Error(name + ' not available on this browser'); 1680 } 1681 return actual; 1682 }; 1683 1684 const getPrototypeOf$1 = Object.getPrototypeOf; 1685 /* 1686 * IE9 and above 1687 * 1688 * MDN no use on this one, but here's the link anyway: 1689 * https://developer.mozilla.org/en/docs/Web/API/HTMLElement 1690 */ 1691 const sandHTMLElement = (scope) => { 1692 return getOrDie('HTMLElement', scope); 1693 }; 1694 const isPrototypeOf = (x) => { 1695 // use Resolve to get the window object for x and just return undefined if it can't find it. 1696 // undefined scope later triggers using the global window. 1697 const scope = resolve$3('ownerDocument.defaultView', x); 1698 // TINY-7374: We can't rely on looking at the owner window HTMLElement as the element may have 1699 // been constructed in a different window and then appended to the current window document. 1700 return isObject(x) && (sandHTMLElement(scope).prototype.isPrototypeOf(x) || /^HTML\w*Element$/.test(getPrototypeOf$1(x).constructor.name)); 1701 }; 1702 1703 /** 1704 * This class contains various environment constants like browser versions etc. 1705 * Normally you don't want to sniff specific browser versions but sometimes you have 1706 * to when it's impossible to feature detect. So use this with care. 1707 * 1708 * @class tinymce.Env 1709 * @static 1710 */ 1711 const userAgent = window.navigator.userAgent; 1712 const platform$3 = detect$1(); 1713 const browser$3 = platform$3.browser; 1714 const os$1 = platform$3.os; 1715 const deviceType = platform$3.deviceType; 1716 const windowsPhone = userAgent.indexOf('Windows Phone') !== -1; 1717 const Env = { 1718 /** 1719 * Transparent image data url. 1720 * 1721 * @property transparentSrc 1722 * @type Boolean 1723 * @final 1724 */ 1725 transparentSrc: 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7', 1726 /** 1727 * Returns the IE document mode. For non IE browsers, this will fake IE 10 document mode. 1728 * 1729 * @property documentMode 1730 * @type Number 1731 */ 1732 documentMode: browser$3.isIE() ? (document.documentMode || 7) : 10, 1733 cacheSuffix: null, 1734 container: null, 1735 /** 1736 * Constant if CSP mode is possible or not. Meaning we can't use script urls for the iframe. 1737 */ 1738 canHaveCSP: !browser$3.isIE(), 1739 windowsPhone, 1740 /** 1741 * @include ../../../../../tools/docs/tinymce.Env.js 1742 */ 1743 browser: { 1744 current: browser$3.current, 1745 version: browser$3.version, 1746 isChromium: browser$3.isChromium, 1747 isEdge: browser$3.isEdge, 1748 isFirefox: browser$3.isFirefox, 1749 isIE: browser$3.isIE, 1750 isOpera: browser$3.isOpera, 1751 isSafari: browser$3.isSafari 1752 }, 1753 os: { 1754 current: os$1.current, 1755 version: os$1.version, 1756 isAndroid: os$1.isAndroid, 1757 isChromeOS: os$1.isChromeOS, 1758 isFreeBSD: os$1.isFreeBSD, 1759 isiOS: os$1.isiOS, 1760 isLinux: os$1.isLinux, 1761 isMacOS: os$1.isMacOS, 1762 isSolaris: os$1.isSolaris, 1763 isWindows: os$1.isWindows 1764 }, 1765 deviceType: { 1766 isDesktop: deviceType.isDesktop, 1767 isiPad: deviceType.isiPad, 1768 isiPhone: deviceType.isiPhone, 1769 isPhone: deviceType.isPhone, 1770 isTablet: deviceType.isTablet, 1771 isTouch: deviceType.isTouch, 1772 isWebView: deviceType.isWebView 1773 } 1774 }; 1775 1776 /** 1777 * This class contains various utility functions. These are also exposed 1778 * directly on the tinymce namespace. 1779 * 1780 * @class tinymce.util.Tools 1781 */ 1782 /** 1783 * Removes whitespace from the beginning and end of a string. 1784 * 1785 * @method trim 1786 * @param {String} s String to remove whitespace from. 1787 * @return {String} New string with removed whitespace. 1788 */ 1789 const whiteSpaceRegExp$1 = /^\s*|\s*$/g; 1790 const trim$3 = (str) => { 1791 return isNullable(str) ? '' : ('' + str).replace(whiteSpaceRegExp$1, ''); 1792 }; 1793 /** 1794 * Checks if a object is of a specific type for example an array. 1795 * 1796 * @method is 1797 * @param {Object} obj Object to check type of. 1798 * @param {String} type Optional type to check for. 1799 * @return {Boolean} true/false if the object is of the specified type. 1800 */ 1801 const is$3 = (obj, type) => { 1802 if (!type) { 1803 return obj !== undefined; 1804 } 1805 if (type === 'array' && isArray(obj)) { 1806 return true; 1807 } 1808 return typeof obj === type; 1809 }; 1810 /** 1811 * Makes a name/object map out of an array with names. 1812 * 1813 * @method makeMap 1814 * @param {Array/String} items Items to make map out of. 1815 * @param {String} delim Optional delimiter to split string by. 1816 * @param {Object} map Optional map to add items to. 1817 * @return {Object} Name/value map of items. 1818 */ 1819 const makeMap$4 = (items, delim, map = {}) => { 1820 const resolvedItems = isString(items) ? items.split(delim || ',') : (items || []); 1821 let i = resolvedItems.length; 1822 while (i--) { 1823 map[resolvedItems[i]] = {}; 1824 } 1825 return map; 1826 }; 1827 /** 1828 * JavaScript does not protect hasOwnProperty method, so it is possible to overwrite it. This is 1829 * an object independent version. 1830 * Checks if the input object "<code>obj</code>" has the property "<code>prop</code>". 1831 * 1832 * @method hasOwnProperty 1833 * @param {Object} obj Object to check if the property exists. 1834 * @param {String} prop Name of a property on the object. 1835 * @returns {Boolean} true if the object has the specified property. 1836 */ 1837 const hasOwnProperty = has$2; 1838 const extend$3 = (obj, ...exts) => { 1839 for (let i = 0; i < exts.length; i++) { 1840 const ext = exts[i]; 1841 for (const name in ext) { 1842 if (has$2(ext, name)) { 1843 const value = ext[name]; 1844 if (value !== undefined) { 1845 obj[name] = value; 1846 } 1847 } 1848 } 1849 } 1850 return obj; 1851 }; 1852 /** 1853 * Executed the specified function for each item in a object tree. 1854 * 1855 * @method walk 1856 * @param {Object} o Object tree to walk though. 1857 * @param {Function} f Function to call for each item. 1858 * @param {String} n Optional name of collection inside the objects to walk for example childNodes. 1859 * @param {String} s Optional scope to execute the function in. 1860 */ 1861 const walk$4 = function (o, f, n, s) { 1862 s = s || this; 1863 if (o) { 1864 if (n) { 1865 o = o[n]; 1866 } 1867 each$c(o, (o, i) => { 1868 if (f.call(s, o, i, n) === false) { 1869 return false; 1870 } 1871 else { 1872 walk$4(o, f, n, s); 1873 return true; 1874 } 1875 }); 1876 } 1877 }; 1878 /** 1879 * Resolves a string and returns the object from a specific structure. 1880 * 1881 * @method resolve 1882 * @param {String} n Path to resolve for example a.b.c.d. 1883 * @param {Object} o Optional object to search though, defaults to window. 1884 * @return {Object} Last object in path or null if it couldn't be resolved. 1885 * @example 1886 * // Resolve a path into an object reference 1887 * const obj = tinymce.resolve('a.b.c.d'); 1888 */ 1889 const resolve$2 = (n, o = window) => { 1890 const path = n.split('.'); 1891 for (let i = 0, l = path.length; i < l; i++) { 1892 o = o[path[i]]; 1893 if (!o) { 1894 break; 1895 } 1896 } 1897 return o; 1898 }; 1899 /** 1900 * Splits a string but removes the whitespace before and after each value. 1901 * 1902 * @method explode 1903 * @param {String} s String to split. 1904 * @param {String} d Delimiter to split by. 1905 * @example 1906 * // Split a string into an array with a,b,c 1907 * const arr = tinymce.explode('a, b, c'); 1908 */ 1909 const explode$3 = (s, d) => { 1910 if (isArray$1(s)) { 1911 return s; 1912 } 1913 else if (s === '') { 1914 return []; 1915 } 1916 else { 1917 return map$1(s.split(d || ','), trim$3); 1918 } 1919 }; 1920 const _addCacheSuffix = (url) => { 1921 const cacheSuffix = Env.cacheSuffix; 1922 if (cacheSuffix) { 1923 url += (url.indexOf('?') === -1 ? '?' : '&') + cacheSuffix; 1924 } 1925 return url; 1926 }; 1927 const Tools = { 1928 trim: trim$3, 1929 /** 1930 * Returns true/false if the object is an array or not. 1931 * 1932 * @method isArray 1933 * @param {Object} obj Object to check. 1934 * @return {Boolean} true/false state if the object is an array or not. 1935 */ 1936 isArray: isArray, 1937 is: is$3, 1938 /** 1939 * Converts the specified object into a real JavaScript array. 1940 * 1941 * @method toArray 1942 * @param {Object} obj Object to convert into array. 1943 * @return {Array} Array object based in input. 1944 */ 1945 toArray: toArray$1, 1946 makeMap: makeMap$4, 1947 /** 1948 * Performs an iteration of all items in a collection such as an object or array. This method will execute the 1949 * callback function for each item in the collection, if the callback returns false the iteration will terminate. 1950 * The callback has the following format: `cb(value, key_or_index)`. 1951 * 1952 * @method each 1953 * @param {Object} o Collection to iterate. 1954 * @param {Function} cb Callback function to execute for each item. 1955 * @param {Object} s Optional scope to execute the callback in. 1956 * @example 1957 * // Iterate an array 1958 * tinymce.each([ 1,2,3 ], (v, i) => { 1959 * console.debug("Value: " + v + ", Index: " + i); 1960 * }); 1961 * 1962 * // Iterate an object 1963 * tinymce.each({ a: 1, b: 2, c: 3 }, (v, k) => { 1964 * console.debug("Value: " + v + ", Key: " + k); 1965 * }); 1966 */ 1967 each: each$c, 1968 /** 1969 * Creates a new array by the return value of each iteration function call. This enables you to convert 1970 * one array list into another. 1971 * 1972 * @method map 1973 * @param {Array} array Array of items to iterate. 1974 * @param {Function} callback Function to call for each item. It's return value will be the new value. 1975 * @return {Array} Array with new values based on function return values. 1976 */ 1977 map: map$1, 1978 /** 1979 * Filters out items from the input array by calling the specified function for each item. 1980 * If the function returns false the item will be excluded if it returns true it will be included. 1981 * 1982 * @method grep 1983 * @param {Array} a Array of items to loop though. 1984 * @param {Function} f Function to call for each item. Include/exclude depends on it's return value. 1985 * @return {Array} New array with values imported and filtered based in input. 1986 * @example 1987 * // Filter out some items, this will return an array with 4 and 5 1988 * const items = tinymce.grep([ 1,2,3,4,5 ], (v) => v > 3); 1989 */ 1990 grep: filter$3, 1991 /** 1992 * Returns an index of the item or -1 if item is not present in the array. 1993 * 1994 * @method inArray 1995 * @param {any} item Item to search for. 1996 * @param {Array} arr Array to search in. 1997 * @return {Number} index of the item or -1 if item was not found. 1998 */ 1999 inArray: indexOf, 2000 hasOwn: hasOwnProperty, 2001 extend: extend$3, 2002 walk: walk$4, 2003 resolve: resolve$2, 2004 explode: explode$3, 2005 _addCacheSuffix 2006 }; 2007 2008 const fromHtml$1 = (html, scope) => { 2009 const doc = scope || document; 2010 const div = doc.createElement('div'); 2011 div.innerHTML = html; 2012 if (!div.hasChildNodes() || div.childNodes.length > 1) { 2013 const message = 'HTML does not have a single root node'; 2014 // eslint-disable-next-line no-console 2015 console.error(message, html); 2016 throw new Error(message); 2017 } 2018 return fromDom$2(div.childNodes[0]); 2019 }; 2020 const fromTag = (tag, scope) => { 2021 const doc = scope || document; 2022 const node = doc.createElement(tag); 2023 return fromDom$2(node); 2024 }; 2025 const fromText = (text, scope) => { 2026 const doc = scope || document; 2027 const node = doc.createTextNode(text); 2028 return fromDom$2(node); 2029 }; 2030 const fromDom$2 = (node) => { 2031 // TODO: Consider removing this check, but left atm for safety 2032 if (node === null || node === undefined) { 2033 throw new Error('Node cannot be null or undefined'); 2034 } 2035 return { 2036 dom: node 2037 }; 2038 }; 2039 const fromPoint$2 = (docElm, x, y) => Optional.from(docElm.dom.elementFromPoint(x, y)).map(fromDom$2); 2040 // tslint:disable-next-line:variable-name 2041 const SugarElement = { 2042 fromHtml: fromHtml$1, 2043 fromTag, 2044 fromText, 2045 fromDom: fromDom$2, 2046 fromPoint: fromPoint$2 2047 }; 2048 2049 // NOTE: Mutates the range. 2050 const setStart = (rng, situ) => { 2051 situ.fold((e) => { 2052 rng.setStartBefore(e.dom); 2053 }, (e, o) => { 2054 rng.setStart(e.dom, o); 2055 }, (e) => { 2056 rng.setStartAfter(e.dom); 2057 }); 2058 }; 2059 const setFinish = (rng, situ) => { 2060 situ.fold((e) => { 2061 rng.setEndBefore(e.dom); 2062 }, (e, o) => { 2063 rng.setEnd(e.dom, o); 2064 }, (e) => { 2065 rng.setEndAfter(e.dom); 2066 }); 2067 }; 2068 const relativeToNative = (win, startSitu, finishSitu) => { 2069 const range = win.document.createRange(); 2070 setStart(range, startSitu); 2071 setFinish(range, finishSitu); 2072 return range; 2073 }; 2074 const exactToNative = (win, start, soffset, finish, foffset) => { 2075 const rng = win.document.createRange(); 2076 rng.setStart(start.dom, soffset); 2077 rng.setEnd(finish.dom, foffset); 2078 return rng; 2079 }; 2080 2081 const adt$3 = Adt.generate([ 2082 { ltr: ['start', 'soffset', 'finish', 'foffset'] }, 2083 { rtl: ['start', 'soffset', 'finish', 'foffset'] } 2084 ]); 2085 const fromRange = (win, type, range) => type(SugarElement.fromDom(range.startContainer), range.startOffset, SugarElement.fromDom(range.endContainer), range.endOffset); 2086 const getRanges$1 = (win, selection) => selection.match({ 2087 domRange: (rng) => { 2088 return { 2089 ltr: constant(rng), 2090 rtl: Optional.none 2091 }; 2092 }, 2093 relative: (startSitu, finishSitu) => { 2094 return { 2095 ltr: cached(() => relativeToNative(win, startSitu, finishSitu)), 2096 rtl: cached(() => Optional.some(relativeToNative(win, finishSitu, startSitu))) 2097 }; 2098 }, 2099 exact: (start, soffset, finish, foffset) => { 2100 return { 2101 ltr: cached(() => exactToNative(win, start, soffset, finish, foffset)), 2102 rtl: cached(() => Optional.some(exactToNative(win, finish, foffset, start, soffset))) 2103 }; 2104 } 2105 }); 2106 const doDiagnose = (win, ranges) => { 2107 // If we cannot create a ranged selection from start > finish, it could be RTL 2108 const rng = ranges.ltr(); 2109 if (rng.collapsed) { 2110 // Let's check if it's RTL ... if it is, then reversing the direction will not be collapsed 2111 const reversed = ranges.rtl().filter((rev) => rev.collapsed === false); 2112 return reversed.map((rev) => 2113 // We need to use "reversed" here, because the original only has one point (collapsed) 2114 adt$3.rtl(SugarElement.fromDom(rev.endContainer), rev.endOffset, SugarElement.fromDom(rev.startContainer), rev.startOffset)).getOrThunk(() => fromRange(win, adt$3.ltr, rng)); 2115 } 2116 else { 2117 return fromRange(win, adt$3.ltr, rng); 2118 } 2119 }; 2120 const diagnose = (win, selection) => { 2121 const ranges = getRanges$1(win, selection); 2122 return doDiagnose(win, ranges); 2123 }; 2124 adt$3.ltr; 2125 adt$3.rtl; 2126 2127 const COMMENT = 8; 2128 const DOCUMENT = 9; 2129 const DOCUMENT_FRAGMENT = 11; 2130 const ELEMENT = 1; 2131 const TEXT = 3; 2132 2133 const is$2 = (element, selector) => { 2134 const dom = element.dom; 2135 if (dom.nodeType !== ELEMENT) { 2136 return false; 2137 } 2138 else { 2139 const elem = dom; 2140 if (elem.matches !== undefined) { 2141 return elem.matches(selector); 2142 } 2143 else if (elem.msMatchesSelector !== undefined) { 2144 return elem.msMatchesSelector(selector); 2145 } 2146 else if (elem.webkitMatchesSelector !== undefined) { 2147 return elem.webkitMatchesSelector(selector); 2148 } 2149 else if (elem.mozMatchesSelector !== undefined) { 2150 // cast to any as mozMatchesSelector doesn't exist in TS DOM lib 2151 return elem.mozMatchesSelector(selector); 2152 } 2153 else { 2154 throw new Error('Browser lacks native selectors'); 2155 } // unfortunately we can't throw this on startup :( 2156 } 2157 }; 2158 const bypassSelector = (dom) => 2159 // Only elements, documents and shadow roots support querySelector 2160 // shadow root element type is DOCUMENT_FRAGMENT 2161 dom.nodeType !== ELEMENT && dom.nodeType !== DOCUMENT && dom.nodeType !== DOCUMENT_FRAGMENT || 2162 // IE fix for complex queries on empty nodes: http://jsfiddle.net/spyder/fv9ptr5L/ 2163 dom.childElementCount === 0; 2164 const all = (selector, scope) => { 2165 const base = scope === undefined ? document : scope.dom; 2166 return bypassSelector(base) ? [] : map$3(base.querySelectorAll(selector), SugarElement.fromDom); 2167 }; 2168 const one = (selector, scope) => { 2169 const base = scope === undefined ? document : scope.dom; 2170 return bypassSelector(base) ? Optional.none() : Optional.from(base.querySelector(selector)).map(SugarElement.fromDom); 2171 }; 2172 2173 const eq = (e1, e2) => e1.dom === e2.dom; 2174 // Returns: true if node e1 contains e2, otherwise false. 2175 // (returns false if e1===e2: A node does not contain itself). 2176 const contains = (e1, e2) => { 2177 const d1 = e1.dom; 2178 const d2 = e2.dom; 2179 return d1 === d2 ? false : d1.contains(d2); 2180 }; 2181 const is$1 = is$2; 2182 2183 /** 2184 * Applies f repeatedly until it completes (by returning Optional.none()). 2185 * 2186 * Normally would just use recursion, but JavaScript lacks tail call optimisation. 2187 * 2188 * This is what recursion looks like when manually unravelled :) 2189 */ 2190 const toArray = (target, f) => { 2191 const r = []; 2192 const recurse = (e) => { 2193 r.push(e); 2194 return f(e); 2195 }; 2196 let cur = f(target); 2197 do { 2198 cur = cur.bind(recurse); 2199 } while (cur.isSome()); 2200 return r; 2201 }; 2202 2203 const name = (element) => { 2204 const r = element.dom.nodeName; 2205 return r.toLowerCase(); 2206 }; 2207 const type$1 = (element) => element.dom.nodeType; 2208 const isType = (t) => (element) => type$1(element) === t; 2209 const isComment$1 = (element) => type$1(element) === COMMENT || name(element) === '#comment'; 2210 const isHTMLElement$1 = (element) => isElement$8(element) && isPrototypeOf(element.dom); 2211 const isElement$8 = isType(ELEMENT); 2212 const isText$c = isType(TEXT); 2213 const isDocument$2 = isType(DOCUMENT); 2214 const isDocumentFragment$1 = isType(DOCUMENT_FRAGMENT); 2215 const isTag = (tag) => (e) => isElement$8(e) && name(e) === tag; 2216 2217 /** 2218 * The document associated with the current element 2219 * NOTE: this will throw if the owner is null. 2220 */ 2221 const owner$1 = (element) => SugarElement.fromDom(element.dom.ownerDocument); 2222 /** 2223 * If the element is a document, return it. Otherwise, return its ownerDocument. 2224 * @param dos 2225 */ 2226 const documentOrOwner = (dos) => isDocument$2(dos) ? dos : owner$1(dos); 2227 const documentElement = (element) => SugarElement.fromDom(documentOrOwner(element).dom.documentElement); 2228 /** 2229 * The window element associated with the element 2230 * NOTE: this will throw if the defaultView is null. 2231 */ 2232 const defaultView = (element) => SugarElement.fromDom(documentOrOwner(element).dom.defaultView); 2233 const parent = (element) => Optional.from(element.dom.parentNode).map(SugarElement.fromDom); 2234 const parentElement = (element) => Optional.from(element.dom.parentElement).map(SugarElement.fromDom); 2235 const parents$1 = (element, isRoot) => { 2236 const stop = isFunction(isRoot) ? isRoot : never; 2237 // This is used a *lot* so it needs to be performant, not recursive 2238 let dom = element.dom; 2239 const ret = []; 2240 while (dom.parentNode !== null && dom.parentNode !== undefined) { 2241 const rawParent = dom.parentNode; 2242 const p = SugarElement.fromDom(rawParent); 2243 ret.push(p); 2244 if (stop(p) === true) { 2245 break; 2246 } 2247 else { 2248 dom = rawParent; 2249 } 2250 } 2251 return ret; 2252 }; 2253 const siblings = (element) => { 2254 // TODO: Refactor out children so we can just not add self instead of filtering afterwards 2255 const filterSelf = (elements) => filter$5(elements, (x) => !eq(element, x)); 2256 return parent(element).map(children$
vendor: 40,845 bytes, lines 2256-3261
22561).map(filterSelf).getOr([]); 2257 }; 2258 const prevSibling = (element) => Optional.from(element.dom.previousSibling).map(SugarElement.fromDom); 2259 const nextSibling = (element) => Optional.from(element.dom.nextSibling).map(SugarElement.fromDom); 2260 // This one needs to be reversed, so they're still in DOM order 2261 const prevSiblings = (element) => reverse(toArray(element, prevSibling)); 2262 const nextSiblings = (element) => toArray(element, nextSibling); 2263 const children$1 = (element) => map$3(element.dom.childNodes, SugarElement.fromDom); 2264 const child$1 = (element, index) => { 2265 const cs = element.dom.childNodes; 2266 return Optional.from(cs[index]).map(SugarElement.fromDom); 2267 }; 2268 const firstChild = (element) => child$1(element, 0); 2269 const lastChild = (element) => child$1(element, element.dom.childNodes.length - 1); 2270 const childNodesCount = (element) => element.dom.childNodes.length; 2271 2272 const getHead = (doc) => { 2273 /* 2274 * IE9 and above per 2275 * https://developer.mozilla.org/en-US/docs/Web/API/Document/head 2276 */ 2277 const b = doc.dom.head; 2278 if (b === null || b === undefined) { 2279 throw new Error('Head is not available yet'); 2280 } 2281 return SugarElement.fromDom(b); 2282 }; 2283 2284 /** 2285 * Is the element a ShadowRoot? 2286 * 2287 * Note: this is insufficient to test if any element is a shadow root, but it is sufficient to differentiate between 2288 * a Document and a ShadowRoot. 2289 */ 2290 const isShadowRoot = (dos) => isDocumentFragment$1(dos) && isNonNullable(dos.dom.host); 2291 const getRootNode = (e) => SugarElement.fromDom(e.dom.getRootNode()); 2292 /** Where style tags need to go. ShadowRoot or document head */ 2293 const getStyleContainer = (dos) => isShadowRoot(dos) ? dos : getHead(documentOrOwner(dos)); 2294 /** Where content needs to go. ShadowRoot or document body */ 2295 const getContentContainer = (dos) => 2296 // Can't use SugarBody.body without causing a circular module reference (since SugarBody.inBody uses SugarShadowDom) 2297 isShadowRoot(dos) ? dos : SugarElement.fromDom(documentOrOwner(dos).dom.body); 2298 /** If this element is in a ShadowRoot, return it. */ 2299 const getShadowRoot = (e) => { 2300 const r = getRootNode(e); 2301 return isShadowRoot(r) ? Optional.some(r) : Optional.none(); 2302 }; 2303 /** Return the host of a ShadowRoot. 2304 * 2305 * This function will throw if Shadow DOM is unsupported in the browser, or if the host is null. 2306 * If you actually have a ShadowRoot, this shouldn't happen. 2307 */ 2308 const getShadowHost = (e) => SugarElement.fromDom(e.dom.host); 2309 /** 2310 * When Events bubble up through a ShadowRoot, the browser changes the target to be the shadow host. 2311 * This function gets the "original" event target if possible. 2312 * This only works if the shadow tree is open - if the shadow tree is closed, event.target is returned. 2313 * See: https://developers.google.com/web/fundamentals/web-components/shadowdom#events 2314 */ 2315 const getOriginalEventTarget = (event) => { 2316 if (isNonNullable(event.target)) { 2317 const el = SugarElement.fromDom(event.target); 2318 if (isElement$8(el) && isOpenShadowHost(el)) { 2319 // When target element is inside Shadow DOM we need to take first element from composedPath 2320 // otherwise we'll get Shadow Root parent, not actual target element. 2321 if (event.composed && event.composedPath) { 2322 const composedPath = event.composedPath(); 2323 if (composedPath) { 2324 return head(composedPath); 2325 } 2326 } 2327 } 2328 } 2329 return Optional.from(event.target); 2330 }; 2331 /** Return true if the element is a host of an open shadow root. 2332 * Return false if the element is a host of a closed shadow root, or if the element is not a host. 2333 */ 2334 const isOpenShadowHost = (element) => isNonNullable(element.dom.shadowRoot); 2335 2336 const mkEvent = (target, x, y, stop, prevent, kill, raw) => ({ 2337 target, 2338 x, 2339 y, 2340 stop, 2341 prevent, 2342 kill, 2343 raw 2344 }); 2345 /** Wraps an Event in an EventArgs structure. 2346 * The returned EventArgs structure has its target set to the "original" target if possible. 2347 * See SugarShadowDom.getOriginalEventTarget 2348 */ 2349 const fromRawEvent = (rawEvent) => { 2350 const target = SugarElement.fromDom(getOriginalEventTarget(rawEvent).getOr(rawEvent.target)); 2351 const stop = () => rawEvent.stopPropagation(); 2352 const prevent = () => rawEvent.preventDefault(); 2353 const kill = compose(prevent, stop); // more of a sequence than a compose, but same effect 2354 // FIX: Don't just expose the raw event. Need to identify what needs standardisation. 2355 return mkEvent(target, rawEvent.clientX, rawEvent.clientY, stop, prevent, kill, rawEvent); 2356 }; 2357 const handle$1 = (filter, handler) => (rawEvent) => { 2358 if (filter(rawEvent)) { 2359 handler(fromRawEvent(rawEvent)); 2360 } 2361 }; 2362 const binder = (element, event, filter, handler, useCapture) => { 2363 const wrapped = handle$1(filter, handler); 2364 // IE9 minimum 2365 element.dom.addEventListener(event, wrapped, useCapture); 2366 return { 2367 unbind: curry(unbind, element, event, wrapped, useCapture) 2368 }; 2369 }; 2370 const bind$2 = (element, event, filter, handler) => binder(element, event, filter, handler, false); 2371 const unbind = (element, event, handler, useCapture) => { 2372 // IE9 minimum 2373 element.dom.removeEventListener(event, handler, useCapture); 2374 }; 2375 2376 const filter$2 = always; // no filter on plain DomEvents 2377 const bind$1 = (element, event, handler) => bind$2(element, event, filter$2, handler); 2378 2379 const getDocument = () => SugarElement.fromDom(document); 2380 2381 const focus$1 = (element, preventScroll = false) => element.dom.focus({ preventScroll }); 2382 const hasFocus$1 = (element) => { 2383 const root = getRootNode(element).dom; 2384 return element.dom === root.activeElement; 2385 }; 2386 // Note: assuming that activeElement will always be a HTMLElement (maybe we should add a runtime check?) 2387 const active = (root = getDocument()) => Optional.from(root.dom.activeElement).map(SugarElement.fromDom); 2388 /** 2389 * Return the descendant element that has focus. 2390 * Use instead of SelectorFind.descendant(container, ':focus') 2391 * because the :focus selector relies on keyboard focus. 2392 */ 2393 const search = (element) => active(getRootNode(element)) 2394 .filter((e) => element.dom.contains(e.dom)); 2395 2396 const before$4 = (marker, element) => { 2397 const parent$1 = parent(marker); 2398 parent$1.each((v) => { 2399 v.dom.insertBefore(element.dom, marker.dom); 2400 }); 2401 }; 2402 const after$4 = (marker, element) => { 2403 const sibling = nextSibling(marker); 2404 sibling.fold(() => { 2405 const parent$1 = parent(marker); 2406 parent$1.each((v) => { 2407 append$1(v, element); 2408 }); 2409 }, (v) => { 2410 before$4(v, element); 2411 }); 2412 }; 2413 const prepend = (parent, element) => { 2414 const firstChild$1 = firstChild(parent); 2415 firstChild$1.fold(() => { 2416 append$1(parent, element); 2417 }, (v) => { 2418 parent.dom.insertBefore(element.dom, v.dom); 2419 }); 2420 }; 2421 const append$1 = (parent, element) => { 2422 parent.dom.appendChild(element.dom); 2423 }; 2424 const wrap$2 = (element, wrapper) => { 2425 before$4(element, wrapper); 2426 append$1(wrapper, element); 2427 }; 2428 2429 const before$3 = (marker, elements) => { 2430 each$e(elements, (x) => { 2431 before$4(marker, x); 2432 }); 2433 }; 2434 const after$3 = (marker, elements) => { 2435 each$e(elements, (x, i) => { 2436 const e = i === 0 ? marker : elements[i - 1]; 2437 after$4(e, x); 2438 }); 2439 }; 2440 const append = (parent, elements) => { 2441 each$e(elements, (x) => { 2442 append$1(parent, x); 2443 }); 2444 }; 2445 2446 const rawSet = (dom, key, value) => { 2447 /* 2448 * JQuery coerced everything to a string, and silently did nothing on text node/null/undefined. 2449 * 2450 * We fail on those invalid cases, only allowing numbers and booleans. 2451 */ 2452 if (isString(value) || isBoolean(value) || isNumber(value)) { 2453 dom.setAttribute(key, value + ''); 2454 } 2455 else { 2456 // eslint-disable-next-line no-console 2457 console.error('Invalid call to Attribute.set. Key ', key, ':: Value ', value, ':: Element ', dom); 2458 throw new Error('Attribute value was not simple'); 2459 } 2460 }; 2461 const set$4 = (element, key, value) => { 2462 rawSet(element.dom, key, value); 2463 }; 2464 const setAll$1 = (element, attrs) => { 2465 const dom = element.dom; 2466 each$d(attrs, (v, k) => { 2467 rawSet(dom, k, v); 2468 }); 2469 }; 2470 const get$9 = (element, key) => { 2471 const v = element.dom.getAttribute(key); 2472 // undefined is the more appropriate value for JS, and this matches JQuery 2473 return v === null ? undefined : v; 2474 }; 2475 const getOpt = (element, key) => Optional.from(get$9(element, key)); 2476 const has$1 = (element, key) => { 2477 const dom = element.dom; 2478 // return false for non-element nodes, no point in throwing an error 2479 return dom && dom.hasAttribute ? dom.hasAttribute(key) : false; 2480 }; 2481 const remove$9 = (element, key) => { 2482 element.dom.removeAttribute(key); 2483 }; 2484 const hasNone = (element) => { 2485 const attrs = element.dom.attributes; 2486 return attrs === undefined || attrs === null || attrs.length === 0; 2487 }; 2488 const clone$4 = (element) => foldl(element.dom.attributes, (acc, attr) => { 2489 acc[attr.name] = attr.value; 2490 return acc; 2491 }, {}); 2492 2493 const empty = (element) => { 2494 // shortcut "empty node" trick. Requires IE 9. 2495 element.dom.textContent = ''; 2496 // If the contents was a single empty text node, the above doesn't remove it. But, it's still faster in general 2497 // than removing every child node manually. 2498 // The following is (probably) safe for performance as 99.9% of the time the trick works and 2499 // Traverse.children will return an empty array. 2500 each$e(children$1(element), (rogue) => { 2501 remove$8(rogue); 2502 }); 2503 }; 2504 const remove$8 = (element) => { 2505 const dom = element.dom; 2506 if (dom.parentNode !== null) { 2507 dom.parentNode.removeChild(dom); 2508 } 2509 }; 2510 const unwrap = (wrapper) => { 2511 const children = children$1(wrapper); 2512 if (children.length > 0) { 2513 after$3(wrapper, children); 2514 } 2515 remove$8(wrapper); 2516 }; 2517 2518 const clone$3 = (original, isDeep) => SugarElement.fromDom(original.dom.cloneNode(isDeep)); 2519 /** Shallow clone - just the tag, no children */ 2520 const shallow = (original) => clone$3(original, false); 2521 /** Deep clone - everything copied including children */ 2522 const deep = (original) => clone$3(original, true); 2523 /** Shallow clone, with a new tag */ 2524 const shallowAs = (original, tag) => { 2525 const nu = SugarElement.fromTag(tag); 2526 const attributes = clone$4(original); 2527 setAll$1(nu, attributes); 2528 return nu; 2529 }; 2530 /** Change the tag name, but keep all children */ 2531 const mutate = (original, tag) => { 2532 const nu = shallowAs(original, tag); 2533 after$4(original, nu); 2534 const children = children$1(original); 2535 append(nu, children); 2536 remove$8(original); 2537 return nu; 2538 }; 2539 2540 const fromHtml = (html, scope) => { 2541 const doc = scope || document; 2542 const div = doc.createElement('div'); 2543 div.innerHTML = html; 2544 return children$1(SugarElement.fromDom(div)); 2545 }; 2546 const fromDom$1 = (nodes) => map$3(nodes, SugarElement.fromDom); 2547 2548 const get$8 = (element) => element.dom.innerHTML; 2549 const set$3 = (element, content) => { 2550 const owner = owner$1(element); 2551 const docDom = owner.dom; 2552 // FireFox has *terrible* performance when using innerHTML = x 2553 const fragment = SugarElement.fromDom(docDom.createDocumentFragment()); 2554 const contentElements = fromHtml(content, docDom); 2555 append(fragment, contentElements); 2556 empty(element); 2557 append$1(element, fragment); 2558 }; 2559 const getOuter = (element) => { 2560 const container = SugarElement.fromTag('div'); 2561 const clone = SugarElement.fromDom(element.dom.cloneNode(true)); 2562 append$1(container, clone); 2563 return get$8(container); 2564 }; 2565 2566 // some elements, such as mathml, don't have style attributes 2567 // others, such as angular elements, have style attributes that aren't a CSSStyleDeclaration 2568 const isSupported = (dom) => dom.style !== undefined && isFunction(dom.style.getPropertyValue); 2569 2570 // Node.contains() is very, very, very good performance 2571 // http://jsperf.com/closest-vs-contains/5 2572 const inBody = (element) => { 2573 // Technically this is only required on IE, where contains() returns false for text nodes. 2574 // But it's cheap enough to run everywhere and Sugar doesn't have platform detection (yet). 2575 const dom = isText$c(element) ? element.dom.parentNode : element.dom; 2576 // use ownerDocument.body to ensure this works inside iframes. 2577 // Normally contains is bad because an element "contains" itself, but here we want that. 2578 if (dom === undefined || dom === null || dom.ownerDocument === null) { 2579 return false; 2580 } 2581 const doc = dom.ownerDocument; 2582 return getShadowRoot(SugarElement.fromDom(dom)).fold(() => doc.body.contains(dom), compose1(inBody, getShadowHost)); 2583 }; 2584 2585 const internalSet = (dom, property, value) => { 2586 // This is going to hurt. Apologies. 2587 // JQuery coerces numbers to pixels for certain property names, and other times lets numbers through. 2588 // we're going to be explicit; strings only. 2589 if (!isString(value)) { 2590 // eslint-disable-next-line no-console 2591 console.error('Invalid call to CSS.set. Property ', property, ':: Value ', value, ':: Element ', dom); 2592 throw new Error('CSS value must be a string: ' + value); 2593 } 2594 // removed: support for dom().style[property] where prop is camel case instead of normal property name 2595 if (isSupported(dom)) { 2596 dom.style.setProperty(property, value); 2597 } 2598 }; 2599 const internalRemove = (dom, property) => { 2600 /* 2601 * IE9 and above - MDN doesn't have details, but here's a couple of random internet claims 2602 * 2603 * http://help.dottoro.com/ljopsjck.php 2604 * http://stackoverflow.com/a/7901886/7546 2605 */ 2606 if (isSupported(dom)) { 2607 dom.style.removeProperty(property); 2608 } 2609 }; 2610 const set$2 = (element, property, value) => { 2611 const dom = element.dom; 2612 internalSet(dom, property, value); 2613 }; 2614 const setAll = (element, css) => { 2615 const dom = element.dom; 2616 each$d(css, (v, k) => { 2617 internalSet(dom, k, v); 2618 }); 2619 }; 2620 /* 2621 * NOTE: For certain properties, this returns the "used value" which is subtly different to the "computed value" (despite calling getComputedStyle). 2622 * Blame CSS 2.0. 2623 * 2624 * https://developer.mozilla.org/en-US/docs/Web/CSS/used_value 2625 */ 2626 const get$7 = (element, property) => { 2627 const dom = element.dom; 2628 /* 2629 * IE9 and above per 2630 * https://developer.mozilla.org/en/docs/Web/API/window.getComputedStyle 2631 * 2632 * Not in numerosity, because it doesn't memoize and looking this up dynamically in performance critical code would be horrendous. 2633 * 2634 * JQuery has some magic here for IE popups, but we don't really need that. 2635 * It also uses element.ownerDocument.defaultView to handle iframes but that hasn't been required since FF 3.6. 2636 */ 2637 const styles = window.getComputedStyle(dom); 2638 const r = styles.getPropertyValue(property); 2639 // jquery-ism: If r is an empty string, check that the element is not in a document. If it isn't, return the raw value. 2640 // Turns out we do this a lot. 2641 return (r === '' && !inBody(element)) ? getUnsafeProperty(dom, property) : r; 2642 }; 2643 // removed: support for dom().style[property] where prop is camel case instead of normal property name 2644 // empty string is what the browsers (IE11 and Chrome) return when the propertyValue doesn't exists. 2645 const getUnsafeProperty = (dom, property) => isSupported(dom) ? dom.style.getPropertyValue(property) : ''; 2646 /* 2647 * Gets the raw value from the style attribute. Useful for retrieving "used values" from the DOM: 2648 * https://developer.mozilla.org/en-US/docs/Web/CSS/used_value 2649 * 2650 * Returns NONE if the property isn't set, or the value is an empty string. 2651 */ 2652 const getRaw$1 = (element, property) => { 2653 const dom = element.dom; 2654 const raw = getUnsafeProperty(dom, property); 2655 return Optional.from(raw).filter((r) => r.length > 0); 2656 }; 2657 const getAllRaw = (element) => { 2658 const css = {}; 2659 const dom = element.dom; 2660 if (isSupported(dom)) { 2661 for (let i = 0; i < dom.style.length; i++) { 2662 const ruleName = dom.style.item(i); 2663 css[ruleName] = dom.style[ruleName]; 2664 } 2665 } 2666 return css; 2667 }; 2668 const remove$7 = (element, property) => { 2669 const dom = element.dom; 2670 internalRemove(dom, property); 2671 if (is$4(getOpt(element, 'style').map(trim$4), '')) { 2672 // No more styles left, remove the style attribute as well 2673 remove$9(element, 'style'); 2674 } 2675 }; 2676 /* NOTE: This function is here for the side effect it triggers. 2677 The value itself is not used. 2678 Be sure to not use the return value, and that it is not removed by a minifier. 2679 */ 2680 const reflow = (e) => e.dom.offsetWidth; 2681 2682 const Dimension = (name, getOffset) => { 2683 const set = (element, h) => { 2684 if (!isNumber(h) && !h.match(/^[0-9]+$/)) { 2685 throw new Error(name + '.set accepts only positive integer values. Value was ' + h); 2686 } 2687 const dom = element.dom; 2688 if (isSupported(dom)) { 2689 dom.style[name] = h + 'px'; 2690 } 2691 }; 2692 /* 2693 * jQuery supports querying width and height on the document and window objects. 2694 * 2695 * TBIO doesn't do this, so the code is removed to save space, but left here just in case. 2696 */ 2697 /* 2698 var getDocumentWidth = (element) => { 2699 var dom = element.dom; 2700 if (Node.isDocument(element)) { 2701 var body = dom.body; 2702 var doc = dom.documentElement; 2703 return Math.max( 2704 body.scrollHeight, 2705 doc.scrollHeight, 2706 body.offsetHeight, 2707 doc.offsetHeight, 2708 doc.clientHeight 2709 ); 2710 } 2711 }; 2712 2713 var getWindowWidth = (element) => { 2714 var dom = element.dom; 2715 if (dom.window === dom) { 2716 // There is no offsetHeight on a window, so use the clientHeight of the document 2717 return dom.document.documentElement.clientHeight; 2718 } 2719 }; 2720 */ 2721 const get = (element) => { 2722 const r = getOffset(element); 2723 // zero or null means non-standard or disconnected, fall back to CSS 2724 if (r <= 0 || r === null) { 2725 const css = get$7(element, name); 2726 // ugh this feels dirty, but it saves cycles 2727 return parseFloat(css) || 0; 2728 } 2729 return r; 2730 }; 2731 // in jQuery, getOuter replicates (or uses) box-sizing: border-box calculations 2732 // although these calculations only seem relevant for quirks mode, and edge cases TBIO doesn't rely on 2733 const getOuter = get; 2734 const aggregate = (element, properties) => foldl(properties, (acc, property) => { 2735 const val = get$7(element, property); 2736 const value = val === undefined ? 0 : parseInt(val, 10); 2737 return isNaN(value) ? acc : acc + value; 2738 }, 0); 2739 const max = (element, value, properties) => { 2740 const cumulativeInclusions = aggregate(element, properties); 2741 // if max-height is 100px and your cumulativeInclusions is 150px, there is no way max-height can be 100px, so we return 0. 2742 const absoluteMax = value > cumulativeInclusions ? value - cumulativeInclusions : 0; 2743 return absoluteMax; 2744 }; 2745 return { 2746 set, 2747 get, 2748 getOuter, 2749 aggregate, 2750 max 2751 }; 2752 }; 2753 2754 const api$1 = Dimension('height', (element) => { 2755 // getBoundingClientRect gives better results than offsetHeight for tables with captions on Firefox 2756 const dom = element.dom; 2757 return inBody(element) ? dom.getBoundingClientRect().height : dom.offsetHeight; 2758 }); 2759 const get$6 = (element) => api$1.get(element); 2760 2761 const r = (left, top) => { 2762 const translate = (x, y) => r(left + x, top + y); 2763 return { 2764 left, 2765 top, 2766 translate 2767 }; 2768 }; 2769 // tslint:disable-next-line:variable-name 2770 const SugarPosition = r; 2771 2772 const boxPosition = (dom) => { 2773 const box = dom.getBoundingClientRect(); 2774 return SugarPosition(box.left, box.top); 2775 }; 2776 // Avoids falsy false fallthrough 2777 const firstDefinedOrZero = (a, b) => { 2778 if (a !== undefined) { 2779 return a; 2780 } 2781 else { 2782 return b !== undefined ? b : 0; 2783 } 2784 }; 2785 const absolute = (element) => { 2786 const doc = element.dom.ownerDocument; 2787 const body = doc.body; 2788 const win = doc.defaultView; 2789 const html = doc.documentElement; 2790 if (body === element.dom) { 2791 return SugarPosition(body.offsetLeft, body.offsetTop); 2792 } 2793 const scrollTop = firstDefinedOrZero(win?.pageYOffset, html.scrollTop); 2794 const scrollLeft = firstDefinedOrZero(win?.pageXOffset, html.scrollLeft); 2795 const clientTop = firstDefinedOrZero(html.clientTop, body.clientTop); 2796 const clientLeft = firstDefinedOrZero(html.clientLeft, body.clientLeft); 2797 return viewport(element).translate(scrollLeft - clientLeft, scrollTop - clientTop); 2798 }; 2799 const viewport = (element) => { 2800 const dom = element.dom; 2801 const doc = dom.ownerDocument; 2802 const body = doc.body; 2803 if (body === dom) { 2804 return SugarPosition(body.offsetLeft, body.offsetTop); 2805 } 2806 if (!inBody(element)) { 2807 return SugarPosition(0, 0); 2808 } 2809 return boxPosition(dom); 2810 }; 2811 2812 // get scroll position (x,y) relative to document _doc (or global if not supplied) 2813 const get$5 = (_DOC) => { 2814 const doc = _DOC !== undefined ? _DOC.dom : document; 2815 // ASSUMPTION: This is for cross-browser support, body works for Safari & EDGE, and when we have an iframe body scroller 2816 const x = doc.body.scrollLeft || doc.documentElement.scrollLeft; 2817 const y = doc.body.scrollTop || doc.documentElement.scrollTop; 2818 return SugarPosition(x, y); 2819 }; 2820 // Scroll content to (x,y) relative to document _doc (or global if not supplied) 2821 const to = (x, y, _DOC) => { 2822 const doc = _DOC !== undefined ? _DOC.dom : document; 2823 const win = doc.defaultView; 2824 if (win) { 2825 win.scrollTo(x, y); 2826 } 2827 }; 2828 // TBIO-4472 Safari 10 - Scrolling typeahead with keyboard scrolls page 2829 const intoView = (element, alignToTop) => { 2830 const isSafari = detect$1().browser.isSafari(); 2831 // this method isn't in TypeScript 2832 if (isSafari && isFunction(element.dom.scrollIntoViewIfNeeded)) { 2833 element.dom.scrollIntoViewIfNeeded(false); // false=align to nearest edge 2834 } 2835 else { 2836 element.dom.scrollIntoView(alignToTop); // true=to top, false=to bottom 2837 } 2838 }; 2839 2840 const NodeValue = (is, name) => { 2841 const get = (element) => { 2842 if (!is(element)) { 2843 throw new Error('Can only get ' + name + ' value of a ' + name + ' node'); 2844 } 2845 return getOption(element).getOr(''); 2846 }; 2847 const getOption = (element) => is(element) ? Optional.from(element.dom.nodeValue) : Optional.none(); 2848 const set = (element, value) => { 2849 if (!is(element)) { 2850 throw new Error('Can only set raw ' + name + ' value of a ' + name + ' node'); 2851 } 2852 element.dom.nodeValue = value; 2853 }; 2854 return { 2855 get, 2856 getOption, 2857 set 2858 }; 2859 }; 2860 2861 const fromElements = (elements, scope) => { 2862 const doc = scope || document; 2863 const fragment = doc.createDocumentFragment(); 2864 each$e(elements, (element) => { 2865 fragment.appendChild(element.dom); 2866 }); 2867 return SugarElement.fromDom(fragment); 2868 }; 2869 2870 const api = NodeValue(isText$c, 'text'); 2871 const get$4 = (element) => api.get(element); 2872 const getOption = (element) => api.getOption(element); 2873 const set$1 = (element, value) => api.set(element, value); 2874 2875 // Methods for handling attributes that contain a list of values <div foo="alpha beta theta"> 2876 const read$4 = (element, attr) => { 2877 const value = get$9(element, attr); 2878 return value === undefined || value === '' ? [] : value.split(' '); 2879 }; 2880 const add$4 = (element, attr, id) => { 2881 const old = read$4(element, attr); 2882 const nu = old.concat([id]); 2883 set$4(element, attr, nu.join(' ')); 2884 return true; 2885 }; 2886 const remove$6 = (element, attr, id) => { 2887 const nu = filter$5(read$4(element, attr), (v) => v !== id); 2888 if (nu.length > 0) { 2889 set$4(element, attr, nu.join(' ')); 2890 } 2891 else { 2892 remove$9(element, attr); 2893 } 2894 return false; 2895 }; 2896 2897 var ClosestOrAncestor = (is, ancestor, scope, a, isRoot) => { 2898 if (is(scope, a)) { 2899 return Optional.some(scope); 2900 } 2901 else if (isFunction(isRoot) && isRoot(scope)) { 2902 return Optional.none(); 2903 } 2904 else { 2905 return ancestor(scope, a, isRoot); 2906 } 2907 }; 2908 2909 const ancestor$5 = (scope, predicate, isRoot) => { 2910 let element = scope.dom; 2911 const stop = isFunction(isRoot) ? isRoot : never; 2912 while (element.parentNode) { 2913 element = element.parentNode; 2914 const el = SugarElement.fromDom(element); 2915 if (predicate(el)) { 2916 return Optional.some(el); 2917 } 2918 else if (stop(el)) { 2919 break; 2920 } 2921 } 2922 return Optional.none(); 2923 }; 2924 const closest$5 = (scope, predicate, isRoot) => { 2925 // This is required to avoid ClosestOrAncestor passing the predicate to itself 2926 const is = (s, test) => test(s); 2927 return ClosestOrAncestor(is, ancestor$5, scope, predicate, isRoot); 2928 }; 2929 const sibling$1 = (scope, predicate) => { 2930 const element = scope.dom; 2931 if (!element.parentNode) { 2932 return Optional.none(); 2933 } 2934 return child(SugarElement.fromDom(element.parentNode), (x) => !eq(scope, x) && predicate(x)); 2935 }; 2936 const child = (scope, predicate) => { 2937 const pred = (node) => predicate(SugarElement.fromDom(node)); 2938 const result = find$2(scope.dom.childNodes, pred); 2939 return result.map(SugarElement.fromDom); 2940 }; 2941 const descendant$2 = (scope, predicate) => { 2942 const descend = (node) => { 2943 // tslint:disable-next-line:prefer-for-of 2944 for (let i = 0; i < node.childNodes.length; i++) { 2945 const child = SugarElement.fromDom(node.childNodes[i]); 2946 if (predicate(child)) { 2947 return Optional.some(child); 2948 } 2949 const res = descend(node.childNodes[i]); 2950 if (res.isSome()) { 2951 return res; 2952 } 2953 } 2954 return Optional.none(); 2955 }; 2956 return descend(scope.dom); 2957 }; 2958 2959 const ancestor$4 = (scope, selector, isRoot) => ancestor$5(scope, (e) => is$2(e, selector), isRoot); 2960 const descendant$1 = (scope, selector) => one(selector, scope); 2961 // Returns Some(closest ancestor element (sugared)) matching 'selector' up to isRoot, or None() otherwise 2962 const closest$4 = (scope, selector, isRoot) => { 2963 const is = (element, selector) => is$2(element, selector); 2964 return ClosestOrAncestor(is, ancestor$4, scope, selector, isRoot); 2965 }; 2966 2967 // IE11 Can return undefined for a classList on elements such as math, so we make sure it's not undefined before attempting to use it. 2968 const supports = (element) => element.dom.classList !== undefined; 2969 const get$3 = (element) => read$4(element, 'class'); 2970 const add$3 = (element, clazz) => add$4(element, 'class', clazz); 2971 const remove$5 = (element, clazz) => remove$6(element, 'class', clazz); 2972 const toggle$2 = (element, clazz) => { 2973 if (contains$2(get$3(element), clazz)) { 2974 return remove$5(element, clazz); 2975 } 2976 else { 2977 return add$3(element, clazz); 2978 } 2979 }; 2980 2981 /* 2982 * ClassList is IE10 minimum: 2983 * https://developer.mozilla.org/en-US/docs/Web/API/Element.classList 2984 * 2985 * Note that IE doesn't support the second argument to toggle (at all). 2986 * If it did, the toggler could be better. 2987 */ 2988 const add$2 = (element, clazz) => { 2989 if (supports(element)) { 2990 element.dom.classList.add(clazz); 2991 } 2992 else { 2993 add$3(element, clazz); 2994 } 2995 }; 2996 const cleanClass = (element) => { 2997 const classList = supports(element) ? element.dom.classList : get$3(element); 2998 // classList is a "live list", so this is up to date already 2999 if (classList.length === 0) { 3000 // No more classes left, remove the class attribute as well 3001 remove$9(element, 'class'); 3002 } 3003 }; 3004 const remove$4 = (element, clazz) => { 3005 if (supports(element)) { 3006 const classList = element.dom.classList; 3007 classList.remove(clazz); 3008 } 3009 else { 3010 remove$5(element, clazz); 3011 } 3012 cleanClass(element); 3013 }; 3014 const toggle$1 = (element, clazz) => { 3015 const result = supports(element) ? element.dom.classList.toggle(clazz) : toggle$2(element, clazz); 3016 cleanClass(element); 3017 return result; 3018 }; 3019 const has = (element, clazz) => supports(element) && element.dom.classList.contains(clazz); 3020 3021 /* 3022 * ClassList is IE10 minimum: 3023 * https://developer.mozilla.org/en-US/docs/Web/API/Element.classList 3024 */ 3025 const add$1 = (element, classes) => { 3026 each$e(classes, (x) => { 3027 add$2(element, x); 3028 }); 3029 }; 3030 const remove$3 = (element, classes) => { 3031 each$e(classes, (x) => { 3032 remove$4(element, x); 3033 }); 3034 }; 3035 3036 const closest$3 = (target) => closest$4(target, '[contenteditable]'); 3037 const isEditable$2 = (element, assumeEditable = false) => { 3038 if (inBody(element)) { 3039 return element.dom.isContentEditable; 3040 } 3041 else { 3042 // Find the closest contenteditable element and check if it's editable 3043 return closest$3(element).fold(constant(assumeEditable), (editable) => getRaw(editable) === 'true'); 3044 } 3045 }; 3046 const getRaw = (element) => element.dom.contentEditable; 3047 const set = (element, editable) => { 3048 element.dom.contentEditable = editable ? 'true' : 'false'; 3049 }; 3050 3051 const ancestors$1 = (scope, predicate, isRoot) => filter$5(parents$1(scope, isRoot), predicate); 3052 const children = (scope, predicate) => filter$5(children$1(scope), predicate); 3053 const descendants$1 = (scope, predicate) => { 3054 let result = []; 3055 // Recurse.toArray() might help here 3056 each$e(children$1(scope), (x) => { 3057 if (predicate(x)) { 3058 result = result.concat([x]); 3059 } 3060 result = result.concat(descendants$1(x, predicate)); 3061 }); 3062 return result; 3063 }; 3064 3065 // For all of the following: 3066 // 3067 // jQuery does siblings of firstChild. IE9+ supports scope.dom.children (similar to Traverse.children but elements only). 3068 // Traverse should also do this (but probably not by default). 3069 // 3070 const ancestors = (scope, selector, isRoot) => 3071 // It may surprise you to learn this is exactly what JQuery does 3072 // TODO: Avoid all this wrapping and unwrapping 3073 ancestors$1(scope, (e) => is$2(e, selector), isRoot); 3074 const descendants = (scope, selector) => all(selector, scope); 3075 3076 const ancestor$3 = (scope, predicate, isRoot) => ancestor$5(scope, predicate, isRoot).isSome(); 3077 const sibling = (scope, predicate) => sibling$1(scope, predicate).isSome(); 3078 const descendant = (scope, predicate) => descendant$2(scope, predicate).isSome(); 3079 3080 const ancestor$2 = (element, target) => ancestor$3(element, curry(eq, target)); 3081 3082 const ancestor$1 = (scope, selector, isRoot) => ancestor$4(scope, selector, isRoot).isSome(); 3083 const closest$2 = (scope, selector, isRoot) => closest$4(scope, selector, isRoot).isSome(); 3084 3085 const ensureIsRoot = (isRoot) => isFunction(isRoot) ? isRoot : never; 3086 const ancestor = (scope, transform, isRoot) => { 3087 let element = scope.dom; 3088 const stop = ensureIsRoot(isRoot); 3089 while (element.parentNode) { 3090 element = element.parentNode; 3091 const el = SugarElement.fromDom(element); 3092 const transformed = transform(el); 3093 if (transformed.isSome()) { 3094 return transformed; 3095 } 3096 else if (stop(el)) { 3097 break; 3098 } 3099 } 3100 return Optional.none(); 3101 }; 3102 const closest$1 = (scope, transform, isRoot) => { 3103 const current = transform(scope); 3104 const stop = ensureIsRoot(isRoot); 3105 return current.orThunk(() => stop(scope) ? Optional.none() : ancestor(scope, transform, stop)); 3106 }; 3107 3108 const isTextNodeWithCursorPosition = (el) => getOption(el).filter((text) => 3109 // For the purposes of finding cursor positions only allow text nodes with content, 3110 // but trim removes and that's allowed 3111 text.trim().length !== 0 || text.indexOf(nbsp) > -1).isSome(); 3112 const isContentEditableFalse$b = (elem) => isHTMLElement$1(elem) && (get$9(elem, 'contenteditable') === 'false'); 3113 const elementsWithCursorPosition = ['img', 'br']; 3114 const isCursorPosition = (elem) => { 3115 const hasCursorPosition = isTextNodeWithCursorPosition(elem); 3116 return hasCursorPosition || contains$2(elementsWithCursorPosition, name(elem)) || isContentEditableFalse$b(elem); 3117 }; 3118 3119 const first = (element) => descendant$2(element, isCursorPosition); 3120 3121 const create$c = (start, soffset, finish, foffset) => ({ 3122 start, 3123 soffset, 3124 finish, 3125 foffset 3126 }); 3127 // tslint:disable-next-line:variable-name 3128 const SimRange = { 3129 create: create$c 3130 }; 3131 3132 const adt$2 = Adt.generate([ 3133 { before: ['element'] }, 3134 { on: ['element', 'offset'] }, 3135 { after: ['element'] } 3136 ]); 3137 // Probably don't need this given that we now have "match" 3138 const cata = (subject, onBefore, onOn, onAfter) => subject.fold(onBefore, onOn, onAfter); 3139 const getStart$2 = (situ) => situ.fold(identity, identity, identity); 3140 const before$2 = adt$2.before; 3141 const on = adt$2.on; 3142 const after$2 = adt$2.after; 3143 // tslint:disable-next-line:variable-name 3144 const Situ = { 3145 before: before$2, 3146 on, 3147 after: after$2, 3148 cata, 3149 getStart: getStart$2 3150 }; 3151 3152 // Consider adding a type for "element" 3153 const adt$1 = Adt.generate([ 3154 { domRange: ['rng'] }, 3155 { relative: ['startSitu', 'finishSitu'] }, 3156 { exact: ['start', 'soffset', 'finish', 'foffset'] } 3157 ]); 3158 const exactFromRange = (simRange) => adt$1.exact(simRange.start, simRange.soffset, simRange.finish, simRange.foffset); 3159 const getStart$1 = (selection) => selection.match({ 3160 domRange: (rng) => SugarElement.fromDom(rng.startContainer), 3161 relative: (startSitu, _finishSitu) => Situ.getStart(startSitu), 3162 exact: (start, _soffset, _finish, _foffset) => start 3163 }); 3164 const domRange = adt$1.domRange; 3165 const relative = adt$1.relative; 3166 const exact = adt$1.exact; 3167 const getWin = (selection) => { 3168 const start = getStart$1(selection); 3169 return defaultView(start); 3170 }; 3171 // This is out of place but it's API so I can't remove it 3172 const range = SimRange.create; 3173 // tslint:disable-next-line:variable-name 3174 const SimSelection = { 3175 domRange, 3176 relative, 3177 exact, 3178 exactFromRange, 3179 getWin, 3180 range 3181 }; 3182 3183 const caretPositionFromPoint = (doc, x, y) => Optional.from(doc.caretPositionFromPoint?.(x, y)) 3184 .bind((pos) => { 3185 // It turns out that Firefox can return null for pos.offsetNode 3186 if (pos.offsetNode === null) { 3187 return Optional.none(); 3188 } 3189 const r = doc.createRange(); 3190 r.setStart(pos.offsetNode, pos.offset); 3191 r.collapse(); 3192 return Optional.some(r); 3193 }); 3194 const caretRangeFromPoint = (doc, x, y) => Optional.from(doc.caretRangeFromPoint?.(x, y)); 3195 const availableSearch = (doc, x, y) => { 3196 if (doc.caretPositionFromPoint) { 3197 return caretPositionFromPoint(doc, x, y); // defined standard, firefox only 3198 } 3199 else if (doc.caretRangeFromPoint) { 3200 return caretRangeFromPoint(doc, x, y); // webkit/blink implementation 3201 } 3202 else { 3203 return Optional.none(); // unsupported browser 3204 } 3205 }; 3206 const fromPoint$1 = (win, x, y) => { 3207 const doc = win.document; 3208 return availableSearch(doc, x, y).map((rng) => SimRange.create(SugarElement.fromDom(rng.startContainer), rng.startOffset, SugarElement.fromDom(rng.endContainer), rng.endOffset)); 3209 }; 3210 3211 const beforeSpecial = (element, offset) => { 3212 // From memory, we don't want to use <br> directly on Firefox because it locks the keyboard input. 3213 // It turns out that <img> directly on IE locks the keyboard as well. 3214 // If the offset is 0, use before. If the offset is 1, use after. 3215 // TBIO-3889: Firefox Situ.on <input> results in a child of the <input>; Situ.before <input> results in platform inconsistencies 3216 const name$1 = name(element); 3217 if ('input' === name$1) { 3218 return Situ.after(element); 3219 } 3220 else if (!contains$2(['br', 'img'], name$1)) { 3221 return Situ.on(element, offset); 3222 } 3223 else { 3224 return offset === 0 ? Situ.before(element) : Situ.after(element); 3225 } 3226 }; 3227 const preprocessRelative = (startSitu, finishSitu) => { 3228 const start = startSitu.fold(Situ.before, beforeSpecial, Situ.after); 3229 const finish = finishSitu.fold(Situ.before, beforeSpecial, Situ.after); 3230 return SimSelection.relative(start, finish); 3231 }; 3232 const preprocessExact = (start, soffset, finish, foffset) => { 3233 const startSitu = beforeSpecial(start, soffset); 3234 const finishSitu = beforeSpecial(finish, foffset); 3235 return SimSelection.relative(startSitu, finishSitu); 3236 }; 3237 const preprocess = (selection) => selection.match({ 3238 domRange: (rng) => { 3239 const start = SugarElement.fromDom(rng.startContainer); 3240 const finish = SugarElement.fromDom(rng.endContainer); 3241 return preprocessExact(start, rng.startOffset, finish, rng.endOffset); 3242 }, 3243 relative: preprocessRelative, 3244 exact: preprocessExact 3245 }); 3246 3247 const toNative = (selection) => { 3248 const win = SimSelection.getWin(selection).dom; 3249 const getDomRange = (start, soffset, finish, foffset) => exactToNative(win, start, soffset, finish, foffset); 3250 const filtered = preprocess(selection); 3251 return diagnose(win, filtered).match({ 3252 ltr: getDomRange, 3253 rtl: getDomRange 3254 }); 3255 }; 3256 const getAtPoint = (win, x, y) => fromPoint$1(win, x, y); 3257 3258 const get$2 = (_win) => { 3259 const win = _win === undefined ? window : _win; 3260 if (detect$1().browser.isFirefox()) { 3261 // TINY-7984: Firefox 91 is returning incorrect values for visualVie
3261wport.pageTop, so disable it for now 3262 return Optional.none(); 3263 } 3264 else { 3265 return Optional.from(win.visualViewport); 3266 } 3267 }; 3268 const bounds = (x, y, width, height) => ({ 3269 x, 3270 y, 3271 width, 3272 height, 3273 right: x + width, 3274 bottom: y + height 3275 }); 3276 const getBounds = (_win) => { 3277 const win = _win === undefined ? window : _win; 3278 const doc = win.document; 3279 const scroll = get$5
vendor: 19,717 bytes, lines 3279-3713
3279(SugarElement.fromDom(doc)); 3280 return get$2(win).fold(() => { 3281 const html = win.document.documentElement; 3282 // Don't use window.innerWidth/innerHeight here, as we don't want to include scrollbars 3283 // since the right/bottom position is based on the edge of the scrollbar not the window 3284 const width = html.clientWidth; 3285 const height = html.clientHeight; 3286 return bounds(scroll.left, scroll.top, width, height); 3287 }, (visualViewport) => 3288 // iOS doesn't update the pageTop/pageLeft when element.scrollIntoView() is called, so we need to fallback to the 3289 // scroll position which will always be less than the page top/left values when page top/left are accurate/correct. 3290 bounds(Math.max(visualViewport.pageLeft, scroll.left), Math.max(visualViewport.pageTop, scroll.top), visualViewport.width, visualViewport.height)); 3291 }; 3292 3293 /** 3294 * TreeWalker class enables you to walk the DOM in a linear manner. 3295 * 3296 * @class tinymce.dom.TreeWalker 3297 * @example 3298 * const walker = new tinymce.dom.TreeWalker(startNode); 3299 * 3300 * do { 3301 * console.log(walker.current()); 3302 * } while (walker.next()); 3303 */ 3304 class DomTreeWalker { 3305 rootNode; 3306 node; 3307 constructor(startNode, rootNode) { 3308 this.node = startNode; 3309 this.rootNode = rootNode; 3310 // This is a bit hacky but needed to ensure the 'this' variable 3311 // always references the instance and not the caller scope 3312 this.current = this.current.bind(this); 3313 this.next = this.next.bind(this); 3314 this.prev = this.prev.bind(this); 3315 this.prev2 = this.prev2.bind(this); 3316 } 3317 /** 3318 * Returns the current node. 3319 * 3320 * @method current 3321 * @return {Node/undefined} Current node where the walker is, or undefined if the walker has reached the end. 3322 */ 3323 current() { 3324 return this.node; 3325 } 3326 /** 3327 * Walks to the next node in tree. 3328 * 3329 * @method next 3330 * @return {Node/undefined} Current node where the walker is after moving to the next node, or undefined if the walker has reached the end. 3331 */ 3332 next(shallow) { 3333 this.node = this.findSibling(this.node, 'firstChild', 'nextSibling', shallow); 3334 return this.node; 3335 } 3336 /** 3337 * Walks to the previous node in tree. 3338 * 3339 * @method prev 3340 * @return {Node/undefined} Current node where the walker is after moving to the previous node, or undefined if the walker has reached the end. 3341 */ 3342 prev(shallow) { 3343 this.node = this.findSibling(this.node, 'lastChild', 'previousSibling', shallow); 3344 return this.node; 3345 } 3346 prev2(shallow) { 3347 this.node = this.findPreviousNode(this.node, shallow); 3348 return this.node; 3349 } 3350 findSibling(node, startName, siblingName, shallow) { 3351 if (node) { 3352 // Walk into nodes if it has a start 3353 if (!shallow && node[startName]) { 3354 return node[startName]; 3355 } 3356 // Return the sibling if it has one 3357 if (node !== this.rootNode) { 3358 let sibling = node[siblingName]; 3359 if (sibling) { 3360 return sibling; 3361 } 3362 // Walk up the parents to look for siblings 3363 for (let parent = node.parentNode; parent && parent !== this.rootNode; parent = parent.parentNode) { 3364 sibling = parent[siblingName]; 3365 if (sibling) { 3366 return sibling; 3367 } 3368 } 3369 } 3370 } 3371 return undefined; 3372 } 3373 findPreviousNode(node, shallow) { 3374 if (node) { 3375 const sibling = node.previousSibling; 3376 if (this.rootNode && sibling === this.rootNode) { 3377 return; 3378 } 3379 if (sibling) { 3380 if (!shallow) { 3381 // Walk down to the most distant child 3382 for (let child = sibling.lastChild; child; child = child.lastChild) { 3383 if (!child.lastChild) { 3384 return child; 3385 } 3386 } 3387 } 3388 return sibling; 3389 } 3390 const parent = node.parentNode; 3391 if (parent && parent !== this.rootNode) { 3392 return parent; 3393 } 3394 } 3395 return undefined; 3396 } 3397 } 3398 3399 const whiteSpaceRegExp = /^[ \t\r\n]*$/; 3400 const isWhitespaceText = (text) => whiteSpaceRegExp.test(text); 3401 const isZwsp$1 = (text) => { 3402 for (const c of text) { 3403 if (!isZwsp$2(c)) { 3404 return false; 3405 } 3406 } 3407 return true; 3408 }; 3409 // Don't compare other unicode spaces here, as we're only concerned about whitespace the browser would collapse 3410 const isCollapsibleWhitespace$1 = (c) => ' \f\t\v'.indexOf(c) !== -1; 3411 const isNewLineChar = (c) => c === '\n' || c === '\r'; 3412 const isNewline = (text, idx) => (idx < text.length && idx >= 0) ? isNewLineChar(text[idx]) : false; 3413 // Converts duplicate whitespace to alternating space/nbsps and tabs to spaces 3414 const normalize$4 = (text, tabSpaces = 4, isStartOfContent = true, isEndOfContent = true) => { 3415 // Replace tabs with a variable amount of spaces 3416 // Note: We don't use an actual tab character here, as it only works when in a "whitespace: pre" element, 3417 // which will cause other issues, such as trying to type the content will also be treated as being in a pre. 3418 const tabSpace = repeat(' ', tabSpaces); 3419 const normalizedText = text.replace(/\t/g, tabSpace); 3420 const result = foldl(normalizedText, (acc, c) => { 3421 // Are we dealing with a char other than some collapsible whitespace or nbsp? if so then just use it as is 3422 if (isCollapsibleWhitespace$1(c) || c === nbsp) { 3423 // If the previous char is a space, we are at the start or end, or if the next char is a new line char, then we need 3424 // to convert the space to a nbsp 3425 if (acc.pcIsSpace || (acc.str === '' && isStartOfContent) || (acc.str.length === normalizedText.length - 1 && isEndOfContent) || isNewline(normalizedText, acc.str.length + 1)) { 3426 return { pcIsSpace: false, str: acc.str + nbsp }; 3427 } 3428 else { 3429 return { pcIsSpace: true, str: acc.str + ' ' }; 3430 } 3431 } 3432 else { 3433 // Treat newlines as being a space, since we'll need to convert any leading spaces to nsbps 3434 return { pcIsSpace: isNewLineChar(c), str: acc.str + c }; 3435 } 3436 }, { pcIsSpace: false, str: '' }); 3437 return result.str; 3438 }; 3439 3440 const isNodeType = (type) => { 3441 return (node) => { 3442 return !!node && node.nodeType === type; 3443 }; 3444 }; 3445 // Firefox can allow you to get a selection on a restricted node, such as file/number inputs. These nodes 3446 // won't implement the Object prototype, so Object.getPrototypeOf() will return null or something similar. 3447 const isRestrictedNode = (node) => !!node && !Object.getPrototypeOf(node); 3448 const isElement$7 = isNodeType(1); 3449 const isHTMLElement = (node) => isElement$7(node) && isHTMLElement$1(SugarElement.fromDom(node)); 3450 const isSVGElement = (node) => isElement$7(node) && node.namespaceURI === 'http://www.w3.org/2000/svg'; 3451 const matchNodeName$1 = (name) => { 3452 const lowerCasedName = name.toLowerCase(); 3453 return (node) => isNonNullable(node) && node.nodeName.toLowerCase() === lowerCasedName; 3454 }; 3455 const matchNodeNames$1 = (names) => { 3456 const lowerCasedNames = names.map((s) => s.toLowerCase()); 3457 return (node) => { 3458 if (node && node.nodeName) { 3459 const nodeName = node.nodeName.toLowerCase(); 3460 return contains$2(lowerCasedNames, nodeName); 3461 } 3462 return false; 3463 }; 3464 }; 3465 const matchStyleValues = (name, values) => { 3466 const items = values.toLowerCase().split(' '); 3467 return (node) => { 3468 if (isElement$7(node)) { 3469 const win = node.ownerDocument.defaultView; 3470 if (win) { 3471 for (let i = 0; i < items.length; i++) { 3472 const computed = win.getComputedStyle(node, null); 3473 const cssValue = computed ? computed.getPropertyValue(name) : null; 3474 if (cssValue === items[i]) { 3475 return true; 3476 } 3477 } 3478 } 3479 } 3480 return false; 3481 }; 3482 }; 3483 const hasAttribute = (attrName) => { 3484 return (node) => { 3485 return isElement$7(node) && node.hasAttribute(attrName); 3486 }; 3487 }; 3488 const isBogus$1 = (node) => isElement$7(node) && node.hasAttribute('data-mce-bogus'); 3489 const isBogusAll = (node) => isElement$7(node) && node.getAttribute('data-mce-bogus') === 'all'; 3490 const isTable$2 = (node) => isElement$7(node) && node.tagName === 'TABLE'; 3491 const hasContentEditableState = (value) => { 3492 return (node) => { 3493 if (isHTMLElement(node)) { 3494 if (node.contentEditable === value) { 3495 return true; 3496 } 3497 if (node.getAttribute('data-mce-contenteditable') === value) { 3498 return true; 3499 } 3500 } 3501 return false; 3502 }; 3503 }; 3504 const isTextareaOrInput = matchNodeNames$1(['textarea', 'input']); 3505 const isText$b = isNodeType(3); 3506 const isCData = isNodeType(4); 3507 const isPi = isNodeType(7); 3508 const isComment = isNodeType(8); 3509 const isDocument$1 = isNodeType(9); 3510 const isDocumentFragment = isNodeType(11); 3511 const isBr$7 = matchNodeName$1('br'); 3512 const isImg = matchNodeName$1('img'); 3513 const isAnchor = matchNodeName$1('a'); 3514 const isContentEditableTrue$3 = hasContentEditableState('true'); 3515 const isContentEditableFalse$a = hasContentEditableState('false'); 3516 const isEditingHost = (node) => isHTMLElement(node) && node.isContentEditable && isNonNullable(node.parentElement) && !node.parentElement.isContentEditable; 3517 const isTableCell$3 = matchNodeNames$1(['td', 'th']); 3518 const isTableCellOrCaption = matchNodeNames$1(['td', 'th', 'caption']); 3519 const isTemplate = matchNodeName$1('template'); 3520 const isMedia$2 = matchNodeNames$1(['video', 'audio', 'object', 'embed']); 3521 const isListItem$3 = matchNodeName$1('li'); 3522 const isDetails = matchNodeName$1('details'); 3523 const isSummary$1 = matchNodeName$1('summary'); 3524 const ucVideoNodeName = 'uc-video'; 3525 const isUcVideo = matchNodeName$1(ucVideoNodeName); 3526 3527 const defaultOptionValues = { 3528 skipBogus: true, 3529 includeZwsp: false, 3530 checkRootAsContent: false, 3531 }; 3532 const hasWhitespacePreserveParent = (node, rootNode, schema) => { 3533 const rootElement = SugarElement.fromDom(rootNode); 3534 const startNode = SugarElement.fromDom(node); 3535 const whitespaceElements = schema.getWhitespaceElements(); 3536 const predicate = (node) => has$2(whitespaceElements, name(node)); 3537 return ancestor$3(startNode, predicate, curry(eq, rootElement)); 3538 }; 3539 const isNamedAnchor = (node) => { 3540 return isElement$7(node) && node.nodeName === 'A' && !node.hasAttribute('href') && (node.hasAttribute('name') || node.hasAttribute('id')); 3541 }; 3542 const isNonEmptyElement$1 = (node, schema) => { 3543 return isElement$7(node) && has$2(schema.getNonEmptyElements(), node.nodeName); 3544 }; 3545 const isBookmark = hasAttribute('data-mce-bookmark'); 3546 const hasNonEditableParent = (node) => parentElement(SugarElement.fromDom(node)).exists((parent) => !isEditable$2(parent)); 3547 const isWhitespace$1 = (node, rootNode, schema) => isWhitespaceText(node.data) 3548 && !hasWhitespacePreserveParent(node, rootNode, schema); 3549 const isText$a = (node, rootNode, schema, options) => isText$b(node) 3550 && !isWhitespace$1(node, rootNode, schema) 3551 && (!options.includeZwsp || !isZwsp$1(node.data)); 3552 const isContentNode = (schema, node, rootNode, options) => { 3553 return isFunction(options.isContent) && options.isContent(node) 3554 || isNonEmptyElement$1(node, schema) 3555 || isBookmark(node) 3556 || isNamedAnchor(node) 3557 || isText$a(node, rootNode, schema, options) 3558 || isContentEditableFalse$a(node) 3559 || isContentEditableTrue$3(node) && hasNonEditableParent(node); 3560 }; 3561 const isEmptyNode = (schema, targetNode, opts) => { 3562 const options = { ...defaultOptionValues, ...opts }; 3563 if (options.checkRootAsContent) { 3564 if (isContentNode(schema, targetNode, targetNode, options)) { 3565 return false; 3566 } 3567 } 3568 let node = targetNode.firstChild; 3569 let brCount = 0; 3570 if (!node) { 3571 return true; 3572 } 3573 const walker = new DomTreeWalker(node, targetNode); 3574 do { 3575 if (options.skipBogus && isElement$7(node)) { 3576 const bogusValue = node.getAttribute('data-mce-bogus'); 3577 if (bogusValue) { 3578 node = walker.next(bogusValue === 'all'); 3579 continue; 3580 } 3581 } 3582 if (isComment(node)) { 3583 node = walker.next(true); 3584 continue; 3585 } 3586 if (isBr$7(node)) { 3587 brCount++; 3588 node = walker.next(); 3589 continue; 3590 } 3591 if (isContentNode(schema, node, targetNode, options)) { 3592 return false; 3593 } 3594 node = walker.next(); 3595 } while (node); 3596 return brCount <= 1; 3597 }; 3598 const isEmpty$4 = (schema, elm, options) => { 3599 return isEmptyNode(schema, elm.dom, { checkRootAsContent: true, ...options }); 3600 }; 3601 const isContent$1 = (schema, node, options) => { 3602 return isContentNode(schema, node, node, { includeZwsp: defaultOptionValues.includeZwsp, ...options }); 3603 }; 3604 3605 const nodeNameToNamespaceType = (name) => { 3606 const lowerCaseName = name.toLowerCase(); 3607 if (lowerCaseName === 'svg') { 3608 return 'svg'; 3609 } 3610 else if (lowerCaseName === 'math') { 3611 return 'math'; 3612 } 3613 else { 3614 return 'html'; 3615 } 3616 }; 3617 const isNonHtmlElementRootName = (name) => nodeNameToNamespaceType(name) !== 'html'; 3618 const isNonHtmlElementRoot = (node) => isNonHtmlElementRootName(node.nodeName); 3619 const toScopeType = (node) => nodeNameToNamespaceType(node.nodeName); 3620 const namespaceElements = ['svg', 'math']; 3621 const createNamespaceTracker = () => { 3622 const currentScope = value$1(); 3623 const current = () => currentScope.get().map(toScopeType).getOr('html'); 3624 const track = (node) => { 3625 if (isNonHtmlElementRoot(node)) { 3626 currentScope.set(node); 3627 } 3628 else if (currentScope.get().exists((scopeNode) => !scopeNode.contains(node))) { 3629 currentScope.clear(); 3630 } 3631 return current(); 3632 }; 3633 const reset = () => { 3634 currentScope.clear(); 3635 }; 3636 return { 3637 track, 3638 current, 3639 reset 3640 }; 3641 }; 3642 3643 const transparentBlockAttr = 'data-mce-block'; 3644 // Returns the lowercase element names form a SchemaMap by excluding anyone that has uppercase letters. 3645 // This method is to avoid having to specify all possible valid characters other than lowercase a-z such as '-' or ':' etc. 3646 const elementNames = (map) => filter$5(keys(map), (key) => !/[A-Z]/.test(key)); 3647 const makeSelectorFromSchemaMap = (map) => map$3(elementNames(map), (name) => { 3648 // Exclude namespace elements from processing 3649 const escapedName = CSS.escape(name); 3650 return `${escapedName}:` + map$3(namespaceElements, (ns) => `not(${ns} ${escapedName})`).join(':'); 3651 }).join(','); 3652 const updateTransparent = (blocksSelector, transparent) => { 3653 if (isNonNullable(transparent.querySelector(blocksSelector))) { 3654 transparent.setAttribute(transparentBlockAttr, 'true'); 3655 if (transparent.getAttribute('data-mce-selected') === 'inline-boundary') { 3656 transparent.removeAttribute('data-mce-selected'); 3657 } 3658 return true; 3659 } 3660 else { 3661 transparent.removeAttribute(transparentBlockAttr); 3662 return false; 3663 } 3664 }; 3665 const updateBlockStateOnChildren = (schema, scope) => { 3666 const transparentSelector = makeSelectorFromSchemaMap(schema.getTransparentElements()); 3667 const blocksSelector = makeSelectorFromSchemaMap(schema.getBlockElements()); 3668 return filter$5(scope.querySelectorAll(transparentSelector), (transparent) => updateTransparent(blocksSelector, transparent)); 3669 }; 3670 const trimEdge = (schema, el, leftSide) => { 3671 const childPropertyName = leftSide ? 'lastChild' : 'firstChild'; 3672 for (let child = el[childPropertyName]; child; child = child[childPropertyName]) { 3673 if (isEmptyNode(schema, child, { checkRootAsContent: true })) { 3674 child.parentNode?.removeChild(child); 3675 return; 3676 } 3677 } 3678 }; 3679 const split$2 = (schema, parentElm, splitElm) => { 3680 const range = document.createRange(); 3681 const parentNode = parentElm.parentNode; 3682 if (parentNode) { 3683 range.setStartBefore(parentElm); 3684 range.setEndBefore(splitElm); 3685 const beforeFragment = range.extractContents(); 3686 trimEdge(schema, beforeFragment, true); 3687 range.setStartAfter(splitElm); 3688 range.setEndAfter(parentElm); 3689 const afterFragment = range.extractContents(); 3690 trimEdge(schema, afterFragment, false); 3691 if (!isEmptyNode(schema, beforeFragment, { checkRootAsContent: true })) { 3692 parentNode.insertBefore(beforeFragment, parentElm); 3693 } 3694 if (!isEmptyNode(schema, splitElm, { checkRootAsContent: true })) { 3695 parentNode.insertBefore(splitElm, parentElm); 3696 } 3697 if (!isEmptyNode(schema, afterFragment, { checkRootAsContent: true })) { 3698 parentNode.insertBefore(afterFragment, parentElm); 3699 } 3700 parentNode.removeChild(parentElm); 3701 } 3702 }; 3703 // This will find invalid blocks wrapped in anchors and split them out so for example 3704 // <h1><a href="#"><h2>x</h2></a></h1> will find that h2 is invalid inside the H1 and split that out. 3705 // This is a simplistic apporach so it's likely not covering all the cases but it's a start. 3706 const splitInvalidChildren = (schema, scope, transparentBlocks) => { 3707 const blocksElements = schema.getBlockElements(); 3708 const rootNode = SugarElement.fromDom(scope); 3709 const isBlock = (el) => name(el) in blocksElements; 3710 const isRoot = (el) => eq(el, rootNode); 3711 each$e(fromDom$1(transparentBlocks), (transparentBlock) => { 3712 ancestor$5(transparentBlock, isBlock, isRoot).each((parentBlock) => { 3713 const invalidChildren = children
vendor: 12,415 bytes, lines 3713-3984
3713(transparentBlock, (el) => isBlock(el) && !schema.isValidChild(name(parentBlock), name(el))); 3714 if (invalidChildren.length > 0) { 3715 const stateScope = parentElement(parentBlock); 3716 each$e(invalidChildren, (child) => { 3717 ancestor$5(child, isBlock, isRoot).each((parentBlock) => { 3718 split$2(schema, parentBlock.dom, child.dom); 3719 }); 3720 }); 3721 stateScope.each((scope) => updateBlockStateOnChildren(schema, scope.dom)); 3722 } 3723 }); 3724 }); 3725 }; 3726 const unwrapInvalidChildren = (schema, scope, transparentBlocks) => { 3727 each$e([...transparentBlocks, ...(isTransparentBlock(schema, scope) ? [scope] : [])], (block) => each$e(descendants(SugarElement.fromDom(block), block.nodeName.toLowerCase()), (elm) => { 3728 if (isTransparentInline(schema, elm.dom)) { 3729 unwrap(elm); 3730 } 3731 })); 3732 }; 3733 const updateChildren = (schema, scope) => { 3734 const transparentBlocks = updateBlockStateOnChildren(schema, scope); 3735 splitInvalidChildren(schema, scope, transparentBlocks); 3736 unwrapInvalidChildren(schema, scope, transparentBlocks); 3737 }; 3738 const updateElement = (schema, target) => { 3739 if (isTransparentElement(schema, target)) { 3740 const blocksSelector = makeSelectorFromSchemaMap(schema.getBlockElements()); 3741 updateTransparent(blocksSelector, target); 3742 } 3743 }; 3744 const updateCaret = (schema, root, caretParent) => { 3745 const isRoot = (el) => eq(el, SugarElement.fromDom(root)); 3746 const parents = parents$1(SugarElement.fromDom(caretParent), isRoot); 3747 // Check the element just above below the root so in if caretParent is I in this 3748 // case <body><p><b><i>|</i></b></p></body> it would use the P as the scope 3749 get$b(parents, parents.length - 2).filter(isElement$8).fold(() => updateChildren(schema, root), (scope) => updateChildren(schema, scope.dom)); 3750 }; 3751 const hasBlockAttr = (el) => el.hasAttribute(transparentBlockAttr); 3752 const isTransparentElementName = (schema, name) => has$2(schema.getTransparentElements(), name); 3753 const isTransparentElement = (schema, node) => isElement$7(node) && isTransparentElementName(schema, node.nodeName); 3754 const isTransparentBlock = (schema, node) => isTransparentElement(schema, node) && hasBlockAttr(node); 3755 const isTransparentInline = (schema, node) => isTransparentElement(schema, node) && !hasBlockAttr(node); 3756 const isTransparentAstBlock = (schema, node) => node.type === 1 && isTransparentElementName(schema, node.name) && isString(node.attr(transparentBlockAttr)); 3757 3758 const browser$2 = detect$1().browser; 3759 const firstElement = (nodes) => find$2(nodes, isElement$8); 3760 // Firefox has a bug where caption height is not included correctly in offset calculations of tables 3761 // this tries to compensate for that by detecting if that offsets are incorrect and then remove the height 3762 const getTableCaptionDeltaY = (elm) => { 3763 if (browser$2.isFirefox() && name(elm) === 'table') { 3764 return firstElement(children$1(elm)).filter((elm) => { 3765 return name(elm) === 'caption'; 3766 }).bind((caption) => { 3767 return firstElement(nextSiblings(caption)).map((body) => { 3768 const bodyTop = body.dom.offsetTop; 3769 const captionTop = caption.dom.offsetTop; 3770 const captionHeight = caption.dom.offsetHeight; 3771 return bodyTop <= captionTop ? -captionHeight : 0; 3772 }); 3773 }).getOr(0); 3774 } 3775 else { 3776 return 0; 3777 } 3778 }; 3779 const hasChild = (elm, child) => elm.children && contains$2(elm.children, child); 3780 const getPos = (body, elm, rootElm) => { 3781 let x = 0, y = 0; 3782 const doc = body.ownerDocument; 3783 rootElm = rootElm ? rootElm : body; 3784 if (elm) { 3785 // Use getBoundingClientRect if it exists since it's faster than looping offset nodes 3786 // Fallback to offsetParent calculations if the body isn't static better since it stops at the body root 3787 if (rootElm === body && elm.getBoundingClientRect && get$7(SugarElement.fromDom(body), 'position') === 'static') { 3788 const pos = elm.getBoundingClientRect(); 3789 // Add scroll offsets from documentElement or body since IE with the wrong box model will use d.body and so do WebKit 3790 // Also remove the body/documentelement clientTop/clientLeft on IE 6, 7 since they offset the position 3791 x = pos.left + (doc.documentElement.scrollLeft || body.scrollLeft) - doc.documentElement.clientLeft; 3792 y = pos.top + (doc.documentElement.scrollTop || body.scrollTop) - doc.documentElement.clientTop; 3793 return { x, y }; 3794 } 3795 let offsetParent = elm; 3796 while (offsetParent && offsetParent !== rootElm && offsetParent.nodeType && !hasChild(offsetParent, rootElm)) { 3797 const castOffsetParent = offsetParent; 3798 x += castOffsetParent.offsetLeft || 0; 3799 y += castOffsetParent.offsetTop || 0; 3800 offsetParent = castOffsetParent.offsetParent; 3801 } 3802 offsetParent = elm.parentNode; 3803 while (offsetParent && offsetParent !== rootElm && offsetParent.nodeType && !hasChild(offsetParent, rootElm)) { 3804 x -= offsetParent.scrollLeft || 0; 3805 y -= offsetParent.scrollTop || 0; 3806 offsetParent = offsetParent.parentNode; 3807 } 3808 y += getTableCaptionDeltaY(SugarElement.fromDom(elm)); 3809 } 3810 return { x, y }; 3811 }; 3812 3813 const getCrossOrigin$1 = (url, settings) => { 3814 const crossOriginFn = settings.crossOrigin; 3815 if (settings.contentCssCors) { 3816 return 'anonymous'; 3817 } 3818 else if (isFunction(crossOriginFn)) { 3819 return crossOriginFn(url); 3820 } 3821 else { 3822 return undefined; 3823 } 3824 }; 3825 const StyleSheetLoader = (documentOrShadowRoot, settings = {}) => { 3826 let idCount = 0; 3827 const loadedStates = {}; 3828 const edos = SugarElement.fromDom(documentOrShadowRoot); 3829 const doc = documentOrOwner(edos); 3830 const _setReferrerPolicy = (referrerPolicy) => { 3831 settings.referrerPolicy = referrerPolicy; 3832 }; 3833 const _setContentCssCors = (contentCssCors) => { 3834 settings.contentCssCors = contentCssCors; 3835 }; 3836 const _setCrossOrigin = (crossOrigin) => { 3837 settings.crossOrigin = crossOrigin; 3838 }; 3839 const addStyle = (element) => { 3840 append$1(getStyleContainer(edos), element); 3841 }; 3842 const removeStyle = (id) => { 3843 const styleContainer = getStyleContainer(edos); 3844 descendant$1(styleContainer, '#' + id).each(remove$8); 3845 }; 3846 const getOrCreateState = (url) => get$a(loadedStates, url).getOrThunk(() => ({ 3847 id: 'mce-u' + (idCount++), 3848 passed: [], 3849 failed: [], 3850 count: 0 3851 })); 3852 /** 3853 * Loads the specified CSS file and returns a Promise that will resolve when the stylesheet is loaded successfully or reject if it failed to load. 3854 * 3855 * @method load 3856 * @param {String} url Url to be loaded. 3857 * @return {Promise} A Promise that will resolve or reject when the stylesheet is loaded. 3858 */ 3859 const load = (url) => new Promise((success, failure) => { 3860 let link; 3861 const urlWithSuffix = Tools._addCacheSuffix(url); 3862 const state = getOrCreateState(urlWithSuffix); 3863 loadedStates[urlWithSuffix] = state; 3864 state.count++; 3865 const resolve = (callbacks, status) => { 3866 each$e(callbacks, call); 3867 state.status = status; 3868 state.passed = []; 3869 state.failed = []; 3870 if (link) { 3871 link.onload = null; 3872 link.onerror = null; 3873 link = null; 3874 } 3875 }; 3876 const passed = () => resolve(state.passed, 2); 3877 const failed = () => resolve(state.failed, 3); 3878 if (success) { 3879 state.passed.push(success); 3880 } 3881 if (failure) { 3882 state.failed.push(failure); 3883 } 3884 // Is loading wait for it to pass 3885 if (state.status === 1) { 3886 return; 3887 } 3888 // Has finished loading and was success 3889 if (state.status === 2) { 3890 passed(); 3891 return; 3892 } 3893 // Has finished loading and was a failure 3894 if (state.status === 3) { 3895 failed(); 3896 return; 3897 } 3898 // Start loading 3899 state.status = 1; 3900 const linkElem = SugarElement.fromTag('link', doc.dom); 3901 setAll$1(linkElem, { 3902 rel: 'stylesheet', 3903 type: 'text/css', 3904 id: state.id 3905 }); 3906 const crossorigin = getCrossOrigin$1(url, settings); 3907 if (crossorigin !== undefined) { 3908 set$4(linkElem, 'crossOrigin', crossorigin); 3909 } 3910 if (settings.referrerPolicy) { 3911 // Note: Don't use link.referrerPolicy = ... here as it doesn't work on Safari 3912 set$4(linkElem, 'referrerpolicy', settings.referrerPolicy); 3913 } 3914 link = linkElem.dom; 3915 link.onload = passed; 3916 link.onerror = failed; 3917 addStyle(linkElem); 3918 set$4(linkElem, 'href', urlWithSuffix); 3919 }); 3920 /** 3921 * Loads the specified css string in as a style element with an unique key. 3922 * 3923 * @method loadRawCss 3924 * @param {String} key Unique key for the style element. 3925 * @param {String} css Css style content to add. 3926 */ 3927 const loadRawCss = (key, css) => { 3928 const state = getOrCreateState(key); 3929 loadedStates[key] = state; 3930 state.count++; 3931 // Start loading 3932 const styleElem = SugarElement.fromTag('style', doc.dom); 3933 setAll$1(styleElem, { 3934 'rel': 'stylesheet', 3935 'type': 'text/css', 3936 'id': state.id, 3937 'data-mce-key': key 3938 }); 3939 styleElem.dom.innerHTML = css; 3940 addStyle(styleElem); 3941 }; 3942 /** 3943 * Loads the specified CSS files and returns a Promise that is resolved when all stylesheets are loaded or rejected if any failed to load. 3944 * 3945 * @method loadAll 3946 * @param {Array} urls URLs to be loaded. 3947 * @return {Promise} A Promise that will resolve or reject when all stylesheets are loaded. 3948 */ 3949 const loadAll = (urls) => { 3950 const loadedUrls = Promise.allSettled(map$3(urls, (url) => load(url).then(constant(url)))); 3951 return loadedUrls.then((results) => { 3952 const parts = partition$2(results, (r) => r.status === 'fulfilled'); 3953 if (parts.fail.length > 0) { 3954 return Promise.reject(map$3(parts.fail, (result) => result.reason)); 3955 } 3956 else { 3957 return map$3(parts.pass, (result) => result.value); 3958 } 3959 }); 3960 }; 3961 /** 3962 * Unloads the specified CSS file if no resources currently depend on it. 3963 * 3964 * @method unload 3965 * @param {String} url URL to unload or remove. 3966 */ 3967 const unload = (url) => { 3968 const urlWithSuffix = Tools._addCacheSuffix(url); 3969 get$a(loadedStates, urlWithSuffix).each((state) => { 3970 const count = --state.count; 3971 if (count === 0) { 3972 delete loadedStates[urlWithSuffix]; 3973 removeStyle(state.id); 3974 } 3975 }); 3976 }; 3977 /** 3978 * Unloads the specified CSS style element by key. 3979 * 3980 * @method unloadRawCss 3981 * @param {String} key Key of CSS style resource to unload. 3982 */ 3983 const unloadRawCss = (key) => { 3984 get$a
vendor: 77,521 bytes, lines 3984-5647
3984(loadedStates, key).each((state) => { 3985 const count = --state.count; 3986 if (count === 0) { 3987 delete loadedStates[key]; 3988 removeStyle(state.id); 3989 } 3990 }); 3991 }; 3992 /** 3993 * Unloads each specified CSS file if no resources currently depend on it. 3994 * 3995 * @method unloadAll 3996 * @param {Array} urls URLs to unload or remove. 3997 */ 3998 const unloadAll = (urls) => { 3999 each$e(urls, (url) => { 4000 unload(url); 4001 }); 4002 }; 4003 return { 4004 load, 4005 loadRawCss, 4006 loadAll, 4007 unload, 4008 unloadRawCss, 4009 unloadAll, 4010 _setReferrerPolicy, 4011 _setContentCssCors, 4012 _setCrossOrigin 4013 }; 4014 }; 4015 4016 /** 4017 * This function is exported for testing purposes only - please use StyleSheetLoader.instance in production code. 4018 */ 4019 const create$b = () => { 4020 const map = new WeakMap(); 4021 const forElement = (referenceElement, settings) => { 4022 const root = getRootNode(referenceElement); 4023 const rootDom = root.dom; 4024 return Optional.from(map.get(rootDom)).getOrThunk(() => { 4025 const sl = StyleSheetLoader(rootDom, settings); 4026 map.set(rootDom, sl); 4027 return sl; 4028 }); 4029 }; 4030 return { 4031 forElement 4032 }; 4033 }; 4034 const instance = create$b(); 4035 4036 const isSpan = (node) => node.nodeName.toLowerCase() === 'span'; 4037 const isInlineContent = (node, schema) => isNonNullable(node) && (isContent$1(schema, node) || schema.isInline(node.nodeName.toLowerCase())); 4038 const surroundedByInlineContent = (node, root, schema) => { 4039 const prev = new DomTreeWalker(node, root).prev(false); 4040 const next = new DomTreeWalker(node, root).next(false); 4041 // Check if the next/previous is either inline content or the start/end (eg is undefined) 4042 const prevIsInline = isUndefined(prev) || isInlineContent(prev, schema); 4043 const nextIsInline = isUndefined(next) || isInlineContent(next, schema); 4044 return prevIsInline && nextIsInline; 4045 }; 4046 const isBookmarkNode$2 = (node) => isSpan(node) && node.getAttribute('data-mce-type') === 'bookmark'; 4047 // Keep text nodes with only spaces if surrounded by spans. 4048 // eg. "<p><span>a</span> <span>b</span></p>" should keep space between a and b 4049 const isKeepTextNode = (node, root, schema) => isText$b(node) && node.data.length > 0 && surroundedByInlineContent(node, root, schema); 4050 // Keep elements as long as they have any children 4051 const isKeepElement = (node) => isElement$7(node) ? node.childNodes.length > 0 : false; 4052 const isDocument = (node) => isDocumentFragment(node) || isDocument$1(node); 4053 // W3C valid browsers tend to leave empty nodes to the left/right side of the contents - this makes sense 4054 // but we don't want that in our code since it serves no purpose for the end user 4055 // For example splitting this html at the bold element: 4056 // <p>text 1<span><b>CHOP</b></span>text 2</p> 4057 // would produce: 4058 // <p>text 1<span></span></p><b>CHOP</b><p><span></span>text 2</p> 4059 // this function will then trim off empty edges and produce: 4060 // <p>text 1</p><b>CHOP</b><p>text 2</p> 4061 const trimNode = (dom, node, schema, root) => { 4062 const rootNode = root || node; 4063 if (isElement$7(node) && isBookmarkNode$2(node)) { 4064 return node; 4065 } 4066 const children = node.childNodes; 4067 for (let i = children.length - 1; i >= 0; i--) { 4068 trimNode(dom, children[i], schema, rootNode); 4069 } 4070 // If the only child is a bookmark then move it up 4071 if (isElement$7(node)) { 4072 const currentChildren = node.childNodes; 4073 if (currentChildren.length === 1 && isBookmarkNode$2(currentChildren[0])) { 4074 node.parentNode?.insertBefore(currentChildren[0], node); 4075 } 4076 } 4077 // Remove any empty nodes 4078 if (!isDocument(node) && !isContent$1(schema, node) && !isKeepElement(node) && !isKeepTextNode(node, rootNode, schema)) { 4079 dom.remove(node); 4080 } 4081 return node; 4082 }; 4083 4084 /** 4085 * Entity encoder class. 4086 * 4087 * @class tinymce.html.Entities 4088 * @static 4089 * @version 3.4 4090 */ 4091 const makeMap$3 = Tools.makeMap; 4092 const attrsCharsRegExp = /[&<>\"\u0060\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g; 4093 const textCharsRegExp = /[<>&\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g; 4094 const rawCharsRegExp = /[<>&\"\']/g; 4095 const entityRegExp = /&#([a-z0-9]+);?|&([a-z0-9]+);/gi; 4096 const asciiMap = { 4097 128: '\u20AC', 130: '\u201A', 131: '\u0192', 132: '\u201E', 133: '\u2026', 134: '\u2020', 4098 135: '\u2021', 136: '\u02C6', 137: '\u2030', 138: '\u0160', 139: '\u2039', 140: '\u0152', 4099 142: '\u017D', 145: '\u2018', 146: '\u2019', 147: '\u201C', 148: '\u201D', 149: '\u2022', 4100 150: '\u2013', 151: '\u2014', 152: '\u02DC', 153: '\u2122', 154: '\u0161', 155: '\u203A', 4101 156: '\u0153', 158: '\u017E', 159: '\u0178' 4102 }; 4103 // Raw entities 4104 const baseEntities = { 4105 '\"': '"', // Needs to be escaped since the YUI compressor would otherwise break the code 4106 '\'': ''', 4107 '<': '<', 4108 '>': '>', 4109 '&': '&', 4110 '\u0060': '`' 4111 }; 4112 // Reverse lookup table for raw entities 4113 const reverseEntities = { 4114 '<': '<', 4115 '>': '>', 4116 '&': '&', 4117 '"': '"', 4118 ''': `'` 4119 }; 4120 // Decodes text by using the browser 4121 const nativeDecode = (text) => { 4122 const elm = SugarElement.fromTag('div').dom; 4123 elm.innerHTML = text; 4124 return elm.textContent || elm.innerText || text; 4125 }; 4126 // Build a two way lookup table for the entities 4127 const buildEntitiesLookup = (items, radix) => { 4128 const lookup = {}; 4129 if (items) { 4130 const itemList = items.split(','); 4131 radix = radix || 10; 4132 // Build entities lookup table 4133 for (let i = 0; i < itemList.length; i += 2) { 4134 const chr = String.fromCharCode(parseInt(itemList[i], radix)); 4135 // Only add non base entities 4136 if (!baseEntities[chr]) { 4137 const entity = '&' + itemList[i + 1] + ';'; 4138 lookup[chr] = entity; 4139 lookup[entity] = chr; 4140 } 4141 } 4142 return lookup; 4143 } 4144 else { 4145 return undefined; 4146 } 4147 }; 4148 // Unpack entities lookup where the numbers are in radix 32 to reduce the size 4149 const namedEntities = buildEntitiesLookup('50,nbsp,51,iexcl,52,cent,53,pound,54,curren,55,yen,56,brvbar,57,sect,58,uml,59,copy,' + 4150 '5a,ordf,5b,laquo,5c,not,5d,shy,5e,reg,5f,macr,5g,deg,5h,plusmn,5i,sup2,5j,sup3,5k,acute,' + 4151 '5l,micro,5m,para,5n,middot,5o,cedil,5p,sup1,5q,ordm,5r,raquo,5s,frac14,5t,frac12,5u,frac34,' + 4152 '5v,iquest,60,Agrave,61,Aacute,62,Acirc,63,Atilde,64,Auml,65,Aring,66,AElig,67,Ccedil,' + 4153 '68,Egrave,69,Eacute,6a,Ecirc,6b,Euml,6c,Igrave,6d,Iacute,6e,Icirc,6f,Iuml,6g,ETH,6h,Ntilde,' + 4154 '6i,Ograve,6j,Oacute,6k,Ocirc,6l,Otilde,6m,Ouml,6n,times,6o,Oslash,6p,Ugrave,6q,Uacute,' + 4155 '6r,Ucirc,6s,Uuml,6t,Yacute,6u,THORN,6v,szlig,70,agrave,71,aacute,72,acirc,73,atilde,74,auml,' + 4156 '75,aring,76,aelig,77,ccedil,78,egrave,79,eacute,7a,ecirc,7b,euml,7c,igrave,7d,iacute,7e,icirc,' + 4157 '7f,iuml,7g,eth,7h,ntilde,7i,ograve,7j,oacute,7k,ocirc,7l,otilde,7m,ouml,7n,divide,7o,oslash,' + 4158 '7p,ugrave,7q,uacute,7r,ucirc,7s,uuml,7t,yacute,7u,thorn,7v,yuml,ci,fnof,sh,Alpha,si,Beta,' + 4159 'sj,Gamma,sk,Delta,sl,Epsilon,sm,Zeta,sn,Eta,so,Theta,sp,Iota,sq,Kappa,sr,Lambda,ss,Mu,' + 4160 'st,Nu,su,Xi,sv,Omicron,t0,Pi,t1,Rho,t3,Sigma,t4,Tau,t5,Upsilon,t6,Phi,t7,Chi,t8,Psi,' + 4161 't9,Omega,th,alpha,ti,beta,tj,gamma,tk,delta,tl,epsilon,tm,zeta,tn,eta,to,theta,tp,iota,' + 4162 'tq,kappa,tr,lambda,ts,mu,tt,nu,tu,xi,tv,omicron,u0,pi,u1,rho,u2,sigmaf,u3,sigma,u4,tau,' + 4163 'u5,upsilon,u6,phi,u7,chi,u8,psi,u9,omega,uh,thetasym,ui,upsih,um,piv,812,bull,816,hellip,' + 4164 '81i,prime,81j,Prime,81u,oline,824,frasl,88o,weierp,88h,image,88s,real,892,trade,89l,alefsym,' + 4165 '8cg,larr,8ch,uarr,8ci,rarr,8cj,darr,8ck,harr,8dl,crarr,8eg,lArr,8eh,uArr,8ei,rArr,8ej,dArr,' + 4166 '8ek,hArr,8g0,forall,8g2,part,8g3,exist,8g5,empty,8g7,nabla,8g8,isin,8g9,notin,8gb,ni,8gf,prod,' + 4167 '8gh,sum,8gi,minus,8gn,lowast,8gq,radic,8gt,prop,8gu,infin,8h0,ang,8h7,and,8h8,or,8h9,cap,8ha,cup,' + 4168 '8hb,int,8hk,there4,8hs,sim,8i5,cong,8i8,asymp,8j0,ne,8j1,equiv,8j4,le,8j5,ge,8k2,sub,8k3,sup,8k4,' + 4169 'nsub,8k6,sube,8k7,supe,8kl,oplus,8kn,otimes,8l5,perp,8m5,sdot,8o8,lceil,8o9,rceil,8oa,lfloor,8ob,' + 4170 'rfloor,8p9,lang,8pa,rang,9ea,loz,9j0,spades,9j3,clubs,9j5,hearts,9j6,diams,ai,OElig,aj,oelig,b0,' + 4171 'Scaron,b1,scaron,bo,Yuml,m6,circ,ms,tilde,802,ensp,803,emsp,809,thinsp,80c,zwnj,80d,zwj,80e,lrm,' + 4172 '80f,rlm,80j,ndash,80k,mdash,80o,lsquo,80p,rsquo,80q,sbquo,80s,ldquo,80t,rdquo,80u,bdquo,810,dagger,' + 4173 '811,Dagger,81g,permil,81p,lsaquo,81q,rsaquo,85c,euro', 32); 4174 /** 4175 * Encodes the specified string using raw entities. This means only the required XML base entities will be encoded. 4176 * 4177 * @method encodeRaw 4178 * @param {String} text Text to encode. 4179 * @param {Boolean} attr Optional flag to specify if the text is attribute contents. 4180 * @return {String} Entity encoded text. 4181 */ 4182 const encodeRaw = (text, attr) => text.replace(attr ? attrsCharsRegExp : textCharsRegExp, (chr) => { 4183 return baseEntities[chr] || chr; 4184 }); 4185 /** 4186 * Encoded the specified text with both the attributes and text entities. This function will produce larger text contents 4187 * since it doesn't know if the context is within an attribute or text node. This was added for compatibility 4188 * and is exposed as the `DOMUtils.encode` function. 4189 * 4190 * @method encodeAllRaw 4191 * @param {String} text Text to encode. 4192 * @return {String} Entity encoded text. 4193 */ 4194 const encodeAllRaw = (text) => ('' + text).replace(rawCharsRegExp, (chr) => { 4195 return baseEntities[chr] || chr; 4196 }); 4197 /** 4198 * Encodes the specified string using numeric entities. The core entities will be 4199 * encoded as named ones but all non lower ascii characters will be encoded into numeric entities. 4200 * 4201 * @method encodeNumeric 4202 * @param {String} text Text to encode. 4203 * @param {Boolean} attr Optional flag to specify if the text is attribute contents. 4204 * @return {String} Entity encoded text. 4205 */ 4206 const encodeNumeric = (text, attr) => text.replace(attr ? attrsCharsRegExp : textCharsRegExp, (chr) => { 4207 // Multi byte sequence convert it to a single entity 4208 if (chr.length > 1) { 4209 return '&#' + (((chr.charCodeAt(0) - 0xD800) * 0x400) + (chr.charCodeAt(1) - 0xDC00) + 0x10000) + ';'; 4210 } 4211 return baseEntities[chr] || '&#' + chr.charCodeAt(0) + ';'; 4212 }); 4213 /** 4214 * Encodes the specified string using named entities. The core entities will be encoded 4215 * as named ones but all non lower ascii characters will be encoded into named entities. 4216 * 4217 * @method encodeNamed 4218 * @param {String} text Text to encode. 4219 * @param {Boolean} attr Optional flag to specify if the text is attribute contents. 4220 * @param {Object} entities Optional parameter with entities to use. 4221 * @return {String} Entity encoded text. 4222 */ 4223 const encodeNamed = (text, attr, entities) => { 4224 const resolveEntities = entities || namedEntities; 4225 return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, (chr) => { 4226 return baseEntities[chr] || resolveEntities[chr] || chr; 4227 }); 4228 }; 4229 /** 4230 * Returns an encode function based on the name(s) and it's optional entities. 4231 * 4232 * @method getEncodeFunc 4233 * @param {String} name Comma separated list of encoders for example named,numeric. 4234 * @param {String} entities Optional parameter with entities to use instead of the built in set. 4235 * @return {Function} Encode function to be used. 4236 */ 4237 const getEncodeFunc = (name, entities) => { 4238 const entitiesMap = buildEntitiesLookup(entities) || namedEntities; 4239 const encodeNamedAndNumeric = (text, attr) => text.replace(attr ? attrsCharsRegExp : textCharsRegExp, (chr) => { 4240 if (baseEntities[chr] !== undefined) { 4241 return baseEntities[chr]; 4242 } 4243 if (entitiesMap[chr] !== undefined) { 4244 return entitiesMap[chr]; 4245 } 4246 // Convert multi-byte sequences to a single entity. 4247 if (chr.length > 1) { 4248 return '&#' + (((chr.charCodeAt(0) - 0xD800) * 0x400) + (chr.charCodeAt(1) - 0xDC00) + 0x10000) + ';'; 4249 } 4250 return '&#' + chr.charCodeAt(0) + ';'; 4251 }); 4252 const encodeCustomNamed = (text, attr) => { 4253 return encodeNamed(text, attr, entitiesMap); 4254 }; 4255 // Replace + with , to be compatible with previous TinyMCE versions 4256 const nameMap = makeMap$3(name.replace(/\+/g, ',')); 4257 // Named and numeric encoder 4258 if (nameMap.named && nameMap.numeric) { 4259 return encodeNamedAndNumeric; 4260 } 4261 // Named encoder 4262 if (nameMap.named) { 4263 // Custom names 4264 if (entities) { 4265 return encodeCustomNamed; 4266 } 4267 return encodeNamed; 4268 } 4269 // Numeric 4270 if (nameMap.numeric) { 4271 return encodeNumeric; 4272 } 4273 // Raw encoder 4274 return encodeRaw; 4275 }; 4276 /** 4277 * Decodes the specified string, this will replace entities with raw UTF characters. 4278 * 4279 * @method decode 4280 * @param {String} text Text to entity decode. 4281 * @return {String} Entity decoded string. 4282 */ 4283 const decode = (text) => text.replace(entityRegExp, (all, numeric) => { 4284 if (numeric) { 4285 if (numeric.charAt(0).toLowerCase() === 'x') { 4286 numeric = parseInt(numeric.substr(1), 16); 4287 } 4288 else { 4289 numeric = parseInt(numeric, 10); 4290 } 4291 // Support upper UTF 4292 if (numeric > 0xFFFF) { 4293 numeric -= 0x10000; 4294 // eslint-disable-next-line no-bitwise 4295 return String.fromCharCode(0xD800 + (numeric >> 10), 0xDC00 + (numeric & 0x3FF)); 4296 } 4297 return asciiMap[numeric] || String.fromCharCode(numeric); 4298 } 4299 return reverseEntities[all] || namedEntities[all] || nativeDecode(all); 4300 }); 4301 const Entities = { 4302 encodeRaw, 4303 encodeAllRaw, 4304 encodeNumeric, 4305 encodeNamed, 4306 getEncodeFunc, 4307 decode 4308 }; 4309 4310 const split$1 = (items, delim) => { 4311 items = Tools.trim(items); 4312 return items ? items.split(delim || ' ') : []; 4313 }; 4314 // Converts a wildcard expression string to a regexp for example *a will become /.*a/. 4315 const patternToRegExp = (str) => new RegExp('^' + str.replace(/([?+*])/g, '.$1') + '$'); 4316 const isRegExp$1 = (obj) => isObject(obj) && obj.source && Object.prototype.toString.call(obj) === '[object RegExp]'; 4317 const deepCloneElementRule = (obj) => { 4318 const helper = (value) => { 4319 if (isArray$1(value)) { 4320 return map$3(value, helper); 4321 } 4322 else if (isRegExp$1(value)) { 4323 return new RegExp(value.source, value.flags); 4324 } 4325 else if (isObject(value)) { 4326 return map$2(value, helper); 4327 } 4328 else { 4329 return value; 4330 } 4331 }; 4332 return helper(obj); 4333 }; 4334 4335 const parseCustomElementsRules = (value) => { 4336 const customElementRegExp = /^(~)?(.+)$/; 4337 return bind$3(split$1(value, ','), (rule) => { 4338 const matches = customElementRegExp.exec(rule); 4339 if (matches) { 4340 const inline = matches[1] === '~'; 4341 const cloneName = inline ? 'span' : 'div'; 4342 const name = matches[2]; 4343 return [{ cloneName, name }]; 4344 } 4345 else { 4346 return []; 4347 } 4348 }); 4349 }; 4350 4351 const getGlobalAttributeSet = (type) => { 4352 return Object.freeze([ 4353 // Present on all schema types 4354 'id', 4355 'accesskey', 4356 'class', 4357 'dir', 4358 'lang', 4359 'style', 4360 'tabindex', 4361 'title', 4362 'role', 4363 // html5 and html5-strict extra attributes 4364 ...(type !== 'html4' ? ['contenteditable', 'contextmenu', 'draggable', 'dropzone', 'hidden', 'spellcheck', 'translate', 'itemprop', 'itemscope', 'itemtype'] : []), 4365 // html4 and html5 extra attributes 4366 ...(type !== 'html5-strict' ? ['xml:lang'] : []) 4367 ]); 4368 }; 4369 4370 // Missing elements in `phrasing` compared to HTML5 spec at 2024-01-30 (timestamped since spec is constantly evolving) 4371 // area - required to be inside a map element so we should not add it to all elements. 4372 // link - required to be in the body so we should not add it to all elements. 4373 // math - currently not supported. 4374 // meta - Only allowed if the `itemprop` attribute is set so very special. 4375 // slot - We only want these to be accepted in registered custom components. 4376 // Extra element in `phrasing`: command keygen 4377 // 4378 // Missing elements in `flow` compared to HTML5 spec at 2034-01-30 (timestamped since the spec is constantly evolving) 4379 // search - Can be both in a block and inline position but is not a transparent element. So not supported at this time. 4380 const getElementSetsAsStrings = (type) => { 4381 let blockContent; 4382 let phrasingContent; 4383 // Block content elements 4384 blockContent = 4385 'address blockquote div dl fieldset form h1 h2 h3 h4 h5 h6 hr menu ol p pre table ul'; 4386 // Phrasing content elements from the HTML5 spec (inline) 4387 phrasingContent = 4388 'a abbr b bdo br button cite code del dfn em embed i iframe img input ins kbd ' + 4389 'label map noscript object q s samp script select small span strong sub sup ' + 4390 'textarea u var #text #comment'; 4391 // Add HTML5 items to globalAttributes, blockContent, phrasingContent 4392 if (type !== 'html4') { 4393 const transparentContent = 'a ins del canvas map'; 4394 blockContent += ' article aside details dialog figure main header footer hgroup section nav ' + transparentContent; 4395 phrasingContent += ' audio canvas command data datalist mark meter output picture ' + 4396 'progress template time wbr video ruby bdi keygen svg'; 4397 } 4398 // Add HTML4 elements unless it's html5-strict 4399 if (type !== 'html5-strict') { 4400 const html4PhrasingContent = 'acronym applet basefont big font strike tt'; 4401 phrasingContent = [phrasingContent, html4PhrasingContent].join(' '); 4402 const html4BlockContent = 'center dir isindex noframes'; 4403 blockContent = [blockContent, html4BlockContent].join(' '); 4404 } 4405 // Flow content elements from the HTML5 spec (block+inline) 4406 const flowContent = [blockContent, phrasingContent].join(' '); 4407 return { blockContent, phrasingContent, flowContent }; 4408 }; 4409 const getElementSets = (type) => { 4410 const { blockContent, phrasingContent, flowContent } = getElementSetsAsStrings(type); 4411 const toArr = (value) => { 4412 return Object.freeze(value.split(' ')); 4413 }; 4414 return Object.freeze({ 4415 blockContent: toArr(blockContent), 4416 phrasingContent: toArr(phrasingContent), 4417 flowContent: toArr(flowContent) 4418 }); 4419 }; 4420 4421 const cachedSets = { 4422 'html4': cached(() => getElementSets('html4')), 4423 'html5': cached(() => getElementSets('html5')), 4424 'html5-strict': cached(() => getElementSets('html5-strict')) 4425 }; 4426 // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents 4427 const getElementsPreset = (type, name) => { 4428 const { blockContent, phrasingContent, flowContent } = cachedSets[type](); 4429 if (name === 'blocks') { 4430 return Optional.some(blockContent); 4431 } 4432 else if (name === 'phrasing') { 4433 return Optional.some(phrasingContent); 4434 } 4435 else if (name === 'flow') { 4436 return Optional.some(flowContent); 4437 } 4438 else { 4439 return Optional.none(); 4440 } 4441 }; 4442 4443 const makeSchema = (type) => { 4444 const globalAttributes = getGlobalAttributeSet(type); 4445 const { phrasingContent, flowContent } = getElementSetsAsStrings(type); 4446 const schema = {}; 4447 const addElement = (name, attributes, children) => { 4448 schema[name] = { 4449 attributes: mapToObject(attributes, constant({})), 4450 attributesOrder: attributes, 4451 children: mapToObject(children, constant({})) 4452 }; 4453 }; 4454 const add = (name, attributes = '', children = '') => { 4455 const childNames = split$1(children); 4456 const names = split$1(name); 4457 let ni = names.length; 4458 const allAttributes = [...globalAttributes, ...split$1(attributes)]; 4459 while (ni--) { 4460 addElement(names[ni], allAttributes.slice(), childNames); 4461 } 4462 }; 4463 const addAttrs = (name, attributes) => { 4464 const names = split$1(name); 4465 const attrs = split$1(attributes); 4466 let ni = names.length; 4467 while (ni--) { 4468 const schemaItem = schema[names[ni]]; 4469 for (let i = 0, l = attrs.length; i < l; i++) { 4470 schemaItem.attributes[attrs[i]] = {}; 4471 schemaItem.attributesOrder.push(attrs[i]); 4472 } 4473 } 4474 }; 4475 if (type !== 'html5-strict') { 4476 const html4PhrasingContent = 'acronym applet basefont big font strike tt'; 4477 each$e(split$1(html4PhrasingContent), (name) => { 4478 add(name, '', phrasingContent); 4479 }); 4480 const html4BlockContent = 'center dir isindex noframes'; 4481 each$e(split$1(html4BlockContent), (name) => { 4482 add(name, '', flowContent); 4483 }); 4484 } 4485 // HTML4 base schema TODO: Move HTML5 specific attributes to HTML5 specific if statement 4486 // Schema items <element name>, <specific attributes>, <children ..> 4487 add('html', 'manifest', 'head body'); 4488 add('head', '', 'base command link meta noscript script style title'); 4489 add('title hr noscript br'); 4490 add('base', 'href target'); 4491 add('link', 'href rel media hreflang type sizes hreflang'); 4492 add('meta', 'name http-equiv content charset property'); // Property is an RDFa spec attribute. 4493 add('style', 'media type scoped'); 4494 add('script', 'src async defer type charset'); 4495 add('body', 'onafterprint onbeforeprint onbeforeunload onblur onerror onfocus ' + 4496 'onhashchange onload onmessage onoffline ononline onpagehide onpageshow ' + 4497 'onpopstate onresize onscroll onstorage onunload', flowContent); 4498 add('dd div', '', flowContent); 4499 add('address dt caption', '', type === 'html4' ? phrasingContent : flowContent); 4500 add('h1 h2 h3 h4 h5 h6 pre p abbr code var samp kbd sub sup i b u bdo span legend em strong small s cite dfn', '', phrasingContent); 4501 add('blockquote', 'cite', flowContent); 4502 add('ol', 'reversed start type', 'li'); 4503 add('ul', '', 'li'); 4504 add('li', 'value', flowContent); 4505 add('dl', '', 'dt dd'); 4506 add('a', 'href target rel media hreflang type', type === 'html4' ? phrasingContent : flowContent); 4507 add('q', 'cite', phrasingContent); 4508 add('ins del', 'cite datetime', flowContent); 4509 add('img', 'src sizes srcset alt usemap ismap width height'); 4510 add('iframe', 'src name width height', flowContent); 4511 add('embed', 'src type width height'); 4512 add('object', 'data type typemustmatch name usemap form width height', [flowContent, 'param'].join(' ')); 4513 add('param', 'name value'); 4514 add('map', 'name', [flowContent, 'area'].join(' ')); 4515 add('area', 'alt coords shape href target rel media hreflang type'); 4516 add('table', 'border', 'caption colgroup thead tfoot tbody tr' + (type === 'html4' ? ' col' : '')); 4517 add('colgroup', 'span', 'col'); 4518 add('col', 'span'); 4519 add('tbody thead tfoot', '', 'tr'); 4520 add('tr', '', 'td th'); 4521 add('td', 'colspan rowspan headers', flowContent); 4522 add('th', 'colspan rowspan headers scope abbr', flowContent); 4523 add('form', 'accept-charset action autocomplete enctype method name novalidate target', flowContent); 4524 add('fieldset', 'disabled form name', [flowContent, 'legend'].join(' ')); 4525 add('label', 'form for', phrasingContent); 4526 add('input', 'accept alt autocomplete checked dirname disabled form formaction formenctype formmethod formnovalidate ' + 4527 'formtarget height list max maxlength min multiple name pattern readonly required size src step type value width'); 4528 add('button', 'disabled form formaction formenctype formmethod formnovalidate formtarget name type value', type === 'html4' ? flowContent : phrasingContent); 4529 add('select', 'disabled form multiple name required size', 'option optgroup'); 4530 add('optgroup', 'disabled label', 'option'); 4531 add('option', 'disabled label selected value'); 4532 add('textarea', 'cols dirname disabled form maxlength name readonly required rows wrap'); 4533 add('menu', 'type label', [flowContent, 'li'].join(' ')); 4534 add('noscript', '', flowContent); 4535 // Extend with HTML5 elements 4536 if (type !== 'html4') { 4537 add('wbr'); 4538 add('ruby', '', [phrasingContent, 'rt rp'].join(' ')); 4539 add('figcaption', '', flowContent); 4540 add('mark rt rp bdi', '', phrasingContent); 4541 add('summary', '', [phrasingContent, 'h1 h2 h3 h4 h5 h6'].join(' ')); 4542 add('canvas', 'width height', flowContent); 4543 add('data', 'value', phrasingContent); 4544 add('video', 'src crossorigin poster preload autoplay mediagroup loop ' + 4545 'controlslist disablepictureinpicture disableremoteplayback playsinline ' + 4546 'muted controls width height buffered', [flowContent, 'track source'].join(' ')); 4547 add('audio', 'src crossorigin preload autoplay mediagroup loop muted controls ' + 4548 'buffered volume', [flowContent, 'track source'].join(' ')); 4549 add('picture', '', 'img source'); 4550 add('source', 'src srcset type media sizes'); 4551 add('track', 'kind src srclang label default'); 4552 add('datalist', '', [phrasingContent, 'option'].join(' ')); 4553 add('article section nav aside main header footer', '', flowContent); 4554 add('hgroup', '', 'h1 h2 h3 h4 h5 h6'); 4555 add('figure', '', [flowContent, 'figcaption'].join(' ')); 4556 add('time', 'datetime', phrasingContent); 4557 add('dialog', 'open', flowContent); 4558 add('command', 'type label icon disabled checked radiogroup command'); 4559 add('output', 'for form name', phrasingContent); 4560 add('progress', 'value max', phrasingContent); 4561 add('meter', 'value min max low high optimum', phrasingContent); 4562 add('details', 'open', [flowContent, 'summary'].join(' ')); 4563 add('keygen', 'autofocus challenge disabled form keytype name'); 4564 // SVGs only support a subset of the global attributes 4565 addElement('svg', 'id tabindex lang xml:space class style x y width height viewBox preserveAspectRatio zoomAndPan transform'.split(' '), []); 4566 } 4567 // Extend with HTML4 attributes unless it's html5-strict 4568 if (type !== 'html5-strict') { 4569 addAttrs('script', 'language xml:space'); 4570 addAttrs('style', 'xml:space'); 4571 addAttrs('object', 'declare classid code codebase codetype archive standby align border hspace vspace'); 4572 addAttrs('embed', 'align name hspace vspace'); 4573 addAttrs('param', 'valuetype type'); 4574 addAttrs('a', 'charset name rev shape coords'); 4575 addAttrs('br', 'clear'); 4576 addAttrs('applet', 'codebase archive code object alt name width height align hspace vspace'); 4577 addAttrs('img', 'name longdesc align border hspace vspace'); 4578 addAttrs('iframe', 'longdesc frameborder marginwidth marginheight scrolling align'); 4579 addAttrs('font basefont', 'size color face'); 4580 addAttrs('input', 'usemap align'); 4581 addAttrs('select'); 4582 addAttrs('textarea'); 4583 addAttrs('h1 h2 h3 h4 h5 h6 div p legend caption', 'align'); 4584 addAttrs('ul', 'type compact'); 4585 addAttrs('li', 'type'); 4586 addAttrs('ol dl menu dir', 'compact'); 4587 addAttrs('pre', 'width xml:space'); 4588 addAttrs('hr', 'align noshade size width'); 4589 addAttrs('isindex', 'prompt'); 4590 addAttrs('table', 'summary width frame rules cellspacing cellpadding align bgcolor'); 4591 addAttrs('col', 'width align char charoff valign'); 4592 addAttrs('colgroup', 'width align char charoff valign'); 4593 addAttrs('thead', 'align char charoff valign'); 4594 addAttrs('tr', 'align char charoff valign bgcolor'); 4595 addAttrs('th', 'axis align char charoff valign nowrap bgcolor width height'); 4596 addAttrs('form', 'accept'); 4597 addAttrs('td', 'abbr axis scope align char charoff valign nowrap bgcolor width height'); 4598 addAttrs('tfoot', 'align char charoff valign'); 4599 addAttrs('tbody', 'align char charoff valign'); 4600 addAttrs('area', 'nohref'); 4601 addAttrs('body', 'background bgcolor text link vlink alink'); 4602 } 4603 // Extend with HTML5 attributes unless it's html4 4604 if (type !== 'html4') { 4605 addAttrs('input button select textarea', 'autofocus'); 4606 addAttrs('input textarea', 'placeholder'); 4607 addAttrs('a', 'download'); 4608 addAttrs('link script img', 'crossorigin'); 4609 addAttrs('img', 'loading'); 4610 addAttrs('iframe', 'sandbox seamless allow allowfullscreen loading referrerpolicy'); // Excluded: srcdoc 4611 } 4612 // Special: iframe, ruby, video, audio, label 4613 if (type !== 'html4') { 4614 // Video/audio elements cannot have nested children 4615 each$e([schema.video, schema.audio], (item) => { 4616 delete item.children.audio; 4617 delete item.children.video; 4618 }); 4619 } 4620 // Delete children of the same name from it's parent 4621 // For example: form can't have a child of the name form 4622 each$e(split$1('a form meter progress dfn'), (name) => { 4623 if (schema[name]) { 4624 delete schema[name].children[name]; 4625 } 4626 }); 4627 // Delete header, footer, sectioning and heading content descendants 4628 /* each('dt th address', function(name) { 4629 delete schema[name].children[name]; 4630 });*/ 4631 // Caption can't have tables 4632 delete schema.caption.children.table; 4633 // Delete scripts by default due to possible XSS 4634 delete schema.script; 4635 // TODO: LI:s can only have value if parent is OL 4636 return schema; 4637 }; 4638 4639 const prefixToOperation = (prefix) => prefix === '-' ? 'remove' : 'add'; 4640 const parseValidChild = (name) => { 4641 // see: https://html.spec.whatwg.org/#valid-custom-element-name 4642 const validChildRegExp = /^(@?)([A-Za-z0-9_\-.\u00b7\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u037d\u037f-\u1fff\u200c-\u200d\u203f-\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]+)$/; 4643 return Optional.from(validChildRegExp.exec(name)).map((matches) => ({ 4644 preset: matches[1] === '@', 4645 name: matches[2] 4646 })); 4647 }; 4648 const parseValidChildrenRules = (value) => { 4649 // see: https://html.spec.whatwg.org/#valid-custom-element-name 4650 const childRuleRegExp = /^([+\-]?)([A-Za-z0-9_\-.\u00b7\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u037d\u037f-\u1fff\u200c-\u200d\u203f-\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]+)\[([^\]]+)]$/; // from w3c's custom grammar (above) 4651 return bind$3(split$1(value, ','), (rule) => { 4652 const matches = childRuleRegExp.exec(rule); 4653 if (matches) { 4654 const prefix = matches[1]; 4655 const operation = prefix ? prefixToOperation(prefix) : 'replace'; 4656 const name = matches[2]; 4657 const validChildren = bind$3(split$1(matches[3], '|'), (validChild) => parseValidChild(validChild).toArray()); 4658 return [{ operation, name, validChildren }]; 4659 } 4660 else { 4661 return []; 4662 } 4663 }); 4664 }; 4665 4666 const parseValidElementsAttrDataIntoElement = (attrData, targetElement) => { 4667 const attrRuleRegExp = /^([!\-])?(\w+[\\:]:\w+|[^=~<]+)?(?:([=~<])(.*))?$/; 4668 const hasPatternsRegExp = /[*?+]/; 4669 const { attributes, attributesOrder } = targetElement; 4670 return each$e(split$1(attrData, '|'), (rule) => { 4671 const matches = attrRuleRegExp.exec(rule); 4672 if (matches) { 4673 const attr = {}; 4674 const attrType = matches[1]; 4675 const attrName = matches[2].replace(/[\\:]:/g, ':'); 4676 const attrPrefix = matches[3]; 4677 const value = matches[4]; 4678 // Required 4679 if (attrType === '!') { 4680 targetElement.attributesRequired = targetElement.attributesRequired || []; 4681 targetElement.attributesRequired.push(attrName); 4682 attr.required = true; 4683 } 4684 // Denied from global 4685 if (attrType === '-') { 4686 delete attributes[attrName]; 4687 attributesOrder.splice(Tools.inArray(attributesOrder, attrName), 1); 4688 return; 4689 } 4690 // Default value 4691 if (attrPrefix) { 4692 if (attrPrefix === '=') { // Default value 4693 targetElement.attributesDefault = targetElement.attributesDefault || []; 4694 targetElement.attributesDefault.push({ name: attrName, value }); 4695 attr.defaultValue = value; 4696 } 4697 else if (attrPrefix === '~') { // Forced value 4698 targetElement.attributesForced = targetElement.attributesForced || []; 4699 targetElement.attributesForced.push({ name: attrName, value }); 4700 attr.forcedValue = value; 4701 } 4702 else if (attrPrefix === '<') { // Required values 4703 attr.validValues = Tools.makeMap(value, '?'); 4704 } 4705 } 4706 // Check for attribute patterns 4707 if (hasPatternsRegExp.test(attrName)) { 4708 const attrPattern = attr; 4709 targetElement.attributePatterns = targetElement.attributePatterns || []; 4710 attrPattern.pattern = patternToRegExp(attrName); 4711 targetElement.attributePatterns.push(attrPattern); 4712 } 4713 else { 4714 // Add attribute to order list if it doesn't already exist 4715 if (!attributes[attrName]) { 4716 attributesOrder.push(attrName); 4717 } 4718 attributes[attrName] = attr; 4719 } 4720 } 4721 }); 4722 }; 4723 const cloneAttributesInto = (from, to) => { 4724 each$d(from.attributes, (value, key) => { 4725 to.attributes[key] = value; 4726 }); 4727 to.attributesOrder.push(...from.attributesOrder); 4728 }; 4729 const parseValidElementsRules = (globalElement, validElements) => { 4730 const elementRuleRegExp = /^([#+\-])?([^\[!\/]+)(?:\/([^\[!]+))?(?:(!?)\[([^\]]+)])?$/; 4731 return bind$3(split$1(validElements, ','), (rule) => { 4732 const matches = elementRuleRegExp.exec(rule); 4733 if (matches) { 4734 const prefix = matches[1]; 4735 const elementName = matches[2]; 4736 const outputName = matches[3]; 4737 const attrsPrefix = matches[4]; 4738 const attrData = matches[5]; 4739 const element = { 4740 attributes: {}, 4741 attributesOrder: [] 4742 }; 4743 globalElement.each((el) => cloneAttributesInto(el, element)); 4744 if (prefix === '#') { 4745 element.paddEmpty = true; 4746 } 4747 else if (prefix === '-') { 4748 element.removeEmpty = true; 4749 } 4750 if (attrsPrefix === '!') { 4751 element.removeEmptyAttrs = true; 4752 } 4753 if (attrData) { 4754 parseValidElementsAttrDataIntoElement(attrData, element); 4755 } 4756 // Handle substitute elements such as b/strong 4757 if (outputName) { 4758 element.outputName = elementName; 4759 } 4760 // Mutate the local globalElement option state if we find a global @ rule 4761 if (elementName === '@') { 4762 // We only care about the first one 4763 if (globalElement.isNone()) { 4764 globalElement = Optional.some(element); 4765 } 4766 else { 4767 return []; 4768 } 4769 } 4770 return [outputName ? { name: elementName, element, aliasName: outputName } : { name: elementName, element }]; 4771 } 4772 else { 4773 return []; 4774 } 4775 }); 4776 }; 4777 4778 /** 4779 * Schema validator class. 4780 * 4781 * @class tinymce.html.Schema 4782 * @version 3.4 4783 * @example 4784 * if (tinymce.activeEditor.schema.isValidChild('p', 'span')) { 4785 * alert('span is valid child of p.'); 4786 * } 4787 * 4788 * if (tinymce.activeEditor.schema.getElementRule('p')) { 4789 * alert('P is a valid element.'); 4790 * } 4791 */ 4792 const mapCache = {}; 4793 const makeMap$2 = Tools.makeMap, each$b = Tools.each, extend$2 = Tools.extend, explode$2 = Tools.explode; 4794 const createMap = (defaultValue, extendWith = {}) => { 4795 const value = makeMap$2(defaultValue, ' ', makeMap$2(defaultValue.toUpperCase(), ' ')); 4796 return extend$2(value, extendWith); 4797 }; 4798 // A curated list using the textBlockElements map and parts of the blockElements map from the schema 4799 // TODO: TINY-8728 Investigate if the extras can be added directly to the default text block elements 4800 const getTextRootBlockElements = (schema) => createMap('td th li dt dd figcaption caption details summary', schema.getTextBlockElements()); 4801 const compileElementMap = (value, mode) => { 4802 if (value) { 4803 const styles = {}; 4804 if (isString(value)) { 4805 value = { 4806 '*': value 4807 }; 4808 } 4809 // Convert styles into a rule list 4810 each$b(value, (value, key) => { 4811 styles[key] = styles[key.toUpperCase()] = mode === 'map' ? makeMap$2(value, /[, ]/) : explode$2(value, /[, ]/); 4812 }); 4813 return styles; 4814 } 4815 else { 4816 return undefined; 4817 } 4818 }; 4819 const Schema = (settings = {}) => { 4820 const elements = {}; 4821 const children = {}; 4822 let patternElements = []; 4823 const customElementsMap = {}; 4824 const specialElements = {}; 4825 const componentUrls = {}; 4826 // Creates an lookup table map object for the specified option or the default value 4827 const createLookupTable = (option, defaultValue, extendWith) => { 4828 const value = settings[option]; 4829 if (!value) { 4830 // Get cached default map or make it if needed 4831 let newValue = mapCache[option]; 4832 if (!newValue) { 4833 newValue = createMap(defaultValue, extendWith); 4834 mapCache[option] = newValue; 4835 } 4836 return newValue; 4837 } 4838 else { 4839 // Create custom map 4840 return makeMap$2(value, /[, ]/, makeMap$2(value.toUpperCase(), /[, ]/)); 4841 } 4842 }; 4843 const schemaType = settings.schema ?? 'html5'; 4844 const schemaItems = makeSchema(schemaType); 4845 // Allow all elements and attributes if verify_html is set to false 4846 if (settings.verify_html === false) { 4847 settings.valid_elements = '*[*]'; 4848 } 4849 const validStyles = compileElementMap(settings.valid_styles); 4850 const invalidStyles = compileElementMap(settings.invalid_styles, 'map'); 4851 const validClasses = compileElementMap(settings.valid_classes, 'map'); 4852 // Setup map objects 4853 const whitespaceElementsMap = createLookupTable('whitespace_elements', 'pre script noscript style textarea video audio iframe object code'); 4854 const selfClosingElementsMap = createLookupTable('self_closing_elements', 'colgroup dd dt li option p td tfoot th thead tr'); 4855 const voidElementsMap = createLookupTable('void_elements', 'area base basefont br col frame hr img input isindex link ' + 4856 'meta param embed source wbr track'); 4857 const boolAttrMap = createLookupTable('boolean_attributes', 'checked compact declare defer disabled ismap multiple nohref noresize ' + 4858 'noshade nowrap readonly selected autoplay loop controls allowfullscreen'); 4859 const nonEmptyOrMoveCaretBeforeOnEnter = 'td th iframe video audio object script code'; 4860 const nonEmptyElementsMap = createLookupTable('non_empty_elements', nonEmptyOrMoveCaretBeforeOnEnter + ' pre svg textarea summary', voidElementsMap); 4861 const moveCaretBeforeOnEnterElementsMap = createLookupTable('move_caret_before_on_enter_elements', nonEmptyOrMoveCaretBeforeOnEnter + ' table', voidElementsMap); 4862 const headings = 'h1 h2 h3 h4 h5 h6'; 4863 const textBlockElementsMap = createLookupTable('text_block_elements', headings + ' p div address pre form ' + 4864 'blockquote center dir fieldset header footer article section hgroup aside main nav figure'); 4865 const blockElementsMap = createLookupTable('block_elements', 'hr table tbody thead tfoot ' + 4866 'th tr td li ol ul caption dl dt dd noscript menu isindex option ' + 4867 'datalist select optgroup figcaption details summary html body multicol listing colgroup col', textBlockElementsMap); 4868 const textInlineElementsMap = createLookupTable('text_inline_elements', 'span strong b em i font s strike u var cite ' + 4869 'dfn code mark q sup sub samp'); 4870 const transparentElementsMap = createLookupTable('transparent_elements', 'a ins del canvas map'); 4871 const wrapBlockElementsMap = createLookupTable('wrap_block_elements', 'pre ' + headings); 4872 // See https://html.spec.whatwg.org/multipage/parsing.html#parsing-html-fragments 4873 each$b(('script noscript iframe noframes noembed title style textarea xmp plaintext').split(' '), (name) => { 4874 specialElements[name] = new RegExp('<\/' + name + '[^>]*>', 'gi'); 4875 }); 4876 // Parses the specified valid_elements string and adds to the current rules 4877 const addValidElements = (validElements) => { 4878 const globalElement = Optional.from(elements['@']); 4879 const hasPatternsRegExp = /[*?+]/; 4880 each$e(parseValidElementsRules(globalElement, validElements ?? ''), ({ name, element, aliasName }) => { 4881 if (aliasName) { 4882 elements[aliasName] = element; 4883 } 4884 // Add pattern or exact element 4885 if (hasPatternsRegExp.test(name)) { 4886 const patternElement = element; 4887 patternElement.pattern = patternToRegExp(name); 4888 patternElements.push(patternElement); 4889 } 4890 else { 4891 elements[name] = element; 4892 } 4893 }); 4894 }; 4895 const setValidElements = (validElements) => { 4896 // Clear any existing rules. Note that since `elements` is exposed we can't 4897 // overwrite it, so instead we delete all the properties 4898 patternElements = []; 4899 each$e(keys(elements), (name) => { 4900 delete elements[name]; 4901 }); 4902 addValidElements(validElements); 4903 }; 4904 const addCustomElement = (name, spec) => { 4905 // Flush cached items since we are altering the default maps 4906 delete mapCache.text_block_elements; 4907 delete mapCache.block_elements; 4908 const inline = spec.extends ? !isBlock(spec.extends) : false; 4909 const cloneName = spec.extends; 4910 children[name] = cloneName ? children[cloneName] : {}; 4911 customElementsMap[name] = cloneName ?? name; 4912 // Treat all custom elements as being non-empty by default 4913 nonEmptyElementsMap[name.toUpperCase()] = {}; 4914 nonEmptyElementsMap[name] = {}; 4915 // If it's not marked as inline then add it to valid block elements 4916 if (!inline) { 4917 blockElementsMap[name.toUpperCase()] = {}; 4918 blockElementsMap[name] = {}; 4919 } 4920 // Add elements clone if needed 4921 if (cloneName && !elements[name] && elements[cloneName]) { 4922 const customRule = deepCloneElementRule(elements[cloneName]); 4923 delete customRule.removeEmptyAttrs; 4924 delete customRule.removeEmpty; 4925 elements[name] = customRule; 4926 } 4927 else { 4928 elements[name] = { attributesOrder: [], attributes: {} }; 4929 } 4930 // Add custom attributes 4931 if (isArray$1(spec.attributes)) { 4932 const processAttrName = (name) => { 4933 customRule.attributesOrder.push(name); 4934 customRule.attributes[name] = {}; 4935 }; 4936 const customRule = elements[name] ?? {}; 4937 delete customRule.attributesDefault; 4938 delete customRule.attributesForced; 4939 delete customRule.attributePatterns; 4940 delete customRule.attributesRequired; 4941 customRule.attributesOrder = []; 4942 customRule.attributes = {}; 4943 each$e(spec.attributes, (attrName) => { 4944 const globalAttrs = getGlobalAttributeSet(schemaType); 4945 parseValidChild(attrName).each(({ preset, name }) => { 4946 if (preset) { 4947 if (name === 'global') { 4948 each$e(globalAttrs, processAttrName); 4949 } 4950 } 4951 else { 4952 processAttrName(name); 4953 } 4954 }); 4955 }); 4956 elements[name] = customRule; 4957 } 4958 // Add custom pad empty rule 4959 if (isBoolean(spec.padEmpty)) { 4960 const customRule = elements[name] ?? {}; 4961 customRule.paddEmpty = spec.padEmpty; 4962 elements[name] = customRule; 4963 } 4964 // Add custom children 4965 if (isArray$1(spec.children)) { 4966 const customElementChildren = {}; 4967 const processNodeName = (name) => { 4968 customElementChildren[name] = {}; 4969 }; 4970 const processPreset = (name) => { 4971 getElementsPreset(schemaType, name).each((names) => { 4972 each$e(names, processNodeName); 4973 }); 4974 }; 4975 each$e(spec.children, (child) => { 4976 parseValidChild(child).each(({ preset, name }) => { 4977 if (preset) { 4978 processPreset(name); 4979 } 4980 else { 4981 processNodeName(name); 4982 } 4983 }); 4984 }); 4985 children[name] = customElementChildren; 4986 } 4987 // Add custom elements at extends positions 4988 if (cloneName) { 4989 each$d(children, (element, elmName) => { 4990 if (element[cloneName]) { 4991 children[elmName] = element = extend$2({}, children[elmName]); 4992 element[name] = element[cloneName]; 4993 } 4994 }); 4995 } 4996 }; 4997 const addCustomElementsFromString = (customElements) => { 4998 each$e(parseCustomElementsRules(customElements ?? ''), ({ name, cloneName }) => { 4999 addCustomElement(name, { extends: cloneName }); 5000 }); 5001 }; 5002 const addComponentUrl = (elementName, componentUrl) => { 5003 componentUrls[elementName] = componentUrl; 5004 }; 5005 const addCustomElements = (customElements) => { 5006 if (isObject(customElements)) { 5007 each$d(customElements, (spec, name) => { 5008 const componentUrl = spec.componentUrl; 5009 if (isString(componentUrl)) { 5010 addComponentUrl(name, componentUrl); 5011 } 5012 addCustomElement(name, spec); 5013 }); 5014 } 5015 else if (isString(customElements)) { 5016 addCustomElementsFromString(customElements); 5017 } 5018 }; 5019 // Adds valid children to the schema object 5020 const addValidChildren = (validChildren) => { 5021 each$e(parseValidChildrenRules(validChildren ?? ''), ({ operation, name, validChildren }) => { 5022 const parent = operation === 'replace' ? { '#comment': {} } : children[name]; 5023 const processNodeName = (name) => { 5024 if (operation === 'remove') { 5025 delete parent[name]; 5026 } 5027 else { 5028 parent[name] = {}; 5029 } 5030 }; 5031 const processPreset = (name) => { 5032 getElementsPreset(schemaType, name).each((names) => { 5033 each$e(names, processNodeName); 5034 }); 5035 }; 5036 each$e(validChildren, ({ preset, name }) => { 5037 if (preset) { 5038 processPreset(name); 5039 } 5040 else { 5041 processNodeName(name); 5042 } 5043 }); 5044 children[name] = parent; 5045 }); 5046 }; 5047 const getElementRule = (name) => { 5048 const element = elements[name]; 5049 // Exact match found 5050 if (element) { 5051 return element; 5052 } 5053 // No exact match then try the patterns 5054 let i = patternElements.length; 5055 while (i--) { 5056 const patternElement = patternElements[i]; 5057 if (patternElement.pattern.test(name)) { 5058 return patternElement; 5059 } 5060 } 5061 return undefined; 5062 }; 5063 const setup = () => { 5064 if (!settings.valid_elements) { 5065 // No valid elements defined then clone the elements from the schema spec 5066 each$b(schemaItems, (element, name) => { 5067 elements[name] = { 5068 attributes: element.attributes, 5069 attributesOrder: element.attributesOrder 5070 }; 5071 children[name] = element.children; 5072 }); 5073 // Prefer strong/em over b/i 5074 each$b(split$1('strong/b em/i'), (item) => { 5075 const items = split$1(item, '/'); 5076 elements[items[1]].outputName = items[0]; 5077 }); 5078 // Add default alt attribute for images, removed since alt="" is treated as presentational. 5079 // elements.img.attributesDefault = [{name: 'alt', value: ''}]; 5080 // By default, 5081 // - padd the text inline element if it is empty and also a child of an empty root block 5082 // - in all other cases, remove the text inline element if it is empty 5083 each$b(textInlineElementsMap, (_val, name) => { 5084 if (elements[name]) { 5085 if (settings.padd_empty_block_inline_children) { 5086 elements[name].paddInEmptyBlock = true; 5087 } 5088 elements[name].removeEmpty = true; 5089 } 5090 }); 5091 // Remove these if they are empty by default 5092 each$b(split$1('ol ul blockquote a table tbody'), (name) => { 5093 if (elements[name]) { 5094 elements[name].removeEmpty = true; 5095 } 5096 }); 5097 // Padd these by default 5098 each$b(split$1('p h1 h2 h3 h4 h5 h6 th td pre div address caption li summary'), (name) => { 5099 if (elements[name]) { 5100 elements[name].paddEmpty = true; 5101 } 5102 }); 5103 // Remove these if they have no attributes 5104 each$b(split$1('span'), (name) => { 5105 elements[name].removeEmptyAttrs = true; 5106 }); 5107 // Remove these by default 5108 // TODO: Reenable in 4.1 5109 /* each(split('script style'), function(name) { 5110 delete elements[name]; 5111 });*/ 5112 } 5113 else { 5114 setValidElements(settings.valid_elements); 5115 each$b(schemaItems, (element, name) => { 5116 children[name] = element.children; 5117 }); 5118 } 5119 // Opt in is done with options like `extended_valid_elements` 5120 delete elements.svg; 5121 addCustomElements(settings.custom_elements); 5122 addValidChildren(settings.valid_children); 5123 addValidElements(settings.extended_valid_elements); 5124 // Todo: Remove this when we fix list handling to be valid 5125 addValidChildren('+ol[ul|ol],+ul[ul|ol]'); 5126 // Some elements are not valid by themselves - require parents 5127 each$b({ 5128 dd: 'dl', 5129 dt: 'dl', 5130 li: 'ul ol', 5131 td: 'tr', 5132 th: 'tr', 5133 tr: 'tbody thead tfoot', 5134 tbody: 'table', 5135 thead: 'table', 5136 tfoot: 'table', 5137 legend: 'fieldset', 5138 area: 'map', 5139 param: 'video audio object' 5140 }, (parents, item) => { 5141 if (elements[item]) { 5142 elements[item].parentsRequired = split$1(parents); 5143 } 5144 }); 5145 // Delete invalid elements 5146 if (settings.invalid_elements) { 5147 each$b(explode$2(settings.invalid_elements), (item) => { 5148 if (elements[item]) { 5149 delete elements[item]; 5150 } 5151 }); 5152 } 5153 // If the user didn't allow span only allow internal spans 5154 if (!getElementRule('span')) { 5155 addValidElements('span[!data-mce-type|*]'); 5156 } 5157 }; 5158 /** 5159 * Name/value map object with valid parents and children to those parents. 5160 * 5161 * @field children 5162 * @type Object 5163 * @example 5164 * children = { 5165 * div: { p:{}, h1:{} } 5166 * }; 5167 */ 5168 /** 5169 * Name/value map object with valid styles for each element. 5170 * 5171 * @method getValidStyles 5172 * @type Object 5173 */ 5174 const getValidStyles = constant(validStyles); 5175 /** 5176 * Name/value map object with valid styles for each element. 5177 * 5178 * @method getInvalidStyles 5179 * @type Object 5180 */ 5181 const getInvalidStyles = constant(invalidStyles); 5182 /** 5183 * Name/value map object with valid classes for each element. 5184 * 5185 * @method getValidClasses 5186 * @type Object 5187 */ 5188 const getValidClasses = constant(validClasses); 5189 /** 5190 * Returns a map with boolean attributes. 5191 * 5192 * @method getBoolAttrs 5193 * @return {Object} Name/value lookup map for boolean attributes. 5194 */ 5195 const getBoolAttrs = constant(boolAttrMap); 5196 /** 5197 * Returns a map with block elements. 5198 * 5199 * @method getBlockElements 5200 * @return {Object} Name/value lookup map for block elements. 5201 */ 5202 const getBlockElements = constant(blockElementsMap); 5203 /** 5204 * Returns a map with text block elements. For example: <code><p></code>, <code><h1></code> to <code><h6></code>, <code><div></code> or <code><address></code>. 5205 * 5206 * @method getTextBlockElements 5207 * @return {Object} Name/value lookup map for block elements. 5208 */ 5209 const getTextBlockElements = constant(textBlockElementsMap); 5210 /** 5211 * Returns a map of inline text format nodes. For example: <code><strong></code>, <code><span></code> or <code><ins></code>. 5212 * 5213 * @method getTextInlineElements 5214 * @return {Object} Name/value lookup map for text format elements. 5215 */ 5216 const getTextInlineElements = constant(textInlineElementsMap); 5217 /** 5218 * Returns a map with void elements. For example: <code><br></code> or <code><img></code>. 5219 * 5220 * @method getVoidElements 5221 * @return {Object} Name/value lookup map for void elements. 5222 */ 5223 const getVoidElements = constant(Object.seal(voidElementsMap)); 5224 /** 5225 * Returns a map with self closing tags. For example: <code><li></code>. 5226 * 5227 * @method getSelfClosingElements 5228 * @return {Object} Name/value lookup map for self closing tags elements. 5229 */ 5230 const getSelfClosingElements = constant(selfClosingElementsMap); 5231 /** 5232 * Returns a map with elements that should be treated as contents regardless if it has text 5233 * content in them or not. For example: <code><td></code>, <code><video></code> or <code><img></code>. 5234 * 5235 * @method getNonEmptyElements 5236 * @return {Object} Name/value lookup map for non empty elements. 5237 */ 5238 const getNonEmptyElements = constant(nonEmptyElementsMap); 5239 /** 5240 * Returns a map with elements that the caret should be moved in front of after enter is 5241 * pressed. 5242 * 5243 * @method getMoveCaretBeforeOnEnterElements 5244 * @return {Object} Name/value lookup map for elements to place the caret in front of. 5245 */ 5246 const getMoveCaretBeforeOnEnterElements = constant(moveCaretBeforeOnEnterElementsMap); 5247 /** 5248 * Returns a map with elements where white space is to be preserved. For example: <code><pre></code> or <code><script></code>. 5249 * 5250 * @method getWhitespaceElements 5251 * @return {Object} Name/value lookup map for white space elements. 5252 */ 5253 const getWhitespaceElements = constant(whitespaceElementsMap); 5254 /** 5255 * Returns a map with elements that should be treated as transparent. 5256 * 5257 * @method getTransparentElements 5258 * @return {Object} Name/value lookup map for special elements. 5259 */ 5260 const getTransparentElements = constant(transparentElementsMap); 5261 const getWrapBlockElements = constant(wrapBlockElementsMap); 5262 /** 5263 * Returns a map with special elements. These are elements that needs to be parsed 5264 * in a special way such as script, style, textarea etc. The map object values 5265 * are regexps used to find the end of the element. 5266 * 5267 * @method getSpecialElements 5268 * @return {Object} Name/value lookup map for special elements. 5269 */ 5270 const getSpecialElements = constant(Object.seal(specialElements)); 5271 /** 5272 * Returns true/false if the specified element and it's child is valid or not 5273 * according to the schema. 5274 * 5275 * @method isValidChild 5276 * @param {String} name Element name to check for. 5277 * @param {String} child Element child to verify. 5278 * @return {Boolean} True/false if the element is a valid child of the specified parent. 5279 */ 5280 const isValidChild = (name, child) => { 5281 const parent = children[name.toLowerCase()]; 5282 return !!(parent && parent[child.toLowerCase()]); 5283 }; 5284 /** 5285 * Returns true/false if the specified element name and optional attribute is 5286 * valid according to the schema. 5287 * 5288 * @method isValid 5289 * @param {String} name Name of element to check. 5290 * @param {String} attr Optional attribute name to check for. 5291 * @return {Boolean} True/false if the element and attribute is valid. 5292 */ 5293 const isValid = (name, attr) => { 5294 const rule = getElementRule(name); 5295 // Check if it's a valid element 5296 if (rule) { 5297 if (attr) { 5298 // Check if attribute name exists 5299 if (rule.attributes[attr]) { 5300 return true; 5301 } 5302 // Check if attribute matches a regexp pattern 5303 const attrPatterns = rule.attributePatterns; 5304 if (attrPatterns) { 5305 let i = attrPatterns.length; 5306 while (i--) { 5307 if (attrPatterns[i].pattern.test(attr)) { 5308 return true; 5309 } 5310 } 5311 } 5312 } 5313 else { 5314 return true; 5315 } 5316 } 5317 // No match 5318 return false; 5319 }; 5320 const isBlock = (name) => has$2(getBlockElements(), name); 5321 // Check if name starts with # to detect non-element node names like #text and #comment 5322 const isInline = (name) => !startsWith(name, '#') && isValid(name) && !isBlock(name); 5323 const isWrapper = (name) => has$2(getWrapBlockElements(), name) || isInline(name); 5324 /** 5325 * Returns true/false if the specified element is valid or not 5326 * according to the schema. 5327 * 5328 * @method getElementRule 5329 * @param {String} name Element name to check for. 5330 * @return {Object} Element object or undefined if the element isn't valid. 5331 */ 5332 /** 5333 * Returns an map object of all custom elements. 5334 * 5335 * @method getCustomElements 5336 * @return {Object} Name/value map object of all custom elements. 5337 */ 5338 const getCustomElements = constant(customElementsMap); 5339 /** 5340 * Parses a valid elements string and adds it to the schema. The valid elements 5341 * format is for example <code>element[attr=default|otherattr]</code>. 5342 * Existing rules will be replaced with the ones specified, so this extends the schema. 5343 * 5344 * @method addValidElements 5345 * @param {String} valid_elements String in the valid elements format to be parsed. 5346 */ 5347 /** 5348 * Parses a valid elements string and sets it to the schema. The valid elements 5349 * format is for example <code>element[attr=default|otherattr]</code>. 5350 * Existing rules will be replaced with the ones specified, so this extends the schema. 5351 * 5352 * @method setValidElements 5353 * @param {String} valid_elements String in the valid elements format to be parsed. 5354 */ 5355 /** 5356 * Adds custom non-HTML elements to the schema. For more information about adding custom elements see: 5357 * <a href="https://www.tiny.cloud/docs/tinymce/latest/content-filtering/#custom_elements">custom_elements</a> 5358 * 5359 * @method addCustomElements 5360 * @param {String/Object} custom_elements Comma separated list or record of custom elements to add. 5361 */ 5362 /** 5363 * Parses a valid children string and adds them to the schema structure. The valid children 5364 * format is for example <code>element[child1|child2]</code>. 5365 * 5366 * @method addValidChildren 5367 * @param {String} valid_children Valid children elements string to parse 5368 */ 5369 /** 5370 * Returns an object of all custom elements that have component URLs. 5371 * 5372 * @method getComponentUrls 5373 * @return {Object} Object with where key is the component and the value is the url for that component. 5374 */ 5375 const getComponentUrls = constant(componentUrls); 5376 setup(); 5377 return { 5378 type: schemaType, 5379 children, 5380 elements, 5381 getValidStyles, 5382 getValidClasses, 5383 getBlockElements, 5384 getInvalidStyles, 5385 getVoidElements, 5386 getTextBlockElements, 5387 getTextInlineElements, 5388 getBoolAttrs, 5389 getElementRule, 5390 getSelfClosingElements, 5391 getNonEmptyElements, 5392 getMoveCaretBeforeOnEnterElements, 5393 getWhitespaceElements, 5394 getTransparentElements, 5395 getSpecialElements, 5396 getComponentUrls, 5397 isValidChild, 5398 isValid, 5399 isBlock, 5400 isInline, 5401 isWrapper, 5402 getCustomElements, 5403 addValidElements, 5404 setValidElements, 5405 addCustomElements, 5406 addValidChildren, 5407 }; 5408 }; 5409 5410 const hexColour = (value) => ({ 5411 value: normalizeHex(value) 5412 }); 5413 const normalizeHex = (hex) => removeLeading(hex, '#').toUpperCase(); 5414 const toHex = (component) => { 5415 const hex = component.toString(16); 5416 return (hex.length === 1 ? '0' + hex : hex).toUpperCase(); 5417 }; 5418 const fromRgba = (rgbaColour) => { 5419 const value = toHex(rgbaColour.red) + toHex(rgbaColour.green) + toHex(rgbaColour.blue); 5420 return hexColour(value); 5421 }; 5422 5423 const rgbRegex = /^\s*rgb\s*\(\s*(\d+)\s*[,\s]\s*(\d+)\s*[,\s]\s*(\d+)\s*\)\s*$/i; 5424 // This regex will match rgba(0, 0, 0, 0.5) or rgba(0, 0, 0, 50%) , or without commas 5425 const rgbaRegex = /^\s*rgba\s*\(\s*(\d+)\s*[,\s]\s*(\d+)\s*[,\s]\s*(\d+)\s*[,\s]\s*((?:\d?\.\d+|\d+)%?)\s*\)\s*$/i; 5426 const rgbaColour = (red, green, blue, alpha) => ({ 5427 red, 5428 green, 5429 blue, 5430 alpha 5431 }); 5432 const fromStringValues = (red, green, blue, alpha) => { 5433 const r = parseInt(red, 10); 5434 const g = parseInt(green, 10); 5435 const b = parseInt(blue, 10); 5436 const a = parseFloat(alpha); 5437 return rgbaColour(r, g, b, a); 5438 }; 5439 const getColorFormat = (colorString) => { 5440 if (rgbRegex.test(colorString)) { 5441 return 'rgb'; 5442 } 5443 else if (rgbaRegex.test(colorString)) { 5444 return 'rgba'; 5445 } 5446 return 'other'; 5447 }; 5448 const fromString = (rgbaString) => { 5449 const rgbMatch = rgbRegex.exec(rgbaString); 5450 if (rgbMatch !== null) { 5451 return Optional.some(fromStringValues(rgbMatch[1], rgbMatch[2], rgbMatch[3], '1')); 5452 } 5453 const rgbaMatch = rgbaRegex.exec(rgbaString); 5454 if (rgbaMatch !== null) { 5455 return Optional.some(fromStringValues(rgbaMatch[1], rgbaMatch[2], rgbaMatch[3], rgbaMatch[4])); 5456 } 5457 return Optional.none(); 5458 }; 5459 const toString = (rgba) => `rgba(${rgba.red},${rgba.green},${rgba.blue},${rgba.alpha})`; 5460 5461 const rgbaToHexString = (color) => fromString(color) 5462 .map(fromRgba) 5463 .map((h) => '#' + h.value) 5464 .getOr(color); 5465 5466 /** 5467 * This class is used to parse CSS styles. It also compresses styles to reduce the output size. 5468 * 5469 * @class tinymce.html.Styles 5470 * @version 3.4 5471 * @example 5472 * const Styles = tinymce.html.Styles({ 5473 * url_converter: (url) => { 5474 * return url; 5475 * } 5476 * }); 5477 * 5478 * styles = Styles.parse('border: 1px solid red'); 5479 * styles.color = 'red'; 5480 * 5481 * console.log(tinymce.html.Styles().serialize(styles)); 5482 */ 5483 const Styles = (settings = {}, schema) => { 5484 /* jshint maxlen:255 */ 5485 /* eslint max-len:0 */ 5486 const urlOrStrRegExp = /(?:url(?:(?:\(\s*\"([^\"]+)\"\s*\))|(?:\(\s*\'([^\']+)\'\s*\))|(?:\(\s*([^)\s]+)\s*\))))|(?:\'([^\']+)\')|(?:\"([^\"]+)\")/gi; 5487 const styleRegExp = /\s*([^:]+):\s*([^;]+);?/g; 5488 const trimRightRegExp = /\s+$/; 5489 const encodingLookup = {}; 5490 let validStyles; 5491 let invalidStyles; 5492 const invisibleChar = zeroWidth; 5493 if (schema) { 5494 validStyles = schema.getValidStyles(); 5495 invalidStyles = schema.getInvalidStyles(); 5496 } 5497 const encodingItems = (`\\" \\' \\; \\: ; : ` + invisibleChar).split(' '); 5498 for (let i = 0; i < encodingItems.length; i++) { 5499 encodingLookup[encodingItems[i]] = invisibleChar + i; 5500 encodingLookup[invisibleChar + i] = encodingItems[i]; 5501 } 5502 // eslint-disable-next-line consistent-this 5503 const self = { 5504 /** 5505 * Parses the specified style value into an object collection. This parser will also 5506 * merge and remove any redundant items that browsers might have added. URLs inside 5507 * the styles will also be converted to absolute/relative based on the settings. 5508 * 5509 * @method parse 5510 * @param {String} css Style value to parse. For example: `border:1px solid red;` 5511 * @return {Object} Object representation of that style. For example: `{ border: '1px solid red' }` 5512 */ 5513 parse: (css) => { 5514 const styles = {}; 5515 let isEncoded = false; 5516 const urlConverter = settings.url_converter; 5517 const urlConverterScope = settings.url_converter_scope || self; 5518 const compress = (prefix, suffix, noJoin) => { 5519 const top = styles[prefix + '-top' + suffix]; 5520 if (!top) { 5521 return; 5522 } 5523 const right = styles[prefix + '-right' + suffix]; 5524 if (!right) { 5525 return; 5526 } 5527 const bottom = styles[prefix + '-bottom' + suffix]; 5528 if (!bottom) { 5529 return; 5530 } 5531 const left = styles[prefix + '-left' + suffix]; 5532 if (!left) { 5533 return; 5534 } 5535 const box = [top, right, bottom, left]; 5536 let i = box.length - 1; 5537 while (i--) { 5538 if (box[i] !== box[i + 1]) { 5539 break; 5540 } 5541 } 5542 if (i > -1 && noJoin) { 5543 return; 5544 } 5545 styles[prefix + suffix] = i === -1 ? box[0] : box.join(' '); 5546 delete styles[prefix + '-top' + suffix]; 5547 delete styles[prefix + '-right' + suffix]; 5548 delete styles[prefix + '-bottom' + suffix]; 5549 delete styles[prefix + '-left' + suffix]; 5550 }; 5551 /** 5552 * Checks if the specific style can be compressed in other words if all border-width are equal. 5553 */ 5554 const canCompress = (key) => { 5555 const value = styles[key]; 5556 if (!value) { 5557 return; 5558 } 5559 // Make sure not to split values like 'rgb(100, 50, 100); 5560 const values = value.indexOf(',') > -1 ? [value] : value.split(' '); 5561 let i = values.length; 5562 while (i--) { 5563 if (values[i] !== values[0]) { 5564 return false; 5565 } 5566 } 5567 styles[key] = values[0]; 5568 return true; 5569 }; 5570 /** 5571 * Compresses multiple styles into one style. 5572 */ 5573 const compress2 = (target, a, b, c) => { 5574 if (!canCompress(a)) { 5575 return; 5576 } 5577 if (!canCompress(b)) { 5578 return; 5579 } 5580 if (!canCompress(c)) { 5581 return; 5582 } 5583 // Compress 5584 styles[target] = styles[a] + ' ' + styles[b] + ' ' + styles[c]; 5585 delete styles[a]; 5586 delete styles[b]; 5587 delete styles[c]; 5588 }; 5589 // Encodes the specified string by replacing all \" \' ; : with _<num> 5590 const encode = (str) => { 5591 isEncoded = true; 5592 return encodingLookup[str]; 5593 }; 5594 // Decodes the specified string by replacing all _<num> with it's original value \" \' etc 5595 // It will also decode the \" \' if keepSlashes is set to false or omitted 5596 const decode = (str, keepSlashes) => { 5597 if (isEncoded) { 5598 str = str.replace(/\uFEFF[0-9]/g, (str) => { 5599 return encodingLookup[str]; 5600 }); 5601 } 5602 if (!keepSlashes) { 5603 str = str.replace(/\\([\'\";:])/g, '$1'); 5604 } 5605 return str; 5606 }; 5607 const decodeSingleHexSequence = (escSeq) => { 5608 return String.fromCharCode(parseInt(escSeq.slice(1), 16)); 5609 }; 5610 const decodeHexSequences = (value) => { 5611 return value.replace(/\\[0-9a-f]+/gi, decodeSingleHexSequence); 5612 }; 5613 const processUrl = (match, url, url2, url3, str, str2) => { 5614 str = str || str2; 5615 if (str) { 5616 str = decode(str); 5617 // Force strings into single quote format 5618 return `'` + str.replace(/\'/g, `\\'`) + `'`; 5619 } 5620 url = decode(url || url2 || url3 || ''); 5621 if (!settings.allow_script_urls) { 5622 const scriptUrl = url.replace(/[\s\r\n]+/g, ''); 5623 if (/(java|vb)script:/i.test(scriptUrl)) { 5624 return ''; 5625 } 5626 if (!settings.allow_svg_data_urls && /^data:image\/svg/i.test(scriptUrl)) { 5627 return ''; 5628 } 5629 } 5630 // Convert the URL to relative/absolute depending on config 5631 if (urlConverter) { 5632 url = urlConverter.call(urlConverterScope, url, 'style'); 5633 } 5634 // Output new URL format 5635 return `url('` + url.replace(/\'/g, `\\'`) + `')`; 5636 }; 5637 if (css) { 5638 css = css.replace(/[\u0000-\u001F]/g, ''); 5639 // Encode \" \' % and ; and : inside strings so they don't interfere with the style parsing 5640 css = css.replace(/\\[\"\';:\uFEFF]/g, encode).replace(/\"[^\"]+\"|\'[^\']+\'/g, (str) => { 5641 return str.replace(/[;:]/g, encode); 5642 }); 5643 // Parse styles 5644 let matches; 5645 while ((matches = styleRegExp.exec(css))) { 5646 styleRegExp.lastIndex = matches.index + matches[0].length; 5647 let name = matches[1].replace(trimRightRegExp, '')
vendor: 1,511 bytes, lines 5647-5668
5647.toLowerCase(); 5648 let value = matches[2].replace(trimRightRegExp, ''); 5649 if (name && value) { 5650 // Decode escaped sequences like \65 -> e 5651 name = decodeHexSequences(name); 5652 value = decodeHexSequences(value); 5653 // Skip properties with double quotes and sequences like \" \' in their names 5654 // See 'mXSS Attacks: Attacking well-secured Web-Applications by using innerHTML Mutations' 5655 // https://cure53.de/fp170.pdf 5656 if (name.indexOf(invisibleChar) !== -1 || name.indexOf('"') !== -1) { 5657 continue; 5658 } 5659 // Don't allow behavior name or expression/comments within the values 5660 if (!settings.allow_script_urls && (name === 'behavior' || /expression\s*\(|\/\*|\*\//.test(value))) { 5661 continue; 5662 } 5663 // Opera will produce 700 instead of bold in their style values 5664 if (name === 'font-weight' && value === '700') { 5665 value = 'bold'; 5666 } 5667 else if (name === 'color' || name === 'background-color') { // Lowercase colors like RED 5668 value = value.toLowerCase()
vendor: 25,113 bytes, lines 5668-6221
5668; 5669 } 5670 // Convert RGB colors to HEX 5671 if (getColorFormat(value) === 'rgb') { 5672 fromString(value).each((rgba) => { 5673 value = rgbaToHexString(toString(rgba)).toLowerCase(); 5674 }); 5675 } 5676 // Convert URLs and force them into url('value') format 5677 value = value.replace(urlOrStrRegExp, processUrl); 5678 styles[name] = isEncoded ? decode(value, true) : value; 5679 } 5680 } 5681 // Compress the styles to reduce it's size for example IE will expand styles 5682 compress('border', '', true); 5683 compress('border', '-width'); 5684 compress('border', '-color'); 5685 compress('border', '-style'); 5686 compress('padding', ''); 5687 compress('margin', ''); 5688 if (!/(#.* rgb(a?)\(.*)|(rgb(a?)\(.*\) )/.test(styles['border-color'])) { 5689 compress2('border', 'border-width', 'border-style', 'border-color'); 5690 } 5691 // Remove pointless border, IE produces these 5692 if (styles.border === 'medium none') { 5693 delete styles.border; 5694 } 5695 // IE 11 will produce a border-image: none when getting the style attribute from <p style="border: 1px solid red"></p> 5696 // So let us assume it shouldn't be there 5697 if (styles['border-image'] === 'none') { 5698 delete styles['border-image']; 5699 } 5700 } 5701 return styles; 5702 }, 5703 /** 5704 * Serializes the specified style object into a string. 5705 * 5706 * @method serialize 5707 * @param {Object} styles Object to serialize as string. For example: `{ border: '1px solid red' }` 5708 * @param {String} elementName Optional element name, if specified only the styles that matches the schema will be serialized. 5709 * @return {String} String representation of the style object. For example: `border: 1px solid red` 5710 */ 5711 serialize: (styles, elementName) => { 5712 let css = ''; 5713 const serializeStyles = (elemName, validStyleList) => { 5714 const styleList = validStyleList[elemName]; 5715 if (styleList) { 5716 for (let i = 0, l = styleList.length; i < l; i++) { 5717 const name = styleList[i]; 5718 const value = styles[name]; 5719 if (value) { 5720 css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';'; 5721 } 5722 } 5723 } 5724 }; 5725 const isValid = (name, elemName) => { 5726 if (!invalidStyles || !elemName) { 5727 return true; 5728 } 5729 let styleMap = invalidStyles['*']; 5730 if (styleMap && styleMap[name]) { 5731 return false; 5732 } 5733 styleMap = invalidStyles[elemName]; 5734 return !(styleMap && styleMap[name]); 5735 }; 5736 // Serialize styles according to schema 5737 if (elementName && validStyles) { 5738 // Serialize global styles and element specific styles 5739 serializeStyles('*', validStyles); 5740 serializeStyles(elementName, validStyles); 5741 } 5742 else { 5743 // Output the styles in the order they are inside the object 5744 each$d(styles, (value, name) => { 5745 if (value && isValid(name, elementName)) { 5746 css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';'; 5747 } 5748 }); 5749 } 5750 return css; 5751 } 5752 }; 5753 return self; 5754 }; 5755 5756 // Note: The values here aren't used. This is just used as a hash map to see if the key exists 5757 const deprecated = { 5758 keyLocation: true, 5759 layerX: true, 5760 layerY: true, 5761 returnValue: true, 5762 webkitMovementX: true, 5763 webkitMovementY: true, 5764 keyIdentifier: true, 5765 mozPressure: true, 5766 mozInputSource: true, 5767 }; 5768 // Note: We can't rely on `instanceof` here as it won't work if the event was fired from another window. 5769 // Additionally, the constructor name might be `MouseEvent` or similar so we can't rely on the constructor name. 5770 const isNativeEvent = (event) => event instanceof Event || isFunction(event.initEvent); 5771 // Checks if it is our own isDefaultPrevented function 5772 const hasIsDefaultPrevented = (event) => event.isDefaultPrevented === always || event.isDefaultPrevented === never; 5773 // An event needs normalizing if it doesn't have the prevent default function or if it's a native event 5774 const needsNormalizing = (event) => isNullable(event.preventDefault) || isNativeEvent(event); 5775 const clone$2 = (originalEvent, data) => { 5776 const event = data ?? {}; 5777 // Copy all properties from the original event 5778 for (const name in originalEvent) { 5779 // Some properties are deprecated and produces a warning so don't include them 5780 if (!has$2(deprecated, name)) { 5781 event[name] = originalEvent[name]; 5782 } 5783 } 5784 // The composed path can't be cloned, so delegate instead 5785 if (isNonNullable(originalEvent.composedPath)) { 5786 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 5787 event.composedPath = () => originalEvent.composedPath(); 5788 } 5789 // The getModifierState won't work when cloned, so delegate instead 5790 if (isNonNullable(originalEvent.getModifierState)) { 5791 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 5792 event.getModifierState = (keyArg) => originalEvent.getModifierState(keyArg); 5793 } 5794 // The getTargetRanges won't work when cloned, so delegate instead 5795 if (isNonNullable(originalEvent.getTargetRanges)) { 5796 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 5797 event.getTargetRanges = () => originalEvent.getTargetRanges(); 5798 } 5799 return event; 5800 }; 5801 const normalize$3 = (type, originalEvent, fallbackTarget, data) => { 5802 const event = clone$2(originalEvent, data); 5803 event.type = type; 5804 // Normalize target IE uses srcElement 5805 if (isNullable(event.target)) { 5806 event.target = event.srcElement ?? fallbackTarget; 5807 } 5808 if (needsNormalizing(originalEvent)) { 5809 // Add preventDefault method 5810 event.preventDefault = () => { 5811 event.defaultPrevented = true; 5812 event.isDefaultPrevented = always; 5813 // Execute preventDefault on the original event object 5814 if (isFunction(originalEvent.preventDefault)) { 5815 originalEvent.preventDefault(); 5816 } 5817 }; 5818 // Add stopPropagation 5819 event.stopPropagation = () => { 5820 event.cancelBubble = true; 5821 event.isPropagationStopped = always; 5822 // Execute stopPropagation on the original event object 5823 if (isFunction(originalEvent.stopPropagation)) { 5824 originalEvent.stopPropagation(); 5825 } 5826 }; 5827 // Add stopImmediatePropagation 5828 event.stopImmediatePropagation = () => { 5829 event.isImmediatePropagationStopped = always; 5830 event.stopPropagation(); 5831 }; 5832 // Add event delegation states 5833 if (!hasIsDefaultPrevented(event)) { 5834 event.isDefaultPrevented = event.defaultPrevented === true ? always : never; 5835 event.isPropagationStopped = event.cancelBubble === true ? always : never; 5836 event.isImmediatePropagationStopped = never; 5837 } 5838 } 5839 return event; 5840 }; 5841 5842 /** 5843 * This class wraps the browsers native event logic with more convenient methods. 5844 * 5845 * @class tinymce.dom.EventUtils 5846 */ 5847 const eventExpandoPrefix = 'mce-data-'; 5848 const mouseEventRe = /^(?:mouse|contextmenu)|click/; 5849 /** 5850 * Binds a native event to a callback on the speified target. 5851 */ 5852 const addEvent = (target, name, callback, capture) => { 5853 target.addEventListener(name, callback, capture || false); 5854 }; 5855 /** 5856 * Unbinds a native event callback on the specified target. 5857 */ 5858 const removeEvent = (target, name, callback, capture) => { 5859 target.removeEventListener(name, callback, capture || false); 5860 }; 5861 const isMouseEvent = (event) => isNonNullable(event) && mouseEventRe.test(event.type); 5862 /** 5863 * Normalizes a native event object or just adds the event specific methods on a custom event. 5864 */ 5865 const fix = (originalEvent, data) => { 5866 const event = normalize$3(originalEvent.type, originalEvent, document, data); 5867 // Calculate pageX/Y if missing and clientX/Y available 5868 if (isMouseEvent(originalEvent) && isUndefined(originalEvent.pageX) && !isUndefined(originalEvent.clientX)) { 5869 const eventDoc = event.target.ownerDocument || document; 5870 const doc = eventDoc.documentElement; 5871 const body = eventDoc.body; 5872 const mouseEvent = event; 5873 mouseEvent.pageX = originalEvent.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - 5874 (doc && doc.clientLeft || body && body.clientLeft || 0); 5875 mouseEvent.pageY = originalEvent.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - 5876 (doc && doc.clientTop || body && body.clientTop || 0); 5877 } 5878 return event; 5879 }; 5880 /** 5881 * Bind a DOMContentLoaded event across browsers and executes the callback once the page DOM is initialized. 5882 * It will also set/check the domLoaded state of the event_utils instance so ready isn't called multiple times. 5883 */ 5884 const bindOnReady = (win, callback, eventUtils) => { 5885 const doc = win.document, event = { type: 'ready' }; 5886 if (eventUtils.domLoaded) { 5887 callback(event); 5888 return; 5889 } 5890 const isDocReady = () => { 5891 // Check complete or interactive state if there is a body 5892 // element on some iframes IE 8 will produce a null body 5893 return doc.readyState === 'complete' || (doc.readyState === 'interactive' && doc.body); 5894 }; 5895 // Gets called when the DOM is ready 5896 const readyHandler = () => { 5897 removeEvent(win, 'DOMContentLoaded', readyHandler); 5898 removeEvent(win, 'load', readyHandler); 5899 if (!eventUtils.domLoaded) { 5900 eventUtils.domLoaded = true; 5901 callback(event); 5902 } 5903 // Clean memory for IE 5904 win = null; 5905 }; 5906 if (isDocReady()) { 5907 readyHandler(); 5908 } 5909 else { 5910 addEvent(win, 'DOMContentLoaded', readyHandler); 5911 } 5912 // Fallback if any of the above methods should fail for some odd reason 5913 if (!eventUtils.domLoaded) { 5914 addEvent(win, 'load', readyHandler); 5915 } 5916 }; 5917 /** 5918 * This class enables you to bind/unbind native events to elements and normalize it's behavior across browsers. 5919 */ 5920 class EventUtils { 5921 static Event = new EventUtils(); 5922 // State if the DOMContentLoaded was executed or not 5923 domLoaded = false; 5924 events = {}; 5925 expando; 5926 hasFocusIn; 5927 count = 1; 5928 constructor() { 5929 this.expando = eventExpandoPrefix + (+new Date()).toString(32); 5930 this.hasFocusIn = 'onfocusin' in document.documentElement; 5931 this.count = 1; 5932 } 5933 bind(target, names, callback, scope) { 5934 const self = this; 5935 let callbackList; 5936 const win = window; 5937 // Native event handler function patches the event and executes the callbacks for the expando 5938 const defaultNativeHandler = (evt) => { 5939 self.executeHandlers(fix(evt || win.event), id); 5940 }; 5941 // Don't bind to text nodes or comments 5942 if (!target || isText$b(target) || isComment(target)) { 5943 return callback; 5944 } 5945 // Create or get events id for the target 5946 let id; 5947 if (!target[self.expando]) { 5948 id = self.count++; 5949 target[self.expando] = id; 5950 self.events[id] = {}; 5951 } 5952 else { 5953 id = target[self.expando]; 5954 } 5955 // Setup the specified scope or use the target as a default 5956 scope = scope || target; 5957 // Split names and bind each event, enables you to bind multiple events with one call 5958 const namesList = names.split(' '); 5959 let i = namesList.length; 5960 while (i--) { 5961 let name = namesList[i]; 5962 let nativeHandler = defaultNativeHandler; 5963 let capture = false; 5964 let fakeName = false; 5965 // Use ready instead of DOMContentLoaded 5966 if (name === 'DOMContentLoaded') { 5967 name = 'ready'; 5968 } 5969 // DOM is already ready 5970 if (self.domLoaded && name === 'ready' && target.readyState === 'complete') { 5971 callback.call(scope, fix({ type: name })); 5972 continue; 5973 } 5974 // Fake bubbling of focusin/focusout 5975 if (!self.hasFocusIn && (name === 'focusin' || name === 'focusout')) { 5976 capture = true; 5977 fakeName = name === 'focusin' ? 'focus' : 'blur'; 5978 nativeHandler = (evt) => { 5979 const event = fix(evt || win.event); 5980 event.type = event.type === 'focus' ? 'focusin' : 'focusout'; 5981 self.executeHandlers(event, id); 5982 }; 5983 } 5984 // Setup callback list and bind native event 5985 callbackList = self.events[id][name]; 5986 if (!callbackList) { 5987 self.events[id][name] = callbackList = [{ func: callback, scope }]; 5988 callbackList.fakeName = fakeName; 5989 callbackList.capture = capture; 5990 // callbackList.callback = callback; 5991 // Add the nativeHandler to the callback list so that we can later unbind it 5992 callbackList.nativeHandler = nativeHandler; 5993 // Check if the target has native events support 5994 if (name === 'ready') { 5995 bindOnReady(target, nativeHandler, self); 5996 } 5997 else { 5998 addEvent(target, fakeName || name, nativeHandler, capture); 5999 } 6000 } 6001 else { 6002 if (name === 'ready' && self.domLoaded) { 6003 callback(fix({ type: name })); 6004 } 6005 else { 6006 // If it already has an native handler then just push the callback 6007 callbackList.push({ func: callback, scope }); 6008 } 6009 } 6010 } 6011 target = callbackList = null; // Clean memory for IE 6012 return callback; 6013 } 6014 unbind(target, names, callback) { 6015 // Don't bind to text nodes or comments 6016 if (!target || isText$b(target) || isComment(target)) { 6017 return this; 6018 } 6019 // Unbind event or events if the target has the expando 6020 const id = target[this.expando]; 6021 if (id) { 6022 let eventMap = this.events[id]; 6023 // Specific callback 6024 if (names) { 6025 const namesList = names.split(' '); 6026 let i = namesList.length; 6027 while (i--) { 6028 const name = namesList[i]; 6029 const callbackList = eventMap[name]; 6030 // Unbind the event if it exists in the map 6031 if (callbackList) { 6032 // Remove specified callback 6033 if (callback) { 6034 let ci = callbackList.length; 6035 while (ci--) { 6036 if (callbackList[ci].func === callback) { 6037 const nativeHandler = callbackList.nativeHandler; 6038 const fakeName = callbackList.fakeName, capture = callbackList.capture; 6039 // Clone callbackList since unbind inside a callback would otherwise break the handlers loop 6040 const newCallbackList = callbackList.slice(0, ci).concat(callbackList.slice(ci + 1)); 6041 newCallbackList.nativeHandler = nativeHandler; 6042 newCallbackList.fakeName = fakeName; 6043 newCallbackList.capture = capture; 6044 eventMap[name] = newCallbackList; 6045 } 6046 } 6047 } 6048 // Remove all callbacks if there isn't a specified callback or there is no callbacks left 6049 if (!callback || callbackList.length === 0) { 6050 delete eventMap[name]; 6051 removeEvent(target, callbackList.fakeName || name, callbackList.nativeHandler, callbackList.capture); 6052 } 6053 } 6054 } 6055 } 6056 else { 6057 // All events for a specific element 6058 each$d(eventMap, (callbackList, name) => { 6059 removeEvent(target, callbackList.fakeName || name, callbackList.nativeHandler, callbackList.capture); 6060 }); 6061 eventMap = {}; 6062 } 6063 // Check if object is empty, if it isn't then we won't remove the expando map 6064 for (const name in eventMap) { 6065 if (has$2(eventMap, name)) { 6066 return this; 6067 } 6068 } 6069 // Delete event object 6070 delete this.events[id]; 6071 // Remove expando from target 6072 try { 6073 // IE will fail here since it can't delete properties from window 6074 delete target[this.expando]; 6075 } 6076 catch { 6077 // IE will set it to null 6078 target[this.expando] = null; 6079 } 6080 } 6081 return this; 6082 } 6083 /** 6084 * Fires the specified event on the specified target. 6085 * <br> 6086 * <em>Deprecated in TinyMCE 6.0 and has been marked for removal in TinyMCE 7.0. Use <code>dispatch</code> instead.</em> 6087 * 6088 * @method fire 6089 * @param {Object} target Target node/window or custom object. 6090 * @param {String} name Event name to fire. 6091 * @param {Object} args Optional arguments to send to the observers. 6092 * @return {EventUtils} Event utils instance. 6093 * @deprecated Use dispatch() instead 6094 */ 6095 fire(target, name, args) { 6096 return this.dispatch(target, name, args); 6097 } 6098 /** 6099 * Dispatches the specified event on the specified target. 6100 * 6101 * @method dispatch 6102 * @param {Node/window} target Target node/window or custom object. 6103 * @param {String} name Event name to dispatch. 6104 * @param {Object} args Optional arguments to send to the observers. 6105 * @return {EventUtils} Event utils instance. 6106 */ 6107 dispatch(target, name, args) { 6108 // Don't bind to text nodes or comments 6109 if (!target || isText$b(target) || isComment(target)) { 6110 return this; 6111 } 6112 // Build event object by patching the args 6113 const event = fix({ type: name, target }, args); 6114 do { 6115 // Found an expando that means there is listeners to execute 6116 const id = target[this.expando]; 6117 if (id) { 6118 this.executeHandlers(event, id); 6119 } 6120 // Walk up the DOM 6121 target = target.parentNode || target.ownerDocument || target.defaultView || target.parentWindow; 6122 } while (target && !event.isPropagationStopped()); 6123 return this; 6124 } 6125 /** 6126 * Removes all bound event listeners for the specified target. This will also remove any bound 6127 * listeners to child nodes within that target. 6128 * 6129 * @method clean 6130 * @param {Object} target Target node/window object. 6131 * @return {EventUtils} Event utils instance. 6132 */ 6133 clean(target) { 6134 // Don't bind to text nodes or comments 6135 if (!target || isText$b(target) || isComment(target)) { 6136 return this; 6137 } 6138 // Unbind any element on the specified target 6139 if (target[this.expando]) { 6140 this.unbind(target); 6141 } 6142 // Target doesn't have getElementsByTagName it's probably a window object then use it's document to find the children 6143 if (!target.getElementsByTagName) { 6144 target = target.document; 6145 } 6146 // Remove events from each child element 6147 if (target && target.getElementsByTagName) { 6148 this.unbind(target); 6149 const children = target.getElementsByTagName('*'); 6150 let i = children.length; 6151 while (i--) { 6152 target = children[i]; 6153 if (target[this.expando]) { 6154 this.unbind(target); 6155 } 6156 } 6157 } 6158 return this; 6159 } 6160 /** 6161 * Destroys the event object. Call this to remove memory leaks. 6162 */ 6163 destroy() { 6164 this.events = {}; 6165 } 6166 // Legacy function for canceling events 6167 cancel(e) { 6168 if (e) { 6169 e.preventDefault(); 6170 e.stopImmediatePropagation(); 6171 } 6172 return false; 6173 } 6174 /** 6175 * Executes all event handler callbacks for a specific event. 6176 * 6177 * @private 6178 * @param {Event} evt Event object. 6179 * @param {String} id Expando id value to look for. 6180 */ 6181 executeHandlers(evt, id) { 6182 const container = this.events[id]; 6183 const callbackList = container && container[evt.type]; 6184 if (callbackList) { 6185 for (let i = 0, l = callbackList.length; i < l; i++) { 6186 const callback = callbackList[i]; 6187 // Check if callback exists might be removed if a unbind is called inside the callback 6188 if (callback && callback.func.call(callback.scope, evt) === false) { 6189 evt.preventDefault(); 6190 } 6191 // Should we stop propagation to immediate listeners 6192 if (evt.isImmediatePropagationStopped()) { 6193 return; 6194 } 6195 } 6196 } 6197 } 6198 } 6199 6200 /** 6201 * Utility class for various DOM manipulation and retrieval functions. 6202 * 6203 * @class tinymce.dom.DOMUtils 6204 * @example 6205 * // Add a class to an element by id in the page 6206 * tinymce.DOM.addClass('someid', 'someclass'); 6207 * 6208 * // Add a class to an element by id inside the editor 6209 * tinymce.activeEditor.dom.addClass('someid', 'someclass'); 6210 */ 6211 // Shorten names 6212 const each$a = Tools.each; 6213 const grep = Tools.grep; 6214 const internalStyleName = 'data-mce-style'; 6215 const numericalCssMap = Tools.makeMap('fill-opacity font-weight line-height opacity orphans widows z-index zoom', ' '); 6216 const legacySetAttribute = (elm, name, value) => { 6217 if (isNullable(value) || value === '') { 6218 remove$9(elm, name); 6219 } 6220 else { 6221 set$
vendor: 20,552 bytes, lines 6221-6740
62214(elm, name, value); 6222 } 6223 }; 6224 // Convert camel cased names back to hyphenated names 6225 const camelCaseToHyphens = (name) => name.replace(/[A-Z]/g, (v) => '-' + v.toLowerCase()); 6226 const findNodeIndex = (node, normalized) => { 6227 let idx = 0; 6228 if (node) { 6229 for (let lastNodeType = node.nodeType, tempNode = node.previousSibling; tempNode; tempNode = tempNode.previousSibling) { 6230 const nodeType = tempNode.nodeType; 6231 // Normalize text nodes 6232 if (normalized && isText$b(tempNode)) { 6233 if (nodeType === lastNodeType || !tempNode.data.length) { 6234 continue; 6235 } 6236 } 6237 idx++; 6238 lastNodeType = nodeType; 6239 } 6240 } 6241 return idx; 6242 }; 6243 const updateInternalStyleAttr = (styles, elm) => { 6244 const rawValue = get$9(elm, 'style'); 6245 const value = styles.serialize(styles.parse(rawValue), name(elm)); 6246 legacySetAttribute(elm, internalStyleName, value); 6247 }; 6248 const convertStyleToString = (cssValue, cssName) => { 6249 if (isNumber(cssValue)) { 6250 return has$2(numericalCssMap, cssName) ? cssValue + '' : cssValue + 'px'; 6251 } 6252 else { 6253 return cssValue; 6254 } 6255 }; 6256 const applyStyle$1 = ($elm, cssName, cssValue) => { 6257 const normalizedName = camelCaseToHyphens(cssName); 6258 if (isNullable(cssValue) || cssValue === '') { 6259 remove$7($elm, normalizedName); 6260 } 6261 else { 6262 set$2($elm, normalizedName, convertStyleToString(cssValue, normalizedName)); 6263 } 6264 }; 6265 const setupAttrHooks = (styles, settings, getContext) => { 6266 const keepValues = settings.keep_values; 6267 const keepUrlHook = { 6268 set: (elm, value, name) => { 6269 const sugarElm = SugarElement.fromDom(elm); 6270 if (isFunction(settings.url_converter) && isNonNullable(value)) { 6271 value = settings.url_converter.call(settings.url_converter_scope || getContext(), String(value), name, elm); 6272 } 6273 const internalName = 'data-mce-' + name; 6274 legacySetAttribute(sugarElm, internalName, value); 6275 legacySetAttribute(sugarElm, name, value); 6276 }, 6277 get: (elm, name) => { 6278 const sugarElm = SugarElement.fromDom(elm); 6279 return get$9(sugarElm, 'data-mce-' + name) || get$9(sugarElm, name); 6280 } 6281 }; 6282 const attrHooks = { 6283 style: { 6284 set: (elm, value) => { 6285 const sugarElm = SugarElement.fromDom(elm); 6286 if (keepValues) { 6287 legacySetAttribute(sugarElm, internalStyleName, value); 6288 } 6289 remove$9(sugarElm, 'style'); 6290 // If setting a style then delegate to the css api, otherwise 6291 // this will cause issues when using a content security policy 6292 if (isString(value)) { 6293 setAll(sugarElm, styles.parse(value)); 6294 } 6295 }, 6296 get: (elm) => { 6297 const sugarElm = SugarElement.fromDom(elm); 6298 const value = get$9(sugarElm, internalStyleName) || get$9(sugarElm, 'style'); 6299 return styles.serialize(styles.parse(value), name(sugarElm)); 6300 } 6301 } 6302 }; 6303 if (keepValues) { 6304 attrHooks.href = attrHooks.src = keepUrlHook; 6305 } 6306 return attrHooks; 6307 }; 6308 /** 6309 * Constructs a new DOMUtils instance. Consult the TinyMCE Documentation for more details on settings etc for this class. 6310 * 6311 * @private 6312 * @constructor 6313 * @method DOMUtils 6314 * @param {Document} doc Document reference to bind the utility class to. 6315 * @param {settings} settings Optional settings collection. 6316 */ 6317 const DOMUtils = (doc, settings = {}) => { 6318 const addedStyles = {}; 6319 const win = window; 6320 const files = {}; 6321 let counter = 0; 6322 const stdMode = true; 6323 const boxModel = true; 6324 const styleSheetLoader = instance.forElement(SugarElement.fromDom(doc), { 6325 contentCssCors: settings.contentCssCors, 6326 referrerPolicy: settings.referrerPolicy, 6327 crossOrigin: (url) => { 6328 const crossOrigin = settings.crossOrigin; 6329 if (isFunction(crossOrigin)) { 6330 return crossOrigin(url, 'stylesheet'); 6331 } 6332 else { 6333 return undefined; 6334 } 6335 } 6336 }); 6337 const boundEvents = []; 6338 const schema = settings.schema ? settings.schema : Schema({}); 6339 const styles = Styles({ 6340 url_converter: settings.url_converter, 6341 url_converter_scope: settings.url_converter_scope, 6342 }, settings.schema); 6343 const events = settings.ownEvents ? new EventUtils() : EventUtils.Event; 6344 const blockElementsMap = schema.getBlockElements(); 6345 /** 6346 * Returns true/false if the specified element is a block element or not. 6347 * 6348 * @method isBlock 6349 * @param {Node/String} node Element/Node to check. 6350 * @return {Boolean} True/False state if the node is a block element or not. 6351 */ 6352 const isBlock = (node) => { 6353 if (isString(node)) { 6354 return has$2(blockElementsMap, node); 6355 } 6356 else { 6357 return isElement$7(node) && (has$2(blockElementsMap, node.nodeName) || isTransparentBlock(schema, node)); 6358 } 6359 }; 6360 const get = (elm) => elm && doc && isString(elm) 6361 ? doc.getElementById(elm) 6362 : elm; 6363 const _get = (elm) => { 6364 const value = get(elm); 6365 return isNonNullable(value) ? SugarElement.fromDom(value) : null; 6366 }; 6367 const getAttrib = (elm, name, defaultVal = '') => { 6368 let value; 6369 const $elm = _get(elm); 6370 if (isNonNullable($elm) && isElement$8($elm)) { 6371 const hook = attrHooks[name]; 6372 if (hook && hook.get) { 6373 value = hook.get($elm.dom, name); 6374 } 6375 else { 6376 value = get$9($elm, name); 6377 } 6378 } 6379 return isNonNullable(value) ? value : defaultVal; 6380 }; 6381 const getAttribs = (elm) => { 6382 const node = get(elm); 6383 return isNullable(node) ? [] : node.attributes; 6384 }; 6385 const setAttrib = (elm, name, value) => { 6386 run(elm, (e) => { 6387 if (isElement$7(e)) { 6388 const $elm = SugarElement.fromDom(e); 6389 const val = value === '' ? null : value; 6390 const originalValue = get$9($elm, name); 6391 const hook = attrHooks[name]; 6392 if (hook && hook.set) { 6393 hook.set($elm.dom, val, name); 6394 } 6395 else { 6396 legacySetAttribute($elm, name, val); 6397 } 6398 if (originalValue !== val && settings.onSetAttrib) { 6399 settings.onSetAttrib({ 6400 attrElm: $elm.dom, // We lie here to not break backwards compatibility 6401 attrName: name, 6402 attrValue: val 6403 }); 6404 } 6405 } 6406 }); 6407 }; 6408 const clone = (node, deep) => { 6409 return node.cloneNode(deep); 6410 }; 6411 const getRoot = () => settings.root_element || doc.body; 6412 const getViewPort = (argWin) => { 6413 const vp = getBounds(argWin); 6414 // Returns viewport size excluding scrollbars 6415 return { 6416 x: vp.x, 6417 y: vp.y, 6418 w: vp.width, 6419 h: vp.height 6420 }; 6421 }; 6422 const getPos$1 = (elm, rootElm) => getPos(doc.body, get(elm), rootElm); 6423 const setStyle = (elm, name, value) => { 6424 run(elm, (e) => { 6425 const $elm = SugarElement.fromDom(e); 6426 applyStyle$1($elm, name, value); 6427 if (settings.update_styles) { 6428 updateInternalStyleAttr(styles, $elm); 6429 } 6430 }); 6431 }; 6432 const setStyles = (elm, stylesArg) => { 6433 run(elm, (e) => { 6434 const $elm = SugarElement.fromDom(e); 6435 each$d(stylesArg, (v, n) => { 6436 applyStyle$1($elm, n, v); 6437 }); 6438 if (settings.update_styles) { 6439 updateInternalStyleAttr(styles, $elm); 6440 } 6441 }); 6442 }; 6443 const getStyle = (elm, name, computed) => { 6444 const $elm = get(elm); 6445 if (isNullable($elm) || (!isHTMLElement($elm) && !isSVGElement($elm))) { 6446 return undefined; 6447 } 6448 if (computed) { 6449 return get$7(SugarElement.fromDom($elm), camelCaseToHyphens(name)); 6450 } 6451 else { 6452 // Camelcase it, if needed 6453 name = name.replace(/-(\D)/g, (a, b) => b.toUpperCase()); 6454 if (name === 'float') { 6455 name = 'cssFloat'; 6456 } 6457 return $elm.style ? $elm.style[name] : undefined; 6458 } 6459 }; 6460 const getSize = (elm) => { 6461 const $elm = get(elm); 6462 if (!$elm) { 6463 return { w: 0, h: 0 }; 6464 } 6465 let w = getStyle($elm, 'width'); 6466 let h = getStyle($elm, 'height'); 6467 // Non pixel value, then force offset/clientWidth 6468 if (!w || w.indexOf('px') === -1) { 6469 w = '0'; 6470 } 6471 // Non pixel value, then force offset/clientWidth 6472 if (!h || h.indexOf('px') === -1) { 6473 h = '0'; 6474 } 6475 return { 6476 w: parseInt(w, 10) || $elm.offsetWidth || $elm.clientWidth, 6477 h: parseInt(h, 10) || $elm.offsetHeight || $elm.clientHeight 6478 }; 6479 }; 6480 const getRect = (elm) => { 6481 const $elm = get(elm); 6482 const pos = getPos$1($elm); 6483 const size = getSize($elm); 6484 return { 6485 x: pos.x, y: pos.y, 6486 w: size.w, h: size.h 6487 }; 6488 }; 6489 const is = (elm, selector) => { 6490 if (!elm) { 6491 return false; 6492 } 6493 const elms = isArray$1(elm) ? elm : [elm]; 6494 return exists(elms, (e) => { 6495 return is$2(SugarElement.fromDom(e), selector); 6496 }); 6497 }; 6498 const getParents = (elm, selector, root, collect) => { 6499 const result = []; 6500 let node = get(elm); 6501 collect = collect === undefined; 6502 // Default root on inline mode 6503 const resolvedRoot = root || (getRoot().nodeName !== 'BODY' ? getRoot().parentNode : null); 6504 // Wrap node name as func 6505 if (isString(selector)) { 6506 if (selector === '*') { 6507 selector = isElement$7; 6508 } 6509 else { 6510 const selectorVal = selector; 6511 selector = (node) => is(node, selectorVal); 6512 } 6513 } 6514 while (node) { 6515 // TODO: Remove nullable check once TINY-6599 is complete 6516 if (node === resolvedRoot || isNullable(node.nodeType) || isDocument$1(node) || isDocumentFragment(node)) { 6517 break; 6518 } 6519 if (!selector || selector(node)) { 6520 if (collect) { 6521 result.push(node); 6522 } 6523 else { 6524 return [node]; 6525 } 6526 } 6527 node = node.parentNode; 6528 } 6529 return collect ? result : null; 6530 }; 6531 const getParent = (node, selector, root) => { 6532 const parents = getParents(node, selector, root, false); 6533 return parents && parents.length > 0 ? parents[0] : null; 6534 }; 6535 const _findSib = (node, selector, name) => { 6536 let func = selector; 6537 if (node) { 6538 // If expression make a function of it using is 6539 if (isString(selector)) { 6540 func = (node) => { 6541 return is(node, selector); 6542 }; 6543 } 6544 // Loop all siblings 6545 for (let tempNode = node[name]; tempNode; tempNode = tempNode[name]) { 6546 if (isFunction(func) && func(tempNode)) { 6547 return tempNode; 6548 } 6549 } 6550 } 6551 return null; 6552 }; 6553 const getNext = (node, selector) => _findSib(node, selector, 'nextSibling'); 6554 const getPrev = (node, selector) => _findSib(node, selector, 'previousSibling'); 6555 const isParentNode = (node) => isFunction(node.querySelectorAll); 6556 const select = (selector, scope) => { 6557 const elm = get(scope) ?? settings.root_element ?? doc; 6558 return isParentNode(elm) ? from(elm.querySelectorAll(selector)) : []; 6559 }; 6560 const run = function (elm, func, scope) { 6561 const context = scope ?? this; 6562 if (isArray$1(elm)) { 6563 const result = []; 6564 each$a(elm, (e, i) => { 6565 const node = get(e); 6566 if (node) { 6567 result.push(func.call(context, node, i)); 6568 } 6569 }); 6570 return result; 6571 } 6572 else { 6573 const node = get(elm); 6574 return !node ? false : func.call(context, node); 6575 } 6576 }; 6577 const setAttribs = (elm, attrs) => { 6578 run(elm, ($elm) => { 6579 each$d(attrs, (value, name) => { 6580 setAttrib($elm, name, value); 6581 }); 6582 }); 6583 }; 6584 const setHTML = (elm, html) => { 6585 run(elm, (e) => { 6586 const $elm = SugarElement.fromDom(e); 6587 set$3($elm, html); 6588 }); 6589 }; 6590 const add = (parentElm, name, attrs, html, create) => run(parentElm, (parentElm) => { 6591 const newElm = isString(name) ? doc.createElement(name) : name; 6592 if (isNonNullable(attrs)) { 6593 setAttribs(newElm, attrs); 6594 } 6595 if (html) { 6596 if (!isString(html) && html.nodeType) { 6597 newElm.appendChild(html); 6598 } 6599 else if (isString(html)) { 6600 setHTML(newElm, html); 6601 } 6602 } 6603 return !create ? parentElm.appendChild(newElm) : newElm; 6604 }); 6605 const create = (name, attrs, html) => add(doc.createElement(name), name, attrs, html, true); 6606 const decode = Entities.decode; 6607 const encode = Entities.encodeAllRaw; 6608 const createHTML = (name, attrs, html = '') => { 6609 let outHtml = '<' + name; 6610 for (const key in attrs) { 6611 if (hasNonNullableKey(attrs, key)) { 6612 outHtml += ' ' + key + '="' + encode(attrs[key]) + '"'; 6613 } 6614 } 6615 if (isEmpty$5(html) && has$2(schema.getVoidElements(), name)) { 6616 return outHtml + ' />'; 6617 } 6618 else { 6619 return outHtml + '>' + html + '</' + name + '>'; 6620 } 6621 }; 6622 const createFragment = (html) => { 6623 const container = doc.createElement('div'); 6624 const frag = doc.createDocumentFragment(); 6625 // Append the container to the fragment so as to remove it from 6626 // the current document context 6627 frag.appendChild(container); 6628 if (html) { 6629 container.innerHTML = html; 6630 } 6631 let node; 6632 while ((node = container.firstChild)) { 6633 frag.appendChild(node); 6634 } 6635 // Remove the container now that all the children have been transferred 6636 frag.removeChild(container); 6637 return frag; 6638 }; 6639 const remove = (node, keepChildren) => { 6640 return run(node, (n) => { 6641 const $node = SugarElement.fromDom(n); 6642 if (keepChildren) { 6643 // Unwrap but don't keep any empty text nodes 6644 each$e(children$1($node), (child) => { 6645 if (isText$c(child) && child.dom.length === 0) { 6646 remove$8(child); 6647 } 6648 else { 6649 before$4($node, child); 6650 } 6651 }); 6652 } 6653 remove$8($node); 6654 return $node.dom; 6655 }); 6656 }; 6657 const removeAllAttribs = (e) => run(e, (e) => { 6658 const attrs = e.attributes; 6659 for (let i = attrs.length - 1; i >= 0; i--) { 6660 e.removeAttributeNode(attrs.item(i)); 6661 } 6662 }); 6663 const parseStyle = (cssText) => styles.parse(cssText); 6664 const serializeStyle = (stylesArg, name) => styles.serialize(stylesArg, name); 6665 const addStyle = (cssText) => { 6666 // Prevent inline from loading the same styles twice 6667 if (self !== DOMUtils.DOM && doc === document) { 6668 if (addedStyles[cssText]) { 6669 return; 6670 } 6671 addedStyles[cssText] = true; 6672 } 6673 // Create style element if needed 6674 let styleElm = doc.getElementById('mceDefaultStyles'); 6675 if (!styleElm) { 6676 styleElm = doc.createElement('style'); 6677 styleElm.id = 'mceDefaultStyles'; 6678 styleElm.type = 'text/css'; 6679 const head = doc.head; 6680 if (head.firstChild) { 6681 head.insertBefore(styleElm, head.firstChild); 6682 } 6683 else { 6684 head.appendChild(styleElm); 6685 } 6686 } 6687 // Append style data to old or new style element 6688 if (styleElm.styleSheet) { 6689 styleElm.styleSheet.cssText += cssText; 6690 } 6691 else { 6692 styleElm.appendChild(doc.createTextNode(cssText)); 6693 } 6694 }; 6695 const loadCSS = (urls) => { 6696 if (!urls) { 6697 urls = ''; 6698 } 6699 each$e(urls.split(','), (url) => { 6700 files[url] = true; 6701 styleSheetLoader.load(url).catch(noop); 6702 }); 6703 }; 6704 const toggleClass = (elm, cls, state) => { 6705 run(elm, (e) => { 6706 if (isElement$7(e)) { 6707 const $elm = SugarElement.fromDom(e); 6708 // TINY-4520: DomQuery used to handle specifying multiple classes and the 6709 // formatter relies on it due to the changes made for TINY-7227 6710 const classes = cls.split(' '); 6711 each$e(classes, (c) => { 6712 if (isNonNullable(state)) { 6713 const fn = state ? add$2 : remove$4; 6714 fn($elm, c); 6715 } 6716 else { 6717 toggle$1($elm, c); 6718 } 6719 }); 6720 } 6721 }); 6722 }; 6723 const addClass = (elm, cls) => { 6724 toggleClass(elm, cls, true); 6725 }; 6726 const removeClass = (elm, cls) => { 6727 toggleClass(elm, cls, false); 6728 }; 6729 const hasClass = (elm, cls) => { 6730 const $elm = _get(elm); 6731 // TINY-4520: DomQuery used to handle specifying multiple classes and the 6732 // formatter relies on it due to the changes made for TINY-7227 6733 const classes = cls.split(' '); 6734 return isNonNullable($elm) && forall(classes, (c) => has($elm, c)); 6735 }; 6736 const show = (elm) => { 6737 run(elm, (e) => remove$7(SugarElement.fromDom(e), 'display')); 6738 }; 6739 const hide = (elm) => { 6740 run(elm, (e) => set$2
vendor: 48,653 bytes, lines 6740-7796
6740(SugarElement.fromDom(e), 'display', 'none')); 6741 }; 6742 const isHidden = (elm) => { 6743 const $elm = _get(elm); 6744 return isNonNullable($elm) && is$4(getRaw$1($elm, 'display'), 'none'); 6745 }; 6746 const uniqueId = (prefix) => (!prefix ? 'mce_' : prefix) + (counter++); 6747 const getOuterHTML = (elm) => { 6748 const $elm = _get(elm); 6749 if (isNonNullable($elm)) { 6750 return isElement$7($elm.dom) ? $elm.dom.outerHTML : getOuter($elm); 6751 } 6752 else { 6753 return ''; 6754 } 6755 }; 6756 const setOuterHTML = (elm, html) => { 6757 run(elm, ($elm) => { 6758 if (isElement$7($elm)) { 6759 $elm.outerHTML = html; 6760 } 6761 }); 6762 }; 6763 const insertAfter = (node, reference) => { 6764 const referenceNode = get(reference); 6765 return run(node, (node) => { 6766 const parent = referenceNode?.parentNode; 6767 const nextSibling = referenceNode?.nextSibling; 6768 if (parent) { 6769 if (nextSibling) { 6770 parent.insertBefore(node, nextSibling); 6771 } 6772 else { 6773 parent.appendChild(node); 6774 } 6775 } 6776 return node; 6777 }); 6778 }; 6779 const replace = (newElm, oldElm, keepChildren) => run(oldElm, (elm) => { 6780 const replacee = isArray$1(oldElm) ? newElm.cloneNode(true) : newElm; 6781 if (keepChildren) { 6782 each$a(grep(elm.childNodes), (node) => { 6783 replacee.appendChild(node); 6784 }); 6785 } 6786 elm.parentNode?.replaceChild(replacee, elm); 6787 return elm; 6788 }); 6789 const rename = (elm, name) => { 6790 if (elm.nodeName !== name.toUpperCase()) { 6791 // Rename block element 6792 const newElm = create(name); 6793 // Copy attribs to new block 6794 each$a(getAttribs(elm), (attrNode) => { 6795 setAttrib(newElm, attrNode.nodeName, getAttrib(elm, attrNode.nodeName)); 6796 }); 6797 // Replace block 6798 replace(newElm, elm, true); 6799 return newElm; 6800 } 6801 else { 6802 return elm; 6803 } 6804 }; 6805 const findCommonAncestor = (a, b) => { 6806 let ps = a; 6807 while (ps) { 6808 let pe = b; 6809 while (pe && ps !== pe) { 6810 pe = pe.parentNode; 6811 } 6812 if (ps === pe) { 6813 break; 6814 } 6815 ps = ps.parentNode; 6816 } 6817 if (!ps && a.ownerDocument) { 6818 return a.ownerDocument.documentElement; 6819 } 6820 else { 6821 return ps; 6822 } 6823 }; 6824 const isEmpty = (node, elements, options) => { 6825 if (isPlainObject(elements)) { 6826 const isContent = (node) => { 6827 const name = node.nodeName.toLowerCase(); 6828 return Boolean(elements[name]); 6829 }; 6830 return isEmptyNode(schema, node, { ...options, isContent }); 6831 } 6832 else { 6833 return isEmptyNode(schema, node, options); 6834 } 6835 }; 6836 const createRng = () => doc.createRange(); 6837 const split = (parentElm, splitElm, replacementElm) => { 6838 let range = createRng(); 6839 let beforeFragment; 6840 let afterFragment; 6841 if (parentElm && splitElm && parentElm.parentNode && splitElm.parentNode) { 6842 const parentNode = parentElm.parentNode; 6843 // Get before chunk 6844 range.setStart(parentNode, findNodeIndex(parentElm)); 6845 range.setEnd(splitElm.parentNode, findNodeIndex(splitElm)); 6846 beforeFragment = range.extractContents(); 6847 // Get after chunk 6848 range = createRng(); 6849 range.setStart(splitElm.parentNode, findNodeIndex(splitElm) + 1); 6850 range.setEnd(parentNode, findNodeIndex(parentElm) + 1); 6851 afterFragment = range.extractContents(); 6852 // Insert before chunk 6853 parentNode.insertBefore(trimNode(self, beforeFragment, schema), parentElm); 6854 // Insert middle chunk 6855 if (replacementElm) { 6856 parentNode.insertBefore(replacementElm, parentElm); 6857 // pa.replaceChild(replacementElm, splitElm); 6858 } 6859 else { 6860 parentNode.insertBefore(splitElm, parentElm); 6861 } 6862 // Insert after chunk 6863 parentNode.insertBefore(trimNode(self, afterFragment, schema), parentElm); 6864 remove(parentElm); 6865 return replacementElm || splitElm; 6866 } 6867 else { 6868 return undefined; 6869 } 6870 }; 6871 const bind = (target, name, func, scope) => { 6872 if (isArray$1(target)) { 6873 let i = target.length; 6874 const rv = []; 6875 while (i--) { 6876 rv[i] = bind(target[i], name, func, scope); 6877 } 6878 return rv; 6879 } 6880 else { 6881 // Collect all window/document events bound by editor instance 6882 if (settings.collect && (target === doc || target === win)) { 6883 boundEvents.push([target, name, func, scope]); 6884 } 6885 return events.bind(target, name, func, scope || self); 6886 } 6887 }; 6888 const unbind = (target, name, func) => { 6889 if (isArray$1(target)) { 6890 let i = target.length; 6891 const rv = []; 6892 while (i--) { 6893 rv[i] = unbind(target[i], name, func); 6894 } 6895 return rv; 6896 } 6897 else { 6898 // Remove any bound events matching the input 6899 if (boundEvents.length > 0 && (target === doc || target === win)) { 6900 let i = boundEvents.length; 6901 while (i--) { 6902 const [boundTarget, boundName, boundFunc] = boundEvents[i]; 6903 if (target === boundTarget && (!name || name === boundName) && (!func || func === boundFunc)) { 6904 events.unbind(boundTarget, boundName, boundFunc); 6905 } 6906 } 6907 } 6908 return events.unbind(target, name, func); 6909 } 6910 }; 6911 const dispatch = (target, name, evt) => events.dispatch(target, name, evt); 6912 const fire = (target, name, evt) => events.dispatch(target, name, evt); 6913 const getContentEditable = (node) => { 6914 if (node && isHTMLElement(node)) { 6915 // Check for fake content editable 6916 const contentEditable = node.getAttribute('data-mce-contenteditable'); 6917 if (contentEditable && contentEditable !== 'inherit') { 6918 return contentEditable; 6919 } 6920 // Check for real content editable 6921 return node.contentEditable !== 'inherit' ? node.contentEditable : null; 6922 } 6923 else { 6924 return null; 6925 } 6926 }; 6927 const getContentEditableParent = (node) => { 6928 const root = getRoot(); 6929 let state = null; 6930 for (let tempNode = node; tempNode && tempNode !== root; tempNode = tempNode.parentNode) { 6931 state = getContentEditable(tempNode); 6932 if (state !== null) { 6933 break; 6934 } 6935 } 6936 return state; 6937 }; 6938 const isEditable = (node) => { 6939 if (isNonNullable(node)) { 6940 const scope = isElement$7(node) ? node : node.parentElement; 6941 return isNonNullable(scope) && isHTMLElement(scope) && isEditable$2(SugarElement.fromDom(scope)); 6942 } 6943 else { 6944 return false; 6945 } 6946 }; 6947 const destroy = () => { 6948 // Unbind all events bound to window/document by editor instance 6949 if (boundEvents.length > 0) { 6950 let i = boundEvents.length; 6951 while (i--) { 6952 const [boundTarget, boundName, boundFunc] = boundEvents[i]; 6953 events.unbind(boundTarget, boundName, boundFunc); 6954 } 6955 } 6956 // Remove CSS files added to the dom 6957 each$d(files, (_, url) => { 6958 styleSheetLoader.unload(url); 6959 delete files[url]; 6960 }); 6961 }; 6962 const isChildOf = (node, parent) => { 6963 return node === parent || parent.contains(node); 6964 }; 6965 const dumpRng = (r) => ('startContainer: ' + r.startContainer.nodeName + 6966 ', startOffset: ' + r.startOffset + 6967 ', endContainer: ' + r.endContainer.nodeName + 6968 ', endOffset: ' + r.endOffset); 6969 // eslint-disable-next-line consistent-this 6970 const self = { 6971 doc, 6972 settings, 6973 win, 6974 files, 6975 stdMode, 6976 boxModel, 6977 styleSheetLoader, 6978 boundEvents, 6979 styles, 6980 schema, 6981 events, 6982 isBlock: isBlock, 6983 root: null, 6984 clone, 6985 /** 6986 * Returns the root node of the document. This is normally the body but might be a DIV. Parents like getParent will not 6987 * go above the point of this root node. 6988 * 6989 * @method getRoot 6990 * @return {Element} Root element for the utility class. 6991 */ 6992 getRoot, 6993 /** 6994 * Returns the viewport of the window. 6995 * 6996 * @method getViewPort 6997 * @param {Window} win Optional window to get viewport of. 6998 * @return {Object} Viewport object with fields x, y, w and h. 6999 */ 7000 getViewPort, 7001 /** 7002 * Returns the rectangle for a specific element. 7003 * 7004 * @method getRect 7005 * @param {Element/String} elm Element object or element ID to get rectangle from. 7006 * @return {Object} Rectangle for specified element object with x, y, w, h fields. 7007 */ 7008 getRect, 7009 /** 7010 * Returns the size dimensions of the specified element. 7011 * 7012 * @method getSize 7013 * @param {Element/String} elm Element object or element ID to get rectangle from. 7014 * @return {Object} Rectangle for specified element object with w, h fields. 7015 */ 7016 getSize, 7017 /** 7018 * Returns a node by the specified selector function. This function will 7019 * loop through all parent nodes and call the specified function for each node. 7020 * If the function then returns true indicating that it has found what it was looking for, the loop execution will then end 7021 * and the node it found will be returned. 7022 * 7023 * @method getParent 7024 * @param {Node/String} node DOM node to search parents on or ID string. 7025 * @param {Function} selector Selection function or CSS selector to execute on each node. 7026 * @param {Node} root Optional root element, never go beyond this point. 7027 * @return {Node} DOM Node or null if it wasn't found. 7028 */ 7029 getParent, 7030 /** 7031 * Returns a node list of all parents matching the specified selector function or pattern. 7032 * If the function then returns true indicating that it has found what it was looking for and that node will be collected. 7033 * 7034 * @method getParents 7035 * @param {Node/String} node DOM node to search parents on or ID string. 7036 * @param {Function} selector Selection function to execute on each node or CSS pattern. 7037 * @param {Node} root Optional root element, never go beyond this point. 7038 * @return {Array} Array of nodes or null if it wasn't found. 7039 */ 7040 getParents: getParents, 7041 /** 7042 * Returns the specified element by ID or the input element if it isn't a string. 7043 * 7044 * @method get 7045 * @param {String/Element} n Element id to look for or element to just pass though. 7046 * @return {Element} Element matching the specified id or null if it wasn't found. 7047 */ 7048 get, 7049 /** 7050 * Returns the next node that matches selector or function 7051 * 7052 * @method getNext 7053 * @param {Node} node Node to find siblings from. 7054 * @param {String/function} selector Selector CSS expression or function. 7055 * @return {Node} Next node item matching the selector or null if it wasn't found. 7056 */ 7057 getNext, 7058 /** 7059 * Returns the previous node that matches selector or function 7060 * 7061 * @method getPrev 7062 * @param {Node} node Node to find siblings from. 7063 * @param {String/function} selector Selector CSS expression or function. 7064 * @return {Node} Previous node item matching the selector or null if it wasn't found. 7065 */ 7066 getPrev, 7067 // #ifndef jquery 7068 /** 7069 * Returns a list of the elements specified by the given CSS selector. For example: `div#a1 p.test` 7070 * 7071 * @method select 7072 * @param {String} selector Target CSS selector. 7073 * @param {Object} scope Optional root element/scope element to search in. 7074 * @return {Array} Array with all matched elements. 7075 * @example 7076 * // Adds a class to all paragraphs in the currently active editor 7077 * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'someclass'); 7078 * 7079 * // Adds a class to all spans that have the test class in the currently active editor 7080 * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('span.test'), 'someclass') 7081 */ 7082 select, 7083 /** 7084 * Returns true/false if the specified element matches the specified css pattern. 7085 * 7086 * @method is 7087 * @param {Node/NodeList} elm DOM node to match or an array of nodes to match. 7088 * @param {String} selector CSS pattern to match the element against. 7089 */ 7090 is, 7091 // #endif 7092 /** 7093 * Adds the specified element to another element or elements. 7094 * 7095 * @method add 7096 * @param {String/Element/Array} parentElm Element id string, DOM node element or array of ids or elements to add to. 7097 * @param {String/Element} name Name of new element to add or existing element to add. 7098 * @param {Object} attrs Optional object collection with arguments to add to the new element(s). 7099 * @param {String} html Optional inner HTML contents to add for each element. 7100 * @param {Boolean} create Optional flag if the element should be created or added. 7101 * @return {Element/Array} Element that got created, or an array of created elements if multiple input elements 7102 * were passed in. 7103 * @example 7104 * // Adds a new paragraph to the end of the active editor 7105 * tinymce.activeEditor.dom.add(tinymce.activeEditor.getBody(), 'p', { title: 'my title' }, 'Some content'); 7106 */ 7107 add, 7108 /** 7109 * Creates a new element. 7110 * 7111 * @method create 7112 * @param {String} name Name of new element. 7113 * @param {Object} attrs Optional object name/value collection with element attributes. 7114 * @param {String} html Optional HTML string to set as inner HTML of the element. 7115 * @return {Element} HTML DOM node element that got created. 7116 * @example 7117 * // Adds an element where the caret/selection is in the active editor 7118 * var el = tinymce.activeEditor.dom.create('div', { id: 'test', 'class': 'myclass' }, 'some content'); 7119 * tinymce.activeEditor.selection.setNode(el); 7120 */ 7121 create, 7122 /** 7123 * Creates HTML string for element. The element will be closed unless an empty inner HTML string is passed in. 7124 * 7125 * @method createHTML 7126 * @param {String} name Name of new element. 7127 * @param {Object} attrs Optional object name/value collection with element attributes. 7128 * @param {String} html Optional HTML string to set as inner HTML of the element. 7129 * @return {String} String with new HTML element, for example: <a href="#">test</a>. 7130 * @example 7131 * // Creates a html chunk and inserts it at the current selection/caret location 7132 * tinymce.activeEditor.insertContent(tinymce.activeEditor.dom.createHTML('a', { href: 'test.html' }, 'some line')); 7133 */ 7134 createHTML, 7135 /** 7136 * Creates a document fragment out of the specified HTML string. 7137 * 7138 * @method createFragment 7139 * @param {String} html Html string to create fragment from. 7140 * @return {DocumentFragment} Document fragment node. 7141 */ 7142 createFragment, 7143 /** 7144 * Removes/deletes the specified element(s) from the DOM. 7145 * 7146 * @method remove 7147 * @param {String/Element/Array} node ID of element or DOM element object or array containing multiple elements/ids. 7148 * @param {Boolean} keepChildren Optional state to keep children or not. If set to true all children will be 7149 * placed at the location of the removed element. 7150 * @return {Element/Array} HTML DOM element that got removed, or an array of removed elements if multiple input elements 7151 * were passed in. 7152 * @example 7153 * // Removes all paragraphs in the active editor 7154 * tinymce.activeEditor.dom.remove(tinymce.activeEditor.dom.select('p')); 7155 * 7156 * // Removes an element by id in the document 7157 * tinymce.DOM.remove('mydiv'); 7158 */ 7159 remove, 7160 /** 7161 * Sets the CSS style value on a HTML element. The name can be a camelcase string 7162 * or the CSS style name like background-color. 7163 * 7164 * @method setStyle 7165 * @param {String/Element/Array} elm HTML element/Array of elements to set CSS style value on. 7166 * @param {String} name Name of the style value to set. 7167 * @param {String} value Value to set on the style. 7168 * @example 7169 * // Sets a style value on all paragraphs in the currently active editor 7170 * tinymce.activeEditor.dom.setStyle(tinymce.activeEditor.dom.select('p'), 'background-color', 'red'); 7171 * 7172 * // Sets a style value to an element by id in the current document 7173 * tinymce.DOM.setStyle('mydiv', 'background-color', 'red'); 7174 */ 7175 setStyle, 7176 /** 7177 * Returns the current style or runtime/computed value of an element. 7178 * 7179 * @method getStyle 7180 * @param {String/Element} elm HTML element or element id string to get style from. 7181 * @param {String} name Style name to return. 7182 * @param {Boolean} computed Computed style. 7183 * @return {String} Current style or computed style value of an element. 7184 */ 7185 getStyle: getStyle, 7186 /** 7187 * Sets multiple styles on the specified element(s). 7188 * 7189 * @method setStyles 7190 * @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set styles on. 7191 * @param {Object} styles Name/Value collection of style items to add to the element(s). 7192 * @example 7193 * // Sets styles on all paragraphs in the currently active editor 7194 * tinymce.activeEditor.dom.setStyles(tinymce.activeEditor.dom.select('p'), { 'background-color': 'red', 'color': 'green' }); 7195 * 7196 * // Sets styles to an element by id in the current document 7197 * tinymce.DOM.setStyles('mydiv', { 'background-color': 'red', 'color': 'green' }); 7198 */ 7199 setStyles, 7200 /** 7201 * Removes all attributes from an element or elements. 7202 * 7203 * @method removeAllAttribs 7204 * @param {Element/String/Array} e DOM element, element id string or array of elements/ids to remove attributes from. 7205 */ 7206 removeAllAttribs, 7207 /** 7208 * Sets the specified attribute of an element or elements. 7209 * 7210 * @method setAttrib 7211 * @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set attribute on. 7212 * @param {String} name Name of attribute to set. 7213 * @param {String} value Value to set on the attribute - if this value is falsy like null, 0 or '' it will remove 7214 * the attribute instead. 7215 * @example 7216 * // Sets class attribute on all paragraphs in the active editor 7217 * tinymce.activeEditor.dom.setAttrib(tinymce.activeEditor.dom.select('p'), 'class', 'myclass'); 7218 * 7219 * // Sets class attribute on a specific element in the current page 7220 * tinymce.dom.setAttrib('mydiv', 'class', 'myclass'); 7221 */ 7222 setAttrib, 7223 /** 7224 * Sets two or more specified attributes of an element or elements. 7225 * 7226 * @method setAttribs 7227 * @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set attributes on. 7228 * @param {Object} attrs Name/Value collection of attribute items to add to the element(s). 7229 * @example 7230 * // Sets class and title attributes on all paragraphs in the active editor 7231 * tinymce.activeEditor.dom.setAttribs(tinymce.activeEditor.dom.select('p'), { 'class': 'myclass', title: 'some title' }); 7232 * 7233 * // Sets class and title attributes on a specific element in the current page 7234 * tinymce.DOM.setAttribs('mydiv', { 'class': 'myclass', title: 'some title' }); 7235 */ 7236 setAttribs, 7237 /** 7238 * Returns the specified attribute by name. 7239 * 7240 * @method getAttrib 7241 * @param {String/Element} elm Element string id or DOM element to get attribute from. 7242 * @param {String} name Name of attribute to get. 7243 * @param {String} defaultVal Optional default value to return if the attribute didn't exist. 7244 * @return {String} Attribute value string, default value or null if the attribute wasn't found. 7245 */ 7246 getAttrib, 7247 /** 7248 * Returns the absolute x, y position of a node. The position will be returned in an object with x, y fields. 7249 * 7250 * @method getPos 7251 * @param {Element/String} elm HTML element or element id to get x, y position from. 7252 * @param {Element} rootElm Optional root element to stop calculations at. 7253 * @return {Object} Absolute position of the specified element object with x, y fields. 7254 */ 7255 getPos: getPos$1, 7256 /** 7257 * Parses the specified style value into an object collection. This parser will also 7258 * merge and remove any redundant items that browsers might have added. It will also convert non-hex 7259 * colors to hex values. Urls inside the styles will also be converted to absolute/relative based on settings. 7260 * 7261 * @method parseStyle 7262 * @param {String} cssText Style value to parse, for example: border:1px solid red;. 7263 * @return {Object} Object representation of that style, for example: {border: '1px solid red'} 7264 */ 7265 parseStyle, 7266 /** 7267 * Serializes the specified style object into a string. 7268 * 7269 * @method serializeStyle 7270 * @param {Object} styles Object to serialize as string, for example: {border: '1px solid red'} 7271 * @param {String} name Optional element name. 7272 * @return {String} String representation of the style object, for example: border: 1px solid red. 7273 */ 7274 serializeStyle, 7275 /** 7276 * Adds a style element at the top of the document with the specified cssText content. 7277 * 7278 * @method addStyle 7279 * @param {String} cssText CSS Text style to add to top of head of document. 7280 */ 7281 addStyle, 7282 /** 7283 * Imports/loads the specified CSS file into the document bound to the class. 7284 * 7285 * @method loadCSS 7286 * @param {String} url URL to CSS file to load. 7287 * @example 7288 * // Loads a CSS file dynamically into the current document 7289 * tinymce.DOM.loadCSS('somepath/some.css'); 7290 * 7291 * // Loads a CSS file into the currently active editor instance 7292 * tinymce.activeEditor.dom.loadCSS('somepath/some.css'); 7293 * 7294 * // Loads a CSS file into an editor instance by id 7295 * tinymce.get('someid').dom.loadCSS('somepath/some.css'); 7296 * 7297 * // Loads multiple CSS files into the current document 7298 * tinymce.DOM.loadCSS('somepath/some.css,somepath/someother.css'); 7299 */ 7300 loadCSS, 7301 /** 7302 * Adds a class to the specified element or elements. 7303 * 7304 * @method addClass 7305 * @param {String/Element/Array} elm Element ID string or DOM element or array with elements or IDs. 7306 * @param {String} cls Class name to add to each element. 7307 * @return {String/Array} String with new class value or array with new class values for all elements. 7308 * @example 7309 * // Adds a class to all paragraphs in the active editor 7310 * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'myclass'); 7311 * 7312 * // Adds a class to a specific element in the current page 7313 * tinymce.DOM.addClass('mydiv', 'myclass'); 7314 */ 7315 addClass, 7316 /** 7317 * Removes a class from the specified element or elements. 7318 * 7319 * @method removeClass 7320 * @param {String/Element/Array} elm Element ID string or DOM element or array with elements or IDs. 7321 * @param {String} cls Class name to remove from each element. 7322 * @return {String/Array} String of remaining class name(s), or an array of strings if multiple input elements 7323 * were passed in. 7324 * @example 7325 * // Removes a class from all paragraphs in the active editor 7326 * tinymce.activeEditor.dom.removeClass(tinymce.activeEditor.dom.select('p'), 'myclass'); 7327 * 7328 * // Removes a class from a specific element in the current page 7329 * tinymce.DOM.removeClass('mydiv', 'myclass'); 7330 */ 7331 removeClass, 7332 /** 7333 * Returns true if the specified element has the specified class. 7334 * 7335 * @method hasClass 7336 * @param {String/Element} elm HTML element or element id string to check CSS class on. 7337 * @param {String} cls CSS class to check for. 7338 * @return {Boolean} true/false if the specified element has the specified class. 7339 */ 7340 hasClass, 7341 /** 7342 * Toggles the specified class on/off. 7343 * 7344 * @method toggleClass 7345 * @param {Element} elm Element to toggle class on. 7346 * @param {String} cls Class to toggle on/off. 7347 * @param {Boolean} state Optional state to set. 7348 */ 7349 toggleClass, 7350 /** 7351 * Shows the specified element(s) by ID by setting the "display" style. 7352 * 7353 * @method show 7354 * @param {String/Element/Array} elm ID of DOM element or DOM element or array with elements or IDs to show. 7355 */ 7356 show, 7357 /** 7358 * Hides the specified element(s) by ID by setting the "display" style. 7359 * 7360 * @method hide 7361 * @param {String/Element/Array} elm ID of DOM element or DOM element or array with elements or IDs to hide. 7362 * @example 7363 * // Hides an element by id in the document 7364 * tinymce.DOM.hide('myid'); 7365 */ 7366 hide, 7367 /** 7368 * Returns true/false if the element is hidden or not by checking the "display" style. 7369 * 7370 * @method isHidden 7371 * @param {String/Element} elm Id or element to check display state on. 7372 * @return {Boolean} true/false if the element is hidden or not. 7373 */ 7374 isHidden, 7375 /** 7376 * Returns a unique id. This can be useful when generating elements on the fly. 7377 * This method will not check if the element already exists. 7378 * 7379 * @method uniqueId 7380 * @param {String} prefix Optional prefix to add in front of all ids - defaults to "mce_". 7381 * @return {String} Unique id. 7382 */ 7383 uniqueId, 7384 /** 7385 * Sets the specified HTML content inside the element or elements. The HTML will first be processed. This means 7386 * URLs will get converted, hex color values fixed etc. Check processHTML for details. 7387 * 7388 * @method setHTML 7389 * @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set HTML inside of. 7390 * @param {String} html HTML content to set as inner HTML of the element. 7391 * @example 7392 * // Sets the inner HTML of all paragraphs in the active editor 7393 * tinymce.activeEditor.dom.setHTML(tinymce.activeEditor.dom.select('p'), 'some inner html'); 7394 * 7395 * // Sets the inner HTML of an element by id in the document 7396 * tinymce.DOM.setHTML('mydiv', 'some inner html'); 7397 */ 7398 setHTML, 7399 /** 7400 * Returns the outer HTML of an element. 7401 * 7402 * @method getOuterHTML 7403 * @param {String/Element} elm Element ID or element object to get outer HTML from. 7404 * @return {String} Outer HTML string. 7405 * @example 7406 * tinymce.DOM.getOuterHTML(editorElement); 7407 * tinymce.activeEditor.getOuterHTML(tinymce.activeEditor.getBody()); 7408 */ 7409 getOuterHTML, 7410 /** 7411 * Sets the specified outer HTML on an element or elements. 7412 * 7413 * @method setOuterHTML 7414 * @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set outer HTML on. 7415 * @param {Object} html HTML code to set as outer value for the element. 7416 * @example 7417 * // Sets the outer HTML of all paragraphs in the active editor 7418 * tinymce.activeEditor.dom.setOuterHTML(tinymce.activeEditor.dom.select('p'), '<div>some html</div>'); 7419 * 7420 * // Sets the outer HTML of an element by id in the document 7421 * tinymce.DOM.setOuterHTML('mydiv', '<div>some html</div>'); 7422 */ 7423 setOuterHTML, 7424 /** 7425 * Entity decodes a string. This method decodes any HTML entities, such as `&aring;`. 7426 * 7427 * @method decode 7428 * @param {String} s String to decode entities on. 7429 * @return {String} Entity decoded string. 7430 */ 7431 decode, 7432 /** 7433 * Entity encodes a string. This method encodes the most common entities, such as `<`, `>`, `"` and `&`. 7434 * 7435 * @method encode 7436 * @param {String} text String to encode with entities. 7437 * @return {String} Entity encoded string. 7438 */ 7439 encode, 7440 /** 7441 * Inserts an element after the reference element. 7442 * 7443 * @method insertAfter 7444 * @param {Element} node Element to insert after the reference. 7445 * @param {Element/String/Array} referenceNode Reference element, element id or array of elements to insert after. 7446 * @return {Element/Array} Element that got added or an array with elements. 7447 */ 7448 insertAfter, 7449 /** 7450 * Replaces the specified element or elements with the new element specified. The new element will 7451 * be cloned if multiple input elements are passed in. 7452 * 7453 * @method replace 7454 * @param {Element} newElm New element to replace old ones with. 7455 * @param {Element/String/Array} oldElm Element DOM node, element id or array of elements or ids to replace. 7456 * @param {Boolean} keepChildren Optional keep children state, if set to true child nodes from the old object will be added 7457 * to new ones. 7458 */ 7459 replace, 7460 /** 7461 * Renames the specified element and keeps its attributes and children. 7462 * 7463 * @method rename 7464 * @param {Element} elm Element to rename. 7465 * @param {String} name Name of the new element. 7466 * @return {Element} New element or the old element if it needed renaming. 7467 */ 7468 rename, 7469 /** 7470 * Find the common ancestor of two elements. This is a shorter method than using the DOM Range logic. 7471 * 7472 * @method findCommonAncestor 7473 * @param {Element} a Element to find common ancestor of. 7474 * @param {Element} b Element to find common ancestor of. 7475 * @return {Element} Common ancestor element of the two input elements. 7476 */ 7477 findCommonAncestor, 7478 /** 7479 * Executes the specified function on the element by id or dom element node or array of elements/id. 7480 * 7481 * @method run 7482 * @param {String/Element/Array} elm ID or DOM element object or array with ids or elements. 7483 * @param {Function} func Function to execute for each item. 7484 * @param {Object} scope Optional scope to execute the function in. 7485 * @return {Object/Array} Single object, or an array of objects if multiple input elements were passed in. 7486 */ 7487 run, 7488 /** 7489 * Returns a NodeList with attributes for the element. 7490 * 7491 * @method getAttribs 7492 * @param {HTMLElement/string} elm Element node or string id to get attributes from. 7493 * @return {NodeList} NodeList with attributes. 7494 */ 7495 getAttribs, 7496 /** 7497 * Returns true/false if the specified node is to be considered empty or not. 7498 * 7499 * @method isEmpty 7500 * @param {Node} node The target node to check if it's empty. 7501 * @param {Object} elements Optional name/value object with elements that are automatically treated as non-empty elements. 7502 * @return {Boolean} true/false if the node is empty or not. 7503 * @example 7504 * tinymce.DOM.isEmpty(node, { img: true }); 7505 */ 7506 isEmpty, 7507 /** 7508 * Creates a new DOM Range object. This will use the native DOM Range API if it's 7509 * available. If it's not, it will fall back to the custom TinyMCE implementation. 7510 * 7511 * @method createRng 7512 * @return {DOMRange} DOM Range object. 7513 * @example 7514 * const rng = tinymce.DOM.createRng(); 7515 * alert(rng.startContainer + "," + rng.startOffset); 7516 */ 7517 createRng, 7518 /** 7519 * Returns the index of the specified node within its parent. 7520 * 7521 * @method nodeIndex 7522 * @param {Node} node Node to look for. 7523 * @param {Boolean} normalized Optional true/false state if the index is what it would be after a normalization. 7524 * @return {Number} Index of the specified node. 7525 */ 7526 nodeIndex: findNodeIndex, 7527 /** 7528 * Splits an element into two new elements and places the specified split 7529 * element or elements between the new ones. For example splitting the paragraph at the bold element in 7530 * this example `<p>abc<b>abc</b>123</p>` would produce `<p>abc</p><b>abc</b><p>123</p>`. 7531 * 7532 * @method split 7533 * @param {Element} parentElm Parent element to split. 7534 * @param {Element} splitElm Element to split at. 7535 * @param {Element} replacementElm Optional replacement element to replace the split element with. 7536 * @return {Element} Returns the split element or the replacement element if that is specified. 7537 */ 7538 split, 7539 /** 7540 * Adds an event handler to the specified object. 7541 * 7542 * @method bind 7543 * @param {Element/Document/Window/Array} target Target element to bind events to. 7544 * handler to or an array of elements/ids/documents. 7545 * @param {String} name Name of event handler to add, for example: click. 7546 * @param {Function} func Function to execute when the event occurs. 7547 * @param {Object} scope Optional scope to execute the function in. 7548 * @return {Function} Function callback handler the same as the one passed in. 7549 */ 7550 bind: bind, 7551 /** 7552 * Removes the specified event handler by name and function from an element or collection of elements. 7553 * 7554 * @method unbind 7555 * @param {Element/Document/Window/Array} target Target element to unbind events on. 7556 * @param {String} name Event handler name, for example: "click" 7557 * @param {Function} func Function to remove. 7558 * @return {Boolean/Array} Bool state of true if the handler was removed, or an array of states if multiple input elements 7559 * were passed in. 7560 */ 7561 unbind: unbind, 7562 /** 7563 * Fires the specified event name and optional object on the specified target. 7564 * <br> 7565 * <em>Deprecated in TinyMCE 6.0 and has been marked for removal in TinyMCE 7.0. Use <code>dispatch</code> instead.</em> 7566 * 7567 * @method fire 7568 * @param {Node/Document/Window} target Target element or object to fire event on. 7569 * @param {String} name Event name to fire. 7570 * @param {Object} evt Event object to send. 7571 * @return {Event} Event object. 7572 * @deprecated Use dispatch() instead 7573 */ 7574 fire, 7575 /** 7576 * Dispatches the specified event name and optional object on the specified target. 7577 * 7578 * @method dispatch 7579 * @param {Node/Document/Window} target Target element or object to dispatch event on. 7580 * @param {String} name Name of the event to fire. 7581 * @param {Object} evt Event object to send. 7582 * @return {Event} Event object. 7583 */ 7584 dispatch, 7585 // Returns the content editable state of a node 7586 getContentEditable, 7587 getContentEditableParent, 7588 /** 7589 * Checks if the specified node is editable within the given context of its parents. 7590 * 7591 * @method isEditable 7592 * @param {Node} node Node to check if it's editable. 7593 * @return {Boolean} Will be true if the node is editable and false if it's not editable. 7594 */ 7595 isEditable, 7596 /** 7597 * Destroys all internal references to the DOM to solve memory leak issues. 7598 * 7599 * @method destroy 7600 */ 7601 destroy, 7602 isChildOf, 7603 dumpRng 7604 }; 7605 const attrHooks = setupAttrHooks(styles, settings, constant(self)); 7606 return self; 7607 }; 7608 /** 7609 * Instance of DOMUtils for the current document. 7610 * 7611 * @static 7612 * @property DOM 7613 * @type tinymce.dom.DOMUtils 7614 * @example 7615 * // Example of how to add a class to some element by id 7616 * tinymce.DOM.addClass('someid', 'someclass'); 7617 */ 7618 DOMUtils.DOM = DOMUtils(document); 7619 DOMUtils.nodeIndex = findNodeIndex; 7620 7621 /** 7622 * This class handles asynchronous/synchronous loading of JavaScript files it will execute callbacks 7623 * when various items gets loaded. This class is useful to load external JavaScript files. 7624 * 7625 * @class tinymce.dom.ScriptLoader 7626 * @example 7627 * // Load a script from a specific URL using the global script loader 7628 * tinymce.ScriptLoader.load('somescript.js'); 7629 * 7630 * // Load a script using a unique instance of the script loader 7631 * const scriptLoader = new tinymce.dom.ScriptLoader(); 7632 * 7633 * scriptLoader.load('somescript.js'); 7634 * 7635 * // Load multiple scripts 7636 * scriptLoader.add('somescript1.js'); 7637 * scriptLoader.add('somescript2.js'); 7638 * scriptLoader.add('somescript3.js'); 7639 * 7640 * scriptLoader.loadQueue().then(() => { 7641 * alert('All scripts are now loaded.'); 7642 * }); 7643 */ 7644 const DOM$e = DOMUtils.DOM; 7645 const QUEUED = 0; 7646 const LOADING = 1; 7647 const LOADED = 2; 7648 const FAILED = 3; 7649 class ScriptLoader { 7650 static ScriptLoader = new ScriptLoader(); 7651 settings; 7652 states = {}; 7653 queue = []; 7654 scriptLoadedCallbacks = {}; 7655 queueLoadedCallbacks = []; 7656 loading = false; 7657 constructor(settings = {}) { 7658 this.settings = settings; 7659 } 7660 _setReferrerPolicy(referrerPolicy) { 7661 this.settings.referrerPolicy = referrerPolicy; 7662 } 7663 _setCrossOrigin(crossOrigin) { 7664 this.settings.crossOrigin = crossOrigin; 7665 } 7666 /** 7667 * Loads a specific script directly without adding it to the load queue. 7668 * 7669 * @method loadScript 7670 * @param {String} url Absolute URL to script to add. 7671 * @return {Promise} A promise that will resolve when the script loaded successfully or reject if it failed to load. 7672 */ 7673 loadScript(url) { 7674 return new Promise((resolve, reject) => { 7675 const dom = DOM$e; 7676 const doc = document; 7677 let elm; 7678 const cleanup = () => { 7679 dom.remove(id); 7680 if (elm) { 7681 elm.onerror = elm.onload = elm = null; 7682 } 7683 }; 7684 // Execute callback when script is loaded 7685 const done = () => { 7686 cleanup(); 7687 resolve(); 7688 }; 7689 const error = () => { 7690 // We can't mark it as done if there is a load error since 7691 // A) We don't want to produce 404 errors on the server and 7692 // B) the onerror event won't fire on all browsers. 7693 cleanup(); 7694 reject('Failed to load script: ' + url); 7695 }; 7696 const id = dom.uniqueId(); 7697 // Create new script element 7698 elm = doc.createElement('script'); 7699 elm.id = id; 7700 elm.type = 'text/javascript'; 7701 elm.src = Tools._addCacheSuffix(url); 7702 if (this.settings.referrerPolicy) { 7703 // Note: Don't use elm.referrerPolicy = ... here as it doesn't work on Safari 7704 dom.setAttrib(elm, 'referrerpolicy', this.settings.referrerPolicy); 7705 } 7706 const crossOrigin = this.settings.crossOrigin; 7707 if (isFunction(crossOrigin)) { 7708 const resultCrossOrigin = crossOrigin(url); 7709 if (resultCrossOrigin !== undefined) { 7710 dom.setAttrib(elm, 'crossorigin', resultCrossOrigin); 7711 } 7712 } 7713 elm.onload = done; 7714 // Add onerror event will get fired on some browsers but not all of them 7715 elm.onerror = error; 7716 // Add script to document 7717 (doc.head || doc.body).appendChild(elm); 7718 }); 7719 } 7720 /** 7721 * Returns true/false if a script has been loaded or not. 7722 * 7723 * @method isDone 7724 * @param {String} url URL to check for. 7725 * @return {Boolean} true/false if the URL is loaded. 7726 */ 7727 isDone(url) { 7728 return this.states[url] === LOADED; 7729 } 7730 /** 7731 * Marks a specific script to be loaded. This can be useful if a script got loaded outside 7732 * the script loader or to skip it from loading some script. 7733 * 7734 * @method markDone 7735 * @param {String} url Absolute URL to the script to mark as loaded. 7736 */ 7737 markDone(url) { 7738 this.states[url] = LOADED; 7739 } 7740 /** 7741 * Adds a specific script to the load queue of the script loader. 7742 * 7743 * @method add 7744 * @param {String} url Absolute URL to script to add. 7745 * @return {Promise} A promise that will resolve when the script loaded successfully or reject if it failed to load. 7746 */ 7747 add(url) { 7748 const self = this; 7749 self.queue.push(url); 7750 // Add url to load queue 7751 const state = self.states[url]; 7752 if (state === undefined) { 7753 self.states[url] = QUEUED; 7754 } 7755 return new Promise((resolve, reject) => { 7756 // Store away callback for later execution 7757 if (!self.scriptLoadedCallbacks[url]) { 7758 self.scriptLoadedCallbacks[url] = []; 7759 } 7760 self.scriptLoadedCallbacks[url].push({ 7761 resolve, 7762 reject 7763 }); 7764 }); 7765 } 7766 load(url) { 7767 return this.add(url); 7768 } 7769 remove(url) { 7770 delete this.states[url]; 7771 delete this.scriptLoadedCallbacks[url]; 7772 } 7773 /** 7774 * Starts the loading of the queue. 7775 * 7776 * @method loadQueue 7777 * @return {Promise} A promise that is resolved when all queued items are loaded or rejected with the script urls that failed to load. 7778 */ 7779 loadQueue() { 7780 const queue = this.queue; 7781 this.queue = []; 7782 return this.loadScripts(queue); 7783 } 7784 /** 7785 * Loads the specified queue of files and executes the callback ones they are loaded. 7786 * This method is generally not used outside this class but it might be useful in some scenarios. 7787 * 7788 * @method loadScripts 7789 * @param {Array} scripts Array of queue items to load. 7790 * @return {Promise} A promise that is resolved when all scripts are loaded or rejected with the script urls that failed to load. 7791 */ 7792 loadScripts(scripts) { 7793 const self = this; 7794 const execCallbacks = (name, url) => { 7795 // Execute URL callback functions 7796 get$
vendor: 29,189 bytes, lines 7796-8503
7796a(self.scriptLoadedCallbacks, url).each((callbacks) => { 7797 each$e(callbacks, (callback) => callback[name](url)); 7798 }); 7799 delete self.scriptLoadedCallbacks[url]; 7800 }; 7801 const processResults = (results) => { 7802 const failures = filter$5(results, (result) => result.status === 'rejected'); 7803 if (failures.length > 0) { 7804 return Promise.reject(bind$3(failures, ({ reason }) => isArray$1(reason) ? reason : [reason])); 7805 } 7806 else { 7807 return Promise.resolve(); 7808 } 7809 }; 7810 const load = (urls) => Promise.allSettled(map$3(urls, (url) => { 7811 // Script is already loaded then execute script callbacks directly 7812 if (self.states[url] === LOADED) { 7813 execCallbacks('resolve', url); 7814 return Promise.resolve(); 7815 } 7816 else if (self.states[url] === FAILED) { 7817 execCallbacks('reject', url); 7818 return Promise.reject(url); 7819 } 7820 else { 7821 // Script is not already loaded, so load it 7822 self.states[url] = LOADING; 7823 return self.loadScript(url).then(() => { 7824 self.states[url] = LOADED; 7825 execCallbacks('resolve', url); 7826 // Immediately load additional scripts if any were added to the queue while loading this script 7827 const queue = self.queue; 7828 if (queue.length > 0) { 7829 self.queue = []; 7830 return load(queue).then(processResults); 7831 } 7832 else { 7833 return Promise.resolve(); 7834 } 7835 }, () => { 7836 self.states[url] = FAILED; 7837 execCallbacks('reject', url); 7838 return Promise.reject(url); 7839 }); 7840 } 7841 })); 7842 const processQueue = (urls) => { 7843 self.loading = true; 7844 return load(urls).then((results) => { 7845 self.loading = false; 7846 // Start loading the next queued item 7847 const nextQueuedItem = self.queueLoadedCallbacks.shift(); 7848 Optional.from(nextQueuedItem).each(call); 7849 return processResults(results); 7850 }); 7851 }; 7852 // Wait for any other scripts to finish loading first, otherwise load immediately 7853 const uniqueScripts = stringArray(scripts); 7854 if (self.loading) { 7855 return new Promise((resolve, reject) => { 7856 self.queueLoadedCallbacks.push(() => { 7857 processQueue(uniqueScripts).then(resolve, reject); 7858 }); 7859 }); 7860 } 7861 else { 7862 return processQueue(uniqueScripts); 7863 } 7864 } 7865 /** 7866 * Returns the attributes that should be added to a script tag when loading the specified URL. 7867 * 7868 * @method getScriptAttributes 7869 * @param {String} url Url to get attributes for. 7870 * @return {Object} Object with attributes to add to the script tag. 7871 */ 7872 getScriptAttributes(url) { 7873 const attrs = {}; 7874 if (this.settings.referrerPolicy) { 7875 attrs.referrerpolicy = this.settings.referrerPolicy; 7876 } 7877 const crossOrigin = this.settings.crossOrigin; 7878 if (isFunction(crossOrigin)) { 7879 const resultCrossOrigin = crossOrigin(url); 7880 if (isString(resultCrossOrigin)) { 7881 attrs.crossorigin = resultCrossOrigin; 7882 } 7883 } 7884 return attrs; 7885 } 7886 } 7887 7888 const isDuplicated = (items, item) => { 7889 const firstIndex = items.indexOf(item); 7890 return firstIndex !== -1 && items.indexOf(item, firstIndex + 1) > firstIndex; 7891 }; 7892 const isRaw = (str) => isObject(str) && has$2(str, 'raw'); 7893 const isTokenised = (str) => isArray$1(str) && str.length > 1; 7894 const data = {}; 7895 const currentCode = Cell('en'); 7896 const getLanguageData = () => get$a(data, currentCode.get()); 7897 const getData$1 = () => map$2(data, (value) => ({ ...value })); 7898 /** 7899 * Sets the current language code. 7900 * 7901 * @method setCode 7902 * @param {String} newCode Current language code. 7903 */ 7904 const setCode = (newCode) => { 7905 if (newCode) { 7906 currentCode.set(newCode); 7907 } 7908 }; 7909 /** 7910 * Returns the current language code. 7911 * 7912 * @method getCode 7913 * @return {String} Current language code. 7914 */ 7915 const getCode = () => currentCode.get(); 7916 /** 7917 * Adds translations for a specific language code. 7918 * Translation keys are set to be case insensitive. 7919 * 7920 * @method add 7921 * @param {String} code Language code like sv_SE. 7922 * @param {Object} items Name/value object where key is english and value is the translation. 7923 */ 7924 const add = (code, items) => { 7925 let langData = data[code]; 7926 if (!langData) { 7927 data[code] = langData = {}; 7928 } 7929 const lcNames = map$3(keys(items), (name) => name.toLowerCase()); 7930 each$d(items, (translation, name) => { 7931 const lcName = name.toLowerCase(); 7932 if (lcName !== name && isDuplicated(lcNames, lcName)) { 7933 if (!has$2(items, lcName)) { 7934 langData[lcName] = translation; 7935 } 7936 langData[name] = translation; 7937 } 7938 else { 7939 langData[lcName] = translation; 7940 } 7941 }); 7942 }; 7943 /** 7944 * Translates the specified text. 7945 * 7946 * It has a few formats: 7947 * I18n.translate("Text"); 7948 * I18n.translate(["Text {0}/{1}", 0, 1]); 7949 * I18n.translate({raw: "Raw string"}); 7950 * 7951 * @method translate 7952 * @param {String/Object/Array} text Text to translate. 7953 * @return {String} String that got translated. 7954 */ 7955 const translate = (text) => { 7956 const langData = getLanguageData().getOr({}); 7957 /* 7958 * number - string 7959 * null, undefined and empty string - empty string 7960 * array - comma-delimited string 7961 * object - in [object Object] 7962 * function - in [object Function] 7963 */ 7964 const toString = (obj) => { 7965 if (isFunction(obj)) { 7966 return Object.prototype.toString.call(obj); 7967 } 7968 return !isEmpty(obj) ? '' + obj : ''; 7969 }; 7970 const isEmpty = (text) => text === '' || text === null || text === undefined; 7971 const getLangData = (text) => { 7972 // make sure we work on a string and return a string 7973 const textStr = toString(text); 7974 return has$2(langData, textStr) 7975 ? toString(langData[textStr]) 7976 : get$a(langData, textStr.toLowerCase()).map(toString).getOr(textStr); 7977 }; 7978 const removeContext = (str) => str.replace(/{context:\w+}$/, ''); 7979 const replaceWithEllipsisChar = (text) => text.replaceAll('...', ellipsis); 7980 // empty strings 7981 if (isEmpty(text)) { 7982 return ''; 7983 } 7984 // Raw, already translated 7985 if (isRaw(text)) { 7986 return replaceWithEllipsisChar(toString(text.raw)); 7987 } 7988 // Tokenised {translations} 7989 if (isTokenised(text)) { 7990 const values = text.slice(1); 7991 const substitued = getLangData(text[0]).replace(/\{([0-9]+)\}/g, ($1, $2) => has$2(values, $2) ? toString(values[$2]) : $1); 7992 return replaceWithEllipsisChar(removeContext(substitued)); 7993 } 7994 // straight forward translation mapping 7995 return replaceWithEllipsisChar(removeContext(getLangData(text))); 7996 }; 7997 /** 7998 * Returns true/false if the currently active language pack is rtl or not. 7999 * 8000 * @method isRtl 8001 * @return {Boolean} True if the current language pack is rtl. 8002 */ 8003 const isRtl$1 = () => getLanguageData() 8004 .bind((items) => get$a(items, '_dir')) 8005 .exists((dir) => dir === 'rtl'); 8006 /** 8007 * Returns true/false if specified language pack exists. 8008 * 8009 * @method hasCode 8010 * @param {String} code Code to check for. 8011 * @return {Boolean} True if the current language pack for the specified code exists. 8012 */ 8013 const hasCode = (code) => has$2(data, code); 8014 const I18n = { 8015 getData: getData$1, 8016 setCode, 8017 getCode, 8018 add, 8019 translate, 8020 isRtl: isRtl$1, 8021 hasCode 8022 }; 8023 8024 const AddOnManager = () => { 8025 const items = []; 8026 const urls = {}; 8027 const lookup = {}; 8028 const _listeners = []; 8029 const runListeners = (name, state) => { 8030 const matchedListeners = filter$5(_listeners, (listener) => listener.name === name && listener.state === state); 8031 each$e(matchedListeners, (listener) => listener.resolve()); 8032 }; 8033 const isLoaded = (name) => has$2(urls, name); 8034 const isAdded = (name) => has$2(lookup, name); 8035 const get = (name) => { 8036 if (lookup[name]) { 8037 return lookup[name].instance; 8038 } 8039 return undefined; 8040 }; 8041 const loadLanguagePack = (name, languages) => { 8042 const language = I18n.getCode(); 8043 const wrappedLanguages = ',' + (languages || '') + ','; 8044 if (!language || languages && wrappedLanguages.indexOf(',' + language + ',') === -1) { 8045 return; 8046 } 8047 // eslint-disable-next-line @typescript-eslint/no-floating-promises 8048 ScriptLoader.ScriptLoader.add(urls[name] + '/langs/' + language + '.js'); 8049 }; 8050 const requireLangPack = (name, languages) => { 8051 if (AddOnManager.languageLoad !== false) { 8052 if (isLoaded(name)) { 8053 loadLanguagePack(name, languages); 8054 } 8055 else { 8056 // eslint-disable-next-line @typescript-eslint/no-floating-promises 8057 waitFor(name, 'loaded').then(() => loadLanguagePack(name, languages)); 8058 } 8059 } 8060 }; 8061 const add = (id, addOn) => { 8062 items.push(addOn); 8063 lookup[id] = { instance: addOn }; 8064 runListeners(id, 'added'); 8065 return addOn; 8066 }; 8067 const remove = (name) => { 8068 delete urls[name]; 8069 delete lookup[name]; 8070 }; 8071 const createUrl = (baseUrl, dep) => { 8072 if (isString(dep)) { 8073 return isString(baseUrl) ? 8074 { prefix: '', resource: dep, suffix: '' } : 8075 { prefix: baseUrl.prefix, resource: dep, suffix: baseUrl.suffix }; 8076 } 8077 else { 8078 return dep; 8079 } 8080 }; 8081 const load = (name, addOnUrl) => { 8082 if (urls[name]) { 8083 return Promise.resolve(); 8084 } 8085 let urlString = isString(addOnUrl) ? addOnUrl : addOnUrl.prefix + addOnUrl.resource + addOnUrl.suffix; 8086 if (urlString.indexOf('/') !== 0 && urlString.indexOf('://') === -1) { 8087 urlString = AddOnManager.baseURL + '/' + urlString; 8088 } 8089 urls[name] = urlString.substring(0, urlString.lastIndexOf('/')); 8090 const done = () => { 8091 runListeners(name, 'loaded'); 8092 return Promise.resolve(); 8093 }; 8094 if (lookup[name]) { 8095 return done(); 8096 } 8097 else { 8098 return ScriptLoader.ScriptLoader.add(urlString).then(done); 8099 } 8100 }; 8101 const waitFor = (name, state = 'added') => { 8102 if (state === 'added' && isAdded(name)) { 8103 return Promise.resolve(); 8104 } 8105 else if (state === 'loaded' && isLoaded(name)) { 8106 return Promise.resolve(); 8107 } 8108 else { 8109 return new Promise((resolve) => { 8110 _listeners.push({ name, state, resolve }); 8111 }); 8112 } 8113 }; 8114 return { 8115 items, 8116 urls, 8117 lookup, 8118 /** 8119 * Returns the specified add on by the short name. 8120 * 8121 * @method get 8122 * @param {String} name Add-on to look for. 8123 * @return {tinymce.Theme/tinymce.Plugin} Theme or plugin add-on instance or undefined. 8124 */ 8125 get, 8126 /** 8127 * Loads a language pack for the specified add-on. 8128 * 8129 * @method requireLangPack 8130 * @param {String} name Short name of the add-on. 8131 * @param {String} languages Optional comma or space separated list of languages to check if it matches the name. 8132 */ 8133 requireLangPack, 8134 /** 8135 * Adds a instance of the add-on by it's short name. 8136 * 8137 * @method add 8138 * @param {String} id Short name/id for the add-on. 8139 * @param {tinymce.Theme/tinymce.Plugin} addOn Theme or plugin to add. 8140 * @return {tinymce.Theme/tinymce.Plugin} The same theme or plugin instance that got passed in. 8141 * @example 8142 * // Create a simple plugin 8143 * const TestPlugin = (ed, url) => { 8144 * ed.on('click', (e) => { 8145 * ed.windowManager.alert('Hello World!'); 8146 * }); 8147 * }; 8148 * 8149 * // Register plugin using the add method 8150 * tinymce.PluginManager.add('test', TestPlugin); 8151 * 8152 * // Initialize TinyMCE 8153 * tinymce.init({ 8154 * ... 8155 * plugins: '-test' // Init the plugin but don't try to load it 8156 * }); 8157 */ 8158 add, 8159 remove, 8160 createUrl, 8161 /** 8162 * Loads an add-on from a specific url. 8163 * 8164 * @method load 8165 * @param {String} name Short name of the add-on that gets loaded. 8166 * @param {String} addOnUrl URL to the add-on that will get loaded. 8167 * @return {Promise} A promise that will resolve when the add-on is loaded successfully or reject if it failed to load. 8168 * @example 8169 * // Loads a plugin from an external URL 8170 * tinymce.PluginManager.load('myplugin', '/some/dir/someplugin/plugin.js'); 8171 * 8172 * // Initialize TinyMCE 8173 * tinymce.init({ 8174 * ... 8175 * plugins: '-myplugin' // Don't try to load it again 8176 * }); 8177 */ 8178 load, 8179 waitFor 8180 }; 8181 }; 8182 AddOnManager.languageLoad = true; 8183 AddOnManager.baseURL = ''; 8184 AddOnManager.PluginManager = AddOnManager(); 8185 AddOnManager.ThemeManager = AddOnManager(); 8186 AddOnManager.ModelManager = AddOnManager(); 8187 8188 const annotation = constant('mce-annotation'); 8189 const dataAnnotation = constant('data-mce-annotation'); 8190 const dataAnnotationId = constant('data-mce-annotation-uid'); 8191 const dataAnnotationActive = constant('data-mce-annotation-active'); 8192 const dataAnnotationClasses = constant('data-mce-annotation-classes'); 8193 const dataAnnotationAttributes = constant('data-mce-annotation-attrs'); 8194 8195 const isRoot$1 = (root) => (node) => eq(node, root); 8196 // Given the current editor selection, identify the uid of any current 8197 // annotation 8198 const identify = (editor, annotationName) => { 8199 const rng = editor.selection.getRng(); 8200 const start = SugarElement.fromDom(rng.startContainer); 8201 const root = SugarElement.fromDom(editor.getBody()); 8202 const selector = annotationName.fold(() => '.' + annotation(), (an) => `[${dataAnnotation()}="${an}"]`); 8203 const newStart = child$1(start, rng.startOffset).getOr(start); 8204 const closest = closest$4(newStart, selector, isRoot$1(root)); 8205 return closest.bind((c) => getOpt(c, `${dataAnnotationId()}`).bind((uid) => getOpt(c, `${dataAnnotation()}`).map((name) => { 8206 const elements = findMarkers(editor, uid); 8207 return { 8208 uid, 8209 name, 8210 elements 8211 }; 8212 }))); 8213 }; 8214 const isAnnotation = (elem) => isElement$8(elem) && has(elem, annotation()); 8215 const isBogusElement = (elem, root) => has$1(elem, 'data-mce-bogus') || ancestor$1(elem, '[data-mce-bogus="all"]', isRoot$1(root)); 8216 const findMarkers = (editor, uid) => { 8217 const body = SugarElement.fromDom(editor.getBody()); 8218 const descendants$1 = descendants(body, `[${dataAnnotationId()}="${uid}"]`); 8219 return filter$5(descendants$1, (descendant) => !isBogusElement(descendant, body)); 8220 }; 8221 const findAll = (editor, name) => { 8222 const body = SugarElement.fromDom(editor.getBody()); 8223 const markers = descendants(body, `[${dataAnnotation()}="${name}"]`); 8224 const directory = {}; 8225 each$e(markers, (m) => { 8226 if (!isBogusElement(m, body)) { 8227 const uid = get$9(m, dataAnnotationId()); 8228 const nodesAlready = get$a(directory, uid).getOr([]); 8229 directory[uid] = nodesAlready.concat([m]); 8230 } 8231 }); 8232 return directory; 8233 }; 8234 8235 const setup$E = (editor, registry) => { 8236 const changeCallbacks = Cell({}); 8237 const initData = () => ({ 8238 listeners: [], 8239 previous: value$1() 8240 }); 8241 const withCallbacks = (name, f) => { 8242 updateCallbacks(name, (data) => { 8243 f(data); 8244 return data; 8245 }); 8246 }; 8247 const updateCallbacks = (name, f) => { 8248 const callbackMap = changeCallbacks.get(); 8249 const data = get$a(callbackMap, name).getOrThunk(initData); 8250 const outputData = f(data); 8251 callbackMap[name] = outputData; 8252 changeCallbacks.set(callbackMap); 8253 }; 8254 const fireCallbacks = (name, uid, elements) => { 8255 withCallbacks(name, (data) => { 8256 each$e(data.listeners, (f) => f(true, name, { 8257 uid, 8258 nodes: map$3(elements, (elem) => elem.dom) 8259 })); 8260 }); 8261 }; 8262 const fireNoAnnotation = (name) => { 8263 withCallbacks(name, (data) => { 8264 each$e(data.listeners, (f) => f(false, name)); 8265 }); 8266 }; 8267 const toggleActiveAttr = (uid, state) => { 8268 each$e(findMarkers(editor, uid), (elem) => { 8269 if (state) { 8270 set$4(elem, dataAnnotationActive(), 'true'); 8271 } 8272 else { 8273 remove$9(elem, dataAnnotationActive()); 8274 } 8275 }); 8276 }; 8277 // NOTE: Runs in alphabetical order. 8278 const onNodeChange = last$1(() => { 8279 const annotations = sort(registry.getNames()); 8280 each$e(annotations, (name) => { 8281 updateCallbacks(name, (data) => { 8282 const prev = data.previous.get(); 8283 identify(editor, Optional.some(name)).fold(() => { 8284 prev.each((uid) => { 8285 // Changed from something to nothing. 8286 fireNoAnnotation(name); 8287 data.previous.clear(); 8288 toggleActiveAttr(uid, false); 8289 }); 8290 }, ({ uid, name, elements }) => { 8291 // Changed from a different annotation (or nothing) 8292 if (!is$4(prev, uid)) { 8293 prev.each((uid) => toggleActiveAttr(uid, false)); 8294 fireCallbacks(name, uid, elements); 8295 data.previous.set(uid); 8296 toggleActiveAttr(uid, true); 8297 } 8298 }); 8299 return { 8300 previous: data.previous, 8301 listeners: data.listeners 8302 }; 8303 }); 8304 }); 8305 }, 30); 8306 editor.on('remove', () => { 8307 onNodeChange.cancel(); 8308 }); 8309 editor.on('NodeChange', () => { 8310 onNodeChange.throttle(); 8311 }); 8312 const addListener = (name, f) => { 8313 updateCallbacks(name, (data) => ({ 8314 previous: data.previous, 8315 listeners: data.listeners.concat([f]) 8316 })); 8317 }; 8318 return { 8319 addListener 8320 }; 8321 }; 8322 8323 const setup$D = (editor, registry) => { 8324 const dataAnnotation$1 = dataAnnotation(); 8325 const identifyParserNode = (node) => Optional.from(node.attr(dataAnnotation$1)).bind(registry.lookup); 8326 const removeDirectAnnotation = (node) => { 8327 node.attr(dataAnnotationId(), null); 8328 node.attr(dataAnnotation(), null); 8329 node.attr(dataAnnotationActive(), null); 8330 const customAttrNames = Optional.from(node.attr(dataAnnotationAttributes())).map((names) => names.split(',')).getOr([]); 8331 const customClasses = Optional.from(node.attr(dataAnnotationClasses())).map((names) => names.split(',')).getOr([]); 8332 each$e(customAttrNames, (name) => node.attr(name, null)); 8333 const classList = node.attr('class')?.split(' ') ?? []; 8334 const newClassList = difference(classList, [annotation()].concat(customClasses)); 8335 node.attr('class', newClassList.length > 0 ? newClassList.join(' ') : null); 8336 node.attr(dataAnnotationClasses(), null); 8337 node.attr(dataAnnotationAttributes(), null); 8338 }; 8339 editor.serializer.addTempAttr(dataAnnotationActive()); 8340 editor.serializer.addAttributeFilter(dataAnnotation$1, (nodes) => { 8341 for (const node of nodes) { 8342 identifyParserNode(node).each((settings) => { 8343 if (settings.persistent === false) { 8344 if (node.name === 'span') { 8345 node.unwrap(); 8346 } 8347 else { 8348 removeDirectAnnotation(node); 8349 } 8350 } 8351 }); 8352 } 8353 }); 8354 }; 8355 8356 const create$a = () => { 8357 const annotations = {}; 8358 const register = (name, settings) => { 8359 annotations[name] = { 8360 name, 8361 settings 8362 }; 8363 }; 8364 const lookup = (name) => get$a(annotations, name).map((a) => a.settings); 8365 const getNames = () => keys(annotations); 8366 return { 8367 register, 8368 lookup, 8369 getNames 8370 }; 8371 }; 8372 8373 const TextWalker = (startNode, rootNode, isBoundary = never) => { 8374 const walker = new DomTreeWalker(startNode, rootNode); 8375 const walk = (direction) => { 8376 let next; 8377 do { 8378 next = walker[direction](); 8379 } while (next && !isText$b(next) && !isBoundary(next)); 8380 return Optional.from(next).filter(isText$b); 8381 }; 8382 return { 8383 current: () => Optional.from(walker.current()).filter(isText$b), 8384 next: () => walk('next'), 8385 prev: () => walk('prev'), 8386 prev2: () => walk('prev2') 8387 }; 8388 }; 8389 8390 /** 8391 * The TextSeeker class enables you to seek for a specific point in text across the DOM. 8392 * 8393 * @class tinymce.dom.TextSeeker 8394 * @example 8395 * const seeker = tinymce.dom.TextSeeker(editor.dom); 8396 * const startOfWord = seeker.backwards(startNode, startOffset, (textNode, offset, text) => { 8397 * const lastSpaceCharIndex = text.lastIndexOf(' '); 8398 * if (lastSpaceCharIndex !== -1) { 8399 * return lastSpaceCharIndex + 1; 8400 * } else { 8401 * // No space found so continue searching 8402 * return -1; 8403 * } 8404 * }); 8405 */ 8406 /** 8407 * Constructs a new TextSeeker instance. 8408 * 8409 * @constructor 8410 * @param {tinymce.dom.DOMUtils} dom DOMUtils object reference. 8411 * @param {Function} isBoundary Optional function to determine if the seeker should continue to walk past the node provided. The default is to search until a block or <code>br</code> element is found. 8412 */ 8413 const TextSeeker = (dom, isBoundary) => { 8414 const isBlockBoundary = isBoundary ? isBoundary : (node) => dom.isBlock(node) || isBr$7(node) || isContentEditableFalse$a(node); 8415 const walk = (node, offset, walker, process) => { 8416 if (isText$b(node)) { 8417 const newOffset = process(node, offset, node.data); 8418 if (newOffset !== -1) { 8419 return Optional.some({ container: node, offset: newOffset }); 8420 } 8421 } 8422 return walker().bind((next) => walk(next.container, next.offset, walker, process)); 8423 }; 8424 /** 8425 * Search backwards through text nodes until a match, boundary, or root node has been found. 8426 * 8427 * @method backwards 8428 * @param {Node} node The node to start searching from. 8429 * @param {Number} offset The offset of the node to start searching from. 8430 * @param {Function} process A function that's passed the current text node, the current offset and the text content of the node. It should return the offset of the match or -1 to continue searching. 8431 * @param {Node} root An optional root node to constrain the search to. 8432 * @return {Object} An object containing the matched text node and offset. If no match is found, null will be returned. 8433 */ 8434 const backwards = (node, offset, process, root) => { 8435 const walker = TextWalker(node, root ?? dom.getRoot(), isBlockBoundary); 8436 return walk(node, offset, () => walker.prev().map((prev) => ({ container: prev, offset: prev.length })), process).getOrNull(); 8437 }; 8438 /** 8439 * Search forwards through text nodes until a match, boundary, or root node has been found. 8440 * 8441 * @method forwards 8442 * @param {Node} node The node to start searching from. 8443 * @param {Number} offset The offset of the node to start searching from. 8444 * @param {Function} process A function that's passed the current text node, the current offset and the text content of the node. It should return the offset of the match or -1 to continue searching. 8445 * @param {Node} root An optional root node to constrain the search to. 8446 * @return {Object} An object containing the matched text node and offset. If no match is found, null will be returned. 8447 */ 8448 const forwards = (node, offset, process, root) => { 8449 const walker = TextWalker(node, root ?? dom.getRoot(), isBlockBoundary); 8450 return walk(node, offset, () => walker.next().map((next) => ({ container: next, offset: 0 })), process).getOrNull(); 8451 }; 8452 return { 8453 backwards, 8454 forwards 8455 }; 8456 }; 8457 8458 const tableCells = ['td', 'th']; 8459 const tableSections = ['thead', 'tbody', 'tfoot']; 8460 const textBlocks = [ 8461 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'div', 'address', 'pre', 'form', 8462 'blockquote', 'center', 'dir', 'fieldset', 'header', 'footer', 'article', 8463 'section', 'hgroup', 'aside', 'nav', 'figure' 8464 ]; 8465 const listItems$1 = ['li', 'dd', 'dt']; 8466 const lists = ['ul', 'ol', 'dl']; 8467 const wsElements = ['pre', 'script', 'textarea', 'style']; 8468 const lazyLookup = (items) => { 8469 let lookup; 8470 return (node) => { 8471 lookup = lookup ? lookup : mapToObject(items, always); 8472 return has$2(lookup, name(node)); 8473 }; 8474 }; 8475 // WARNING: don't add anything to this file, the intention is to move these checks into the Schema 8476 const isTable$1 = (node) => name(node) === 'table'; 8477 const isBr$6 = (node) => isElement$8(node) && name(node) === 'br'; 8478 const isTextBlock$3 = lazyLookup(textBlocks); 8479 const isList$1 = lazyLookup(lists); 8480 const isListItem$2 = lazyLookup(listItems$1); 8481 const isTableSection = lazyLookup(tableSections); 8482 const isTableCell$2 = lazyLookup(tableCells); 8483 const isWsPreserveElement = lazyLookup(wsElements); 8484 8485 const getLastChildren$1 = (elm) => { 8486 const children = []; 8487 let rawNode = elm.dom; 8488 while (rawNode) { 8489 children.push(SugarElement.fromDom(rawNode)); 8490 rawNode = rawNode.lastChild; 8491 } 8492 return children; 8493 }; 8494 const removeTrailingBr = (elm) => { 8495 const allBrs = descendants(elm, 'br'); 8496 const brs = filter$5(getLastChildren$1(elm).slice(-1), isBr$6); 8497 if (allBrs.length === brs.length) { 8498 each$e(brs, remove$8); 8499 } 8500 }; 8501 const createPaddingBr = () => { 8502 const br = SugarElement.fromTag('br'); 8503 set$4
vendor: 49,750 bytes, lines 8503-9709
8503(br, 'data-mce-bogus', '1'); 8504 return br; 8505 }; 8506 const fillWithPaddingBr = (elm) => { 8507 empty(elm); 8508 append$1(elm, createPaddingBr()); 8509 }; 8510 const trimBlockTrailingBr = (elm, schema) => { 8511 lastChild(elm).each((lastChild) => { 8512 prevSibling(lastChild).each((lastChildPrevSibling) => { 8513 if (schema.isBlock(name(elm)) && isBr$6(lastChild) && schema.isBlock(name(lastChildPrevSibling))) { 8514 remove$8(lastChild); 8515 } 8516 }); 8517 }); 8518 }; 8519 8520 /** 8521 * Utility functions for working with zero width space 8522 * characters used as character containers etc. 8523 * 8524 * @private 8525 * @class tinymce.text.Zwsp 8526 * @example 8527 * const isZwsp = Zwsp.isZwsp('\uFEFF'); 8528 * const abc = Zwsp.trim('a\uFEFFc'); 8529 */ 8530 // This is technically not a ZWSP but a ZWNBSP or a BYTE ORDER MARK it used to be a ZWSP 8531 const ZWSP$1 = zeroWidth; 8532 const isZwsp = isZwsp$2; 8533 const trim$2 = removeZwsp; 8534 const insert$5 = (editor) => editor.insertContent(ZWSP$1, { preserve_zwsp: true }); 8535 8536 /** 8537 * This module handles caret containers. A caret container is a node that 8538 * holds the caret for positional purposes. 8539 * 8540 * @private 8541 * @class tinymce.caret.CaretContainer 8542 */ 8543 const isElement$6 = isElement$7; 8544 const isText$9 = isText$b; 8545 const isCaretContainerBlock$1 = (node) => { 8546 if (isText$9(node)) { 8547 node = node.parentNode; 8548 } 8549 return isElement$6(node) && node.hasAttribute('data-mce-caret'); 8550 }; 8551 const isCaretContainerInline = (node) => isText$9(node) && isZwsp(node.data); 8552 const isCaretContainer$2 = (node) => isCaretContainerBlock$1(node) || isCaretContainerInline(node); 8553 const hasContent = (node) => node.firstChild !== node.lastChild || !isBr$7(node.firstChild); 8554 const insertInline$1 = (node, before) => { 8555 const doc = node.ownerDocument ?? document; 8556 const textNode = doc.createTextNode(ZWSP$1); 8557 const parentNode = node.parentNode; 8558 if (!before) { 8559 const sibling = node.nextSibling; 8560 if (isText$9(sibling)) { 8561 if (isCaretContainer$2(sibling)) { 8562 return sibling; 8563 } 8564 if (startsWithCaretContainer$1(sibling)) { 8565 sibling.splitText(1); 8566 return sibling; 8567 } 8568 } 8569 if (node.nextSibling) { 8570 parentNode?.insertBefore(textNode, node.nextSibling); 8571 } 8572 else { 8573 parentNode?.appendChild(textNode); 8574 } 8575 } 8576 else { 8577 const sibling = node.previousSibling; 8578 if (isText$9(sibling)) { 8579 if (isCaretContainer$2(sibling)) { 8580 return sibling; 8581 } 8582 if (endsWithCaretContainer$1(sibling)) { 8583 return sibling.splitText(sibling.data.length - 1); 8584 } 8585 } 8586 parentNode?.insertBefore(textNode, node); 8587 } 8588 return textNode; 8589 }; 8590 const isBeforeInline = (pos) => { 8591 const container = pos.container(); 8592 if (!isText$b(container)) { 8593 return false; 8594 } 8595 // The text nodes may not be normalized, so check the current node and the previous one 8596 return container.data.charAt(pos.offset()) === ZWSP$1 || pos.isAtStart() && isCaretContainerInline(container.previousSibling); 8597 }; 8598 const isAfterInline = (pos) => { 8599 const container = pos.container(); 8600 if (!isText$b(container)) { 8601 return false; 8602 } 8603 // The text nodes may not be normalized, so check the current node and the next one 8604 return container.data.charAt(pos.offset() - 1) === ZWSP$1 || pos.isAtEnd() && isCaretContainerInline(container.nextSibling); 8605 }; 8606 const insertBlock = (blockName, node, before) => { 8607 const doc = node.ownerDocument ?? document; 8608 const blockNode = doc.createElement(blockName); 8609 blockNode.setAttribute('data-mce-caret', before ? 'before' : 'after'); 8610 blockNode.setAttribute('data-mce-bogus', 'all'); 8611 blockNode.appendChild(createPaddingBr().dom); 8612 const parentNode = node.parentNode; 8613 if (!before) { 8614 if (node.nextSibling) { 8615 parentNode?.insertBefore(blockNode, node.nextSibling); 8616 } 8617 else { 8618 parentNode?.appendChild(blockNode); 8619 } 8620 } 8621 else { 8622 parentNode?.insertBefore(blockNode, node); 8623 } 8624 return blockNode; 8625 }; 8626 const startsWithCaretContainer$1 = (node) => isText$9(node) && node.data[0] === ZWSP$1; 8627 const endsWithCaretContainer$1 = (node) => isText$9(node) && node.data[node.data.length - 1] === ZWSP$1; 8628 const trimBogusBr = (elm) => { 8629 const brs = elm.getElementsByTagName('br'); 8630 const lastBr = brs[brs.length - 1]; 8631 if (isBogus$1(lastBr)) { 8632 lastBr.parentNode?.removeChild(lastBr); 8633 } 8634 }; 8635 const showCaretContainerBlock = (caretContainer) => { 8636 if (caretContainer && caretContainer.hasAttribute('data-mce-caret')) { 8637 trimBogusBr(caretContainer); 8638 caretContainer.removeAttribute('data-mce-caret'); 8639 caretContainer.removeAttribute('data-mce-bogus'); 8640 caretContainer.removeAttribute('style'); 8641 caretContainer.removeAttribute('data-mce-style'); 8642 caretContainer.removeAttribute('_moz_abspos'); 8643 return caretContainer; 8644 } 8645 return null; 8646 }; 8647 const isRangeInCaretContainerBlock = (range) => isCaretContainerBlock$1(range.startContainer); 8648 8649 const round$2 = Math.round; 8650 const clone$1 = (rect) => { 8651 if (!rect) { 8652 return { left: 0, top: 0, bottom: 0, right: 0, width: 0, height: 0 }; 8653 } 8654 return { 8655 left: round$2(rect.left), 8656 top: round$2(rect.top), 8657 bottom: round$2(rect.bottom), 8658 right: round$2(rect.right), 8659 width: round$2(rect.width), 8660 height: round$2(rect.height) 8661 }; 8662 }; 8663 const collapse = (rect, toStart) => { 8664 rect = clone$1(rect); 8665 if (toStart) { 8666 rect.right = rect.left; 8667 } 8668 else { 8669 rect.left = rect.left + rect.width; 8670 rect.right = rect.left; 8671 } 8672 rect.width = 0; 8673 return rect; 8674 }; 8675 const isEqual = (rect1, rect2) => (rect1.left === rect2.left && 8676 rect1.top === rect2.top && 8677 rect1.bottom === rect2.bottom && 8678 rect1.right === rect2.right); 8679 const isValidOverflow = (overflowY, rect1, rect2) => overflowY >= 0 && overflowY <= Math.min(rect1.height, rect2.height) / 2; 8680 const isAbove$1 = (rect1, rect2) => { 8681 const halfHeight = Math.min(rect2.height / 2, rect1.height / 2); 8682 if ((rect1.bottom - halfHeight) < rect2.top) { 8683 return true; 8684 } 8685 if (rect1.top > rect2.bottom) { 8686 return false; 8687 } 8688 return isValidOverflow(rect2.top - rect1.bottom, rect1, rect2); 8689 }; 8690 const isBelow$1 = (rect1, rect2) => { 8691 if (rect1.top > rect2.bottom) { 8692 return true; 8693 } 8694 if (rect1.bottom < rect2.top) { 8695 return false; 8696 } 8697 return isValidOverflow(rect2.bottom - rect1.top, rect1, rect2); 8698 }; 8699 const containsXY = (rect, clientX, clientY) => (clientX >= rect.left && 8700 clientX <= rect.right && 8701 clientY >= rect.top && 8702 clientY <= rect.bottom); 8703 const boundingClientRectFromRects = (rects) => { 8704 return foldl(rects, (acc, rect) => { 8705 return acc.fold(() => Optional.some(rect), (prevRect) => { 8706 const left = Math.min(rect.left, prevRect.left); 8707 const top = Math.min(rect.top, prevRect.top); 8708 const right = Math.max(rect.right, prevRect.right); 8709 const bottom = Math.max(rect.bottom, prevRect.bottom); 8710 return Optional.some({ 8711 top, 8712 right, 8713 bottom, 8714 left, 8715 width: right - left, 8716 height: bottom - top 8717 }); 8718 }); 8719 }, Optional.none()); 8720 }; 8721 const distanceToRectEdgeFromXY = (rect, x, y) => { 8722 const cx = Math.max(Math.min(x, rect.left + rect.width), rect.left); 8723 const cy = Math.max(Math.min(y, rect.top + rect.height), rect.top); 8724 return Math.sqrt((x - cx) * (x - cx) + (y - cy) * (y - cy)); 8725 }; 8726 const overlapY = (r1, r2) => Math.max(0, Math.min(r1.bottom, r2.bottom) - Math.max(r1.top, r2.top)); 8727 8728 const getSelectedNode = (range) => { 8729 const startContainer = range.startContainer, startOffset = range.startOffset; 8730 if (startContainer === range.endContainer && startContainer.hasChildNodes() && range.endOffset === startOffset + 1) { 8731 return startContainer.childNodes[startOffset]; 8732 } 8733 return null; 8734 }; 8735 const getNode$1 = (container, offset) => { 8736 if (isElement$7(container) && container.hasChildNodes()) { 8737 const childNodes = container.childNodes; 8738 const safeOffset = clamp$2(offset, 0, childNodes.length - 1); 8739 return childNodes[safeOffset]; 8740 } 8741 else { 8742 return container; 8743 } 8744 }; 8745 /** @deprecated Use getNode instead */ 8746 const getNodeUnsafe = (container, offset) => { 8747 // If a negative offset is used on an element then `undefined` should be returned 8748 if (offset < 0 && isElement$7(container) && container.hasChildNodes()) { 8749 return undefined; 8750 } 8751 else { 8752 return getNode$1(container, offset); 8753 } 8754 }; 8755 8756 /** 8757 * This class contains logic for detecting extending characters. 8758 * 8759 * @private 8760 * @class tinymce.text.ExtendingChar 8761 * @example 8762 * const isExtending = ExtendingChar.isExtendingChar('a'); 8763 */ 8764 // Generated from: http://www.unicode.org/Public/UNIDATA/DerivedCoreProperties.txt 8765 // Only includes the characters in that fit into UCS-2 16 bit 8766 const extendingChars = new RegExp('[\u0300-\u036F\u0483-\u0487\u0488-\u0489\u0591-\u05BD\u05BF\u05C1-\u05C2\u05C4-\u05C5\u05C7\u0610-\u061A' + 8767 '\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7-\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0' + 8768 '\u07EB-\u07F3\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08E3-\u0902\u093A\u093C' + 8769 '\u0941-\u0948\u094D\u0951-\u0957\u0962-\u0963\u0981\u09BC\u09BE\u09C1-\u09C4\u09CD\u09D7\u09E2-\u09E3' + 8770 '\u0A01-\u0A02\u0A3C\u0A41-\u0A42\u0A47-\u0A48\u0A4B-\u0A4D\u0A51\u0A70-\u0A71\u0A75\u0A81-\u0A82\u0ABC' + 8771 '\u0AC1-\u0AC5\u0AC7-\u0AC8\u0ACD\u0AE2-\u0AE3\u0B01\u0B3C\u0B3E\u0B3F\u0B41-\u0B44\u0B4D\u0B56\u0B57' + 8772 '\u0B62-\u0B63\u0B82\u0BBE\u0BC0\u0BCD\u0BD7\u0C00\u0C3E-\u0C40\u0C46-\u0C48\u0C4A-\u0C4D\u0C55-\u0C56' + 8773 '\u0C62-\u0C63\u0C81\u0CBC\u0CBF\u0CC2\u0CC6\u0CCC-\u0CCD\u0CD5-\u0CD6\u0CE2-\u0CE3\u0D01\u0D3E\u0D41-\u0D44' + 8774 '\u0D4D\u0D57\u0D62-\u0D63\u0DCA\u0DCF\u0DD2-\u0DD4\u0DD6\u0DDF\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9' + 8775 '\u0EBB-\u0EBC\u0EC8-\u0ECD\u0F18-\u0F19\u0F35\u0F37\u0F39\u0F71-\u0F7E\u0F80-\u0F84\u0F86-\u0F87\u0F8D-\u0F97' + 8776 '\u0F99-\u0FBC\u0FC6\u102D-\u1030\u1032-\u1037\u1039-\u103A\u103D-\u103E\u1058-\u1059\u105E-\u1060\u1071-\u1074' + 8777 '\u1082\u1085-\u1086\u108D\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752-\u1753\u1772-\u1773\u17B4-\u17B5' + 8778 '\u17B7-\u17BD\u17C6\u17C9-\u17D3\u17DD\u180B-\u180D\u18A9\u1920-\u1922\u1927-\u1928\u1932\u1939-\u193B\u1A17-\u1A18' + 8779 '\u1A1B\u1A56\u1A58-\u1A5E\u1A60\u1A62\u1A65-\u1A6C\u1A73-\u1A7C\u1A7F\u1AB0-\u1ABD\u1ABE\u1B00-\u1B03\u1B34' + 8780 '\u1B36-\u1B3A\u1B3C\u1B42\u1B6B-\u1B73\u1B80-\u1B81\u1BA2-\u1BA5\u1BA8-\u1BA9\u1BAB-\u1BAD\u1BE6\u1BE8-\u1BE9' + 8781 '\u1BED\u1BEF-\u1BF1\u1C2C-\u1C33\u1C36-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE0\u1CE2-\u1CE8\u1CED\u1CF4\u1CF8-\u1CF9' + 8782 '\u1DC0-\u1DF5\u1DFC-\u1DFF\u200C-\u200D\u20D0-\u20DC\u20DD-\u20E0\u20E1\u20E2-\u20E4\u20E5-\u20F0\u2CEF-\u2CF1' + 8783 '\u2D7F\u2DE0-\u2DFF\u302A-\u302D\u302E-\u302F\u3099-\u309A\uA66F\uA670-\uA672\uA674-\uA67D\uA69E-\uA69F\uA6F0-\uA6F1' + 8784 '\uA802\uA806\uA80B\uA825-\uA826\uA8C4\uA8E0-\uA8F1\uA926-\uA92D\uA947-\uA951\uA980-\uA982\uA9B3\uA9B6-\uA9B9\uA9BC' + 8785 '\uA9E5\uAA29-\uAA2E\uAA31-\uAA32\uAA35-\uAA36\uAA43\uAA4C\uAA7C\uAAB0\uAAB2-\uAAB4\uAAB7-\uAAB8\uAABE-\uAABF\uAAC1' + 8786 '\uAAEC-\uAAED\uAAF6\uABE5\uABE8\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F\uFF9E-\uFF9F]'); 8787 const isExtendingChar = (ch) => isString(ch) && ch.charCodeAt(0) >= 768 && extendingChars.test(ch); 8788 8789 const or = (...args) => { 8790 return (x) => { 8791 for (let i = 0; i < args.length; i++) { 8792 if (args[i](x)) { 8793 return true; 8794 } 8795 } 8796 return false; 8797 }; 8798 }; 8799 const and = (...args) => { 8800 return (x) => { 8801 for (let i = 0; i < args.length; i++) { 8802 if (!args[i](x)) { 8803 return false; 8804 } 8805 } 8806 return true; 8807 }; 8808 }; 8809 8810 /** 8811 * This module contains logic for handling caret candidates. A caret candidate is 8812 * for example text nodes, images, input elements, cE=false elements etc. 8813 * 8814 * @private 8815 * @class tinymce.caret.CaretCandidate 8816 */ 8817 const isContentEditableTrue$2 = isContentEditableTrue$3; 8818 const isContentEditableFalse$9 = isContentEditableFalse$a; 8819 const isBr$5 = isBr$7; 8820 const isText$8 = isText$b; 8821 const isInvalidTextElement = matchNodeNames$1(['script', 'style', 'textarea']); 8822 const isAtomicInline = matchNodeNames$1(['img', 'input', 'textarea', 'hr', 'iframe', 'video', 'audio', 'object', 'embed']); 8823 const isTable = matchNodeNames$1(['table']); 8824 const isCaretContainer$1 = isCaretContainer$2; 8825 const isCaretCandidate$3 = (node) => { 8826 if (isCaretContainer$1(node)) { 8827 return false; 8828 } 8829 if (isText$8(node)) { 8830 return !isInvalidTextElement(node.parentNode); 8831 } 8832 return isAtomicInline(node) || isBr$5(node) || isTable(node) || isNonUiContentEditableFalse(node); 8833 }; 8834 // UI components on IE is marked with contenteditable=false and unselectable=true so lets not handle those as real content editables 8835 const isUnselectable = (node) => isElement$7(node) && node.getAttribute('unselectable') === 'true'; 8836 const isNonUiContentEditableFalse = (node) => !isUnselectable(node) && isContentEditableFalse$9(node); 8837 const isInEditable = (node, root) => { 8838 for (let tempNode = node.parentNode; tempNode && tempNode !== root; tempNode = tempNode.parentNode) { 8839 if (isNonUiContentEditableFalse(tempNode)) { 8840 return false; 8841 } 8842 if (isContentEditableTrue$2(tempNode)) { 8843 return true; 8844 } 8845 } 8846 return true; 8847 }; 8848 const isAtomicContentEditableFalse = (node) => { 8849 if (!isNonUiContentEditableFalse(node)) { 8850 return false; 8851 } 8852 return !foldl(from(node.getElementsByTagName('*')), (result, elm) => { 8853 return result || isContentEditableTrue$2(elm); 8854 }, false); 8855 }; 8856 const isAtomic$1 = (node) => isAtomicInline(node) || isAtomicContentEditableFalse(node); 8857 const isEditableCaretCandidate$1 = (node, root) => isCaretCandidate$3(node) && isInEditable(node, root); 8858 8859 /** 8860 * This module contains logic for creating caret positions within a document a caretposition 8861 * is similar to a DOMRange object but it doesn't have two endpoints and is also more lightweight 8862 * since it's now updated live when the DOM changes. 8863 * 8864 * @private 8865 * @class tinymce.caret.CaretPosition 8866 * @example 8867 * const caretPos1 = CaretPosition(container, offset); 8868 * const caretPos2 = CaretPosition.fromRangeStart(someRange); 8869 */ 8870 const isElement$5 = isElement$7; 8871 const isCaretCandidate$2 = isCaretCandidate$3; 8872 const isBlock$3 = matchStyleValues('display', 'block table'); 8873 const isFloated = matchStyleValues('float', 'left right'); 8874 const isValidElementCaretCandidate = and(isElement$5, isCaretCandidate$2, not(isFloated)); 8875 const isNotPre = not(matchStyleValues('white-space', 'pre pre-line pre-wrap')); 8876 const isText$7 = isText$b; 8877 const isBr$4 = isBr$7; 8878 const nodeIndex$1 = DOMUtils.nodeIndex; 8879 const resolveIndex$1 = getNodeUnsafe; 8880 const createRange$1 = (doc) => doc ? doc.createRange() : DOMUtils.DOM.createRng(); 8881 const isWhiteSpace$1 = (chr) => isString(chr) && /[\r\n\t ]/.test(chr); 8882 const isRange = (rng) => !!rng.setStart && !!rng.setEnd; 8883 const isHiddenWhiteSpaceRange = (range) => { 8884 const container = range.startContainer; 8885 const offset = range.startOffset; 8886 if (isWhiteSpace$1(range.toString()) && isNotPre(container.parentNode) && isText$b(container)) { 8887 const text = container.data; 8888 if (isWhiteSpace$1(text[offset - 1]) || isWhiteSpace$1(text[offset + 1])) { 8889 return true; 8890 } 8891 } 8892 return false; 8893 }; 8894 // Hack for older WebKit versions that doesn't 8895 // support getBoundingClientRect on BR elements 8896 const getBrClientRect = (brNode) => { 8897 const doc = brNode.ownerDocument; 8898 const rng = createRange$1(doc); 8899 const nbsp$1 = doc.createTextNode(nbsp); 8900 const parentNode = brNode.parentNode; 8901 parentNode.insertBefore(nbsp$1, brNode); 8902 rng.setStart(nbsp$1, 0); 8903 rng.setEnd(nbsp$1, 1); 8904 const clientRect = clone$1(rng.getBoundingClientRect()); 8905 parentNode.removeChild(nbsp$1); 8906 return clientRect; 8907 }; 8908 // Safari will not return a rect for <p>a<br>|b</p> for some odd reason 8909 const getBoundingClientRectWebKitText = (rng) => { 8910 const sc = rng.startContainer; 8911 const ec = rng.endContainer; 8912 const so = rng.startOffset; 8913 const eo = rng.endOffset; 8914 if (sc === ec && isText$b(ec) && so === 0 && eo === 1) { 8915 const newRng = rng.cloneRange(); 8916 newRng.setEndAfter(ec); 8917 return getBoundingClientRect$1(newRng); 8918 } 8919 else { 8920 return null; 8921 } 8922 }; 8923 const isZeroRect = (r) => r.left === 0 && r.right === 0 && r.top === 0 && r.bottom === 0; 8924 const getBoundingClientRect$1 = (item) => { 8925 let clientRect; 8926 const clientRects = item.getClientRects(); 8927 if (clientRects.length > 0) { 8928 clientRect = clone$1(clientRects[0]); 8929 } 8930 else { 8931 clientRect = clone$1(item.getBoundingClientRect()); 8932 } 8933 if (!isRange(item) && isBr$4(item) && isZeroRect(clientRect)) { 8934 return getBrClientRect(item); 8935 } 8936 if (isZeroRect(clientRect) && isRange(item)) { 8937 return getBoundingClientRectWebKitText(item) ?? clientRect; 8938 } 8939 return clientRect; 8940 }; 8941 const collapseAndInflateWidth = (clientRect, toStart) => { 8942 const newClientRect = collapse(clientRect, toStart); 8943 newClientRect.width = 1; 8944 newClientRect.right = newClientRect.left + 1; 8945 return newClientRect; 8946 }; 8947 const getCaretPositionClientRects = (caretPosition) => { 8948 const clientRects = []; 8949 const addUniqueAndValidRect = (clientRect) => { 8950 if (clientRect.height === 0) { 8951 return; 8952 } 8953 if (clientRects.length > 0) { 8954 if (isEqual(clientRect, clientRects[clientRects.length - 1])) { 8955 return; 8956 } 8957 } 8958 clientRects.push(clientRect); 8959 }; 8960 const addCharacterOffset = (container, offset) => { 8961 const range = createRange$1(container.ownerDocument); 8962 if (offset < container.data.length) { 8963 if (isExtendingChar(container.data[offset])) { 8964 return; 8965 } 8966 // WebKit returns two client rects for a position after an extending 8967 // character a\uxxx|b so expand on "b" and collapse to start of "b" box 8968 if (isExtendingChar(container.data[offset - 1])) { 8969 range.setStart(container, offset); 8970 range.setEnd(container, offset + 1); 8971 if (!isHiddenWhiteSpaceRange(range)) { 8972 addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect$1(range), false)); 8973 return; 8974 } 8975 } 8976 } 8977 if (offset > 0) { 8978 range.setStart(container, offset - 1); 8979 range.setEnd(container, offset); 8980 if (!isHiddenWhiteSpaceRange(range)) { 8981 addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect$1(range), false)); 8982 } 8983 } 8984 if (offset < container.data.length) { 8985 range.setStart(container, offset); 8986 range.setEnd(container, offset + 1); 8987 if (!isHiddenWhiteSpaceRange(range)) { 8988 addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect$1(range), true)); 8989 } 8990 } 8991 }; 8992 const container = caretPosition.container(); 8993 const offset = caretPosition.offset(); 8994 if (isText$7(container)) { 8995 addCharacterOffset(container, offset); 8996 return clientRects; 8997 } 8998 if (isElement$5(container)) { 8999 if (caretPosition.isAtEnd()) { 9000 const node = resolveIndex$1(container, offset); 9001 if (isText$7(node)) { 9002 addCharacterOffset(node, node.data.length); 9003 } 9004 if (isValidElementCaretCandidate(node) && !isBr$4(node)) { 9005 addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect$1(node), false)); 9006 } 9007 } 9008 else { 9009 const node = resolveIndex$1(container, offset); 9010 if (isText$7(node)) { 9011 addCharacterOffset(node, 0); 9012 } 9013 if (isValidElementCaretCandidate(node) && caretPosition.isAtEnd()) { 9014 addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect$1(node), false)); 9015 return clientRects; 9016 } 9017 const beforeNode = resolveIndex$1(caretPosition.container(), caretPosition.offset() - 1); 9018 if (isValidElementCaretCandidate(beforeNode) && !isBr$4(beforeNode)) { 9019 if (isBlock$3(beforeNode) || isBlock$3(node) || !isValidElementCaretCandidate(node)) { 9020 addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect$1(beforeNode), false)); 9021 } 9022 } 9023 if (isValidElementCaretCandidate(node)) { 9024 addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect$1(node), true)); 9025 } 9026 } 9027 } 9028 return clientRects; 9029 }; 9030 /** 9031 * Represents a location within the document by a container and an offset. 9032 * 9033 * @constructor 9034 * @param {Node} container Container node. 9035 * @param {Number} offset Offset within that container node. 9036 * @param {Array} clientRects Optional client rects array for the position. 9037 */ 9038 const CaretPosition = (container, offset, clientRects) => { 9039 const isAtStart = () => { 9040 if (isText$7(container)) { 9041 return offset === 0; 9042 } 9043 return offset === 0; 9044 }; 9045 const isAtEnd = () => { 9046 if (isText$7(container)) { 9047 return offset >= container.data.length; 9048 } 9049 return offset >= container.childNodes.length; 9050 }; 9051 const toRange = () => { 9052 const range = createRange$1(container.ownerDocument); 9053 range.setStart(container, offset); 9054 range.setEnd(container, offset); 9055 return range; 9056 }; 9057 const getClientRects = () => { 9058 if (!clientRects) { 9059 clientRects = getCaretPositionClientRects(CaretPosition(container, offset)); 9060 } 9061 return clientRects; 9062 }; 9063 const isVisible = () => getClientRects().length > 0; 9064 const isEqual = (caretPosition) => caretPosition && container === caretPosition.container() && offset === caretPosition.offset(); 9065 const getNode = (before) => resolveIndex$1(container, before ? offset - 1 : offset); 9066 return { 9067 /** 9068 * Returns the container node. 9069 * 9070 * @method container 9071 * @return {Node} Container node. 9072 */ 9073 container: constant(container), 9074 /** 9075 * Returns the offset within the container node. 9076 * 9077 * @method offset 9078 * @return {Number} Offset within the container node. 9079 */ 9080 offset: constant(offset), 9081 /** 9082 * Returns a range out of a the caret position. 9083 * 9084 * @method toRange 9085 * @return {DOMRange} range for the caret position. 9086 */ 9087 toRange, 9088 /** 9089 * Returns the client rects for the caret position. Might be multiple rects between 9090 * block elements. 9091 * 9092 * @method getClientRects 9093 * @return {Array} Array of client rects. 9094 */ 9095 getClientRects, 9096 /** 9097 * Returns true if the caret location is visible/displayed on screen. 9098 * 9099 * @method isVisible 9100 * @return {Boolean} true/false if the position is visible or not. 9101 */ 9102 isVisible, 9103 /** 9104 * Returns true if the caret location is at the beginning of text node or container. 9105 * 9106 * @method isVisible 9107 * @return {Boolean} true/false if the position is at the beginning. 9108 */ 9109 isAtStart, 9110 /** 9111 * Returns true if the caret location is at the end of text node or container. 9112 * 9113 * @method isVisible 9114 * @return {Boolean} true/false if the position is at the end. 9115 */ 9116 isAtEnd, 9117 /** 9118 * Compares the caret position to another caret position. This will only compare the 9119 * container and offset not it's visual position. 9120 * 9121 * @method isEqual 9122 * @param {tinymce.caret.CaretPosition} caretPosition Caret position to compare with. 9123 * @return {Boolean} true if the caret positions are equal. 9124 */ 9125 isEqual, 9126 /** 9127 * Returns the closest resolved node from a node index. That means if you have an offset after the 9128 * last node in a container it will return that last node. 9129 * 9130 * @method getNode 9131 * @return {Node} Node that is closest to the index. 9132 */ 9133 getNode 9134 }; 9135 }; 9136 /** 9137 * Creates a caret position from the start of a range. 9138 * 9139 * @method fromRangeStart 9140 * @param {DOMRange} range DOM Range to create caret position from. 9141 * @return {tinymce.caret.CaretPosition} Caret position from the start of DOM range. 9142 */ 9143 CaretPosition.fromRangeStart = (range) => CaretPosition(range.startContainer, range.startOffset); 9144 /** 9145 * Creates a caret position from the end of a range. 9146 * 9147 * @method fromRangeEnd 9148 * @param {DOMRange} range DOM Range to create caret position from. 9149 * @return {tinymce.caret.CaretPosition} Caret position from the end of DOM range. 9150 */ 9151 CaretPosition.fromRangeEnd = (range) => CaretPosition(range.endContainer, range.endOffset); 9152 /** 9153 * Creates a caret position from a node and places the offset after it. 9154 * 9155 * @method after 9156 * @param {Node} node Node to get caret position from. 9157 * @return {tinymce.caret.CaretPosition} Caret position from the node. 9158 */ 9159 // TODO: TINY-8865 - This may not be safe to cast as Node and alternative solutions need to be looked into 9160 CaretPosition.after = (node) => CaretPosition(node.parentNode, nodeIndex$1(node) + 1); 9161 /** 9162 * Creates a caret position from a node and places the offset before it. 9163 * 9164 * @method before 9165 * @param {Node} node Node to get caret position from. 9166 * @return {tinymce.caret.CaretPosition} Caret position from the node. 9167 */ 9168 // TODO: TINY-8865 - This may not be safe to cast as Node and alternative solutions need to be looked into 9169 CaretPosition.before = (node) => CaretPosition(node.parentNode, nodeIndex$1(node)); 9170 CaretPosition.isAbove = (pos1, pos2) => lift2(head(pos2.getClientRects()), last$2(pos1.getClientRects()), isAbove$1).getOr(false); 9171 CaretPosition.isBelow = (pos1, pos2) => lift2(last$2(pos2.getClientRects()), head(pos1.getClientRects()), isBelow$1).getOr(false); 9172 CaretPosition.isAtStart = (pos) => pos ? pos.isAtStart() : false; 9173 CaretPosition.isAtEnd = (pos) => pos ? pos.isAtEnd() : false; 9174 CaretPosition.isTextPosition = (pos) => pos ? isText$b(pos.container()) : false; 9175 CaretPosition.isElementPosition = (pos) => !CaretPosition.isTextPosition(pos); 9176 9177 const trimEmptyTextNode$1 = (dom, node) => { 9178 if (isText$b(node) && node.data.length === 0) { 9179 dom.remove(node); 9180 } 9181 }; 9182 const insertNode = (dom, rng, node) => { 9183 rng.insertNode(node); 9184 trimEmptyTextNode$1(dom, node.previousSibling); 9185 trimEmptyTextNode$1(dom, node.nextSibling); 9186 }; 9187 const insertFragment = (dom, rng, frag) => { 9188 const firstChild = Optional.from(frag.firstChild); 9189 const lastChild = Optional.from(frag.lastChild); 9190 rng.insertNode(frag); 9191 firstChild.each((child) => trimEmptyTextNode$1(dom, child.previousSibling)); 9192 lastChild.each((child) => trimEmptyTextNode$1(dom, child.nextSibling)); 9193 }; 9194 // Wrapper to Range.insertNode which removes any empty text nodes created in the process. 9195 // Doesn't merge adjacent text nodes - this is according to the DOM spec. 9196 const rangeInsertNode = (dom, rng, node) => { 9197 if (isDocumentFragment(node)) { 9198 insertFragment(dom, rng, node); 9199 } 9200 else { 9201 insertNode(dom, rng, node); 9202 } 9203 }; 9204 9205 /** 9206 * This module creates or resolves xpath like string representation of a CaretPositions. 9207 * 9208 * The format is a / separated list of chunks with: 9209 * <element|text()>[index|after|before] 9210 * 9211 * For example: 9212 * p[0]/b[0]/text()[0],1 = <p><b>a|c</b></p> 9213 * p[0]/img[0],before = <p>|<img></p> 9214 * p[0]/img[0],after = <p><img>|</p> 9215 * 9216 * @private 9217 * @static 9218 * @class tinymce.caret.CaretBookmark 9219 * @example 9220 * const bookmark = CaretBookmark.create(rootElm, CaretPosition.before(rootElm.firstChild)); 9221 * const caretPosition = CaretBookmark.resolve(bookmark); 9222 */ 9223 const isText$6 = isText$b; 9224 const isBogus = isBogus$1; 9225 const nodeIndex = DOMUtils.nodeIndex; 9226 const normalizedParent = (node) => { 9227 const parentNode = node.parentNode; 9228 if (isBogus(parentNode)) { 9229 return normalizedParent(parentNode); 9230 } 9231 return parentNode; 9232 }; 9233 const getChildNodes = (node) => { 9234 if (!node) { 9235 return []; 9236 } 9237 return reduce(node.childNodes, (result, node) => { 9238 if (isBogus(node) && node.nodeName !== 'BR') { 9239 result = result.concat(getChildNodes(node)); 9240 } 9241 else { 9242 result.push(node); 9243 } 9244 return result; 9245 }, []); 9246 }; 9247 const normalizedTextOffset = (node, offset) => { 9248 let tempNode = node; 9249 while ((tempNode = tempNode.previousSibling)) { 9250 if (!isText$6(tempNode)) { 9251 break; 9252 } 9253 offset += tempNode.data.length; 9254 } 9255 return offset; 9256 }; 9257 const equal = (a) => (b) => a === b; 9258 const normalizedNodeIndex = (node) => { 9259 let nodes, index; 9260 nodes = getChildNodes(normalizedParent(node)); 9261 index = findIndex$1(nodes, equal(node), node); 9262 nodes = nodes.slice(0, index + 1); 9263 const numTextFragments = reduce(nodes, (result, node, i) => { 9264 if (isText$6(node) && isText$6(nodes[i - 1])) { 9265 result++; 9266 } 9267 return result; 9268 }, 0); 9269 nodes = filter$3(nodes, matchNodeNames$1([node.nodeName])); 9270 index = findIndex$1(nodes, equal(node), node); 9271 return index - numTextFragments; 9272 }; 9273 const createPathItem = (node) => { 9274 const name = isText$6(node) ? 'text()' : node.nodeName.toLowerCase(); 9275 return name + '[' + normalizedNodeIndex(node) + ']'; 9276 }; 9277 const parentsUntil$1 = (root, node, predicate) => { 9278 const parents = []; 9279 for (let tempNode = node.parentNode; tempNode && tempNode !== root; tempNode = tempNode.parentNode) { 9280 if (predicate && predicate(tempNode)) { 9281 break; 9282 } 9283 parents.push(tempNode); 9284 } 9285 return parents; 9286 }; 9287 const create$9 = (root, caretPosition) => { 9288 let path = []; 9289 let container = caretPosition.container(); 9290 let offset = caretPosition.offset(); 9291 let outputOffset; 9292 if (isText$6(container)) { 9293 outputOffset = normalizedTextOffset(container, offset); 9294 } 9295 else { 9296 const childNodes = container.childNodes; 9297 if (offset >= childNodes.length) { 9298 outputOffset = 'after'; 9299 offset = childNodes.length - 1; 9300 } 9301 else { 9302 outputOffset = 'before'; 9303 } 9304 container = childNodes[offset]; 9305 } 9306 path.push(createPathItem(container)); 9307 let parents = parentsUntil$1(root, container); 9308 parents = filter$3(parents, not(isBogus$1)); 9309 path = path.concat(map$1(parents, (node) => { 9310 return createPathItem(node); 9311 })); 9312 return path.reverse().join('/') + ',' + outputOffset; 9313 }; 9314 const resolvePathItem = (node, name, index) => { 9315 let nodes = getChildNodes(node); 9316 nodes = filter$3(nodes, (node, index) => { 9317 return !isText$6(node) || !isText$6(nodes[index - 1]); 9318 }); 9319 nodes = filter$3(nodes, matchNodeNames$1([name])); 9320 return nodes[index]; 9321 }; 9322 const findTextPosition = (container, offset) => { 9323 let node = container; 9324 let targetOffset = 0; 9325 while (isText$6(node)) { 9326 const dataLen = node.data.length; 9327 if (offset >= targetOffset && offset <= targetOffset + dataLen) { 9328 container = node; 9329 offset = offset - targetOffset; 9330 break; 9331 } 9332 if (!isText$6(node.nextSibling)) { 9333 container = node; 9334 offset = dataLen; 9335 break; 9336 } 9337 targetOffset += dataLen; 9338 node = node.nextSibling; 9339 } 9340 if (isText$6(container) && offset > container.data.length) { 9341 offset = container.data.length; 9342 } 9343 return CaretPosition(container, offset); 9344 }; 9345 const resolve$1 = (root, path) => { 9346 if (!path) { 9347 return null; 9348 } 9349 const parts = path.split(','); 9350 const paths = parts[0].split('/'); 9351 const offset = parts.length > 1 ? parts[1] : 'before'; 9352 const container = reduce(paths, (result, value) => { 9353 const match = /([\w\-\(\)]+)\[([0-9]+)\]/.exec(value); 9354 if (!match) { 9355 return null; 9356 } 9357 if (match[1] === 'text()') { 9358 match[1] = '#text'; 9359 } 9360 return resolvePathItem(result, match[1], parseInt(match[2], 10)); 9361 }, root); 9362 if (!container) { 9363 return null; 9364 } 9365 if (!isText$6(container) && container.parentNode) { 9366 let nodeOffset; 9367 if (offset === 'after') { 9368 nodeOffset = nodeIndex(container) + 1; 9369 } 9370 else { 9371 nodeOffset = nodeIndex(container); 9372 } 9373 return CaretPosition(container.parentNode, nodeOffset); 9374 } 9375 return findTextPosition(container, parseInt(offset, 10)); 9376 }; 9377 9378 const isContentEditableFalse$8 = isContentEditableFalse$a; 9379 const getNormalizedTextOffset$1 = (trim, container, offset) => { 9380 let trimmedOffset = trim(container.data.slice(0, offset)).length; 9381 for (let node = container.previousSibling; node && isText$b(node); node = node.previousSibling) { 9382 trimmedOffset += trim(node.data).length; 9383 } 9384 return trimmedOffset; 9385 }; 9386 const getPoint = (dom, trim, normalized, rng, start) => { 9387 const container = start ? rng.startContainer : rng.endContainer; 9388 let offset = start ? rng.startOffset : rng.endOffset; 9389 const point = []; 9390 const root = dom.getRoot(); 9391 if (isText$b(container)) { 9392 point.push(normalized ? getNormalizedTextOffset$1(trim, container, offset) : offset); 9393 } 9394 else { 9395 let after = 0; 9396 const childNodes = container.childNodes; 9397 if (offset >= childNodes.length && childNodes.length) { 9398 after = 1; 9399 offset = Math.max(0, childNodes.length - 1); 9400 } 9401 point.push(dom.nodeIndex(childNodes[offset], normalized) + after); 9402 } 9403 for (let node = container; node && node !== root; node = node.parentNode) { 9404 point.push(dom.nodeIndex(node, normalized)); 9405 } 9406 return point; 9407 }; 9408 const getLocation = (trim, selection, normalized, rng) => { 9409 const dom = selection.dom; 9410 const start = getPoint(dom, trim, normalized, rng, true); 9411 const forward = selection.isForward(); 9412 const fakeCaret = isRangeInCaretContainerBlock(rng) ? { isFakeCaret: true } : {}; 9413 if (!selection.isCollapsed()) { 9414 const end = getPoint(dom, trim, normalized, rng, false); 9415 return { start, end, forward, ...fakeCaret }; 9416 } 9417 else { 9418 return { start, forward, ...fakeCaret }; 9419 } 9420 }; 9421 const findIndex = (dom, name, element) => { 9422 let count = 0; 9423 Tools.each(dom.select(name), (node) => { 9424 if (node.getAttribute('data-mce-bogus') === 'all') { 9425 return; 9426 } 9427 else if (node === element) { 9428 return false; 9429 } 9430 else { 9431 count++; 9432 return; 9433 } 9434 }); 9435 return count; 9436 }; 9437 const moveEndPoint$1 = (rng, start) => { 9438 let container = start ? rng.startContainer : rng.endContainer; 9439 let offset = start ? rng.startOffset : rng.endOffset; 9440 // normalize Table Cell selection 9441 if (isElement$7(container) && container.nodeName === 'TR') { 9442 const childNodes = container.childNodes; 9443 container = childNodes[Math.min(start ? offset : offset - 1, childNodes.length - 1)]; 9444 if (container) { 9445 offset = start ? 0 : container.childNodes.length; 9446 if (start) { 9447 rng.setStart(container, offset); 9448 } 9449 else { 9450 rng.setEnd(container, offset); 9451 } 9452 } 9453 } 9454 }; 9455 const normalizeTableCellSelection = (rng) => { 9456 moveEndPoint$1(rng, true); 9457 moveEndPoint$1(rng, false); 9458 return rng; 9459 }; 9460 const findSibling = (node, offset) => { 9461 if (isElement$7(node)) { 9462 node = getNode$1(node, offset); 9463 if (isContentEditableFalse$8(node)) { 9464 return node; 9465 } 9466 } 9467 if (isCaretContainer$2(node)) { 9468 if (isText$b(node) && isCaretContainerBlock$1(node)) { 9469 node = node.parentNode; 9470 } 9471 let sibling = node.previousSibling; 9472 if (isContentEditableFalse$8(sibling)) { 9473 return sibling; 9474 } 9475 sibling = node.nextSibling; 9476 if (isContentEditableFalse$8(sibling)) { 9477 return sibling; 9478 } 9479 } 9480 return undefined; 9481 }; 9482 const findAdjacentContentEditableFalseElm = (rng) => { 9483 return findSibling(rng.startContainer, rng.startOffset) || findSibling(rng.endContainer, rng.endOffset); 9484 }; 9485 const getOffsetBookmark = (trim, normalized, selection) => { 9486 const element = selection.getNode(); 9487 const rng = selection.getRng(); 9488 if (element.nodeName === 'IMG' || isContentEditableFalse$8(element)) { 9489 const name = element.nodeName; 9490 return { name, index: findIndex(selection.dom, name, element) }; 9491 } 9492 const sibling = findAdjacentContentEditableFalseElm(rng); 9493 if (sibling) { 9494 const name = sibling.tagName; 9495 return { name, index: findIndex(selection.dom, name, sibling) }; 9496 } 9497 return getLocation(trim, selection, normalized, rng); 9498 }; 9499 const getCaretBookmark = (selection) => { 9500 const rng = selection.getRng(); 9501 return { 9502 start: create$9(selection.dom.getRoot(), CaretPosition.fromRangeStart(rng)), 9503 end: create$9(selection.dom.getRoot(), CaretPosition.fromRangeEnd(rng)), 9504 forward: selection.isForward() 9505 }; 9506 }; 9507 const getRangeBookmark = (selection) => { 9508 return { rng: selection.getRng(), forward: selection.isForward() }; 9509 }; 9510 const createBookmarkSpan = (dom, id, filled) => { 9511 const args = { 'data-mce-type': 'bookmark', id, 'style': 'overflow:hidden;line-height:0px' }; 9512 return filled ? dom.create('span', args, '') : dom.create('span', args); 9513 }; 9514 const getPersistentBookmark = (selection, filled) => { 9515 const dom = selection.dom; 9516 let rng = selection.getRng(); 9517 const id = dom.uniqueId(); 9518 const collapsed = selection.isCollapsed(); 9519 const element = selection.getNode(); 9520 const name = element.nodeName; 9521 const forward = selection.isForward(); 9522 if (name === 'IMG') { 9523 return { name, index: findIndex(dom, name, element) }; 9524 } 9525 // W3C method 9526 const rng2 = normalizeTableCellSelection(rng.cloneRange()); 9527 // Insert end marker 9528 if (!collapsed) { 9529 rng2.collapse(false); 9530 const endBookmarkNode = createBookmarkSpan(dom, id + '_end', filled); 9531 rangeInsertNode(dom, rng2, endBookmarkNode); 9532 } 9533 rng = normalizeTableCellSelection(rng); 9534 rng.collapse(true); 9535 const startBookmarkNode = createBookmarkSpan(dom, id + '_start', filled); 9536 rangeInsertNode(dom, rng, startBookmarkNode); 9537 selection.moveToBookmark({ id, keep: true, forward }); 9538 return { id, forward }; 9539 }; 9540 const getBookmark$2 = (selection, type, normalized = false) => { 9541 if (type === 2) { 9542 return getOffsetBookmark(trim$2, normalized, selection); 9543 } 9544 else if (type === 3) { 9545 return getCaretBookmark(selection); 9546 } 9547 else if (type) { 9548 return getRangeBookmark(selection); 9549 } 9550 else { 9551 return getPersistentBookmark(selection, false); 9552 } 9553 }; 9554 const getUndoBookmark = curry(getOffsetBookmark, identity, true); 9555 9556 /** 9557 * Checks if the direction is forwards. 9558 */ 9559 const isForwards = (direction) => direction === 1 /* HDirection.Forwards */; 9560 /** 9561 * Checks if the direction is backwards. 9562 */ 9563 const isBackwards = (direction) => direction === -1 /* HDirection.Backwards */; 9564 9565 var SimpleResultType; 9566 (function (SimpleResultType) { 9567 SimpleResultType[SimpleResultType["Error"] = 0] = "Error"; 9568 SimpleResultType[SimpleResultType["Value"] = 1] = "Value"; 9569 })(SimpleResultType || (SimpleResultType = {})); 9570 const fold$1 = (res, onError, onValue) => res.stype === SimpleResultType.Error ? onError(res.serror) : onValue(res.svalue); 9571 const partition = (results) => { 9572 const values = []; 9573 const errors = []; 9574 each$e(results, (obj) => { 9575 fold$1(obj, (err) => errors.push(err), (val) => values.push(val)); 9576 }); 9577 return { values, errors }; 9578 }; 9579 const mapError = (res, f) => { 9580 if (res.stype === SimpleResultType.Error) { 9581 return { stype: SimpleResultType.Error, serror: f(res.serror) }; 9582 } 9583 else { 9584 return res; 9585 } 9586 }; 9587 const map = (res, f) => { 9588 if (res.stype === SimpleResultType.Value) { 9589 return { stype: SimpleResultType.Value, svalue: f(res.svalue) }; 9590 } 9591 else { 9592 return res; 9593 } 9594 }; 9595 const bind = (res, f) => { 9596 if (res.stype === SimpleResultType.Value) { 9597 return f(res.svalue); 9598 } 9599 else { 9600 return res; 9601 } 9602 }; 9603 const bindError = (res, f) => { 9604 if (res.stype === SimpleResultType.Error) { 9605 return f(res.serror); 9606 } 9607 else { 9608 return res; 9609 } 9610 }; 9611 const svalue = (v) => ({ stype: SimpleResultType.Value, svalue: v }); 9612 const serror = (e) => ({ stype: SimpleResultType.Error, serror: e }); 9613 const toResult = (res) => fold$1(res, Result.error, Result.value); 9614 const fromResult = (res) => res.fold(serror, svalue); 9615 const SimpleResult = { 9616 fromResult, 9617 toResult, 9618 svalue, 9619 partition, 9620 serror, 9621 bind, 9622 bindError, 9623 map, 9624 mapError, 9625 fold: fold$1 9626 }; 9627 9628 const formatObj = (input) => { 9629 return isObject(input) && keys(input).length > 100 ? ' removed due to size' : JSON.stringify(input, null, 2); 9630 }; 9631 const formatErrors = (errors) => { 9632 const es = errors.length > 10 ? errors.slice(0, 10).concat([ 9633 { 9634 path: [], 9635 getErrorInfo: constant('... (only showing first ten failures)') 9636 } 9637 ]) : errors; 9638 // TODO: Work out a better split between PrettyPrinter and SchemaError 9639 return map$3(es, (e) => { 9640 return 'Failed path: (' + e.path.join(' > ') + ')\n' + e.getErrorInfo(); 9641 }); 9642 }; 9643 9644 const nu = (path, getErrorInfo) => { 9645 return SimpleResult.serror([{ 9646 path, 9647 // This is lazy so that it isn't calculated unnecessarily 9648 getErrorInfo 9649 }]); 9650 }; 9651 const missingRequired = (path, key, obj) => nu(path, () => 'Could not find valid *required* value for "' + key + '" in ' + formatObj(obj)); 9652 const custom = (path, err) => nu(path, constant(err)); 9653 9654 const value = (validator) => { 9655 const extract = (path, val) => { 9656 return SimpleResult.bindError(validator(val), (err) => custom(path, err)); 9657 }; 9658 const toString = constant('val'); 9659 return { 9660 extract, 9661 toString 9662 }; 9663 }; 9664 const anyValue$1 = value(SimpleResult.svalue); 9665 9666 const anyValue = constant(anyValue$1); 9667 const typedValue = (validator, expectedType) => value((a) => { 9668 const actualType = typeof a; 9669 return validator(a) ? SimpleResult.svalue(a) : SimpleResult.serror(`Expected type: ${expectedType} but got: ${actualType}`); 9670 }); 9671 const number = typedValue(isNumber, 'number'); 9672 const string = typedValue(isString, 'string'); 9673 const functionProcessor = typedValue(isFunction, 'function'); 9674 9675 const required$1 = () => ({ tag: "required" /* FieldPresenceTag.Required */, process: {} }); 9676 const defaultedThunk = (fallbackThunk) => ({ tag: "defaultedThunk" /* FieldPresenceTag.DefaultedThunk */, process: fallbackThunk }); 9677 const defaulted$1 = (fallback) => defaultedThunk(constant(fallback)); 9678 const asOption = () => ({ tag: "option" /* FieldPresenceTag.Option */, process: {} }); 9679 9680 const field$1 = (key, newKey, presence, prop) => ({ tag: "field" /* FieldTag.Field */, key, newKey, presence, prop }); 9681 const fold = (value, ifField, ifCustom) => { 9682 switch (value.tag) { 9683 case "field" /* FieldTag.Field */: 9684 return ifField(value.key, value.newKey, value.presence, value.prop); 9685 case "custom" /* FieldTag.CustomField */: 9686 return ifCustom(value.newKey, value.instantiator); 9687 } 9688 }; 9689 9690 const mergeValues = (values, base) => { 9691 return SimpleResult.svalue(deepMerge(base, merge$1.apply(undefined, values))); 9692 }; 9693 const mergeErrors = (errors) => compose(SimpleResult.serror, flatten$1)(errors); 9694 const consolidateObj = (objects, base) => { 9695 const partition = SimpleResult.partition(objects); 9696 return partition.errors.length > 0 ? mergeErrors(partition.errors) : mergeValues(partition.values, base); 9697 }; 9698 const consolidateArr = (objects) => { 9699 const partitions = SimpleResult.partition(objects); 9700 return partitions.errors.length > 0 ? mergeErrors(partitions.errors) : SimpleResult.svalue(partitions.values); 9701 }; 9702 const ResultCombine = { 9703 consolidateObj, 9704 consolidateArr 9705 }; 9706 9707 const requiredAccess = (path, obj, key, bundle) => 9708 // In required mode, if it is undefined, it is an error. 9709 get$
9709a(obj, key).fold(() => missingRequired(path, key, obj), bundle); 9710 const fallbackAccess = (obj, key, fallback, bundle) => { 9711 const v = get$a(obj, key).getOrThunk(() => fallback(obj)); 9712 return bundle(v); 9713 }; 9714 const optionAccess = (obj, key, bundle) => bundle(get$a(obj, key)); 9715 const optionDefaultedAccess = (obj, key, fallback, bundle) => { 9716 const opt = get$a
vendor: 13,726 bytes, lines 9716-10009
9716(obj, key).map((val) => val === true ? fallback(obj) : val); 9717 return bundle(opt); 9718 }; 9719 const extractField = (field, path, obj, key, prop) => { 9720 const bundle = (av) => prop.extract(path.concat([key]), av); 9721 const bundleAsOption = (optValue) => optValue.fold(() => SimpleResult.svalue(Optional.none()), (ov) => { 9722 const result = prop.extract(path.concat([key]), ov); 9723 return SimpleResult.map(result, Optional.some); 9724 }); 9725 switch (field.tag) { 9726 case "required" /* FieldPresenceTag.Required */: 9727 return requiredAccess(path, obj, key, bundle); 9728 case "defaultedThunk" /* FieldPresenceTag.DefaultedThunk */: 9729 return fallbackAccess(obj, key, field.process, bundle); 9730 case "option" /* FieldPresenceTag.Option */: 9731 return optionAccess(obj, key, bundleAsOption); 9732 case "defaultedOptionThunk" /* FieldPresenceTag.DefaultedOptionThunk */: 9733 return optionDefaultedAccess(obj, key, field.process, bundleAsOption); 9734 case "mergeWithThunk" /* FieldPresenceTag.MergeWithThunk */: { 9735 return fallbackAccess(obj, key, constant({}), (v) => { 9736 const result = deepMerge(field.process(obj), v); 9737 return bundle(result); 9738 }); 9739 } 9740 } 9741 }; 9742 const extractFields = (path, obj, fields) => { 9743 const success = {}; 9744 const errors = []; 9745 // PERFORMANCE: We use a for loop here instead of Arr.each as this is a hot code path 9746 for (const field of fields) { 9747 fold(field, (key, newKey, presence, prop) => { 9748 const result = extractField(presence, path, obj, key, prop); 9749 SimpleResult.fold(result, (err) => { 9750 errors.push(...err); 9751 }, (res) => { 9752 success[newKey] = res; 9753 }); 9754 }, (newKey, instantiator) => { 9755 success[newKey] = instantiator(obj); 9756 }); 9757 } 9758 return errors.length > 0 ? SimpleResult.serror(errors) : SimpleResult.svalue(success); 9759 }; 9760 const objOf = (values) => { 9761 const extract = (path, o) => extractFields(path, o, values); 9762 const toString = () => { 9763 const fieldStrings = map$3(values, (value) => fold(value, (key, _okey, _presence, prop) => key + ' -> ' + prop.toString(), (newKey, _instantiator) => 'state(' + newKey + ')')); 9764 return 'obj{\n' + fieldStrings.join('\n') + '}'; 9765 }; 9766 return { 9767 extract, 9768 toString 9769 }; 9770 }; 9771 const arrOf = (prop) => { 9772 const extract = (path, array) => { 9773 const results = map$3(array, (a, i) => prop.extract(path.concat(['[' + i + ']']), a)); 9774 return ResultCombine.consolidateArr(results); 9775 }; 9776 const toString = () => 'array(' + prop.toString() + ')'; 9777 return { 9778 extract, 9779 toString 9780 }; 9781 }; 9782 const arrOfObj = compose(arrOf, objOf); 9783 9784 const valueOf = (validator) => value((v) => validator(v).fold(SimpleResult.serror, SimpleResult.svalue)); 9785 const extractValue = (label, prop, obj) => { 9786 const res = prop.extract([label], obj); 9787 return SimpleResult.mapError(res, (errs) => ({ input: obj, errors: errs })); 9788 }; 9789 const asRaw = (label, prop, obj) => SimpleResult.toResult(extractValue(label, prop, obj)); 9790 const formatError = (errInfo) => { 9791 return 'Errors: \n' + formatErrors(errInfo.errors).join('\n') + 9792 '\n\nInput object: ' + formatObj(errInfo.input); 9793 }; 9794 9795 const field = field$1; 9796 const required = (key) => field(key, key, required$1(), anyValue()); 9797 const requiredOf = (key, schema) => field(key, key, required$1(), schema); 9798 const requiredString = (key) => requiredOf(key, string); 9799 const requiredFunction = (key) => requiredOf(key, functionProcessor); 9800 const requiredArrayOf = (key, schema) => field(key, key, required$1(), arrOf(schema)); 9801 const option$1 = (key) => field(key, key, asOption(), anyValue()); 9802 const optionOf = (key, schema) => field(key, key, asOption(), schema); 9803 const optionString = (key) => optionOf(key, string); 9804 const optionFunction = (key) => optionOf(key, functionProcessor); 9805 const defaulted = (key, fallback) => field(key, key, defaulted$1(fallback), anyValue()); 9806 const defaultedOf = (key, fallback, schema) => field(key, key, defaulted$1(fallback), schema); 9807 const defaultedNumber = (key, fallback) => defaultedOf(key, fallback, number); 9808 const defaultedArrayOf = (key, fallback, schema) => defaultedOf(key, fallback, arrOf(schema)); 9809 9810 const isInlinePattern = (pattern) => pattern.type === 'inline-command' || pattern.type === 'inline-format'; 9811 const isBlockPattern = (pattern) => pattern.type === 'block-command' || pattern.type === 'block-format'; 9812 const hasBlockTrigger = (pattern, trigger) => (pattern.type === 'block-command' || pattern.type === 'block-format') && pattern.trigger === trigger; 9813 const normalizePattern = (pattern) => { 9814 const err = (message) => Result.error({ message, pattern }); 9815 const formatOrCmd = (name, onFormat, onCommand) => { 9816 if (pattern.format !== undefined) { 9817 let formats; 9818 if (isArray$1(pattern.format)) { 9819 if (!forall(pattern.format, isString)) { 9820 return err(name + ' pattern has non-string items in the `format` array'); 9821 } 9822 formats = pattern.format; 9823 } 9824 else if (isString(pattern.format)) { 9825 formats = [pattern.format]; 9826 } 9827 else { 9828 return err(name + ' pattern has non-string `format` parameter'); 9829 } 9830 return Result.value(onFormat(formats)); 9831 } 9832 else if (pattern.cmd !== undefined) { 9833 if (!isString(pattern.cmd)) { 9834 return err(name + ' pattern has non-string `cmd` parameter'); 9835 } 9836 return Result.value(onCommand(pattern.cmd, pattern.value)); 9837 } 9838 else { 9839 return err(name + ' pattern is missing both `format` and `cmd` parameters'); 9840 } 9841 }; 9842 if (!isObject(pattern)) { 9843 return err('Raw pattern is not an object'); 9844 } 9845 if (!isString(pattern.start)) { 9846 return err('Raw pattern is missing `start` parameter'); 9847 } 9848 if (pattern.end !== undefined) { 9849 // inline pattern 9850 if (!isString(pattern.end)) { 9851 return err('Inline pattern has non-string `end` parameter'); 9852 } 9853 if (pattern.start.length === 0 && pattern.end.length === 0) { 9854 return err('Inline pattern has empty `start` and `end` parameters'); 9855 } 9856 let start = pattern.start; 9857 let end = pattern.end; 9858 // when the end is empty swap with start as it is more efficient 9859 if (end.length === 0) { 9860 end = start; 9861 start = ''; 9862 } 9863 return formatOrCmd('Inline', (format) => ({ type: 'inline-format', start, end, format }), (cmd, value) => ({ type: 'inline-command', start, end, cmd, value })); 9864 } 9865 else if (pattern.replacement !== undefined) { 9866 // replacement pattern 9867 if (!isString(pattern.replacement)) { 9868 return err('Replacement pattern has non-string `replacement` parameter'); 9869 } 9870 if (pattern.start.length === 0) { 9871 return err('Replacement pattern has empty `start` parameter'); 9872 } 9873 return Result.value({ 9874 type: 'inline-command', 9875 start: '', 9876 end: pattern.start, 9877 cmd: 'mceInsertContent', 9878 value: pattern.replacement 9879 }); 9880 } 9881 else { 9882 // block pattern 9883 const trigger = pattern.trigger ?? 'space'; 9884 if (pattern.start.length === 0) { 9885 return err('Block pattern has empty `start` parameter'); 9886 } 9887 return formatOrCmd('Block', (formats) => ({ 9888 type: 'block-format', 9889 start: pattern.start, 9890 format: formats[0], 9891 trigger 9892 }), (command, commandValue) => ({ 9893 type: 'block-command', 9894 start: pattern.start, 9895 cmd: command, 9896 value: commandValue, 9897 trigger 9898 })); 9899 } 9900 }; 9901 const getBlockPatterns = (patterns) => filter$5(patterns, isBlockPattern); 9902 const getInlinePatterns = (patterns) => filter$5(patterns, isInlinePattern); 9903 const createPatternSet = (patterns, dynamicPatternsLookup) => ({ 9904 inlinePatterns: getInlinePatterns(patterns), 9905 blockPatterns: getBlockPatterns(patterns), 9906 dynamicPatternsLookup 9907 }); 9908 const filterByTrigger = (patterns, trigger) => { 9909 return { 9910 ...patterns, 9911 blockPatterns: filter$5(patterns.blockPatterns, (pattern) => hasBlockTrigger(pattern, trigger)) 9912 }; 9913 }; 9914 const fromRawPatterns = (patterns) => { 9915 const normalized = partition$1(map$3(patterns, normalizePattern)); 9916 // eslint-disable-next-line no-console 9917 each$e(normalized.errors, (err) => console.error(err.message, err.pattern)); 9918 return normalized.values; 9919 }; 9920 const fromRawPatternsLookup = (lookupFn) => { 9921 return (ctx) => { 9922 const rawPatterns = lookupFn(ctx); 9923 return fromRawPatterns(rawPatterns); 9924 }; 9925 }; 9926 9927 const firePreProcess = (editor, args) => editor.dispatch('PreProcess', args); 9928 const firePostProcess = (editor, args) => editor.dispatch('PostProcess', args); 9929 const fireRemove = (editor) => { 9930 editor.dispatch('remove'); 9931 }; 9932 const fireDetach = (editor) => { 9933 editor.dispatch('detach'); 9934 }; 9935 const fireSwitchMode = (editor, mode) => { 9936 editor.dispatch('SwitchMode', { mode }); 9937 }; 9938 const fireObjectResizeStart = (editor, target, width, height, origin) => { 9939 editor.dispatch('ObjectResizeStart', { target, width, height, origin }); 9940 }; 9941 const fireObjectResized = (editor, target, width, height, origin) => { 9942 editor.dispatch('ObjectResized', { target, width, height, origin }); 9943 }; 9944 const firePreInit = (editor) => { 9945 editor.dispatch('PreInit'); 9946 }; 9947 const firePostRender = (editor) => { 9948 editor.dispatch('PostRender'); 9949 }; 9950 const fireInit = (editor) => { 9951 editor.dispatch('Init'); 9952 }; 9953 const firePlaceholderToggle = (editor, state) => { 9954 editor.dispatch('PlaceholderToggle', { state }); 9955 }; 9956 const fireError = (editor, errorType, error) => { 9957 editor.dispatch(errorType, error); 9958 }; 9959 const fireFormatApply = (editor, format, node, vars) => { 9960 editor.dispatch('FormatApply', { format, node, vars }); 9961 }; 9962 const fireFormatRemove = (editor, format, node, vars) => { 9963 editor.dispatch('FormatRemove', { format, node, vars }); 9964 }; 9965 const fireBeforeSetContent = (editor, args) => editor.dispatch('BeforeSetContent', args); 9966 const fireSetContent = (editor, args) => editor.dispatch('SetContent', args); 9967 const fireBeforeGetContent = (editor, args) => editor.dispatch('BeforeGetContent', args); 9968 const fireGetContent = (editor, args) => editor.dispatch('GetContent', args); 9969 const fireAutocompleterStart = (editor, args) => { 9970 editor.dispatch('AutocompleterStart', args); 9971 }; 9972 const fireAutocompleterUpdate = (editor, args) => { 9973 editor.dispatch('AutocompleterUpdate', args); 9974 }; 9975 const fireAutocompleterUpdateActiveRange = (editor, args) => { 9976 editor.dispatch('AutocompleterUpdateActiveRange', args); 9977 }; 9978 const fireAutocompleterEnd = (editor) => { 9979 editor.dispatch('AutocompleterEnd'); 9980 }; 9981 const firePastePreProcess = (editor, html, internal) => editor.dispatch('PastePreProcess', { content: html, internal }); 9982 const firePastePostProcess = (editor, node, internal) => editor.dispatch('PastePostProcess', { node, internal }); 9983 const firePastePlainTextToggle = (editor, state) => editor.dispatch('PastePlainTextToggle', { state }); 9984 const fireEditableRootStateChange = (editor, state) => editor.dispatch('EditableRootStateChange', { state }); 9985 const fireDisabledStateChange = (editor, state) => editor.dispatch('DisabledStateChange', { state }); 9986 const fireCloseTooltips = (editor) => editor.dispatch('CloseActiveTooltips'); 9987 9988 const deviceDetection$1 = detect$1().deviceType; 9989 const isTouch = deviceDetection$1.isTouch(); 9990 const DOM$d = DOMUtils.DOM; 9991 const getHash = (value) => { 9992 const items = value.indexOf('=') > 0 ? value.split(/[;,](?![^=;,]*(?:[;,]|$))/) : value.split(','); 9993 return foldl(items, (output, item) => { 9994 const arr = item.split('='); 9995 const key = arr[0]; 9996 const val = arr.length > 1 ? arr[1] : key; 9997 output[trim$4(key)] = trim$4(val); 9998 return output; 9999 }, {}); 10000 }; 10001 const isRegExp = (x) => is$5(x, RegExp); 10002 const option = (name) => (editor) => editor.options.get(name); 10003 const stringOrObjectProcessor = (value) => isString(value) || isObject(value); 10004 const bodyOptionProcessor = (editor, defaultValue = '') => (value) => { 10005 const valid = isString(value); 10006 if (valid) { 10007 if (value.indexOf('=') !== -1) { 10008 const bodyObj = getHash(value); 10009 return { value: get$
vendor: 7,863 bytes, lines 10009-10238
10009a(bodyObj, editor.id).getOr(defaultValue), valid }; 10010 } 10011 else { 10012 return { value, valid }; 10013 } 10014 } 10015 else { 10016 return { valid: false, message: 'Must be a string.' }; 10017 } 10018 }; 10019 const register$7 = (editor) => { 10020 const registerOption = editor.options.register; 10021 registerOption('id', { 10022 processor: 'string', 10023 default: editor.id 10024 }); 10025 registerOption('selector', { 10026 processor: 'string' 10027 }); 10028 registerOption('target', { 10029 processor: 'object' 10030 }); 10031 registerOption('suffix', { 10032 processor: 'string' 10033 }); 10034 registerOption('cache_suffix', { 10035 processor: 'string' 10036 }); 10037 registerOption('base_url', { 10038 processor: 'string' 10039 }); 10040 registerOption('referrer_policy', { 10041 processor: 'string', 10042 default: '' 10043 }); 10044 registerOption('crossorigin', { 10045 processor: 'function', 10046 default: constant(undefined) 10047 }); 10048 registerOption('language_load', { 10049 processor: 'boolean', 10050 default: true 10051 }); 10052 registerOption('inline', { 10053 processor: 'boolean', 10054 default: false 10055 }); 10056 registerOption('iframe_attrs', { 10057 processor: 'object', 10058 default: {} 10059 }); 10060 registerOption('doctype', { 10061 processor: 'string', 10062 default: '<!DOCTYPE html>' 10063 }); 10064 registerOption('document_base_url', { 10065 processor: 'string', 10066 default: editor.editorManager.documentBaseURL 10067 }); 10068 registerOption('body_id', { 10069 processor: bodyOptionProcessor(editor, 'tinymce'), 10070 default: 'tinymce' 10071 }); 10072 registerOption('body_class', { 10073 processor: bodyOptionProcessor(editor), 10074 default: '' 10075 }); 10076 registerOption('content_security_policy', { 10077 processor: 'string', 10078 default: '' 10079 }); 10080 registerOption('br_in_pre', { 10081 processor: 'boolean', 10082 default: true 10083 }); 10084 registerOption('forced_root_block', { 10085 processor: (value) => { 10086 const valid = isString(value) && isNotEmpty(value); 10087 if (valid) { 10088 return { value, valid }; 10089 } 10090 else { 10091 return { valid: false, message: 'Must be a non-empty string.' }; 10092 } 10093 }, 10094 default: 'p' 10095 }); 10096 registerOption('forced_root_block_attrs', { 10097 processor: 'object', 10098 default: {} 10099 }); 10100 registerOption('newline_behavior', { 10101 processor: (value) => { 10102 const valid = contains$2(['block', 'linebreak', 'invert', 'default'], value); 10103 return valid ? { value, valid } : { valid: false, message: 'Must be one of: block, linebreak, invert or default.' }; 10104 }, 10105 default: 'default' 10106 }); 10107 registerOption('br_newline_selector', { 10108 processor: 'string', 10109 default: '.mce-toc h2,figcaption,caption' 10110 }); 10111 registerOption('no_newline_selector', { 10112 processor: 'string', 10113 default: '' 10114 }); 10115 registerOption('keep_styles', { 10116 processor: 'boolean', 10117 default: true 10118 }); 10119 registerOption('end_container_on_empty_block', { 10120 processor: (value) => { 10121 if (isBoolean(value)) { 10122 return { valid: true, value }; 10123 } 10124 else if (isString(value)) { 10125 return { valid: true, value }; 10126 } 10127 else { 10128 return { valid: false, message: 'Must be boolean or a string' }; 10129 } 10130 }, 10131 default: 'blockquote' 10132 }); 10133 registerOption('font_size_style_values', { 10134 processor: 'string', 10135 default: 'xx-small,x-small,small,medium,large,x-large,xx-large' 10136 }); 10137 registerOption('font_size_legacy_values', { 10138 processor: 'string', 10139 // See: http://www.w3.org/TR/CSS2/fonts.html#propdef-font-size 10140 default: 'xx-small,small,medium,large,x-large,xx-large,300%' 10141 }); 10142 registerOption('font_size_classes', { 10143 processor: 'string', 10144 default: '' 10145 }); 10146 registerOption('automatic_uploads', { 10147 processor: 'boolean', 10148 default: true 10149 }); 10150 registerOption('images_reuse_filename', { 10151 processor: 'boolean', 10152 default: false 10153 }); 10154 registerOption('images_replace_blob_uris', { 10155 processor: 'boolean', 10156 default: true 10157 }); 10158 registerOption('icons', { 10159 processor: 'string', 10160 default: '' 10161 }); 10162 registerOption('icons_url', { 10163 processor: 'string', 10164 default: '' 10165 }); 10166 registerOption('images_upload_url', { 10167 processor: 'string', 10168 default: '' 10169 }); 10170 registerOption('images_upload_base_path', { 10171 processor: 'string', 10172 default: '' 10173 }); 10174 registerOption('images_upload_credentials', { 10175 processor: 'boolean', 10176 default: false 10177 }); 10178 registerOption('images_upload_handler', { 10179 processor: 'function' 10180 }); 10181 registerOption('language', { 10182 processor: 'string', 10183 default: 'en' 10184 }); 10185 registerOption('language_url', { 10186 processor: 'string', 10187 default: '' 10188 }); 10189 registerOption('entity_encoding', { 10190 processor: 'string', 10191 default: 'named' 10192 }); 10193 registerOption('indent', { 10194 processor: 'boolean', 10195 default: true 10196 }); 10197 registerOption('indent_before', { 10198 processor: 'string', 10199 default: 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,th,ul,ol,li,dl,dt,dd,area,table,thead,' + 10200 'tfoot,tbody,tr,section,details,summary,article,hgroup,aside,figure,figcaption,option,optgroup,datalist' 10201 }); 10202 registerOption('indent_after', { 10203 processor: 'string', 10204 default: 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,th,ul,ol,li,dl,dt,dd,area,table,thead,' + 10205 'tfoot,tbody,tr,section,details,summary,article,hgroup,aside,figure,figcaption,option,optgroup,datalist' 10206 }); 10207 registerOption('indent_use_margin', { 10208 processor: 'boolean', 10209 default: false 10210 }); 10211 registerOption('indentation', { 10212 processor: 'string', 10213 default: '40px' 10214 }); 10215 registerOption('content_css', { 10216 processor: (value) => { 10217 const valid = value === false || isString(value) || isArrayOf(value, isString); 10218 if (valid) { 10219 if (isString(value)) { 10220 return { value: map$3(value.split(','), trim$4), valid }; 10221 } 10222 else if (isArray$1(value)) { 10223 return { value, valid }; 10224 } 10225 else if (value === false) { 10226 return { value: [], valid }; 10227 } 10228 else { 10229 return { value, valid }; 10230 } 10231 } 10232 else { 10233 return { valid: false, message: 'Must be false, a string or an array of strings.' }; 10234 } 10235 }, 10236 default: isInline$2(editor) ? [] : ['default'] 10237 }); 10238 registerOption('content_styl
vendor: 22,488 bytes, lines 10238-10850
10238e', { 10239 processor: 'string' 10240 }); 10241 registerOption('content_css_cors', { 10242 processor: 'boolean', 10243 default: false 10244 }); 10245 registerOption('font_css', { 10246 processor: (value) => { 10247 const valid = isString(value) || isArrayOf(value, isString); 10248 if (valid) { 10249 const newValue = isArray$1(value) ? value : map$3(value.split(','), trim$4); 10250 return { value: newValue, valid }; 10251 } 10252 else { 10253 return { valid: false, message: 'Must be a string or an array of strings.' }; 10254 } 10255 }, 10256 default: [] 10257 }); 10258 registerOption('extended_mathml_attributes', { 10259 processor: 'string[]' 10260 }); 10261 registerOption('extended_mathml_elements', { 10262 processor: 'string[]' 10263 }); 10264 registerOption('inline_boundaries', { 10265 processor: 'boolean', 10266 default: true 10267 }); 10268 registerOption('inline_boundaries_selector', { 10269 processor: 'string', 10270 default: 'a[href],code,span.mce-annotation' 10271 }); 10272 registerOption('object_resizing', { 10273 processor: (value) => { 10274 const valid = isBoolean(value) || isString(value); 10275 if (valid) { 10276 if (value === false || deviceDetection$1.isiPhone() || deviceDetection$1.isiPad()) { 10277 return { value: '', valid }; 10278 } 10279 else { 10280 return { value: value === true ? 'table,img,figure.image,div,video,iframe' : value, valid }; 10281 } 10282 } 10283 else { 10284 return { valid: false, message: 'Must be boolean or a string' }; 10285 } 10286 }, 10287 // No nice way to do object resizing on touch devices at this stage 10288 default: !isTouch 10289 }); 10290 registerOption('resize_img_proportional', { 10291 processor: 'boolean', 10292 default: true 10293 }); 10294 registerOption('event_root', { 10295 processor: 'string' 10296 }); 10297 registerOption('service_message', { 10298 processor: 'string' 10299 }); 10300 registerOption('onboarding', { 10301 processor: 'boolean', 10302 default: true 10303 }); 10304 registerOption('tiny_cloud_entry_url', { 10305 processor: 'string' 10306 }); 10307 registerOption('theme', { 10308 processor: (value) => value === false || isString(value) || isFunction(value), 10309 default: 'silver' 10310 }); 10311 registerOption('theme_url', { 10312 processor: 'string' 10313 }); 10314 registerOption('formats', { 10315 processor: 'object' 10316 }); 10317 registerOption('format_empty_lines', { 10318 processor: 'boolean', 10319 default: false 10320 }); 10321 registerOption('format_noneditable_selector', { 10322 processor: 'string', 10323 default: '' 10324 }); 10325 registerOption('preview_styles', { 10326 processor: (value) => { 10327 const valid = value === false || isString(value); 10328 if (valid) { 10329 return { value: value === false ? '' : value, valid }; 10330 } 10331 else { 10332 return { valid: false, message: 'Must be false or a string' }; 10333 } 10334 }, 10335 default: 'font-family font-size font-weight font-style text-decoration text-transform color background-color border border-radius outline text-shadow' 10336 }); 10337 registerOption('custom_ui_selector', { 10338 processor: 'string', 10339 default: '' 10340 }); 10341 registerOption('hidden_input', { 10342 processor: 'boolean', 10343 default: true 10344 }); 10345 registerOption('submit_patch', { 10346 processor: 'boolean', 10347 default: true 10348 }); 10349 registerOption('encoding', { 10350 processor: 'string' 10351 }); 10352 registerOption('add_form_submit_trigger', { 10353 processor: 'boolean', 10354 default: true 10355 }); 10356 registerOption('add_unload_trigger', { 10357 processor: 'boolean', 10358 default: true 10359 }); 10360 registerOption('custom_undo_redo_levels', { 10361 processor: 'number', 10362 default: 0 10363 }); 10364 registerOption('disable_nodechange', { 10365 processor: 'boolean', 10366 default: false 10367 }); 10368 registerOption('disabled', { 10369 processor: (value) => { 10370 if (isBoolean(value)) { 10371 if (editor.initialized && isDisabled$1(editor) !== value) { 10372 // Schedules the callback to run in the next microtask queue once the option is updated 10373 // TODO: TINY-11586 - Implement `onChange` callback when the value of an option changes 10374 // eslint-disable-next-line @typescript-eslint/no-floating-promises 10375 Promise.resolve().then(() => { 10376 fireDisabledStateChange(editor, value); 10377 }); 10378 } 10379 return { valid: true, value }; 10380 } 10381 return { valid: false, message: 'The value must be a boolean.' }; 10382 }, 10383 default: false 10384 }); 10385 registerOption('readonly', { 10386 processor: 'boolean', 10387 default: false 10388 }); 10389 registerOption('editable_root', { 10390 processor: 'boolean', 10391 default: true 10392 }); 10393 registerOption('plugins', { 10394 processor: 'string[]', 10395 default: [] 10396 }); 10397 registerOption('external_plugins', { 10398 processor: 'object' 10399 }); 10400 registerOption('forced_plugins', { 10401 processor: 'string[]' 10402 }); 10403 registerOption('model', { 10404 processor: 'string', 10405 default: editor.hasPlugin('rtc') ? 'plugin' : 'dom' 10406 }); 10407 registerOption('model_url', { 10408 processor: 'string' 10409 }); 10410 registerOption('block_unsupported_drop', { 10411 processor: 'boolean', 10412 default: true 10413 }); 10414 registerOption('visual', { 10415 processor: 'boolean', 10416 default: true 10417 }); 10418 registerOption('visual_table_class', { 10419 processor: 'string', 10420 default: 'mce-item-table' 10421 }); 10422 registerOption('visual_anchor_class', { 10423 processor: 'string', 10424 default: 'mce-item-anchor' 10425 }); 10426 registerOption('iframe_aria_text', { 10427 processor: 'string', 10428 default: 'Rich Text Area'.concat(editor.hasPlugin('help') ? '. Press ALT-0 for help.' : '') 10429 }); 10430 registerOption('setup', { 10431 processor: 'function' 10432 }); 10433 registerOption('init_instance_callback', { 10434 processor: 'function' 10435 }); 10436 registerOption('url_converter', { 10437 processor: 'function', 10438 // Note: Don't bind here, as the binding is handled via the `url_converter_scope` 10439 // eslint-disable-next-line @typescript-eslint/unbound-method 10440 default: editor.convertURL 10441 }); 10442 registerOption('url_converter_scope', { 10443 processor: 'object', 10444 default: editor 10445 }); 10446 registerOption('urlconverter_callback', { 10447 processor: 'function' 10448 }); 10449 registerOption('allow_conditional_comments', { 10450 processor: 'boolean', 10451 default: false 10452 }); 10453 registerOption('allow_html_data_urls', { 10454 processor: 'boolean', 10455 default: false 10456 }); 10457 registerOption('allow_svg_data_urls', { 10458 processor: 'boolean' 10459 }); 10460 registerOption('allow_html_in_named_anchor', { 10461 processor: 'boolean', 10462 default: false 10463 }); 10464 registerOption('allow_html_in_comments', { 10465 processor: 'boolean', 10466 default: false 10467 }); 10468 registerOption('allow_script_urls', { 10469 processor: 'boolean', 10470 default: false 10471 }); 10472 registerOption('allow_unsafe_link_target', { 10473 processor: 'boolean', 10474 default: false 10475 }); 10476 registerOption('allow_mathml_annotation_encodings', { 10477 processor: (value) => { 10478 const valid = isArrayOf(value, isString); 10479 return valid ? { value, valid } : { valid: false, message: 'Must be an array of strings.' }; 10480 }, 10481 default: [] 10482 }); 10483 registerOption('convert_fonts_to_spans', { 10484 processor: 'boolean', 10485 default: true, 10486 deprecated: true 10487 }); 10488 registerOption('fix_list_elements', { 10489 processor: 'boolean', 10490 default: false 10491 }); 10492 registerOption('preserve_cdata', { 10493 processor: 'boolean', 10494 default: false 10495 }); 10496 registerOption('remove_trailing_brs', { 10497 processor: 'boolean', 10498 default: true 10499 }); 10500 registerOption('pad_empty_with_br', { 10501 processor: 'boolean', 10502 default: false, 10503 }); 10504 registerOption('inline_styles', { 10505 processor: 'boolean', 10506 default: true, 10507 deprecated: true 10508 }); 10509 registerOption('element_format', { 10510 processor: 'string', 10511 default: 'html' 10512 }); 10513 registerOption('entities', { 10514 processor: 'string' 10515 }); 10516 registerOption('schema', { 10517 processor: 'string', 10518 default: 'html5' 10519 }); 10520 registerOption('convert_urls', { 10521 processor: 'boolean', 10522 default: true 10523 }); 10524 registerOption('relative_urls', { 10525 processor: 'boolean', 10526 default: true 10527 }); 10528 registerOption('remove_script_host', { 10529 processor: 'boolean', 10530 default: true 10531 }); 10532 registerOption('custom_elements', { 10533 processor: stringOrObjectProcessor 10534 }); 10535 registerOption('extended_valid_elements', { 10536 processor: 'string' 10537 }); 10538 registerOption('invalid_elements', { 10539 processor: 'string' 10540 }); 10541 registerOption('invalid_styles', { 10542 processor: stringOrObjectProcessor 10543 }); 10544 registerOption('valid_children', { 10545 processor: 'string' 10546 }); 10547 registerOption('valid_classes', { 10548 processor: stringOrObjectProcessor 10549 }); 10550 registerOption('valid_elements', { 10551 processor: 'string' 10552 }); 10553 registerOption('valid_styles', { 10554 processor: stringOrObjectProcessor 10555 }); 10556 registerOption('verify_html', { 10557 processor: 'boolean', 10558 default: true 10559 }); 10560 registerOption('auto_focus', { 10561 processor: (value) => isString(value) || value === true 10562 }); 10563 registerOption('browser_spellcheck', { 10564 processor: 'boolean', 10565 default: false 10566 }); 10567 registerOption('protect', { 10568 processor: 'array' 10569 }); 10570 registerOption('images_file_types', { 10571 processor: 'string', 10572 default: 'jpeg,jpg,jpe,jfi,jif,jfif,png,gif,bmp,webp' 10573 }); 10574 registerOption('deprecation_warnings', { 10575 processor: 'boolean', 10576 default: true 10577 }); 10578 registerOption('a11y_advanced_options', { 10579 processor: 'boolean', 10580 default: false 10581 }); 10582 registerOption('api_key', { 10583 processor: 'string' 10584 }); 10585 registerOption('license_key', { 10586 processor: 'string' 10587 }); 10588 registerOption('paste_block_drop', { 10589 processor: 'boolean', 10590 default: false 10591 }); 10592 registerOption('paste_data_images', { 10593 processor: 'boolean', 10594 default: true 10595 }); 10596 registerOption('paste_preprocess', { 10597 processor: 'function' 10598 }); 10599 registerOption('paste_postprocess', { 10600 processor: 'function' 10601 }); 10602 registerOption('paste_webkit_styles', { 10603 processor: 'string', 10604 default: 'none' 10605 }); 10606 registerOption('paste_remove_styles_if_webkit', { 10607 processor: 'boolean', 10608 default: true 10609 }); 10610 registerOption('paste_merge_formats', { 10611 processor: 'boolean', 10612 default: true 10613 }); 10614 registerOption('smart_paste', { 10615 processor: 'boolean', 10616 default: true 10617 }); 10618 registerOption('paste_as_text', { 10619 processor: 'boolean', 10620 default: false 10621 }); 10622 registerOption('paste_tab_spaces', { 10623 processor: 'number', 10624 default: 4 10625 }); 10626 registerOption('text_patterns', { 10627 processor: (value) => { 10628 if (isArrayOf(value, isObject) || value === false) { 10629 const patterns = value === false ? [] : value; 10630 return { value: fromRawPatterns(patterns), valid: true }; 10631 } 10632 else { 10633 return { valid: false, message: 'Must be an array of objects or false.' }; 10634 } 10635 }, 10636 default: [ 10637 { start: '*', end: '*', format: 'italic' }, 10638 { start: '**', end: '**', format: 'bold' }, 10639 { start: '#', format: 'h1', trigger: 'space' }, 10640 { start: '##', format: 'h2', trigger: 'space' }, 10641 { start: '###', format: 'h3', trigger: 'space' }, 10642 { start: '####', format: 'h4', trigger: 'space' }, 10643 { start: '#####', format: 'h5', trigger: 'space' }, 10644 { start: '######', format: 'h6', trigger: 'space' }, 10645 { start: '1.', cmd: 'InsertOrderedList', trigger: 'space' }, 10646 { start: '*', cmd: 'InsertUnorderedList', trigger: 'space' }, 10647 { start: '-', cmd: 'InsertUnorderedList', trigger: 'space' }, 10648 { start: '>', cmd: 'mceBlockQuote', trigger: 'space' }, 10649 { start: '---', cmd: 'InsertHorizontalRule', trigger: 'space' }, 10650 ] 10651 }); 10652 registerOption('text_patterns_lookup', { 10653 processor: (value) => { 10654 if (isFunction(value)) { 10655 return { 10656 value: fromRawPatternsLookup(value), 10657 valid: true, 10658 }; 10659 } 10660 else { 10661 return { valid: false, message: 'Must be a single function' }; 10662 } 10663 }, 10664 default: (_ctx) => [] 10665 }); 10666 registerOption('allow_noneditable', { 10667 processor: 'boolean', 10668 default: true 10669 }); 10670 registerOption('noneditable_class', { 10671 processor: 'string', 10672 default: 'mceNonEditable' 10673 }); 10674 registerOption('editable_class', { 10675 processor: 'string', 10676 default: 'mceEditable' 10677 }); 10678 registerOption('noneditable_regexp', { 10679 processor: (value) => { 10680 if (isArrayOf(value, isRegExp)) { 10681 return { value, valid: true }; 10682 } 10683 else if (isRegExp(value)) { 10684 return { value: [value], valid: true }; 10685 } 10686 else { 10687 return { valid: false, message: 'Must be a RegExp or an array of RegExp.' }; 10688 } 10689 }, 10690 default: [] 10691 }); 10692 registerOption('table_tab_navigation', { 10693 processor: 'boolean', 10694 default: true 10695 }); 10696 registerOption('highlight_on_focus', { 10697 processor: 'boolean', 10698 default: true 10699 }); 10700 registerOption('xss_sanitization', { 10701 processor: 'boolean', 10702 default: true 10703 }); 10704 registerOption('details_initial_state', { 10705 processor: (value) => { 10706 const valid = contains$2(['inherited', 'collapsed', 'expanded'], value); 10707 return valid ? { value, valid } : { valid: false, message: 'Must be one of: inherited, collapsed, or expanded.' }; 10708 }, 10709 default: 'inherited' 10710 }); 10711 registerOption('details_serialized_state', { 10712 processor: (value) => { 10713 const valid = contains$2(['inherited', 'collapsed', 'expanded'], value); 10714 return valid ? { value, valid } : { valid: false, message: 'Must be one of: inherited, collapsed, or expanded.' }; 10715 }, 10716 default: 'inherited' 10717 }); 10718 registerOption('init_content_sync', { 10719 processor: 'boolean', 10720 default: false 10721 }); 10722 registerOption('newdocument_content', { 10723 processor: 'string', 10724 default: '' 10725 }); 10726 registerOption('sandbox_iframes', { 10727 processor: 'boolean', 10728 default: true 10729 }); 10730 registerOption('sandbox_iframes_exclusions', { 10731 processor: 'string[]', 10732 default: [ 10733 'youtube.com', 10734 'youtu.be', 10735 'vimeo.com', 10736 'player.vimeo.com', 10737 'dailymotion.com', 10738 'embed.music.apple.com', 10739 'open.spotify.com', 10740 'giphy.com', 10741 'dai.ly', 10742 'codepen.io', 10743 ] 10744 }); 10745 registerOption('convert_unsafe_embeds', { 10746 processor: 'boolean', 10747 default: true 10748 }); 10749 registerOption('user_id', { 10750 processor: 'string', 10751 default: 'Anonymous' 10752 }); 10753 registerOption('content_id', { 10754 processor: 'string', 10755 }); 10756 registerOption('fetch_users', { 10757 processor: (value) => { 10758 if (value === undefined) { 10759 return { valid: true, value: undefined }; 10760 } 10761 if (isFunction(value)) { 10762 return { valid: true, value }; 10763 } 10764 return { 10765 valid: false, 10766 message: 'fetch_users must be a function that returns a Promise<ExpectedUser[]>' 10767 }; 10768 } 10769 }); 10770 const documentsFileTypesOptionsSchema = arrOfObj([ 10771 requiredString('mimeType'), 10772 requiredArrayOf('extensions', valueOf((ext) => { 10773 if (isString(ext)) { 10774 return Result.value(ext); 10775 } 10776 else { 10777 return Result.error('Extensions must be an array of strings'); 10778 } 10779 })), 10780 ]); 10781 registerOption('documents_file_types', { 10782 processor: (value) => asRaw('documents_file_types', documentsFileTypesOptionsSchema, value).fold((_err) => ({ 10783 valid: false, 10784 message: 'Must be a non-empty array of objects matching the configuration schema: https://www.tiny.cloud/docs/tinymce/latest/uploadcare-documents/#documents-file-types' 10785 }), (val) => ({ valid: true, value: val })) 10786 }); 10787 // These options must be registered later in the init sequence due to their default values 10788 editor.on('ScriptsLoaded', () => { 10789 registerOption('directionality', { 10790 processor: 'string', 10791 default: I18n.isRtl() ? 'rtl' : undefined 10792 }); 10793 registerOption('placeholder', { 10794 processor: 'string', 10795 // Fallback to the original elements placeholder if not set in the settings 10796 default: DOM$d.getAttrib(editor.getElement(), 'placeholder') 10797 }); 10798 }); 10799 registerOption('lists_indent_on_tab', { 10800 processor: 'boolean', 10801 default: true 10802 }); 10803 registerOption('list_max_depth', { 10804 processor: (value) => { 10805 const valid = isNumber(value); 10806 if (valid) { 10807 if (value < 0) { 10808 throw new Error('list_max_depth cannot be set to lower than 0'); 10809 } 10810 return { value, valid }; 10811 } 10812 else { 10813 return { valid: false, message: 'Must be a number' }; 10814 } 10815 }, 10816 }); 10817 }; 10818 const getIframeAttrs = option('iframe_attrs'); 10819 const getDocType = option('doctype'); 10820 const getDocumentBaseUrl = option('document_base_url'); 10821 const getBodyId = option('body_id'); 10822 const getBodyClass = option('body_class'); 10823 const getContentSecurityPolicy = option('content_security_policy'); 10824 const shouldPutBrInPre$1 = option('br_in_pre'); 10825 const getForcedRootBlock = option('forced_root_block'); 10826 const getForcedRootBlockAttrs = option('forced_root_block_attrs'); 10827 const getNewlineBehavior = option('newline_behavior'); 10828 const getBrNewLineSelector = option('br_newline_selector'); 10829 const getNoNewLineSelector = option('no_newline_selector'); 10830 const shouldKeepStyles = option('keep_styles'); 10831 const shouldEndContainerOnEmptyBlock = option('end_container_on_empty_block'); 10832 const isAutomaticUploadsEnabled = option('automatic_uploads'); 10833 const shouldReuseFileName = option('images_reuse_filename'); 10834 const shouldReplaceBlobUris = option('images_replace_blob_uris'); 10835 const getIconPackName = option('icons'); 10836 const getIconsUrl = option('icons_url'); 10837 const getImageUploadUrl = option('images_upload_url'); 10838 const getImageUploadBasePath = option('images_upload_base_path'); 10839 const getImagesUploadCredentials = option('images_upload_credentials'); 10840 const getImagesUploadHandler = option('images_upload_handler'); 10841 const shouldUseContentCssCors = option('content_css_cors'); 10842 const getReferrerPolicy = option('referrer_policy'); 10843 const getCrossOrigin = option('crossorigin'); 10844 const getLanguageCode = option('language'); 10845 const getLanguageUrl = option('language_url'); 10846 const shouldIndentUseMargin = option('indent_use_margin'); 10847 const getIndentation = option('indentation'); 10848 const getContentCss = option('content_css'); 10849 const getContentStyle = option('content_style'); 10850 const get
10850FontCss = option('font_css'); 10851 const getDirectionality = option('directionality'); 10852 const getInlineBoundarySelector = option('inline_boundaries_selector'); 10853 const getObjectResizing = option('object_resizing'); 10854 const getResizeImgProportional = option('resize_img_proportional'); 10855 const getPlaceholder = option('placeholder'); 10856 const getEventRoot = option('event_root'); 10857 const getServiceMessage = option('service_message'); 10858 const getTheme = option('theme'); 10859 const getThemeUrl = option('theme_url'); 10860 const getModel = option('model'); 10861 const getModelUrl = option('model_url'); 10862 const isInlineBoundariesEnabled = option('inline_boundaries'); 10863 const getFormats = option('formats'); 10864 const getPreviewStyles = option('preview_styles'); 10865 const canFormatEmptyLines = option('format_empty_lines'); 10866 const getFormatNoneditableSelector = option('format_noneditable_selector'); 10867 const getCustomUiSelector = option('custom_ui_selector'); 10868 const isInline$2 = option('inline'); 10869 const hasHiddenInput = option('hidden_input'); 10870 const shouldPatchSubmit = option('submit_patch'); 10871 const shouldAddFormSubmitTrigger = option('add_form_submit_trigger'); 10872 const shouldAddUnloadTrigger = option('add_unload_trigger'); 10873 const getCustomUndoRedoLevels = option('custom_undo_redo_levels'); 10874 const shouldDisableNodeChange = option('disable_nodechange'); 10875 const isReadOnly$1 = option('readonly'); 10876 const hasEditableRoot$1 = option('editable_root'); 10877 const hasContentCssCors = option('content_css_cors'); 10878 const getPlugins = option('plugins'); 10879 const getExternalPlugins$1 = option('external_plugins'); 10880 const shouldBlockUnsupportedDrop = option('block_unsupported_drop'); 10881 const isVisualAidsEnabled = option('visual'); 10882 const getVisualAidsTableClass = option('visual_table_class'); 10883 const getVisualAidsAnchorClass = option('visual_anchor_class'); 10884 const getIframeAriaText = option('iframe_aria_text'); 10885 const getSetupCallback = option('setup'); 10886 const getInitInstanceCallback = option('init_instance_callback'); 10887 const getUrlConverterCallback = option('urlconverter_callback'); 10888 const getAutoFocus = option('auto_focus'); 10889 const shouldBrowserSpellcheck = option('browser_spellcheck'); 10890 const getProtect = option('protect'); 10891 const shouldPasteBlockDrop = option('paste_block_drop'); 10892 const shouldPasteDataImages = option('paste_data_images'); 10893 const getPastePreProcess = option('paste_preprocess'); 10894 const getPastePostProcess = option('paste_postprocess'); 10895 const getNewDocumentContent = option('newdocument_content'); 10896 const getPasteWebkitStyles = option('paste_webkit_styles'); 10897 const shouldPasteRemoveWebKitStyles = option('paste_remove_styles_if_webkit'); 10898 const shouldPasteMergeFormats = option('paste_merge_formats'); 10899 const isSmartPasteEnabled = option('smart_paste'); 10900 const isPasteAsTextEnabled = option('paste_as_text');
vendor: 14,302 bytes, lines 10901-11228
10901 const getPasteTabSpaces = option('paste_tab_spaces'); 10902 const shouldAllowHtmlDataUrls = option('allow_html_data_urls'); 10903 const getTextPatterns = option('text_patterns'); 10904 const getTextPatternsLookup = option('text_patterns_lookup'); 10905 const shouldAllowNonEditable = option('allow_noneditable'); 10906 const getNonEditableClass = option('noneditable_class'); 10907 const getEditableClass = option('editable_class'); 10908 const getNonEditableRegExps = option('noneditable_regexp'); 10909 const shouldPreserveCData = option('preserve_cdata'); 10910 const shouldHighlightOnFocus = option('highlight_on_focus'); 10911 const shouldSanitizeXss = option('xss_sanitization'); 10912 const shouldUseDocumentWrite = option('init_content_sync'); 10913 const hasTextPatternsLookup = (editor) => editor.options.isSet('text_patterns_lookup'); 10914 const getFontStyleValues = (editor) => Tools.explode(editor.options.get('font_size_style_values')); 10915 const getFontSizeClasses = (editor) => Tools.explode(editor.options.get('font_size_classes')); 10916 const isEncodingXml = (editor) => editor.options.get('encoding') === 'xml'; 10917 const getAllowedImageFileTypes = (editor) => Tools.explode(editor.options.get('images_file_types')); 10918 const hasTableTabNavigation = option('table_tab_navigation'); 10919 const getDetailsInitialState = option('details_initial_state'); 10920 const getDetailsSerializedState = option('details_serialized_state'); 10921 const shouldSandboxIframes = option('sandbox_iframes'); 10922 const getSandboxIframesExclusions = (editor) => editor.options.get('sandbox_iframes_exclusions'); 10923 const shouldConvertUnsafeEmbeds = option('convert_unsafe_embeds'); 10924 const getLicenseKey = option('license_key'); 10925 const getApiKey = option('api_key'); 10926 const isDisabled$1 = option('disabled'); 10927 const getUserId = option('user_id'); 10928 const getFetchUsers = option('fetch_users'); 10929 const shouldIndentOnTab = option('lists_indent_on_tab'); 10930 const getListMaxDepth = (editor) => Optional.from(editor.options.get('list_max_depth')); 10931 10932 const isElement$4 = isElement$7; 10933 const isText$5 = isText$b; 10934 const removeNode$1 = (node) => { 10935 const parentNode = node.parentNode; 10936 if (parentNode) { 10937 parentNode.removeChild(node); 10938 } 10939 }; 10940 const trimCount = (text) => { 10941 const trimmedText = trim$2(text); 10942 return { 10943 count: text.length - trimmedText.length, 10944 text: trimmedText 10945 }; 10946 }; 10947 const deleteZwspChars = (caretContainer) => { 10948 // We use the Text.deleteData API here so as to preserve selection offsets 10949 let idx; 10950 while ((idx = caretContainer.data.lastIndexOf(ZWSP$1)) !== -1) { 10951 caretContainer.deleteData(idx, 1); 10952 } 10953 }; 10954 const removeUnchanged = (caretContainer, pos) => { 10955 remove$2(caretContainer); 10956 return pos; 10957 }; 10958 const removeTextAndReposition = (caretContainer, pos) => { 10959 const before = trimCount(caretContainer.data.substr(0, pos.offset())); 10960 const after = trimCount(caretContainer.data.substr(pos.offset())); 10961 const text = before.text + after.text; 10962 if (text.length > 0) { 10963 deleteZwspChars(caretContainer); 10964 return CaretPosition(caretContainer, pos.offset() - before.count); 10965 } 10966 else { 10967 return pos; 10968 } 10969 }; 10970 const removeElementAndReposition = (caretContainer, pos) => { 10971 const parentNode = pos.container(); 10972 const newPosition = indexOf$1(from(parentNode.childNodes), caretContainer).map((index) => { 10973 return index < pos.offset() ? CaretPosition(parentNode, pos.offset() - 1) : pos; 10974 }).getOr(pos); 10975 remove$2(caretContainer); 10976 return newPosition; 10977 }; 10978 const removeTextCaretContainer = (caretContainer, pos) => isText$5(caretContainer) && pos.container() === caretContainer ? removeTextAndReposition(caretContainer, pos) : removeUnchanged(caretContainer, pos); 10979 const removeElementCaretContainer = (caretContainer, pos) => pos.container() === caretContainer.parentNode ? removeElementAndReposition(caretContainer, pos) : removeUnchanged(caretContainer, pos); 10980 const removeAndReposition = (container, pos) => CaretPosition.isTextPosition(pos) ? removeTextCaretContainer(container, pos) : removeElementCaretContainer(container, pos); 10981 const remove$2 = (caretContainerNode) => { 10982 if (isElement$4(caretContainerNode) && isCaretContainer$2(caretContainerNode)) { 10983 if (hasContent(caretContainerNode)) { 10984 caretContainerNode.removeAttribute('data-mce-caret'); 10985 } 10986 else { 10987 removeNode$1(caretContainerNode); 10988 } 10989 } 10990 if (isText$5(caretContainerNode)) { 10991 deleteZwspChars(caretContainerNode); 10992 if (caretContainerNode.data.length === 0) { 10993 removeNode$1(caretContainerNode); 10994 } 10995 } 10996 }; 10997 10998 const isContentEditableFalse$7 = isContentEditableFalse$a; 10999 const isMedia$1 = isMedia$2; 11000 const isTableCell$1 = isTableCell$3; 11001 const inlineFakeCaretSelector = '*[contentEditable=false],video,audio,embed,object'; 11002 const getAbsoluteClientRect = (root, element, before) => { 11003 const clientRect = collapse(element.getBoundingClientRect(), before); 11004 let scrollX; 11005 let scrollY; 11006 if (root.tagName === 'BODY') { 11007 const docElm = root.ownerDocument.documentElement; 11008 scrollX = root.scrollLeft || docElm.scrollLeft; 11009 scrollY = root.scrollTop || docElm.scrollTop; 11010 } 11011 else { 11012 const rootRect = root.getBoundingClientRect(); 11013 scrollX = root.scrollLeft - rootRect.left; 11014 scrollY = root.scrollTop - rootRect.top; 11015 } 11016 clientRect.left += scrollX; 11017 clientRect.right += scrollX; 11018 clientRect.top += scrollY; 11019 clientRect.bottom += scrollY; 11020 clientRect.width = 1; 11021 let margin = element.offsetWidth - element.clientWidth; 11022 if (margin > 0) { 11023 if (before) { 11024 margin *= -1; 11025 } 11026 clientRect.left += margin; 11027 clientRect.right += margin; 11028 } 11029 return clientRect; 11030 }; 11031 const trimInlineCaretContainers = (root) => { 11032 const fakeCaretTargetNodes = descendants(SugarElement.fromDom(root), inlineFakeCaretSelector); 11033 for (let i = 0; i < fakeCaretTargetNodes.length; i++) { 11034 const node = fakeCaretTargetNodes[i].dom; 11035 let sibling = node.previousSibling; 11036 if (endsWithCaretContainer$1(sibling)) { 11037 const data = sibling.data; 11038 if (data.length === 1) { 11039 sibling.parentNode?.removeChild(sibling); 11040 } 11041 else { 11042 sibling.deleteData(data.length - 1, 1); 11043 } 11044 } 11045 sibling = node.nextSibling; 11046 if (startsWithCaretContainer$1(sibling)) { 11047 const data = sibling.data; 11048 if (data.length === 1) { 11049 sibling.parentNode?.removeChild(sibling); 11050 } 11051 else { 11052 sibling.deleteData(0, 1); 11053 } 11054 } 11055 } 11056 }; 11057 const FakeCaret = (editor, root, isBlock, hasFocus) => { 11058 const lastVisualCaret = value$1(); 11059 let cursorInterval; 11060 let caretContainerNode; 11061 const caretBlock = getForcedRootBlock(editor); 11062 const dom = editor.dom; 11063 const show = (before, element) => { 11064 let rng; 11065 hide(); 11066 if (isTableCell$1(element)) { 11067 return null; 11068 } 11069 if (isBlock(element)) { 11070 const caretContainer = insertBlock(caretBlock, element, before); 11071 const clientRect = getAbsoluteClientRect(root, element, before); 11072 dom.setStyle(caretContainer, 'top', clientRect.top); 11073 dom.setStyle(caretContainer, 'caret-color', 'transparent'); 11074 caretContainerNode = caretContainer; 11075 const caret = dom.create('div', { 'class': 'mce-visual-caret', 'data-mce-bogus': 'all' }); 11076 dom.setStyles(caret, { ...clientRect }); 11077 dom.add(root, caret); 11078 lastVisualCaret.set({ caret, element, before }); 11079 if (before) { 11080 dom.addClass(caret, 'mce-visual-caret-before'); 11081 } 11082 startBlink(); 11083 rng = element.ownerDocument.createRange(); 11084 rng.setStart(caretContainer, 0); 11085 rng.setEnd(caretContainer, 0); 11086 } 11087 else { 11088 caretContainerNode = insertInline$1(element, before); 11089 rng = element.ownerDocument.createRange(); 11090 if (isInlineFakeCaretTarget(caretContainerNode.nextSibling)) { 11091 rng.setStart(caretContainerNode, 0); 11092 rng.setEnd(caretContainerNode, 0); 11093 } 11094 else { 11095 rng.setStart(caretContainerNode, 1); 11096 rng.setEnd(caretContainerNode, 1); 11097 } 11098 return rng; 11099 } 11100 return rng; 11101 }; 11102 const hide = () => { 11103 // TODO: TINY-6015 - Ensure cleaning up the fake caret preserves the selection, as currently 11104 // the CaretContainerRemove.remove below will change the selection in some cases 11105 trimInlineCaretContainers(root); 11106 if (caretContainerNode) { 11107 remove$2(caretContainerNode); 11108 caretContainerNode = null; 11109 } 11110 lastVisualCaret.on((caretState) => { 11111 dom.remove(caretState.caret); 11112 lastVisualCaret.clear(); 11113 }); 11114 if (cursorInterval) { 11115 clearInterval(cursorInterval); 11116 cursorInterval = undefined; 11117 } 11118 }; 11119 const startBlink = () => { 11120 cursorInterval = window.setInterval(() => { 11121 lastVisualCaret.on((caretState) => { 11122 if (hasFocus()) { 11123 dom.toggleClass(caretState.caret, 'mce-visual-caret-hidden'); 11124 } 11125 else { 11126 dom.addClass(caretState.caret, 'mce-visual-caret-hidden'); 11127 } 11128 }); 11129 }, 500); 11130 }; 11131 const reposition = () => { 11132 lastVisualCaret.on((caretState) => { 11133 const clientRect = getAbsoluteClientRect(root, caretState.element, caretState.before); 11134 dom.setStyles(caretState.caret, { ...clientRect }); 11135 }); 11136 }; 11137 const destroy = () => clearInterval(cursorInterval); 11138 const getCss = () => ('.mce-visual-caret {' + 11139 'position: absolute;' + 11140 'background-color: black;' + 11141 'background-color: currentcolor;' + 11142 // 'background-color: red;' + 11143 '}' + 11144 '.mce-visual-caret-hidden {' + 11145 'display: none;' + 11146 '}' + 11147 '*[data-mce-caret] {' + 11148 'position: absolute;' + 11149 'left: -1000px;' + 11150 'right: auto;' + 11151 'top: 0;' + 11152 'margin: 0;' + 11153 'padding: 0;' + 11154 '}'); 11155 return { 11156 isShowing: lastVisualCaret.isSet, 11157 show, 11158 hide, 11159 getCss, 11160 reposition, 11161 destroy 11162 }; 11163 }; 11164 const isFakeCaretTableBrowser = () => Env.browser.isFirefox(); 11165 const isInlineFakeCaretTarget = (node) => isContentEditableFalse$7(node) || isMedia$1(node); 11166 const isFakeCaretTarget = (node) => { 11167 const isTarget = isInlineFakeCaretTarget(node) || (isTable$2(node) && isFakeCaretTableBrowser()); 11168 return isTarget && parentElement(SugarElement.fromDom(node)).exists(isEditable$2); 11169 }; 11170 11171 const isContentEditableTrue$1 = isContentEditableTrue$3; 11172 const isContentEditableFalse$6 = isContentEditableFalse$a; 11173 const isMedia = isMedia$2; 11174 const isBlockLike = matchStyleValues('display', 'block table table-cell table-row table-caption list-item'); 11175 const isCaretContainer = isCaretContainer$2; 11176 const isCaretContainerBlock = isCaretContainerBlock$1; 11177 const isElement$3 = isElement$7; 11178 const isText$4 = isText$b; 11179 const isCaretCandidate$1 = isCaretCandidate$3; 11180 const skipCaretContainers = (walk, shallow) => { 11181 let node; 11182 while ((node = walk(shallow))) { 11183 if (!isCaretContainerBlock(node)) { 11184 return node; 11185 } 11186 } 11187 return null; 11188 }; 11189 const findNode = (node, direction, predicateFn, rootNode, shallow) => { 11190 const walker = new DomTreeWalker(node, rootNode); 11191 const isCefOrCaretContainer = isContentEditableFalse$6(node) || isCaretContainerBlock(node); 11192 let tempNode; 11193 if (isBackwards(direction)) { 11194 if (isCefOrCaretContainer) { 11195 tempNode = skipCaretContainers(walker.prev.bind(walker), true); 11196 if (predicateFn(tempNode)) { 11197 return tempNode; 11198 } 11199 } 11200 while ((tempNode = skipCaretContainers(walker.prev.bind(walker), shallow))) { 11201 if (predicateFn(tempNode)) { 11202 return tempNode; 11203 } 11204 } 11205 } 11206 if (isForwards(direction)) { 11207 if (isCefOrCaretContainer) { 11208 tempNode = skipCaretContainers(walker.next.bind(walker), true); 11209 if (predicateFn(tempNode)) { 11210 return tempNode; 11211 } 11212 } 11213 while ((tempNode = skipCaretContainers(walker.next.bind(walker), shallow))) { 11214 if (predicateFn(tempNode)) { 11215 return tempNode; 11216 } 11217 } 11218 } 11219 return null; 11220 }; 11221 const getEditingHost = (node, rootNode) => { 11222 const isCETrue = (node) => isContentEditableTrue$1(node.dom); 11223 const isRoot = (node) => node.dom === rootNode; 11224 return ancestor$5(SugarElement.fromDom(node), isCETrue, isRoot) 11225 .map((elm) => elm.dom) 11226 .getOr(rootNode); 11227 }; 11228 const isAbsPositionedElement = (node) => isElement$7(node) && get$7
vendor: 40,705 bytes, lines 11228-12170
11228(SugarElement.fromDom(node), 'position') === 'absolute'; 11229 const isInlineBlock = (node, rootNode) => node.parentNode !== rootNode; 11230 const isInlineAbsPositionedCEF = (node, rootNode) => isContentEditableFalse$6(node) && isAbsPositionedElement(node) && isInlineBlock(node, rootNode); 11231 const getParentBlock$3 = (node, rootNode) => { 11232 while (node && node !== rootNode) { 11233 // Exclude inline absolutely positioned CEF elements since they have 'display: block' 11234 // Created TINY-12922 to improve handling non CEF elements 11235 if (isBlockLike(node) && !isInlineAbsPositionedCEF(node, rootNode)) { 11236 return node; 11237 } 11238 node = node.parentNode; 11239 } 11240 return null; 11241 }; 11242 const isInSameBlock = (caretPosition1, caretPosition2, rootNode) => getParentBlock$3(caretPosition1.container(), rootNode) === getParentBlock$3(caretPosition2.container(), rootNode); 11243 const getChildNodeAtRelativeOffset = (relativeOffset, caretPosition) => { 11244 if (!caretPosition) { 11245 return Optional.none(); 11246 } 11247 const container = caretPosition.container(); 11248 const offset = caretPosition.offset(); 11249 if (!isElement$3(container)) { 11250 return Optional.none(); 11251 } 11252 return Optional.from(container.childNodes[offset + relativeOffset]); 11253 }; 11254 const beforeAfter = (before, node) => { 11255 const doc = node.ownerDocument ?? document; 11256 const range = doc.createRange(); 11257 if (before) { 11258 range.setStartBefore(node); 11259 range.setEndBefore(node); 11260 } 11261 else { 11262 range.setStartAfter(node); 11263 range.setEndAfter(node); 11264 } 11265 return range; 11266 }; 11267 const isNodesInSameBlock = (root, node1, node2) => getParentBlock$3(node1, root) === getParentBlock$3(node2, root); 11268 const lean = (left, root, node) => { 11269 const siblingName = left ? 'previousSibling' : 'nextSibling'; 11270 let tempNode = node; 11271 while (tempNode && tempNode !== root) { 11272 let sibling = tempNode[siblingName]; 11273 if (sibling && isCaretContainer(sibling)) { 11274 sibling = sibling[siblingName]; 11275 } 11276 if (isContentEditableFalse$6(sibling) || isMedia(sibling)) { 11277 if (isNodesInSameBlock(root, sibling, tempNode)) { 11278 return sibling; 11279 } 11280 break; 11281 } 11282 if (isCaretCandidate$1(sibling)) { 11283 break; 11284 } 11285 tempNode = tempNode.parentNode; 11286 } 11287 return null; 11288 }; 11289 const before$1 = curry(beforeAfter, true); 11290 const after$1 = curry(beforeAfter, false); 11291 const normalizeRange$2 = (direction, root, range) => { 11292 let node; 11293 const leanLeft = curry(lean, true, root); 11294 const leanRight = curry(lean, false, root); 11295 const container = range.startContainer; 11296 const offset = range.startOffset; 11297 if (isCaretContainerBlock$1(container)) { 11298 const block = isText$4(container) ? container.parentNode : container; 11299 const location = block.getAttribute('data-mce-caret'); 11300 if (location === 'before') { 11301 node = block.nextSibling; 11302 if (isFakeCaretTarget(node)) { 11303 return before$1(node); 11304 } 11305 } 11306 if (location === 'after') { 11307 node = block.previousSibling; 11308 if (isFakeCaretTarget(node)) { 11309 return after$1(node); 11310 } 11311 } 11312 } 11313 if (!range.collapsed) { 11314 return range; 11315 } 11316 if (isText$b(container)) { 11317 if (isCaretContainer(container)) { 11318 if (direction === 1) { 11319 node = leanRight(container); 11320 if (node) { 11321 return before$1(node); 11322 } 11323 node = leanLeft(container); 11324 if (node) { 11325 return after$1(node); 11326 } 11327 } 11328 if (direction === -1) { 11329 node = leanLeft(container); 11330 if (node) { 11331 return after$1(node); 11332 } 11333 node = leanRight(container); 11334 if (node) { 11335 return before$1(node); 11336 } 11337 } 11338 return range; 11339 } 11340 if (endsWithCaretContainer$1(container) && offset >= container.data.length - 1) { 11341 if (direction === 1) { 11342 node = leanRight(container); 11343 if (node) { 11344 return before$1(node); 11345 } 11346 } 11347 return range; 11348 } 11349 if (startsWithCaretContainer$1(container) && offset <= 1) { 11350 if (direction === -1) { 11351 node = leanLeft(container); 11352 if (node) { 11353 return after$1(node); 11354 } 11355 } 11356 return range; 11357 } 11358 if (offset === container.data.length) { 11359 node = leanRight(container); 11360 if (node) { 11361 return before$1(node); 11362 } 11363 return range; 11364 } 11365 if (offset === 0) { 11366 node = leanLeft(container); 11367 if (node) { 11368 return after$1(node); 11369 } 11370 return range; 11371 } 11372 } 11373 return range; 11374 }; 11375 const getRelativeCefElm = (forward, caretPosition) => getChildNodeAtRelativeOffset(forward ? 0 : -1, caretPosition).filter(isContentEditableFalse$6); 11376 const getNormalizedRangeEndPoint = (direction, root, range) => { 11377 const normalizedRange = normalizeRange$2(direction, root, range); 11378 return direction === -1 ? CaretPosition.fromRangeStart(normalizedRange) : CaretPosition.fromRangeEnd(normalizedRange); 11379 }; 11380 const getElementFromPosition = (pos) => Optional.from(pos.getNode()).map(SugarElement.fromDom); 11381 const getElementFromPrevPosition = (pos) => Optional.from(pos.getNode(true)).map(SugarElement.fromDom); 11382 const getVisualCaretPosition = (walkFn, caretPosition) => { 11383 let pos = caretPosition; 11384 while ((pos = walkFn(pos))) { 11385 if (pos.isVisible()) { 11386 return pos; 11387 } 11388 } 11389 return pos; 11390 }; 11391 const isMoveInsideSameBlock = (from, to) => { 11392 const inSameBlock = isInSameBlock(from, to); 11393 // Handle bogus BR <p>abc|<br></p> 11394 if (!inSameBlock && isBr$7(from.getNode())) { 11395 return true; 11396 } 11397 return inSameBlock; 11398 }; 11399 11400 /** 11401 * This module contains logic for moving around a virtual caret in logical order within a DOM element. 11402 * 11403 * It ignores the most obvious invalid caret locations such as within a script element or within a 11404 * contentEditable=false element but it will return locations that isn't possible to render visually. 11405 * 11406 * @private 11407 * @class tinymce.caret.CaretWalker 11408 * @example 11409 * const caretWalker = CaretWalker(rootElm); 11410 * 11411 * const prevLogicalCaretPosition = caretWalker.prev(CaretPosition.fromRangeStart(range)); 11412 * const nextLogicalCaretPosition = caretWalker.next(CaretPosition.fromRangeEnd(range)); 11413 */ 11414 const isContentEditableFalse$5 = isContentEditableFalse$a; 11415 const isText$3 = isText$b; 11416 const isElement$2 = isElement$7; 11417 const isBr$3 = isBr$7; 11418 const isCaretCandidate = isCaretCandidate$3; 11419 const isAtomic = isAtomic$1; 11420 const isEditableCaretCandidate = isEditableCaretCandidate$1; 11421 const getParents$3 = (node, root) => { 11422 const parents = []; 11423 let tempNode = node; 11424 while (tempNode && tempNode !== root) { 11425 parents.push(tempNode); 11426 tempNode = tempNode.parentNode; 11427 } 11428 return parents; 11429 }; 11430 const nodeAtIndex = (container, offset) => { 11431 if (container.hasChildNodes() && offset < container.childNodes.length) { 11432 return container.childNodes[offset]; 11433 } 11434 return null; 11435 }; 11436 const getCaretCandidatePosition = (direction, node) => { 11437 if (isForwards(direction)) { 11438 if (isCaretCandidate(node.previousSibling) && !isText$3(node.previousSibling)) { 11439 return CaretPosition.before(node); 11440 } 11441 if (isText$3(node)) { 11442 return CaretPosition(node, 0); 11443 } 11444 } 11445 if (isBackwards(direction)) { 11446 if (isCaretCandidate(node.nextSibling) && !isText$3(node.nextSibling)) { 11447 return CaretPosition.after(node); 11448 } 11449 if (isText$3(node)) { 11450 return CaretPosition(node, node.data.length); 11451 } 11452 } 11453 if (isBackwards(direction)) { 11454 if (isBr$3(node)) { 11455 return CaretPosition.before(node); 11456 } 11457 return CaretPosition.after(node); 11458 } 11459 return CaretPosition.before(node); 11460 }; 11461 const moveForwardFromBr = (root, nextNode) => { 11462 const nextSibling = nextNode.nextSibling; 11463 if (nextSibling && isCaretCandidate(nextSibling)) { 11464 if (isText$3(nextSibling)) { 11465 return CaretPosition(nextSibling, 0); 11466 } 11467 else { 11468 return CaretPosition.before(nextSibling); 11469 } 11470 } 11471 else { 11472 return findCaretPosition$1(1 /* HDirection.Forwards */, CaretPosition.after(nextNode), root); 11473 } 11474 }; 11475 const findCaretPosition$1 = (direction, startPos, root) => { 11476 let node; 11477 let nextNode; 11478 let innerNode; 11479 let caretPosition; 11480 if (!isElement$2(root) || !startPos) { 11481 return null; 11482 } 11483 if (startPos.isEqual(CaretPosition.after(root)) && root.lastChild) { 11484 caretPosition = CaretPosition.after(root.lastChild); 11485 if (isBackwards(direction) && isCaretCandidate(root.lastChild) && isElement$2(root.lastChild)) { 11486 return isBr$3(root.lastChild) ? CaretPosition.before(root.lastChild) : caretPosition; 11487 } 11488 } 11489 else { 11490 caretPosition = startPos; 11491 } 11492 const container = caretPosition.container(); 11493 let offset = caretPosition.offset(); 11494 if (isText$3(container)) { 11495 if (isBackwards(direction) && offset > 0) { 11496 return CaretPosition(container, --offset); 11497 } 11498 if (isForwards(direction) && offset < container.length) { 11499 return CaretPosition(container, ++offset); 11500 } 11501 node = container; 11502 } 11503 else { 11504 if (isBackwards(direction) && offset > 0) { 11505 nextNode = nodeAtIndex(container, offset - 1); 11506 if (isCaretCandidate(nextNode)) { 11507 if (!isAtomic(nextNode)) { 11508 innerNode = findNode(nextNode, direction, isEditableCaretCandidate, nextNode); 11509 if (innerNode) { 11510 if (isText$3(innerNode)) { 11511 return CaretPosition(innerNode, innerNode.data.length); 11512 } 11513 return CaretPosition.after(innerNode); 11514 } 11515 } 11516 if (isText$3(nextNode)) { 11517 return CaretPosition(nextNode, nextNode.data.length); 11518 } 11519 return CaretPosition.before(nextNode); 11520 } 11521 } 11522 if (isForwards(direction) && offset < container.childNodes.length) { 11523 nextNode = nodeAtIndex(container, offset); 11524 if (isCaretCandidate(nextNode)) { 11525 if (isBr$3(nextNode)) { 11526 return moveForwardFromBr(root, nextNode); 11527 } 11528 if (!isAtomic(nextNode)) { 11529 innerNode = findNode(nextNode, direction, isEditableCaretCandidate, nextNode); 11530 if (innerNode) { 11531 if (isText$3(innerNode)) { 11532 return CaretPosition(innerNode, 0); 11533 } 11534 return CaretPosition.before(innerNode); 11535 } 11536 } 11537 if (isText$3(nextNode)) { 11538 return CaretPosition(nextNode, 0); 11539 } 11540 return CaretPosition.after(nextNode); 11541 } 11542 } 11543 node = nextNode ? nextNode : caretPosition.getNode(); 11544 } 11545 if (node && ((isForwards(direction) && caretPosition.isAtEnd()) || (isBackwards(direction) && caretPosition.isAtStart()))) { 11546 node = findNode(node, direction, always, root, true); 11547 if (isEditableCaretCandidate(node, root)) { 11548 return getCaretCandidatePosition(direction, node); 11549 } 11550 } 11551 nextNode = node ? findNode(node, direction, isEditableCaretCandidate, root) : node; 11552 const rootContentEditableFalseElm = last(filter$5(getParents$3(container, root), isContentEditableFalse$5)); 11553 if (rootContentEditableFalseElm && (!nextNode || !rootContentEditableFalseElm.contains(nextNode))) { 11554 if (isForwards(direction)) { 11555 caretPosition = CaretPosition.after(rootContentEditableFalseElm); 11556 } 11557 else { 11558 caretPosition = CaretPosition.before(rootContentEditableFalseElm); 11559 } 11560 return caretPosition; 11561 } 11562 if (nextNode) { 11563 return getCaretCandidatePosition(direction, nextNode); 11564 } 11565 return null; 11566 }; 11567 const CaretWalker = (root) => ({ 11568 /** 11569 * Returns the next logical caret position from the specified input 11570 * caretPosition or null if there isn't any more positions left for example 11571 * at the end specified root element. 11572 * 11573 * @method next 11574 * @param {tinymce.caret.CaretPosition} caretPosition Caret position to start from. 11575 * @return {tinymce.caret.CaretPosition} CaretPosition or null if no position was found. 11576 */ 11577 next: (caretPosition) => { 11578 return findCaretPosition$1(1 /* HDirection.Forwards */, caretPosition, root); 11579 }, 11580 /** 11581 * Returns the previous logical caret position from the specified input 11582 * caretPosition or null if there isn't any more positions left for example 11583 * at the end specified root element. 11584 * 11585 * @method prev 11586 * @param {tinymce.caret.CaretPosition} caretPosition Caret position to start from. 11587 * @return {tinymce.caret.CaretPosition} CaretPosition or null if no position was found. 11588 */ 11589 prev: (caretPosition) => { 11590 return findCaretPosition$1(-1 /* HDirection.Backwards */, caretPosition, root); 11591 } 11592 }); 11593 11594 const walkToPositionIn = (forward, root, start) => { 11595 const position = forward ? CaretPosition.before(start) : CaretPosition.after(start); 11596 return fromPosition(forward, root, position); 11597 }; 11598 const afterElement = (node) => isBr$7(node) ? CaretPosition.before(node) : CaretPosition.after(node); 11599 const isBeforeOrStart = (position) => { 11600 if (CaretPosition.isTextPosition(position)) { 11601 return position.offset() === 0; 11602 } 11603 else { 11604 return isCaretCandidate$3(position.getNode()); 11605 } 11606 }; 11607 const isAfterOrEnd = (position) => { 11608 if (CaretPosition.isTextPosition(position)) { 11609 const container = position.container(); 11610 return position.offset() === container.data.length; 11611 } 11612 else { 11613 return isCaretCandidate$3(position.getNode(true)); 11614 } 11615 }; 11616 const isBeforeAfterSameElement = (from, to) => !CaretPosition.isTextPosition(from) && !CaretPosition.isTextPosition(to) && from.getNode() === to.getNode(true); 11617 const isAtBr = (position) => !CaretPosition.isTextPosition(position) && isBr$7(position.getNode()); 11618 const shouldSkipPosition = (forward, from, to) => { 11619 if (forward) { 11620 return !isBeforeAfterSameElement(from, to) && !isAtBr(from) && isAfterOrEnd(from) && isBeforeOrStart(to); 11621 } 11622 else { 11623 return !isBeforeAfterSameElement(to, from) && isBeforeOrStart(from) && isAfterOrEnd(to); 11624 } 11625 }; 11626 // Finds: <p>a|<b>b</b></p> -> <p>a<b>|b</b></p> 11627 const fromPosition = (forward, root, pos) => { 11628 const walker = CaretWalker(root); 11629 return Optional.from(forward ? walker.next(pos) : walker.prev(pos)); 11630 }; 11631 // Finds: <p>a|<b>b</b></p> -> <p>a<b>b|</b></p> 11632 const navigate = (forward, root, from) => fromPosition(forward, root, from).bind((to) => { 11633 if (isInSameBlock(from, to, root) && shouldSkipPosition(forward, from, to)) { 11634 return fromPosition(forward, root, to); 11635 } 11636 else { 11637 return Optional.some(to); 11638 } 11639 }); 11640 const navigateIgnore = (forward, root, from, ignoreFilter) => navigate(forward, root, from) 11641 .bind((pos) => ignoreFilter(pos) ? navigateIgnore(forward, root, pos, ignoreFilter) : Optional.some(pos)); 11642 const positionIn = (forward, element) => { 11643 const startNode = forward ? element.firstChild : element.lastChild; 11644 if (isText$b(startNode)) { 11645 return Optional.some(CaretPosition(startNode, forward ? 0 : startNode.data.length)); 11646 } 11647 else if (startNode) { 11648 if (isCaretCandidate$3(startNode)) { 11649 return Optional.some(forward ? CaretPosition.before(startNode) : afterElement(startNode)); 11650 } 11651 else { 11652 return walkToPositionIn(forward, element, startNode); 11653 } 11654 } 11655 else { 11656 return Optional.none(); 11657 } 11658 }; 11659 const nextPosition = curry(fromPosition, true); 11660 const prevPosition = curry(fromPosition, false); 11661 const firstPositionIn = curry(positionIn, true); 11662 const lastPositionIn = curry(positionIn, false); 11663 11664 const CARET_ID = '_mce_caret'; 11665 const isCaretNode = (node) => isElement$7(node) && node.id === CARET_ID; 11666 const getParentCaretContainer = (body, node) => { 11667 let currentNode = node; 11668 while (currentNode && currentNode !== body) { 11669 if (isCaretNode(currentNode)) { 11670 return currentNode; 11671 } 11672 currentNode = currentNode.parentNode; 11673 } 11674 return null; 11675 }; 11676 11677 const isStringPathBookmark = (bookmark) => isString(bookmark.start); 11678 const isRangeBookmark = (bookmark) => has$2(bookmark, 'rng'); 11679 const isIdBookmark = (bookmark) => has$2(bookmark, 'id'); 11680 const isIndexBookmark = (bookmark) => has$2(bookmark, 'name'); 11681 const isPathBookmark = (bookmark) => Tools.isArray(bookmark.start); 11682 11683 const isForwardBookmark = (bookmark) => !isIndexBookmark(bookmark) && isBoolean(bookmark.forward) ? bookmark.forward : true; 11684 const addBogus = (dom, node) => { 11685 // Adds a bogus BR element for empty block elements 11686 if (isElement$7(node) && dom.isBlock(node) && !node.innerHTML) { 11687 node.innerHTML = '<br data-mce-bogus="1" />'; 11688 } 11689 return node; 11690 }; 11691 const resolveCaretPositionBookmark = (dom, bookmark) => { 11692 const startPos = Optional.from(resolve$1(dom.getRoot(), bookmark.start)); 11693 const endPos = Optional.from(resolve$1(dom.getRoot(), bookmark.end)); 11694 return lift2(startPos, endPos, (start, end) => { 11695 const range = dom.createRng(); 11696 range.setStart(start.container(), start.offset()); 11697 range.setEnd(end.container(), end.offset()); 11698 return { range, forward: isForwardBookmark(bookmark) }; 11699 }); 11700 }; 11701 const insertZwsp = (node, rng) => { 11702 const doc = node.ownerDocument ?? document; 11703 const textNode = doc.createTextNode(ZWSP$1); 11704 node.appendChild(textNode); 11705 rng.setStart(textNode, 0); 11706 rng.setEnd(textNode, 0); 11707 }; 11708 const isEmpty$3 = (node) => !node.hasChildNodes(); 11709 const tryFindRangePosition = (node, rng) => lastPositionIn(node).fold(never, (pos) => { 11710 rng.setStart(pos.container(), pos.offset()); 11711 rng.setEnd(pos.container(), pos.offset()); 11712 return true; 11713 }); 11714 // Since we trim zwsp from undo levels the caret format containers 11715 // may be empty if so pad them with a zwsp and move caret there 11716 const padEmptyCaretContainer = (root, node, rng) => { 11717 if (isEmpty$3(node) && getParentCaretContainer(root, node)) { 11718 insertZwsp(node, rng); 11719 return true; 11720 } 11721 else { 11722 return false; 11723 } 11724 }; 11725 const setEndPoint = (dom, start, bookmark, rng) => { 11726 const point = bookmark[start ? 'start' : 'end']; 11727 const root = dom.getRoot(); 11728 if (point) { 11729 let node = root; 11730 let offset = point[0]; 11731 // Find container node 11732 for (let i = point.length - 1; node && i >= 1; i--) { 11733 const children = node.childNodes; 11734 if (padEmptyCaretContainer(root, node, rng)) { 11735 return true; 11736 } 11737 if (point[i] > children.length - 1) { 11738 if (padEmptyCaretContainer(root, node, rng)) { 11739 return true; 11740 } 11741 return tryFindRangePosition(node, rng); 11742 } 11743 node = children[point[i]]; 11744 } 11745 // Move text offset to best suitable location 11746 if (isText$b(node)) { 11747 offset = Math.min(point[0], node.data.length); 11748 } 11749 // Move element offset to best suitable location 11750 if (isElement$7(node)) { 11751 offset = Math.min(point[0], node.childNodes.length); 11752 } 11753 // Set offset within container node 11754 if (start) { 11755 rng.setStart(node, offset); 11756 } 11757 else { 11758 rng.setEnd(node, offset); 11759 } 11760 } 11761 return true; 11762 }; 11763 const isValidTextNode = (node) => isText$b(node) && node.data.length > 0; 11764 const restoreEndPoint$1 = (dom, suffix, bookmark) => { 11765 const marker = dom.get(bookmark.id + '_' + suffix); 11766 const markerParent = marker?.parentNode; 11767 const keep = bookmark.keep; 11768 if (marker && markerParent) { 11769 let container; 11770 let offset; 11771 if (suffix === 'start') { 11772 if (!keep) { 11773 container = markerParent; 11774 offset = dom.nodeIndex(marker); 11775 } 11776 else { 11777 if (marker.hasChildNodes()) { 11778 container = marker.firstChild; 11779 offset = 1; 11780 } 11781 else if (isValidTextNode(marker.nextSibling)) { 11782 container = marker.nextSibling; 11783 offset = 0; 11784 } 11785 else if (isValidTextNode(marker.previousSibling)) { 11786 container = marker.previousSibling; 11787 offset = marker.previousSibling.data.length; 11788 } 11789 else { 11790 container = markerParent; 11791 offset = dom.nodeIndex(marker) + 1; 11792 } 11793 } 11794 } 11795 else { 11796 if (!keep) { 11797 container = markerParent; 11798 offset = dom.nodeIndex(marker); 11799 } 11800 else { 11801 if (marker.hasChildNodes()) { 11802 container = marker.firstChild; 11803 offset = 1; 11804 } 11805 else if (isValidTextNode(marker.previousSibling)) { 11806 container = marker.previousSibling; 11807 offset = marker.previousSibling.data.length; 11808 } 11809 else { 11810 container = markerParent; 11811 offset = dom.nodeIndex(marker); 11812 } 11813 } 11814 } 11815 if (!keep) { 11816 const prev = marker.previousSibling; 11817 const next = marker.nextSibling; 11818 // Remove all marker text nodes 11819 Tools.each(Tools.grep(marker.childNodes), (node) => { 11820 if (isText$b(node)) { 11821 node.data = node.data.replace(/\uFEFF/g, ''); 11822 } 11823 }); 11824 // Remove marker but keep children if for example contents where inserted into the marker 11825 // Also remove duplicated instances of the marker for example by a 11826 // split operation or by WebKit auto split on paste feature 11827 let otherMarker; 11828 while ((otherMarker = dom.get(bookmark.id + '_' + suffix))) { 11829 dom.remove(otherMarker, true); 11830 } 11831 // If siblings are text nodes then merge them unless it's Opera since it some how removes the node 11832 // and we are sniffing since adding a lot of detection code for a browser with 3% of the market 11833 // isn't worth the effort. Sorry, Opera but it's just a fact 11834 if (isText$b(next) && isText$b(prev) && !Env.browser.isOpera()) { 11835 const idx = prev.data.length; 11836 prev.appendData(next.data); 11837 dom.remove(next); 11838 container = prev; 11839 offset = idx; 11840 } 11841 } 11842 return Optional.some(CaretPosition(container, offset)); 11843 } 11844 else { 11845 return Optional.none(); 11846 } 11847 }; 11848 const resolvePaths = (dom, bookmark) => { 11849 const range = dom.createRng(); 11850 if (setEndPoint(dom, true, bookmark, range) && setEndPoint(dom, false, bookmark, range)) { 11851 return Optional.some({ range, forward: isForwardBookmark(bookmark) }); 11852 } 11853 else { 11854 return Optional.none(); 11855 } 11856 }; 11857 const resolveId = (dom, bookmark) => { 11858 const startPos = restoreEndPoint$1(dom, 'start', bookmark); 11859 const endPos = restoreEndPoint$1(dom, 'end', bookmark); 11860 return lift2(startPos, endPos.or(startPos), (spos, epos) => { 11861 const range = dom.createRng(); 11862 range.setStart(addBogus(dom, spos.container()), spos.offset()); 11863 range.setEnd(addBogus(dom, epos.container()), epos.offset()); 11864 return { range, forward: isForwardBookmark(bookmark) }; 11865 }); 11866 }; 11867 const resolveIndex = (dom, bookmark) => Optional.from(dom.select(bookmark.name)[bookmark.index]).map((elm) => { 11868 const range = dom.createRng(); 11869 range.selectNode(elm); 11870 return { range, forward: true }; 11871 }); 11872 const resolve = (selection, bookmark) => { 11873 const dom = selection.dom; 11874 if (bookmark) { 11875 if (isPathBookmark(bookmark)) { 11876 return resolvePaths(dom, bookmark); 11877 } 11878 else if (isStringPathBookmark(bookmark)) { 11879 return resolveCaretPositionBookmark(dom, bookmark); 11880 } 11881 else if (isIdBookmark(bookmark)) { 11882 return resolveId(dom, bookmark); 11883 } 11884 else if (isIndexBookmark(bookmark)) { 11885 return resolveIndex(dom, bookmark); 11886 } 11887 else if (isRangeBookmark(bookmark)) { 11888 return Optional.some({ range: bookmark.rng, forward: isForwardBookmark(bookmark) }); 11889 } 11890 } 11891 return Optional.none(); 11892 }; 11893 11894 const getBookmark$1 = (selection, type, normalized) => { 11895 return getBookmark$2(selection, type, normalized); 11896 }; 11897 const moveToBookmark = (selection, bookmark) => { 11898 resolve(selection, bookmark).each(({ range, forward }) => { 11899 selection.setRng(range, forward); 11900 }); 11901 }; 11902 const isBookmarkNode$1 = (node) => { 11903 return isElement$7(node) && node.tagName === 'SPAN' && node.getAttribute('data-mce-type') === 'bookmark'; 11904 }; 11905 11906 const is = (expected) => (actual) => expected === actual; 11907 const isNbsp = is(nbsp); 11908 const isWhiteSpace = (chr) => chr !== '' && ' \f\n\r\t\v'.indexOf(chr) !== -1; 11909 const isContent = (chr) => !isWhiteSpace(chr) && !isNbsp(chr) && !isZwsp$2(chr); 11910 11911 const getRanges = (selection) => { 11912 const ranges = []; 11913 if (selection) { 11914 for (let i = 0; i < selection.rangeCount; i++) { 11915 ranges.push(selection.getRangeAt(i)); 11916 } 11917 } 11918 return ranges; 11919 }; 11920 const getSelectedNodes = (ranges) => { 11921 return bind$3(ranges, (range) => { 11922 const node = getSelectedNode(range); 11923 return node ? [SugarElement.fromDom(node)] : []; 11924 }); 11925 }; 11926 const hasMultipleRanges = (selection) => { 11927 return getRanges(selection).length > 1; 11928 }; 11929 11930 const getCellsFromRanges = (ranges) => filter$5(getSelectedNodes(ranges), isTableCell$2); 11931 const getCellsFromElement = (elm) => descendants(elm, 'td[data-mce-selected],th[data-mce-selected]'); 11932 const getCellsFromElementOrRanges = (ranges, element) => { 11933 const selectedCells = getCellsFromElement(element); 11934 return selectedCells.length > 0 ? selectedCells : getCellsFromRanges(ranges); 11935 }; 11936 const getCellsFromEditor = (editor) => getCellsFromElementOrRanges(getRanges(editor.selection.getSel()), SugarElement.fromDom(editor.getBody())); 11937 const getClosestTable = (cell, isRoot) => ancestor$4(cell, 'table', isRoot); 11938 11939 const getStartNode = (rng) => { 11940 const sc = rng.startContainer, so = rng.startOffset; 11941 if (isText$b(sc)) { 11942 return so === 0 ? Optional.some(SugarElement.fromDom(sc)) : Optional.none(); 11943 } 11944 else { 11945 return Optional.from(sc.childNodes[so]).map(SugarElement.fromDom); 11946 } 11947 }; 11948 const getEndNode = (rng) => { 11949 const ec = rng.endContainer, eo = rng.endOffset; 11950 if (isText$b(ec)) { 11951 return eo === ec.data.length ? Optional.some(SugarElement.fromDom(ec)) : Optional.none(); 11952 } 11953 else { 11954 return Optional.from(ec.childNodes[eo - 1]).map(SugarElement.fromDom); 11955 } 11956 }; 11957 const getFirstChildren = (node) => { 11958 return firstChild(node).fold(constant([node]), (child) => { 11959 return [node].concat(getFirstChildren(child)); 11960 }); 11961 }; 11962 const getLastChildren = (node) => { 11963 return lastChild(node).fold(constant([node]), (child) => { 11964 if (name(child) === 'br') { 11965 return prevSibling(child).map((sibling) => { 11966 return [node].concat(getLastChildren(sibling)); 11967 }).getOr([]); 11968 } 11969 else { 11970 return [node].concat(getLastChildren(child)); 11971 } 11972 }); 11973 }; 11974 const hasAllContentsSelected = (elm, rng) => { 11975 return lift2(getStartNode(rng), getEndNode(rng), (startNode, endNode) => { 11976 const start = find$2(getFirstChildren(elm), curry(eq, startNode)); 11977 const end = find$2(getLastChildren(elm), curry(eq, endNode)); 11978 return start.isSome() && end.isSome(); 11979 }).getOr(false); 11980 }; 11981 const moveEndPoint = (dom, rng, node, start) => { 11982 const root = node; 11983 const walker = new DomTreeWalker(node, root); 11984 const moveCaretBeforeOnEnterElementsMap = filter$4(dom.schema.getMoveCaretBeforeOnEnterElements(), (_, name) => !contains$2(['td', 'th', 'table'], name.toLowerCase())); 11985 let currentNode = node; 11986 do { 11987 if (isText$b(currentNode) && Tools.trim(currentNode.data).length !== 0) { 11988 if (start) { 11989 rng.setStart(currentNode, 0); 11990 } 11991 else { 11992 rng.setEnd(currentNode, currentNode.data.length); 11993 } 11994 return; 11995 } 11996 // BR/IMG/INPUT elements but not table cells 11997 if (moveCaretBeforeOnEnterElementsMap[currentNode.nodeName]) { 11998 if (start) { 11999 rng.setStartBefore(currentNode); 12000 } 12001 else { 12002 if (currentNode.nodeName === 'BR') { 12003 rng.setEndBefore(currentNode); 12004 } 12005 else { 12006 rng.setEndAfter(currentNode); 12007 } 12008 } 12009 return; 12010 } 12011 } while ((currentNode = (start ? walker.next() : walker.prev()))); 12012 // Failed to find any text node or other suitable location then move to the root of body 12013 if (root.nodeName === 'BODY') { 12014 if (start) { 12015 rng.setStart(root, 0); 12016 } 12017 else { 12018 rng.setEnd(root, root.childNodes.length); 12019 } 12020 } 12021 }; 12022 const hasAnyRanges = (editor) => { 12023 const sel = editor.selection.getSel(); 12024 return isNonNullable(sel) && sel.rangeCount > 0; 12025 }; 12026 const runOnRanges = (editor, executor) => { 12027 // Check to see if a fake selection is active. If so then we are simulating a multi range 12028 // selection so we should return a range for each selected node. 12029 // Note: Currently tables are the only thing supported for fake selections. 12030 const fakeSelectionNodes = getCellsFromEditor(editor); 12031 if (fakeSelectionNodes.length > 0) { 12032 each$e(fakeSelectionNodes, (elem) => { 12033 const node = elem.dom; 12034 const fakeNodeRng = editor.dom.createRng(); 12035 fakeNodeRng.setStartBefore(node); 12036 fakeNodeRng.setEndAfter(node); 12037 executor(fakeNodeRng, true); 12038 }); 12039 } 12040 else { 12041 executor(editor.selection.getRng(), false); 12042 } 12043 }; 12044 const preserve = (selection, fillBookmark, executor) => { 12045 const bookmark = getPersistentBookmark(selection, fillBookmark); 12046 executor(bookmark); 12047 selection.moveToBookmark(bookmark); 12048 }; 12049 const isSelectionOverWholeNode = (range, nodeTypePredicate) => range.startContainer === range.endContainer 12050 && range.endOffset - range.startOffset === 1 12051 && nodeTypePredicate(range.startContainer.childNodes[range.startOffset]); 12052 const isSelectionOverWholeHTMLElement = (range) => isSelectionOverWholeNode(range, isHTMLElement); 12053 const isSelectionOverWholeTextNode = (range) => isSelectionOverWholeNode(range, isText$b); 12054 const isSelectionOverWholeAnchor = (range) => isSelectionOverWholeNode(range, isAnchor); 12055 12056 const isNode = (node) => isNumber(node?.nodeType); 12057 const isElementNode$1 = (node) => isElement$7(node) && !isBookmarkNode$1(node) && !isCaretNode(node) && !isBogus$1(node); 12058 // In TinyMCE, directly selected elements are indicated with the data-mce-selected attribute 12059 // Elements that can be directly selected include control elements such as img, media elements, noneditable elements and others 12060 const isElementDirectlySelected = (dom, node) => { 12061 // Table cells are a special case and are separately handled from native editor selection 12062 if (isElementNode$1(node) && !/^(TD|TH)$/.test(node.nodeName)) { 12063 const selectedAttr = dom.getAttrib(node, 'data-mce-selected'); 12064 const value = parseInt(selectedAttr, 10); 12065 // Avoid cases where data-mce-selected is not a positive number e.g. inline-boundary 12066 return !isNaN(value) && value > 0; 12067 } 12068 else { 12069 return false; 12070 } 12071 }; 12072 // TODO: TINY-9130 Look at making SelectionUtils.preserve maintain the noneditable selection instead 12073 const preserveSelection = (editor, action, shouldMoveStart) => { 12074 const { selection, dom } = editor; 12075 const selectedNodeBeforeAction = selection.getNode(); 12076 const isSelectedBeforeNodeNoneditable = isContentEditableFalse$a(selectedNodeBeforeAction); 12077 preserve(selection, true, () => { 12078 action(); 12079 }); 12080 // Check previous selected node before the action still exists in the DOM 12081 // and is still noneditable 12082 const isBeforeNodeStillNoneditable = isSelectedBeforeNodeNoneditable && isContentEditableFalse$a(selectedNodeBeforeAction); 12083 if (isBeforeNodeStillNoneditable && dom.isChildOf(selectedNodeBeforeAction, editor.getBody())) { 12084 editor.selection.select(selectedNodeBeforeAction); 12085 } 12086 else if (shouldMoveStart(selection.getStart())) { 12087 moveStartToNearestText(dom, selection); 12088 } 12089 }; 12090 // Note: The reason why we only care about moving the start is because MatchFormat and its function use the start of the selection to determine if a selection has a given format or not 12091 const moveStartToNearestText = (dom, selection) => { 12092 const rng = selection.getRng(); 12093 const { startContainer, startOffset } = rng; 12094 const selectedNode = selection.getNode(); 12095 if (isElementDirectlySelected(dom, selectedNode)) { 12096 return; 12097 } 12098 // Try move startContainer/startOffset to a suitable text node 12099 if (isElement$7(startContainer)) { 12100 const nodes = startContainer.childNodes; 12101 const root = dom.getRoot(); 12102 let walker; 12103 if (startOffset < nodes.length) { 12104 const startNode = nodes[startOffset]; 12105 walker = new DomTreeWalker(startNode, dom.getParent(startNode, dom.isBlock) ?? root); 12106 } 12107 else { 12108 const startNode = nodes[nodes.length - 1]; 12109 walker = new DomTreeWalker(startNode, dom.getParent(startNode, dom.isBlock) ?? root); 12110 walker.next(true); 12111 } 12112 for (let node = walker.current(); node; node = walker.next()) { 12113 // If we have found a noneditable element before we have found any text 12114 // then we cannot move forward any further as otherwise the start could be put inside 12115 // the non-editable element which is not valid 12116 if (dom.getContentEditable(node) === 'false') { 12117 return; 12118 } 12119 else if (isText$b(node) && !isWhiteSpaceNode$1(node)) { 12120 rng.setStart(node, 0); 12121 selection.setRng(rng); 12122 return; 12123 } 12124 } 12125 } 12126 }; 12127 /** 12128 * Returns the next/previous non whitespace node. 12129 * 12130 * @private 12131 * @param {Node} node Node to start at. 12132 * @param {Boolean} next (Optional) Include next or previous node defaults to previous. 12133 * @param {Boolean} inc (Optional) Include the current node in checking. Defaults to false. 12134 * @return {Node} Next or previous node or undefined if it wasn't found. 12135 */ 12136 const getNonWhiteSpaceSibling = (node, next, inc) => { 12137 if (node) { 12138 const nextName = next ? 'nextSibling' : 'previousSibling'; 12139 for (node = inc ? node : node[nextName]; node; node = node[nextName]) { 12140 if (isElement$7(node) || !isWhiteSpaceNode$1(node)) { 12141 return node; 12142 } 12143 } 12144 } 12145 return undefined; 12146 }; 12147 const isTextBlock$2 = (schema, node) => !!schema.getTextBlockElements()[node.nodeName.toLowerCase()] || isTransparentBlock(schema, node); 12148 const isValid = (ed, parent, child) => { 12149 return ed.schema.isValidChild(parent, child); 12150 }; 12151 const isWhiteSpaceNode$1 = (node, allowSpaces = false) => { 12152 if (isNonNullable(node) && isText$b(node)) { 12153 // If spaces are allowed, treat them as a non-breaking space 12154 const data = allowSpaces ? node.data.replace(/ /g, '\u00a0') : node.data; 12155 return isWhitespaceText(data); 12156 } 12157 else { 12158 return false; 12159 } 12160 }; 12161 const isEmptyTextNode$1 = (node) => { 12162 return isNonNullable(node) && isText$b(node) && node.length === 0; 12163 }; 12164 const isWrapNoneditableTarget = (editor, node) => { 12165 const baseDataSelector = '[data-mce-cef-wrappable]'; 12166 const formatNoneditableSelector = getFormatNoneditableSelector(editor); 12167 const selector = isEmpty$5(formatNoneditableSelector) ? baseDataSelector : `${baseDataSelector},${formatNoneditableSelector}`; 12168 return is$2(SugarElement.fromDom(node), selector); 12169 }; 12170
vendor: 5,370 bytes, lines 12170-12290
12170// A noneditable element is wrappable if it: 12171 // - is valid target (has data-mce-cef-wrappable attribute or matches selector from option) 12172 // - has no editable descendants - removing formats in the editable region can result in the wrapped noneditable being split which is undesirable 12173 const isWrappableNoneditable = (editor, node) => { 12174 const dom = editor.dom; 12175 return (isElementNode$1(node) && 12176 dom.getContentEditable(node) === 'false' && 12177 isWrapNoneditableTarget(editor, node) && 12178 dom.select('[contenteditable="true"]', node).length === 0); 12179 }; 12180 /** 12181 * Replaces variables in the value. The variable format is %var. 12182 * 12183 * @private 12184 * @param {String} value Value to replace variables in. 12185 * @param {Object} vars Name/value array with variables to replace. 12186 * @return {String} New value with replaced variables. 12187 */ 12188 const replaceVars = (value, vars) => { 12189 if (isFunction(value)) { 12190 return value(vars); 12191 } 12192 else if (isNonNullable(vars)) { 12193 value = value.replace(/%(\w+)/g, (str, name) => { 12194 return vars[name] || str; 12195 }); 12196 } 12197 return value; 12198 }; 12199 /** 12200 * Compares two string/nodes regardless of their case. 12201 * 12202 * @private 12203 * @param {String/Node} str1 Node or string to compare. 12204 * @param {String/Node} str2 Node or string to compare. 12205 * @return {Boolean} True/false if they match. 12206 */ 12207 const isEq$5 = (str1, str2) => { 12208 str1 = str1 || ''; 12209 str2 = str2 || ''; 12210 str1 = '' + (str1.nodeName || str1); 12211 str2 = '' + (str2.nodeName || str2); 12212 return str1.toLowerCase() === str2.toLowerCase(); 12213 }; 12214 const normalizeStyleValue = (value, name) => { 12215 if (isNullable(value)) { 12216 return null; 12217 } 12218 else { 12219 let strValue = String(value); 12220 // Force the format to hex 12221 if (name === 'color' || name === 'backgroundColor') { 12222 strValue = rgbaToHexString(strValue); 12223 } 12224 // Opera will return bold as 700 12225 if (name === 'fontWeight' && value === 700) { 12226 strValue = 'bold'; 12227 } 12228 // Normalize fontFamily so "'Font name', Font" becomes: "Font name,Font" 12229 if (name === 'fontFamily') { 12230 strValue = strValue.replace(/[\'\"]/g, '').replace(/,\s+/g, ','); 12231 } 12232 return strValue; 12233 } 12234 }; 12235 const getStyle = (dom, node, name) => { 12236 const style = dom.getStyle(node, name); 12237 return normalizeStyleValue(style, name); 12238 }; 12239 const getTextDecoration = (dom, node) => { 12240 let decoration; 12241 dom.getParent(node, (n) => { 12242 if (isElement$7(n)) { 12243 decoration = dom.getStyle(n, 'text-decoration'); 12244 return !!decoration && decoration !== 'none'; 12245 } 12246 else { 12247 return false; 12248 } 12249 }); 12250 return decoration; 12251 }; 12252 const getParents$2 = (dom, node, selector) => { 12253 return dom.getParents(node, selector, dom.getRoot()); 12254 }; 12255 const isFormatPredicate = (editor, formatName, predicate) => { 12256 const formats = editor.formatter.get(formatName); 12257 return isNonNullable(formats) && exists(formats, predicate); 12258 }; 12259 const isVariableFormatName = (editor, formatName) => { 12260 const hasVariableValues = (format) => { 12261 const isVariableValue = (val) => isFunction(val) || val.length > 1 && val.charAt(0) === '%'; 12262 return exists(['styles', 'attributes'], (key) => get$a(format, key).exists((field) => { 12263 const fieldValues = isArray$1(field) ? field : values(field); 12264 return exists(fieldValues, isVariableValue); 12265 })); 12266 }; 12267 return isFormatPredicate(editor, formatName, hasVariableValues); 12268 }; 12269 /** 12270 * Checks if the two formats are similar based on the format type, attributes, styles and classes 12271 */ 12272 const areSimilarFormats = (editor, formatName, otherFormatName) => { 12273 // Note: MatchFormat.matchNode() uses these parameters to check if a format matches a node 12274 // Therefore, these are ideal to check if two formats are similar 12275 const validKeys = ['inline', 'block', 'selector', 'attributes', 'styles', 'classes']; 12276 const filterObj = (format) => filter$4(format, (_, key) => exists(validKeys, (validKey) => validKey === key)); 12277 return isFormatPredicate(editor, formatName, (fmt1) => { 12278 const filteredFmt1 = filterObj(fmt1); 12279 return isFormatPredicate(editor, otherFormatName, (fmt2) => { 12280 const filteredFmt2 = filterObj(fmt2); 12281 return equal$1(filteredFmt1, filteredFmt2); 12282 }); 12283 }); 12284 }; 12285 const isBlockFormat = (format) => hasNonNullableKey(format, 'block'); 12286 const isWrappingBlockFormat = (format) => isBlockFormat(format) && format.wrapper === true; 12287 const isNonWrappingBlockFormat = (format) => isBlockFormat(format) && format.wrapper !== true; 12288 const isSelectorFormat = (format) => hasNonNullableKey(format, 'selector'); 12289 const isInlineFormat = (format) => hasNonNullableKey(format, 'inline'); 12290 const isMixedFormat = (format) => isSelectorFormat(format) && isInlineFormat(format) && is$4(get$a
vendor: 18,594 bytes, lines 12290-12690
12290(format, 'mixed'), true); 12291 const shouldExpandToSelector = (format) => isSelectorFormat(format) && format.expand !== false && !isInlineFormat(format); 12292 const getEmptyCaretContainers = (node) => { 12293 const nodes = []; 12294 let tempNode = node; 12295 while (tempNode) { 12296 if ((isText$b(tempNode) && tempNode.data !== ZWSP$1) || tempNode.childNodes.length > 1) { 12297 return []; 12298 } 12299 // Collect nodes 12300 if (isElement$7(tempNode)) { 12301 nodes.push(tempNode); 12302 } 12303 tempNode = tempNode.firstChild; 12304 } 12305 return nodes; 12306 }; 12307 const isCaretContainerEmpty = (node) => { 12308 return getEmptyCaretContainers(node).length > 0; 12309 }; 12310 const isEmptyCaretFormatElement = (element) => { 12311 return isCaretNode(element.dom) && isCaretContainerEmpty(element.dom); 12312 }; 12313 12314 const isBookmarkNode = isBookmarkNode$1; 12315 const getParents$1 = getParents$2; 12316 const isWhiteSpaceNode = isWhiteSpaceNode$1; 12317 const isTextBlock$1 = isTextBlock$2; 12318 const isBogusBr$1 = (node) => { 12319 return isBr$7(node) && node.getAttribute('data-mce-bogus') && !node.nextSibling; 12320 }; 12321 // Expands the node to the closes contentEditable false element if it exists 12322 const findParentContentEditable = (dom, node) => { 12323 let parent = node; 12324 while (parent) { 12325 if (isElement$7(parent) && dom.getContentEditable(parent)) { 12326 return dom.getContentEditable(parent) === 'false' ? parent : node; 12327 } 12328 parent = parent.parentNode; 12329 } 12330 return node; 12331 }; 12332 const walkText = (start, node, offset, predicate) => { 12333 const str = node.data; 12334 if (start) { 12335 for (let i = offset; i > 0; i--) { 12336 if (predicate(str.charAt(i - 1))) { 12337 return i; 12338 } 12339 } 12340 } 12341 else { 12342 for (let i = offset; i < str.length; i++) { 12343 if (predicate(str.charAt(i))) { 12344 return i; 12345 } 12346 } 12347 } 12348 return -1; 12349 }; 12350 const findSpace = (start, node, offset) => walkText(start, node, offset, (c) => isNbsp(c) || isWhiteSpace(c)); 12351 const findContent = (start, node, offset) => walkText(start, node, offset, isContent); 12352 const findWordEndPoint = (dom, body, container, offset, start, includeTrailingSpaces) => { 12353 let lastTextNode; 12354 const closestRoot = dom.getParent(container, (node) => isEditingHost(node) || dom.isBlock(node)); 12355 const rootNode = isNonNullable(closestRoot) ? closestRoot : body; 12356 const walk = (container, offset, pred) => { 12357 const textSeeker = TextSeeker(dom); 12358 const walker = start ? textSeeker.backwards : textSeeker.forwards; 12359 return Optional.from(walker(container, offset, (text, textOffset) => { 12360 if (isBookmarkNode(text.parentNode)) { 12361 return -1; 12362 } 12363 else { 12364 lastTextNode = text; 12365 return pred(start, text, textOffset); 12366 } 12367 }, rootNode)); 12368 }; 12369 const spaceResult = walk(container, offset, findSpace); 12370 return spaceResult.bind((result) => includeTrailingSpaces ? 12371 walk(result.container, result.offset + (start ? -1 : 0), findContent) : 12372 Optional.some(result)).orThunk(() => lastTextNode ? 12373 Optional.some({ container: lastTextNode, offset: start ? 0 : lastTextNode.length }) : 12374 Optional.none()); 12375 }; 12376 const findSelectorEndPoint = (dom, formatList, rng, container, siblingName) => { 12377 const sibling = container[siblingName]; 12378 if (isText$b(container) && isEmpty$5(container.data) && sibling) { 12379 container = sibling; 12380 } 12381 const parents = getParents$1(dom, container); 12382 for (let i = 0; i < parents.length; i++) { 12383 for (let y = 0; y < formatList.length; y++) { 12384 const curFormat = formatList[y]; 12385 // If collapsed state is set then skip formats that doesn't match that 12386 if (isNonNullable(curFormat.collapsed) && curFormat.collapsed !== rng.collapsed) { 12387 continue; 12388 } 12389 if (isSelectorFormat(curFormat) && dom.is(parents[i], curFormat.selector)) { 12390 return parents[i]; 12391 } 12392 } 12393 } 12394 return container; 12395 }; 12396 const findBlockEndPoint = (dom, formatList, container, siblingName) => { 12397 let node = container; 12398 const root = dom.getRoot(); 12399 const format = formatList[0]; 12400 // Expand to block of similar type 12401 if (isBlockFormat(format)) { 12402 node = format.wrapper ? null : dom.getParent(container, format.block, root); 12403 } 12404 // Expand to first wrappable block element or any block element 12405 if (!node) { 12406 const scopeRoot = dom.getParent(container, 'LI,TD,TH,SUMMARY') ?? root; 12407 node = dom.getParent(isText$b(container) ? container.parentNode : container, 12408 // Fixes #6183 where it would expand to editable parent element in inline mode 12409 (node) => node !== root && isTextBlock$1(dom.schema, node), scopeRoot); 12410 } 12411 // Exclude inner lists from wrapping 12412 if (node && isBlockFormat(format) && format.wrapper) { 12413 node = getParents$1(dom, node, 'ul,ol').reverse()[0] || node; 12414 } 12415 // Didn't find a block element look for first/last wrappable element 12416 if (!node) { 12417 node = container; 12418 while (node && node[siblingName] && !dom.isBlock(node[siblingName])) { 12419 node = node[siblingName]; 12420 // Break on BR but include it will be removed later on 12421 // we can't remove it now since we need to check if it can be wrapped 12422 if (isEq$5(node, 'br')) { 12423 break; 12424 } 12425 } 12426 } 12427 return node || container; 12428 }; 12429 // We're at the edge if the parent is a block and there's no next sibling. Alternatively, 12430 // if we reach the root or can't walk further we also consider it to be a boundary. 12431 const isAtBlockBoundary$1 = (dom, root, container, siblingName) => { 12432 const parent = container.parentNode; 12433 if (isNonNullable(container[siblingName])) { 12434 return false; 12435 } 12436 else if (parent === root || isNullable(parent) || dom.isBlock(parent)) { 12437 return true; 12438 } 12439 else { 12440 return isAtBlockBoundary$1(dom, root, parent, siblingName); 12441 } 12442 }; 12443 // This function walks up the tree if there is no siblings before/after the node. 12444 // If a sibling is found then the container is returned 12445 const findParentContainer = (dom, formatList, container, offset, start, expandToBlock) => { 12446 let parent = container; 12447 const siblingName = start ? 'previousSibling' : 'nextSibling'; 12448 const root = dom.getRoot(); 12449 // If it's a text node and the offset is inside the text 12450 if (isText$b(container) && !isWhiteSpaceNode(container)) { 12451 if (start ? offset > 0 : offset < container.data.length) { 12452 return container; 12453 } 12454 } 12455 while (parent) { 12456 if (isEditingHost(parent)) { 12457 return container; 12458 } 12459 // Stop expanding on block elements 12460 if (!formatList[0].block_expand && dom.isBlock(parent)) { 12461 return expandToBlock ? parent : container; 12462 } 12463 // Walk left/right 12464 for (let sibling = parent[siblingName]; sibling; sibling = sibling[siblingName]) { 12465 // Allow spaces if not at the edge of a block element, as the spaces won't have been collapsed 12466 const allowSpaces = isText$b(sibling) && !isAtBlockBoundary$1(dom, root, sibling, siblingName); 12467 if (!isBookmarkNode(sibling) && !isBogusBr$1(sibling) && !isWhiteSpaceNode(sibling, allowSpaces)) { 12468 return parent; 12469 } 12470 } 12471 // Check if we can move up are we at root level or body level 12472 if (parent === root || parent.parentNode === root) { 12473 container = parent; 12474 break; 12475 } 12476 parent = parent.parentNode; 12477 } 12478 return container; 12479 }; 12480 const isSelfOrParentBookmark = (container) => isBookmarkNode(container.parentNode) || isBookmarkNode(container); 12481 const expandRng = (dom, rng, formatList, expandOptions = {}) => { 12482 const { includeTrailingSpace = false, expandToBlock = true } = expandOptions; 12483 const editableHost = dom.getParent(rng.commonAncestorContainer, (node) => isEditingHost(node)); 12484 const root = isNonNullable(editableHost) ? editableHost : dom.getRoot(); 12485 let { startContainer, startOffset, endContainer, endOffset } = rng; 12486 const format = formatList[0]; 12487 // If index based start position then resolve it 12488 if (isElement$7(startContainer) && startContainer.hasChildNodes()) { 12489 startContainer = getNode$1(startContainer, startOffset); 12490 if (isText$b(startContainer)) { 12491 startOffset = 0; 12492 } 12493 } 12494 // If index based end position then resolve it 12495 if (isElement$7(endContainer) && endContainer.hasChildNodes()) { 12496 endContainer = getNode$1(endContainer, rng.collapsed ? endOffset : endOffset - 1); 12497 if (isText$b(endContainer)) { 12498 endOffset = endContainer.data.length; 12499 } 12500 } 12501 // Expand to closest contentEditable element 12502 startContainer = findParentContentEditable(dom, startContainer); 12503 endContainer = findParentContentEditable(dom, endContainer); 12504 // Exclude bookmark nodes if possible 12505 if (isSelfOrParentBookmark(startContainer)) { 12506 startContainer = isBookmarkNode(startContainer) ? startContainer : startContainer.parentNode; 12507 if (rng.collapsed) { 12508 startContainer = startContainer.previousSibling || startContainer; 12509 } 12510 else { 12511 startContainer = startContainer.nextSibling || startContainer; 12512 } 12513 if (isText$b(startContainer)) { 12514 startOffset = rng.collapsed ? startContainer.length : 0; 12515 } 12516 } 12517 if (isSelfOrParentBookmark(endContainer)) { 12518 endContainer = isBookmarkNode(endContainer) ? endContainer : endContainer.parentNode; 12519 if (rng.collapsed) { 12520 endContainer = endContainer.nextSibling || endContainer; 12521 } 12522 else { 12523 endContainer = endContainer.previousSibling || endContainer; 12524 } 12525 if (isText$b(endContainer)) { 12526 endOffset = rng.collapsed ? 0 : endContainer.length; 12527 } 12528 } 12529 if (rng.collapsed) { 12530 // Expand left to closest word boundary 12531 const startPoint = findWordEndPoint(dom, root, startContainer, startOffset, true, includeTrailingSpace); 12532 startPoint.each(({ container, offset }) => { 12533 startContainer = container; 12534 startOffset = offset; 12535 }); 12536 // Expand right to closest word boundary 12537 const endPoint = findWordEndPoint(dom, root, endContainer, endOffset, false, includeTrailingSpace); 12538 endPoint.each(({ container, offset }) => { 12539 endContainer = container; 12540 endOffset = offset; 12541 }); 12542 } 12543 // Move start/end point up the tree if the leaves are sharp and if we are in different containers 12544 // Example * becomes !: !<p><b><i>*text</i><i>text*</i></b></p>! 12545 // This will reduce the number of wrapper elements that needs to be created 12546 // Move start point up the tree 12547 if (isInlineFormat(format) || format.block_expand) { 12548 if (!isInlineFormat(format) || (!isText$b(startContainer) || startOffset === 0)) { 12549 startContainer = findParentContainer(dom, formatList, startContainer, startOffset, true, expandToBlock); 12550 } 12551 if (!isInlineFormat(format) || (!isText$b(endContainer) || endOffset === endContainer.data.length)) { 12552 endContainer = findParentContainer(dom, formatList, endContainer, endOffset, false, expandToBlock); 12553 } 12554 } 12555 // Expand start/end container to matching selector 12556 if (shouldExpandToSelector(format)) { 12557 // Find new startContainer/endContainer if there is better one 12558 startContainer = findSelectorEndPoint(dom, formatList, rng, startContainer, 'previousSibling'); 12559 endContainer = findSelectorEndPoint(dom, formatList, rng, endContainer, 'nextSibling'); 12560 } 12561 // Expand start/end container to matching block element or text node 12562 if (isBlockFormat(format) || isSelectorFormat(format)) { 12563 // Find new startContainer/endContainer if there is better one 12564 startContainer = findBlockEndPoint(dom, formatList, startContainer, 'previousSibling'); 12565 endContainer = findBlockEndPoint(dom, formatList, endContainer, 'nextSibling'); 12566 // Non block element then try to expand up the leaf 12567 if (isBlockFormat(format)) { 12568 if (!dom.isBlock(startContainer)) { 12569 startContainer = findParentContainer(dom, formatList, startContainer, startOffset, true, expandToBlock); 12570 if (isText$b(startContainer)) { 12571 startOffset = 0; 12572 } 12573 } 12574 if (!dom.isBlock(endContainer)) { 12575 endContainer = findParentContainer(dom, formatList, endContainer, endOffset, false, expandToBlock); 12576 if (isText$b(endContainer)) { 12577 endOffset = endContainer.data.length; 12578 } 12579 } 12580 } 12581 } 12582 // Setup index for startContainer 12583 if (isElement$7(startContainer) && startContainer.parentNode) { 12584 startOffset = dom.nodeIndex(startContainer); 12585 startContainer = startContainer.parentNode; 12586 } 12587 // Setup index for endContainer 12588 if (isElement$7(endContainer) && endContainer.parentNode) { 12589 endOffset = dom.nodeIndex(endContainer) + 1; 12590 endContainer = endContainer.parentNode; 12591 } 12592 // Return new range like object 12593 return { 12594 startContainer, 12595 startOffset, 12596 endContainer, 12597 endOffset 12598 }; 12599 }; 12600 12601 const walk$3 = (dom, rng, callback) => { 12602 const startOffset = rng.startOffset; 12603 const startContainer = getNode$1(rng.startContainer, startOffset); 12604 const endOffset = rng.endOffset; 12605 const endContainer = getNode$1(rng.endContainer, endOffset - 1); 12606 /** 12607 * Excludes start/end text node if they are out side the range 12608 * 12609 * @private 12610 * @param {Array} nodes Nodes to exclude items from. 12611 * @return {Array} Array with nodes excluding the start/end container if needed. 12612 */ 12613 const exclude = (nodes) => { 12614 // First node is excluded 12615 const firstNode = nodes[0]; 12616 if (isText$b(firstNode) && firstNode === startContainer && startOffset >= firstNode.data.length) { 12617 nodes.splice(0, 1); 12618 } 12619 // Last node is excluded 12620 const lastNode = nodes[nodes.length - 1]; 12621 if (endOffset === 0 && nodes.length > 0 && lastNode === endContainer && isText$b(lastNode)) { 12622 nodes.splice(nodes.length - 1, 1); 12623 } 12624 return nodes; 12625 }; 12626 const collectSiblings = (node, name, endNode) => { 12627 const siblings = []; 12628 for (; node && node !== endNode; node = node[name]) { 12629 siblings.push(node); 12630 } 12631 return siblings; 12632 }; 12633 const findEndPoint = (node, root) => dom.getParent(node, (node) => node.parentNode === root, root); 12634 const walkBoundary = (startNode, endNode, next) => { 12635 const siblingName = next ? 'nextSibling' : 'previousSibling'; 12636 for (let node = startNode, parent = node.parentNode; node && node !== endNode; node = parent) { 12637 parent = node.parentNode; 12638 const siblings = collectSiblings(node === startNode ? node : node[siblingName], siblingName); 12639 if (siblings.length) { 12640 if (!next) { 12641 siblings.reverse(); 12642 } 12643 callback(exclude(siblings)); 12644 } 12645 } 12646 }; 12647 // Same container 12648 if (startContainer === endContainer) { 12649 return callback(exclude([startContainer])); 12650 } 12651 // Find common ancestor and end points 12652 const ancestor = dom.findCommonAncestor(startContainer, endContainer) ?? dom.getRoot(); 12653 // Process left side 12654 if (dom.isChildOf(startContainer, endContainer)) { 12655 return walkBoundary(startContainer, ancestor, true); 12656 } 12657 // Process right side 12658 if (dom.isChildOf(endContainer, startContainer)) { 12659 return walkBoundary(endContainer, ancestor); 12660 } 12661 // Find start/end point 12662 const startPoint = findEndPoint(startContainer, ancestor) || startContainer; 12663 const endPoint = findEndPoint(endContainer, ancestor) || endContainer; 12664 // Walk left leaf 12665 walkBoundary(startContainer, startPoint, true); 12666 // Walk the middle from start to end point 12667 const siblings = collectSiblings(startPoint === startContainer ? startPoint : startPoint.nextSibling, 'nextSibling', endPoint === endContainer ? endPoint.nextSibling : endPoint); 12668 if (siblings.length) { 12669 callback(exclude(siblings)); 12670 } 12671 // Walk right leaf 12672 walkBoundary(endContainer, endPoint); 12673 }; 12674 12675 const validBlocks = [ 12676 // Codesample plugin 12677 'pre[class*=language-][contenteditable="false"]', 12678 // Image plugin - captioned image 12679 'figure.image', 12680 // Mediaembed plugin 12681 'div[data-ephox-embed-iri]', 12682 // Pageembed plugin 12683 'div.tiny-pageembed', 12684 // Tableofcontents plugin 12685 'div.mce-toc', 12686 'div[data-mce-toc]', 12687 // Footnootes plugin 12688 'div.mce-footnotes' 12689 ]; 12690 const isZeroWidth = (elem) => isText$c(elem) && get$
vendor: 13,672 bytes, lines 12690-13000
126904(elem) === ZWSP$1; 12691 const context = (editor, elem, wrapName, nodeName) => parentElement(elem).fold(() => "skipping" /* ChildContext.Skipping */, (parent) => { 12692 // We used to skip these, but given that they might be representing empty paragraphs, it probably 12693 // makes sense to treat them just like text nodes 12694 if (nodeName === 'br' || isZeroWidth(elem)) { 12695 return "valid" /* ChildContext.Valid */; 12696 } 12697 else if (isAnnotation(elem)) { 12698 return "existing" /* ChildContext.Existing */; 12699 } 12700 else if (isCaretNode(elem.dom)) { 12701 return "caret" /* ChildContext.Caret */; 12702 } 12703 else if (exists(validBlocks, (selector) => is$2(elem, selector))) { 12704 return "valid-block" /* ChildContext.ValidBlock */; 12705 } 12706 else if (!isValid(editor, wrapName, nodeName) || !isValid(editor, name(parent), wrapName)) { 12707 return "invalid-child" /* ChildContext.InvalidChild */; 12708 } 12709 else { 12710 return "valid" /* ChildContext.Valid */; 12711 } 12712 }); 12713 12714 const applyWordGrab = (editor, rng) => { 12715 const r = expandRng(editor.dom, rng, [{ inline: 'span' }]); 12716 rng.setStart(r.startContainer, r.startOffset); 12717 rng.setEnd(r.endContainer, r.endOffset); 12718 editor.selection.setRng(rng); 12719 }; 12720 const applyAnnotation = (elem, masterUId, data, annotationName, decorate, directAnnotation) => { 12721 const { uid = masterUId, ...otherData } = data; 12722 add$2(elem, annotation()); 12723 set$4(elem, `${dataAnnotationId()}`, uid); 12724 set$4(elem, `${dataAnnotation()}`, annotationName); 12725 const { attributes = {}, classes = [] } = decorate(uid, otherData); 12726 setAll$1(elem, attributes); 12727 add$1(elem, classes); 12728 if (directAnnotation) { 12729 if (classes.length > 0) { 12730 set$4(elem, `${dataAnnotationClasses()}`, classes.join(',')); 12731 } 12732 const attributeNames = keys(attributes); 12733 if (attributeNames.length > 0) { 12734 set$4(elem, `${dataAnnotationAttributes()}`, attributeNames.join(',')); 12735 } 12736 } 12737 }; 12738 const removeDirectAnnotation = (elem) => { 12739 remove$4(elem, annotation()); 12740 remove$9(elem, `${dataAnnotationId()}`); 12741 remove$9(elem, `${dataAnnotation()}`); 12742 remove$9(elem, `${dataAnnotationActive()}`); 12743 const customAttrNames = getOpt(elem, `${dataAnnotationAttributes()}`).map((names) => names.split(',')).getOr([]); 12744 const customClasses = getOpt(elem, `${dataAnnotationClasses()}`).map((names) => names.split(',')).getOr([]); 12745 each$e(customAttrNames, (name) => remove$9(elem, name)); 12746 remove$3(elem, customClasses); 12747 remove$9(elem, `${dataAnnotationClasses()}`); 12748 remove$9(elem, `${dataAnnotationAttributes()}`); 12749 }; 12750 const makeAnnotation = (eDoc, uid, data, annotationName, decorate) => { 12751 const master = SugarElement.fromTag('span', eDoc); 12752 applyAnnotation(master, uid, data, annotationName, decorate, false); 12753 return master; 12754 }; 12755 const annotate = (editor, rng, uid, annotationName, decorate, data) => { 12756 // Setup all the wrappers that are going to be used. 12757 const newWrappers = []; 12758 // Setup the spans for the comments 12759 const master = makeAnnotation(editor.getDoc(), uid, data, annotationName, decorate); 12760 // Set the current wrapping element 12761 const wrapper = value$1(); 12762 // Clear the current wrapping element, so that subsequent calls to 12763 // getOrOpenWrapper spawns a new one. 12764 const finishWrapper = () => { 12765 wrapper.clear(); 12766 }; 12767 // Get the existing wrapper, or spawn a new one. 12768 const getOrOpenWrapper = () => wrapper.get().getOrThunk(() => { 12769 const nu = shallow(master); 12770 newWrappers.push(nu); 12771 wrapper.set(nu); 12772 return nu; 12773 }); 12774 const processElements = (elems) => { 12775 each$e(elems, processElement); 12776 }; 12777 const processElement = (elem) => { 12778 const ctx = context(editor, elem, 'span', name(elem)); 12779 switch (ctx) { 12780 case "invalid-child" /* ChildContext.InvalidChild */: { 12781 finishWrapper(); 12782 const children = children$1(elem); 12783 processElements(children); 12784 finishWrapper(); 12785 break; 12786 } 12787 case "valid-block" /* ChildContext.ValidBlock */: { 12788 finishWrapper(); 12789 applyAnnotation(elem, uid, data, annotationName, decorate, true); 12790 break; 12791 } 12792 case "valid" /* ChildContext.Valid */: { 12793 const w = getOrOpenWrapper(); 12794 wrap$2(elem, w); 12795 break; 12796 } 12797 } 12798 }; 12799 const processNodes = (nodes) => { 12800 const elems = map$3(nodes, SugarElement.fromDom); 12801 processElements(elems); 12802 }; 12803 walk$3(editor.dom, rng, (nodes) => { 12804 finishWrapper(); 12805 processNodes(nodes); 12806 }); 12807 return newWrappers; 12808 }; 12809 const annotateWithBookmark = (editor, name, settings, data) => { 12810 editor.undoManager.transact(() => { 12811 const selection = editor.selection; 12812 const initialRng = selection.getRng(); 12813 const hasFakeSelection = getCellsFromEditor(editor).length > 0; 12814 const masterUid = generate('mce-annotation'); 12815 if (initialRng.collapsed && !hasFakeSelection) { 12816 applyWordGrab(editor, initialRng); 12817 } 12818 // Even after applying word grab, we could not find a selection. Therefore, 12819 // just make a wrapper and insert it at the current cursor 12820 if (selection.getRng().collapsed && !hasFakeSelection) { 12821 const wrapper = makeAnnotation(editor.getDoc(), masterUid, data, name, settings.decorate); 12822 // Put something visible in the marker 12823 set$3(wrapper, nbsp); 12824 selection.getRng().insertNode(wrapper.dom); 12825 selection.select(wrapper.dom); 12826 } 12827 else { 12828 // The bookmark is responsible for splitting the nodes beforehand at the selection points 12829 // The "false" here means a zero width cursor is NOT put in the bookmark. It seems to be required 12830 // to stop an empty paragraph splitting into two paragraphs. Probably a better way exists. 12831 preserve(selection, false, () => { 12832 runOnRanges(editor, (selectionRng) => { 12833 annotate(editor, selectionRng, masterUid, name, settings.decorate, data); 12834 }); 12835 }); 12836 } 12837 }); 12838 }; 12839 12840 const Annotator = (editor) => { 12841 const registry = create$a(); 12842 setup$D(editor, registry); 12843 const changes = setup$E(editor, registry); 12844 const isSpan = isTag('span'); 12845 const removeAnnotations = (elements) => { 12846 each$e(elements, (element) => { 12847 if (isSpan(element)) { 12848 unwrap(element); 12849 } 12850 else { 12851 removeDirectAnnotation(element); 12852 } 12853 }); 12854 }; 12855 return { 12856 /** 12857 * Registers a specific annotator by name 12858 * 12859 * @method register 12860 * @param {String} name the name of the annotation 12861 * @param {Object} settings settings for the annotation (e.g. decorate) 12862 */ 12863 register: (name, settings) => { 12864 registry.register(name, settings); 12865 }, 12866 /** 12867 * Applies the annotation at the current selection using data 12868 * 12869 * @method annotate 12870 * @param {String} name the name of the annotation to apply 12871 * @param {Object} data information to pass through to this particular 12872 * annotation 12873 */ 12874 annotate: (name, data) => { 12875 registry.lookup(name).each((settings) => { 12876 annotateWithBookmark(editor, name, settings, data); 12877 }); 12878 }, 12879 /** 12880 * Executes the specified callback when the current selection matches the annotation or not. 12881 * 12882 * @method annotationChanged 12883 * @param {String} name Name of annotation to listen for 12884 * @param {Function} callback Callback with (state, name, and data) fired when the annotation 12885 * at the cursor changes. If state if false, data will not be provided. 12886 */ 12887 annotationChanged: (name, callback) => { 12888 changes.addListener(name, callback); 12889 }, 12890 /** 12891 * Removes any annotations from the current selection that match 12892 * the name 12893 * 12894 * @method remove 12895 * @param {String} name the name of the annotation to remove 12896 */ 12897 remove: (name) => { 12898 identify(editor, Optional.some(name)).each(({ elements }) => { 12899 /** 12900 * TINY-9399: It is important to keep the bookmarking in the callback 12901 * because it adjusts selection in a way that `identify` function 12902 * cannot retain the selected word. 12903 */ 12904 const bookmark = editor.selection.getBookmark(); 12905 removeAnnotations(elements); 12906 editor.selection.moveToBookmark(bookmark); 12907 }); 12908 }, 12909 /** 12910 * Removes all annotations that match the specified name from the entire document. 12911 * 12912 * @method removeAll 12913 * @param {String} name the name of the annotation to remove 12914 */ 12915 removeAll: (name) => { 12916 const bookmark = editor.selection.getBookmark(); 12917 each$d(findAll(editor, name), (elements, _) => { 12918 removeAnnotations(elements); 12919 }); 12920 editor.selection.moveToBookmark(bookmark); 12921 }, 12922 /** 12923 * Retrieve all the annotations for a given name 12924 * 12925 * @method getAll 12926 * @param {String} name the name of the annotations to retrieve 12927 * @return {Object} an index of annotations from uid => DOM nodes 12928 */ 12929 getAll: (name) => { 12930 const directory = findAll(editor, name); 12931 return map$2(directory, (elems) => map$3(elems, (elem) => elem.dom)); 12932 } 12933 }; 12934 }; 12935 12936 /** 12937 * Constructs a new BookmarkManager instance for a specific selection instance. 12938 * 12939 * @constructor 12940 * @method BookmarkManager 12941 * @param {tinymce.dom.Selection} selection Selection instance to handle bookmarks for. 12942 */ 12943 const BookmarkManager = (selection) => { 12944 return { 12945 /** 12946 * Returns a bookmark location for the current selection. This bookmark object 12947 * can then be used to restore the selection after some content modification to the document. 12948 * 12949 * @method getBookmark 12950 * @param {Number} type Optional state if the bookmark should be simple or not. Default is complex. 12951 * @param {Boolean} normalized Optional state that enables you to get a position that it would be after normalization. 12952 * @return {Object} Bookmark object, use moveToBookmark with this object to restore the selection. 12953 * @example 12954 * // Stores a bookmark of the current selection 12955 * const bm = tinymce.activeEditor.selection.getBookmark(); 12956 * 12957 * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content'); 12958 * 12959 * // Restore the selection bookmark 12960 * tinymce.activeEditor.selection.moveToBookmark(bm); 12961 */ 12962 getBookmark: curry(getBookmark$1, selection), 12963 /** 12964 * Restores the selection to the specified bookmark. 12965 * 12966 * @method moveToBookmark 12967 * @param {Object} bookmark Bookmark to restore selection from. 12968 * @example 12969 * // Stores a bookmark of the current selection 12970 * const bm = tinymce.activeEditor.selection.getBookmark(); 12971 * 12972 * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content'); 12973 * 12974 * // Restore the selection bookmark 12975 * tinymce.activeEditor.selection.moveToBookmark(bm); 12976 */ 12977 moveToBookmark: curry(moveToBookmark, selection) 12978 }; 12979 }; 12980 /** 12981 * Returns true/false if the specified node is a bookmark node or not. 12982 * 12983 * @static 12984 * @method isBookmarkNode 12985 * @param {DOMNode} node DOM Node to check if it's a bookmark node or not. 12986 * @return {Boolean} true/false if the node is a bookmark node or not. 12987 */ 12988 BookmarkManager.isBookmarkNode = isBookmarkNode$1; 12989 12990 const isXYWithinRange = (clientX, clientY, range) => { 12991 if (range.collapsed) { 12992 return false; 12993 } 12994 else { 12995 return exists(range.getClientRects(), (rect) => containsXY(rect, clientX, clientY)); 12996 } 12997 }; 12998 12999 const clamp$1 = (offset, element) => { 13000 const max = isText$c(element) ? get$4(element).length : children$1
vendor: 13,608 bytes, lines 13000-13309
13000(element).length + 1; 13001 if (offset > max) { 13002 return max; 13003 } 13004 else if (offset < 0) { 13005 return 0; 13006 } 13007 return offset; 13008 }; 13009 const normalizeRng = (rng) => SimSelection.range(rng.start, clamp$1(rng.soffset, rng.start), rng.finish, clamp$1(rng.foffset, rng.finish)); 13010 const isOrContains = (root, elm) => !isRestrictedNode(elm.dom) && (contains(root, elm) || eq(root, elm)); 13011 const isRngInRoot = (root) => (rng) => isOrContains(root, rng.start) && isOrContains(root, rng.finish); 13012 // TINY-9259: We need to store the selection on Firefox since if the editor is hidden the selection.getRng() api will not work as expected. 13013 const shouldStore = (editor) => editor.inline || Env.browser.isFirefox(); 13014 const nativeRangeToSelectionRange = (r) => SimSelection.range(SugarElement.fromDom(r.startContainer), r.startOffset, SugarElement.fromDom(r.endContainer), r.endOffset); 13015 const readRange = (win) => { 13016 const selection = win.getSelection(); 13017 const rng = !selection || selection.rangeCount === 0 ? Optional.none() : Optional.from(selection.getRangeAt(0)); 13018 return rng.map(nativeRangeToSelectionRange); 13019 }; 13020 const getBookmark = (root) => { 13021 const win = defaultView(root); 13022 return readRange(win.dom) 13023 .filter(isRngInRoot(root)); 13024 }; 13025 const validate = (root, bookmark) => Optional.from(bookmark) 13026 .filter(isRngInRoot(root)) 13027 .map(normalizeRng); 13028 const bookmarkToNativeRng = (bookmark) => { 13029 const rng = document.createRange(); 13030 try { 13031 // Might throw IndexSizeError 13032 rng.setStart(bookmark.start.dom, bookmark.soffset); 13033 rng.setEnd(bookmark.finish.dom, bookmark.foffset); 13034 return Optional.some(rng); 13035 } 13036 catch { 13037 return Optional.none(); 13038 } 13039 }; 13040 const store = (editor) => { 13041 const newBookmark = shouldStore(editor) ? getBookmark(SugarElement.fromDom(editor.getBody())) : Optional.none(); 13042 editor.bookmark = newBookmark.isSome() ? newBookmark : editor.bookmark; 13043 }; 13044 const getRng = (editor) => { 13045 const bookmark = editor.bookmark ? editor.bookmark : Optional.none(); 13046 return bookmark 13047 .bind((x) => validate(SugarElement.fromDom(editor.getBody()), x)) 13048 .bind(bookmarkToNativeRng); 13049 }; 13050 const restore = (editor) => { 13051 getRng(editor).each((rng) => editor.selection.setRng(rng)); 13052 }; 13053 13054 /** 13055 * This class manages the focus/blur state of the editor. This class is needed since some 13056 * browsers fire false focus/blur states when the selection is moved to a UI dialog or similar. 13057 * 13058 * This class will fire two events focus and blur on the editor instances that got affected. 13059 * It will also handle the restore of selection when the focus is lost and returned. 13060 * 13061 * @class tinymce.FocusManager 13062 * @private 13063 */ 13064 /** 13065 * Returns true if the specified element is part of the UI for example an button or text input. 13066 * 13067 * @static 13068 * @method isEditorUIElement 13069 * @param {Element} elm Element to check if it's part of the UI or not. 13070 * @return {Boolean} True/false state if the element is part of the UI or not. 13071 */ 13072 const isEditorUIElement$1 = (elm) => { 13073 // Needs to be converted to string since svg can have focus: #6776 13074 const className = elm.className.toString(); 13075 return className.indexOf('tox-') !== -1 || className.indexOf('mce-') !== -1; 13076 }; 13077 const FocusManager = { 13078 isEditorUIElement: isEditorUIElement$1 13079 }; 13080 13081 /** 13082 * Utility class for working with delayed actions like setTimeout. 13083 * 13084 * @class tinymce.util.Delay 13085 */ 13086 const wrappedSetTimeout = (callback, time) => { 13087 if (!isNumber(time)) { 13088 time = 0; 13089 } 13090 return window.setTimeout(callback, time); 13091 }; 13092 const wrappedSetInterval = (callback, time) => { 13093 if (!isNumber(time)) { 13094 time = 0; 13095 } 13096 return window.setInterval(callback, time); 13097 }; 13098 const Delay = { 13099 /** 13100 * Sets a timeout that's similar to the native browser <a href="https://developer.mozilla.org/en-US/docs/Web/API/setTimeout">setTimeout</a> 13101 * API, except that it checks if the editor instance is still alive when the callback gets executed. 13102 * 13103 * @method setEditorTimeout 13104 * @param {tinymce.Editor} editor Editor instance to check the removed state on. 13105 * @param {Function} callback Callback to execute when timer runs out. 13106 * @param {Number} time Optional time to wait before the callback is executed, defaults to 0. 13107 * @return {Number} Timeout id number. 13108 */ 13109 setEditorTimeout: (editor, callback, time) => { 13110 return wrappedSetTimeout(() => { 13111 if (!editor.removed) { 13112 callback(); 13113 } 13114 }, time); 13115 }, 13116 /** 13117 * Sets an interval timer that's similar to native browser <a href="https://developer.mozilla.org/en-US/docs/Web/API/setInterval">setInterval</a> 13118 * API, except that it checks if the editor instance is still alive when the callback gets executed. 13119 * 13120 * @method setEditorInterval 13121 * @param {Function} callback Callback to execute when interval time runs out. 13122 * @param {Number} time Optional time to wait before the callback is executed, defaults to 0. 13123 * @return {Number} Timeout id number. 13124 */ 13125 setEditorInterval: (editor, callback, time) => { 13126 const timer = wrappedSetInterval(() => { 13127 if (!editor.removed) { 13128 callback(); 13129 } 13130 else { 13131 window.clearInterval(timer); 13132 } 13133 }, time); 13134 return timer; 13135 } 13136 }; 13137 13138 const isManualNodeChange = (e) => { 13139 return e.type === 'nodechange' && e.selectionChange; 13140 }; 13141 const registerPageMouseUp = (editor, throttledStore) => { 13142 const mouseUpPage = () => { 13143 throttledStore.throttle(); 13144 }; 13145 DOMUtils.DOM.bind(document, 'mouseup', mouseUpPage); 13146 editor.on('remove', () => { 13147 DOMUtils.DOM.unbind(document, 'mouseup', mouseUpPage); 13148 }); 13149 }; 13150 const registerMouseUp = (editor, throttledStore) => { 13151 editor.on('mouseup touchend', (_e) => { 13152 throttledStore.throttle(); 13153 }); 13154 }; 13155 const registerEditorEvents = (editor, throttledStore) => { 13156 registerMouseUp(editor, throttledStore); 13157 editor.on('keyup NodeChange AfterSetSelectionRange', (e) => { 13158 if (!isManualNodeChange(e)) { 13159 store(editor); 13160 } 13161 }); 13162 }; 13163 const register$6 = (editor) => { 13164 const throttledStore = first$1(() => { 13165 store(editor); 13166 }, 0); 13167 editor.on('init', () => { 13168 if (editor.inline) { 13169 registerPageMouseUp(editor, throttledStore); 13170 } 13171 registerEditorEvents(editor, throttledStore); 13172 }); 13173 editor.on('remove', () => { 13174 throttledStore.cancel(); 13175 }); 13176 }; 13177 13178 let documentFocusInHandler; 13179 const DOM$c = DOMUtils.DOM; 13180 const isEditorUIElement = (elm) => { 13181 // Since this can be overridden by third party we need to use the API reference here 13182 return isElement$7(elm) && FocusManager.isEditorUIElement(elm); 13183 }; 13184 const isEditorContentAreaElement = (elm) => { 13185 const classList = elm.classList; 13186 if (classList !== undefined) { 13187 // tox-edit-area__iframe === iframe container element 13188 // mce-content-body === inline body element 13189 return classList.contains('tox-edit-area') || classList.contains('tox-edit-area__iframe') || classList.contains('mce-content-body'); 13190 } 13191 else { 13192 return false; 13193 } 13194 }; 13195 const isUIElement = (editor, elm) => { 13196 const customSelector = getCustomUiSelector(editor); 13197 const parent = DOM$c.getParent(elm, (elm) => { 13198 return (isEditorUIElement(elm) || 13199 (customSelector ? editor.dom.is(elm, customSelector) : false)); 13200 }); 13201 return parent !== null; 13202 }; 13203 const getActiveElement = (editor) => { 13204 try { 13205 const root = getRootNode(SugarElement.fromDom(editor.getElement())); 13206 return active(root).fold(() => document.body, (x) => x.dom); 13207 } 13208 catch { 13209 // IE sometimes fails to get the activeElement when resizing table 13210 // TODO: Investigate this 13211 return document.body; 13212 } 13213 }; 13214 const registerEvents$1 = (editorManager, e) => { 13215 const editor = e.editor; 13216 register$6(editor); 13217 const toggleContentAreaOnFocus = (editor, fn) => { 13218 // Inline editors have a different approach to highlight the content area on focus 13219 if (shouldHighlightOnFocus(editor) && editor.inline !== true) { 13220 const contentArea = SugarElement.fromDom(editor.getContainer()); 13221 fn(contentArea, 'tox-edit-focus'); 13222 } 13223 }; 13224 editor.on('focusin', () => { 13225 const focusedEditor = editorManager.focusedEditor; 13226 if (isEditorContentAreaElement(getActiveElement(editor))) { 13227 toggleContentAreaOnFocus(editor, add$2); 13228 } 13229 if (focusedEditor !== editor) { 13230 if (focusedEditor) { 13231 focusedEditor.dispatch('blur', { focusedEditor: editor }); 13232 } 13233 editorManager.setActive(editor); 13234 editorManager.focusedEditor = editor; 13235 editor.dispatch('focus', { blurredEditor: focusedEditor }); 13236 editor.focus(true); 13237 } 13238 }); 13239 editor.on('focusout', () => { 13240 Delay.setEditorTimeout(editor, () => { 13241 const focusedEditor = editorManager.focusedEditor; 13242 // Remove focus highlight when the content area is no longer the active editor element, or if the highlighted editor is not the current focused editor 13243 if (!isEditorContentAreaElement(getActiveElement(editor)) || focusedEditor !== editor) { 13244 toggleContentAreaOnFocus(editor, remove$4); 13245 } 13246 // Still the same editor the blur was outside any editor UI 13247 if (!isUIElement(editor, getActiveElement(editor)) && focusedEditor === editor) { 13248 editor.dispatch('blur', { focusedEditor: null }); 13249 editorManager.focusedEditor = null; 13250 } 13251 }); 13252 }); 13253 // Check if focus is moved to an element outside the active editor by checking if the target node 13254 // isn't within the body of the activeEditor nor a UI element such as a dialog child control 13255 if (!documentFocusInHandler) { 13256 documentFocusInHandler = (e) => { 13257 const activeEditor = editorManager.activeEditor; 13258 if (activeEditor) { 13259 getOriginalEventTarget(e).each((target) => { 13260 const elem = target; 13261 if (elem.ownerDocument === document) { 13262 // Fire a blur event if the element isn't a UI element 13263 if (elem !== document.body && !isUIElement(activeEditor, elem) && editorManager.focusedEditor === activeEditor) { 13264 activeEditor.dispatch('blur', { focusedEditor: null }); 13265 editorManager.focusedEditor = null; 13266 } 13267 } 13268 }); 13269 } 13270 }; 13271 DOM$c.bind(document, 'focusin', documentFocusInHandler); 13272 } 13273 }; 13274 const unregisterDocumentEvents = (editorManager, e) => { 13275 if (editorManager.focusedEditor === e.editor) { 13276 editorManager.focusedEditor = null; 13277 } 13278 if (!editorManager.activeEditor && documentFocusInHandler) { 13279 DOM$c.unbind(document, 'focusin', documentFocusInHandler); 13280 documentFocusInHandler = null; 13281 } 13282 }; 13283 const setup$C = (editorManager) => { 13284 editorManager.on('AddEditor', curry(registerEvents$1, editorManager)); 13285 editorManager.on('RemoveEditor', curry(unregisterDocumentEvents, editorManager)); 13286 }; 13287 13288 const getContentEditableHost = (editor, node) => editor.dom.getParent(node, (node) => editor.dom.getContentEditable(node) === 'true'); 13289 const hasContentEditableFalseParent$1 = (editor, node) => editor.dom.getParent(node, (node) => editor.dom.getContentEditable(node) === 'false') !== null; 13290 const getCollapsedNode = (rng) => rng.collapsed ? Optional.from(getNode$1(rng.startContainer, rng.startOffset)).map(SugarElement.fromDom) : Optional.none(); 13291 const getFocusInElement = (root, rng) => getCollapsedNode(rng).bind((node) => { 13292 if (isTableSection(node)) { 13293 return Optional.some(node); 13294 } 13295 else if (!contains(root, node)) { 13296 return Optional.some(root); 13297 } 13298 else { 13299 return Optional.none(); 13300 } 13301 }); 13302 const normalizeSelection = (editor, rng) => { 13303 getFocusInElement(SugarElement.fromDom(editor.getBody()), rng).bind((elm) => { 13304 return firstPositionIn(elm.dom); 13305 }).fold(() => { 13306 editor.selection.normalize(); 13307 }, (caretPos) => editor.selection.setRng(caretPos.toRange())); 13308 }; 13309 const focusBody = (body
vendor: 2,953 bytes, lines 13309-13374
13309) => { 13310 if (body.setActive) { 13311 // IE 11 sometimes throws "Invalid function" then fallback to focus 13312 // setActive is better since it doesn't scroll to the element being focused 13313 try { 13314 body.setActive(); 13315 } 13316 catch { 13317 body.focus(); 13318 } 13319 } 13320 else { 13321 body.focus(); 13322 } 13323 }; 13324 const hasElementFocus = (elm) => hasFocus$1(elm) || search(elm).isSome(); 13325 const hasIframeFocus = (editor) => isNonNullable(editor.iframeElement) && hasFocus$1(SugarElement.fromDom(editor.iframeElement)); 13326 const hasInlineFocus = (editor) => { 13327 const rawBody = editor.getBody(); 13328 return rawBody && hasElementFocus(SugarElement.fromDom(rawBody)); 13329 }; 13330 const hasUiFocus = (editor) => { 13331 const dos = getRootNode(SugarElement.fromDom(editor.getElement())); 13332 // Editor container is the obvious one (Menubar, Toolbar, Status bar, Sidebar) and dialogs and menus are in an auxiliary element (silver theme specific) 13333 // This can't use Focus.search() because only the theme has this element reference 13334 return active(dos) 13335 .filter((elem) => !isEditorContentAreaElement(elem.dom) && isUIElement(editor, elem.dom)) 13336 .isSome(); 13337 }; 13338 const hasFocus = (editor) => editor.inline ? hasInlineFocus(editor) : hasIframeFocus(editor); 13339 const hasEditorOrUiFocus = (editor) => hasFocus(editor) || hasUiFocus(editor); 13340 const focusEditor = (editor) => { 13341 const selection = editor.selection; 13342 const body = editor.getBody(); 13343 let rng = selection.getRng(); 13344 editor.quirks.refreshContentEditable(); 13345 const restoreBookmark = (editor) => { 13346 getRng(editor).each((bookmarkRng) => { 13347 editor.selection.setRng(bookmarkRng); 13348 rng = bookmarkRng; 13349 }); 13350 }; 13351 if (!hasFocus(editor) && editor.hasEditableRoot()) { 13352 restoreBookmark(editor); 13353 } 13354 // Move focus to contentEditable=true child if needed 13355 const contentEditableHost = getContentEditableHost(editor, selection.getNode()); 13356 if (contentEditableHost && editor.dom.isChildOf(contentEditableHost, body)) { 13357 if (!hasContentEditableFalseParent$1(editor, contentEditableHost)) { 13358 focusBody(body); 13359 } 13360 focusBody(contentEditableHost); 13361 if (!editor.hasEditableRoot()) { 13362 restoreBookmark(editor); 13363 } 13364 normalizeSelection(editor, rng); 13365 activateEditor(editor); 13366 return; 13367 } 13368 // Focus the window iframe 13369 if (!editor.inline) { 13370 // WebKit needs this call to fire focusin event properly see #5948 13371 // But Opera pre Blink engine will produce an empty selection so skip Opera 13372 if (!Env.browser.isOpera()) { 13373 focusBody(body); 13374 }
13375 editor.getWin().focus(); 13376 } 13377 // Focus the body as well since it's contentEditable 13378 if (Env.browser.isFirefox() || editor.inline) { 13379 focusBody(body); 13380 normalizeSelection(editor, rng); 13381 } 13382 activateEditor(editor); 13383 }; 13384 const activateEditor = (editor) => editor.editorManager.setActive(editor); 13385 const focus = (editor, skipFocus) => { 13386 if (editor.removed) { 13387 return; 13388 } 13389 if (skipFocus) { 13390 activateEditor(editor); 13391 } 13392 else { 13393 focusEditor(editor); 13394 }
vendor: 38,924 bytes, lines 13394-14250
13394 13395 }; 13396 13397 /** 13398 * This file exposes a set of the common KeyCodes for use. Please grow it as needed. 13399 */ 13400 const VK = { 13401 BACKSPACE: 8, 13402 DELETE: 46, 13403 DOWN: 40, 13404 ENTER: 13, 13405 ESC: 27, 13406 LEFT: 37, 13407 RIGHT: 39, 13408 SPACEBAR: 32, 13409 TAB: 9, 13410 UP: 38, 13411 PAGE_UP: 33, 13412 PAGE_DOWN: 34, 13413 END: 35, 13414 HOME: 36, 13415 modifierPressed: (e) => { 13416 return e.shiftKey || e.ctrlKey || e.altKey || VK.metaKeyPressed(e); 13417 }, 13418 metaKeyPressed: (e) => { 13419 // Check if ctrl or meta key is pressed. Edge case for AltGr on Windows where it produces ctrlKey+altKey states 13420 return Env.os.isMacOS() || Env.os.isiOS() ? e.metaKey : e.ctrlKey && !e.altKey; 13421 } 13422 }; 13423 13424 const elementSelectionAttr = 'data-mce-selected'; 13425 const controlElmSelector = `table,img,figure.image,hr,video,span.mce-preview-object,details,${ucVideoNodeName}`; 13426 const abs = Math.abs; 13427 const round$1 = Math.round; 13428 // Details about each resize handle how to scale etc 13429 const resizeHandles = { 13430 // Name: x multiplier, y multiplier, delta size x, delta size y 13431 nw: [0, 0, -1, -1], 13432 ne: [1, 0, 1, -1], 13433 se: [1, 1, 1, 1], 13434 sw: [0, 1, -1, 1] 13435 }; 13436 const isTouchEvent = (evt) => evt.type === 'longpress' || evt.type.indexOf('touch') === 0; 13437 /** 13438 * This class handles control selection of elements. Controls are elements 13439 * that can be resized and needs to be selected as a whole. It adds custom resize handles 13440 * to all browser engines that support properly disabling the built in resize logic. 13441 * 13442 * @private 13443 * @class tinymce.dom.ControlSelection 13444 */ 13445 const ControlSelection = (selection, editor) => { 13446 const dom = editor.dom; 13447 const editableDoc = editor.getDoc(); 13448 const rootDocument = document; 13449 const rootElement = editor.getBody(); 13450 let selectedElm, selectedElmGhost, resizeHelper, selectedHandle, resizeBackdrop; 13451 let startX, startY, startW, startH, ratio, resizeStarted; 13452 let width; 13453 let height; 13454 let startScrollWidth; 13455 let startScrollHeight; 13456 const isImage = (elm) => isNonNullable(elm) && (isImg(elm) || dom.is(elm, 'figure.image')); 13457 const isMedia = (elm) => isMedia$2(elm) || dom.hasClass(elm, 'mce-preview-object'); 13458 const isEventOnImageOutsideRange = (evt, range) => { 13459 if (isTouchEvent(evt)) { 13460 const touch = evt.touches[0]; 13461 return isImage(evt.target) && !isXYWithinRange(touch.clientX, touch.clientY, range); 13462 } 13463 else { 13464 return isImage(evt.target) && !isXYWithinRange(evt.clientX, evt.clientY, range); 13465 } 13466 }; 13467 const contextMenuSelectImage = (evt) => { 13468 const target = evt.target; 13469 if (isEventOnImageOutsideRange(evt, editor.selection.getRng()) && !evt.isDefaultPrevented()) { 13470 editor.selection.select(target); 13471 } 13472 }; 13473 const getResizeTargets = (elm) => { 13474 if (dom.hasClass(elm, 'mce-preview-object') && isNonNullable(elm.firstElementChild)) { 13475 // When resizing a preview object we need to resize both the original element and the wrapper span 13476 return [elm, elm.firstElementChild]; 13477 } 13478 else if (dom.is(elm, 'figure.image')) { 13479 return [elm.querySelector('img')]; 13480 } 13481 else { 13482 return [elm]; 13483 } 13484 }; 13485 const isResizable = (elm) => { 13486 const selector = getObjectResizing(editor); 13487 if (!selector || editor.mode.isReadOnly()) { 13488 return false; 13489 } 13490 if (elm.getAttribute('data-mce-resize') === 'false') { 13491 return false; 13492 } 13493 if (elm === editor.getBody()) { 13494 return false; 13495 } 13496 if (dom.hasClass(elm, 'mce-preview-object') && isNonNullable(elm.firstElementChild)) { 13497 return is$2(SugarElement.fromDom(elm.firstElementChild), selector); 13498 } 13499 else { 13500 return is$2(SugarElement.fromDom(elm), selector); 13501 } 13502 }; 13503 const createGhostElement = (dom, elm) => { 13504 if (isMedia(elm)) { 13505 return dom.create('img', { src: Env.transparentSrc }); 13506 } 13507 else if (isTable$2(elm)) { 13508 const isNorth = startsWith(selectedHandle.name, 'n'); 13509 const rowSelect = isNorth ? head : last$2; 13510 const tableElm = elm.cloneNode(true); 13511 // Get row, remove all height styles 13512 rowSelect(dom.select('tr', tableElm)).each((tr) => { 13513 const cells = dom.select('td,th', tr); 13514 dom.setStyle(tr, 'height', null); 13515 each$e(cells, (cell) => dom.setStyle(cell, 'height', null)); 13516 }); 13517 return tableElm; 13518 } 13519 else { 13520 return elm.cloneNode(true); 13521 } 13522 }; 13523 const setUcVideoSizeProp = (element, name, value) => { 13524 // this is needed because otherwise the ghost for `uc-video` is not correctly rendered 13525 element[name] = value; 13526 const minimumWidth = 400; 13527 if (element.width > minimumWidth && !(name === 'width' && value < minimumWidth)) { 13528 element[name] = value; 13529 dom.setStyle(element, name, value); 13530 } 13531 else { 13532 const valueConsideringMinWidth = name === 'height' ? minimumWidth * (ratio ?? 1) : minimumWidth; 13533 element[name] = valueConsideringMinWidth; 13534 dom.setStyle(element, name, valueConsideringMinWidth); 13535 } 13536 }; 13537 const setSizeProp = (element, name, value) => { 13538 if (isNonNullable(value)) { 13539 // Resize by using style or attribute 13540 const targets = getResizeTargets(element); 13541 each$e(targets, (target) => { 13542 if (isUcVideo(target)) { 13543 setUcVideoSizeProp(target, name, value); 13544 } 13545 else { 13546 if (target.style[name] || !editor.schema.isValid(target.nodeName.toLowerCase(), name)) { 13547 dom.setStyle(target, name, value); 13548 } 13549 else { 13550 dom.setAttrib(target, name, '' + value); 13551 } 13552 } 13553 }); 13554 } 13555 }; 13556 const setGhostElmSize = (ghostElm, width, height) => { 13557 setSizeProp(ghostElm, 'width', width); 13558 setSizeProp(ghostElm, 'height', height); 13559 }; 13560 const resizeGhostElement = (e) => { 13561 let deltaX, deltaY, proportional; 13562 let resizeHelperX, resizeHelperY; 13563 // Calc new width/height 13564 deltaX = e.screenX - startX; 13565 deltaY = e.screenY - startY; 13566 // Calc new size 13567 width = deltaX * selectedHandle[2] + startW; 13568 height = deltaY * selectedHandle[3] + startH; 13569 // Never scale down lower than 5 pixels 13570 width = width < 5 ? 5 : width; 13571 height = height < 5 ? 5 : height; 13572 if ((isImage(selectedElm) || isMedia(selectedElm) || isUcVideo(selectedElm)) && getResizeImgProportional(editor) !== false) { 13573 proportional = !VK.modifierPressed(e); 13574 } 13575 else { 13576 proportional = VK.modifierPressed(e); 13577 } 13578 // Constrain proportions 13579 if (proportional) { 13580 if (abs(deltaX) > abs(deltaY)) { 13581 height = round$1(width * ratio); 13582 width = round$1(height / ratio); 13583 } 13584 else { 13585 width = round$1(height / ratio); 13586 height = round$1(width * ratio); 13587 } 13588 } 13589 // Update ghost size 13590 setGhostElmSize(selectedElmGhost, width, height); 13591 // Update resize helper position 13592 resizeHelperX = selectedHandle.startPos.x + deltaX; 13593 resizeHelperY = selectedHandle.startPos.y + deltaY; 13594 resizeHelperX = resizeHelperX > 0 ? resizeHelperX : 0; 13595 resizeHelperY = resizeHelperY > 0 ? resizeHelperY : 0; 13596 dom.setStyles(resizeHelper, { 13597 left: resizeHelperX, 13598 top: resizeHelperY, 13599 display: 'block' 13600 }); 13601 resizeHelper.innerHTML = width + ' × ' + height; 13602 /* TODO: TINY-11702 dom.setStyle() has no effect because the value is NaN 13603 // Update ghost X position if needed 13604 if (selectedHandle[2] < 0 && selectedElmGhost.clientWidth <= width) { 13605 dom.setStyle(selectedElmGhost, 'left', selectedElmX + (startW - width)); 13606 } 13607 13608 // Update ghost Y position if needed 13609 if (selectedHandle[3] < 0 && selectedElmGhost.clientHeight <= height) { 13610 dom.setStyle(selectedElmGhost, 'top', selectedElmY + (startH - height)); 13611 } 13612 */ 13613 // Calculate how must overflow we got 13614 deltaX = rootElement.scrollWidth - startScrollWidth; 13615 deltaY = rootElement.scrollHeight - startScrollHeight; 13616 // Re-position the resize helper based on the overflow 13617 if (deltaX + deltaY !== 0) { 13618 dom.setStyles(resizeHelper, { 13619 left: resizeHelperX - deltaX, 13620 top: resizeHelperY - deltaY 13621 }); 13622 } 13623 if (!resizeStarted) { 13624 fireObjectResizeStart(editor, selectedElm, startW, startH, 'corner-' + selectedHandle.name); 13625 resizeStarted = true; 13626 } 13627 }; 13628 const endGhostResize = () => { 13629 const wasResizeStarted = resizeStarted; 13630 resizeStarted = false; 13631 // Set width/height properties 13632 if (wasResizeStarted) { 13633 setSizeProp(selectedElm, 'width', width); 13634 setSizeProp(selectedElm, 'height', height); 13635 } 13636 dom.unbind(editableDoc, 'mousemove', resizeGhostElement); 13637 dom.unbind(editableDoc, 'mouseup', endGhostResize); 13638 if (rootDocument !== editableDoc) { 13639 dom.unbind(rootDocument, 'mousemove', resizeGhostElement); 13640 dom.unbind(rootDocument, 'mouseup', endGhostResize); 13641 } 13642 // Remove ghost/helper and update resize handle positions 13643 dom.remove(selectedElmGhost); 13644 dom.remove(resizeHelper); 13645 dom.remove(resizeBackdrop); 13646 showResizeRect(selectedElm); 13647 if (wasResizeStarted) { 13648 fireObjectResized(editor, selectedElm, width, height, 'corner-' + selectedHandle.name); 13649 dom.setAttrib(selectedElm, 'style', dom.getAttrib(selectedElm, 'style')); 13650 } 13651 editor.nodeChanged(); 13652 }; 13653 const showResizeRect = (targetElm) => { 13654 unbindResizeHandleEvents(); 13655 // Get position and size of target 13656 const position = dom.getPos(targetElm, rootElement); 13657 const selectedElmX = position.x; 13658 const selectedElmY = position.y; 13659 const rect = targetElm.getBoundingClientRect(); // Fix for Gecko offsetHeight for table with caption 13660 const targetWidth = rect.width || (rect.right - rect.left); 13661 const targetHeight = rect.height || (rect.bottom - rect.top); 13662 // Reset width/height if user selects a new image/table 13663 if (selectedElm !== targetElm) { 13664 hideResizeRect(); 13665 selectedElm = targetElm; 13666 width = height = 0; 13667 } 13668 // Makes it possible to disable resizing 13669 const e = editor.dispatch('ObjectSelected', { target: targetElm }); 13670 if (isResizable(targetElm) && !e.isDefaultPrevented()) { 13671 each$d(resizeHandles, (handle, name) => { 13672 const startDrag = (e) => { 13673 // Note: We're guaranteed to have at least one target here 13674 const target = getResizeTargets(selectedElm)[0]; 13675 startX = e.screenX; 13676 startY = e.screenY; 13677 startW = target.clientWidth; 13678 startH = target.clientHeight; 13679 ratio = startH / startW; 13680 selectedHandle = handle; 13681 selectedHandle.name = name; 13682 selectedHandle.startPos = { 13683 x: targetWidth * handle[0] + selectedElmX, 13684 y: targetHeight * handle[1] + selectedElmY 13685 }; 13686 startScrollWidth = rootElement.scrollWidth; 13687 startScrollHeight = rootElement.scrollHeight; 13688 resizeBackdrop = dom.add(rootElement, 'div', { 13689 'class': 'mce-resize-backdrop', 13690 'data-mce-bogus': 'all' 13691 }); 13692 dom.setStyles(resizeBackdrop, { 13693 position: 'fixed', 13694 left: '0', 13695 top: '0', 13696 width: '100%', 13697 height: '100%' 13698 }); 13699 selectedElmGhost = createGhostElement(dom, selectedElm); 13700 dom.addClass(selectedElmGhost, 'mce-clonedresizable'); 13701 dom.setAttrib(selectedElmGhost, 'data-mce-bogus', 'all'); 13702 selectedElmGhost.contentEditable = 'false'; // Hides IE move layer cursor 13703 dom.setStyles(selectedElmGhost, { 13704 left: selectedElmX, 13705 top: selectedElmY, 13706 margin: 0 13707 }); 13708 // Set initial ghost size 13709 setGhostElmSize(selectedElmGhost, targetWidth, targetHeight); 13710 selectedElmGhost.removeAttribute(elementSelectionAttr); 13711 rootElement.appendChild(selectedElmGhost); 13712 dom.bind(editableDoc, 'mousemove', resizeGhostElement); 13713 dom.bind(editableDoc, 'mouseup', endGhostResize); 13714 if (rootDocument !== editableDoc) { 13715 dom.bind(rootDocument, 'mousemove', resizeGhostElement); 13716 dom.bind(rootDocument, 'mouseup', endGhostResize); 13717 } 13718 resizeHelper = dom.add(rootElement, 'div', { 13719 'class': 'mce-resize-helper', 13720 'data-mce-bogus': 'all' 13721 }, startW + ' × ' + startH); 13722 }; 13723 // Get existing or render resize handle 13724 let handleElm = dom.get('mceResizeHandle' + name); 13725 if (handleElm) { 13726 dom.remove(handleElm); 13727 } 13728 handleElm = dom.add(rootElement, 'div', { 13729 'id': 'mceResizeHandle' + name, 13730 'data-mce-bogus': 'all', 13731 'class': 'mce-resizehandle', 13732 'unselectable': true, 13733 'style': 'cursor:' + name + '-resize; margin:0; padding:0' 13734 }); 13735 dom.bind(handleElm, 'mousedown', (e) => { 13736 e.stopImmediatePropagation(); 13737 e.preventDefault(); 13738 startDrag(e); 13739 }); 13740 handle.elm = handleElm; 13741 // Position element 13742 dom.setStyles(handleElm, { 13743 left: (targetWidth * handle[0] + selectedElmX) - (handleElm.offsetWidth / 2), 13744 top: (targetHeight * handle[1] + selectedElmY) - (handleElm.offsetHeight / 2) 13745 }); 13746 }); 13747 } 13748 else { 13749 hideResizeRect(false); 13750 } 13751 }; 13752 const throttledShowResizeRect = first$1(showResizeRect, 0); 13753 const hideResizeRect = (removeSelected = true) => { 13754 throttledShowResizeRect.cancel(); 13755 unbindResizeHandleEvents(); 13756 if (selectedElm && removeSelected) { 13757 selectedElm.removeAttribute(elementSelectionAttr); 13758 } 13759 each$d(resizeHandles, (value, name) => { 13760 const handleElm = dom.get('mceResizeHandle' + name); 13761 if (handleElm) { 13762 dom.unbind(handleElm); 13763 dom.remove(handleElm); 13764 } 13765 }); 13766 }; 13767 const isChildOrEqual = (node, parent) => dom.isChildOf(node, parent); 13768 const updateResizeRect = (e) => { 13769 // Ignore all events while resizing, if the editor instance is composing or the editor was removed 13770 if (resizeStarted || editor.removed || editor.composing) { 13771 return; 13772 } 13773 const targetElm = e.type === 'mousedown' ? e.target : selection.getNode(); 13774 const controlElm = closest$4(SugarElement.fromDom(targetElm), controlElmSelector) 13775 .map((e) => e.dom) 13776 .filter((e) => dom.isEditable(e.parentElement) || (e.nodeName === 'IMG' && dom.isEditable(e))) 13777 .getOrUndefined(); 13778 // Store the original data-mce-selected value or fallback to '1' if not set 13779 const selectedValue = isNonNullable(controlElm) ? dom.getAttrib(controlElm, elementSelectionAttr, '1') : '1'; 13780 // Remove data-mce-selected from all elements since they might have been copied using Ctrl+c/v 13781 each$e(dom.select(`img[${elementSelectionAttr}],hr[${elementSelectionAttr}]`), (img) => { 13782 img.removeAttribute(elementSelectionAttr); 13783 }); 13784 if (isNonNullable(controlElm) && isChildOrEqual(controlElm, rootElement) && hasEditorOrUiFocus(editor)) { 13785 disableGeckoResize(); 13786 const startElm = selection.getStart(true); 13787 if (isChildOrEqual(startElm, controlElm) && isChildOrEqual(selection.getEnd(true), controlElm)) { 13788 // Note: We must ensure the selected attribute is added first before showing the rect so that we don't get any selection flickering 13789 dom.setAttrib(controlElm, elementSelectionAttr, selectedValue); 13790 throttledShowResizeRect.throttle(controlElm); 13791 return; 13792 } 13793 } 13794 hideResizeRect(); 13795 }; 13796 const unbindResizeHandleEvents = () => { 13797 each$d(resizeHandles, (handle) => { 13798 if (handle.elm) { 13799 dom.unbind(handle.elm); 13800 // eslint-disable-next-line @typescript-eslint/no-array-delete 13801 delete handle.elm; 13802 } 13803 }); 13804 }; 13805 const disableGeckoResize = () => { 13806 try { 13807 // Disable object resizing on Gecko 13808 editor.getDoc().execCommand('enableObjectResizing', false, 'false'); 13809 } 13810 catch { 13811 // Ignore 13812 } 13813 }; 13814 editor.on('init', () => { 13815 disableGeckoResize(); 13816 editor.on('NodeChange ResizeEditor ResizeWindow ResizeContent drop', updateResizeRect); 13817 // Update resize rect while typing in a table 13818 editor.on('keyup compositionend', (e) => { 13819 // Don't update the resize rect while composing since it blows away the IME see: #2710 13820 if (selectedElm && selectedElm.nodeName === 'TABLE') { 13821 updateResizeRect(e); 13822 } 13823 }); 13824 editor.on('hide blur', hideResizeRect); 13825 editor.on('contextmenu longpress', contextMenuSelectImage, true); 13826 // Hide rect on focusout since it would float on top of windows otherwise 13827 // editor.on('focusout', hideResizeRect); 13828 }); 13829 editor.on('remove', unbindResizeHandleEvents); 13830 const destroy = () => { 13831 throttledShowResizeRect.cancel(); 13832 selectedElm = selectedElmGhost = resizeBackdrop = null; 13833 }; 13834 return { 13835 isResizable, 13836 showResizeRect, 13837 hideResizeRect, 13838 updateResizeRect, 13839 destroy 13840 }; 13841 }; 13842 13843 const fromPoint = (clientX, clientY, doc) => { 13844 const win = defaultView(SugarElement.fromDom(doc)); 13845 return getAtPoint(win.dom, clientX, clientY).map((simRange) => { 13846 const rng = doc.createRange(); 13847 rng.setStart(simRange.start.dom, simRange.soffset); 13848 rng.setEnd(simRange.finish.dom, simRange.foffset); 13849 return rng; 13850 }).getOrUndefined(); 13851 }; 13852 13853 const isEq$4 = (rng1, rng2) => { 13854 return isNonNullable(rng1) && isNonNullable(rng2) && 13855 (rng1.startContainer === rng2.startContainer && rng1.startOffset === rng2.startOffset) && 13856 (rng1.endContainer === rng2.endContainer && rng1.endOffset === rng2.endOffset); 13857 }; 13858 13859 const findParent = (node, rootNode, predicate) => { 13860 let currentNode = node; 13861 while (currentNode && currentNode !== rootNode) { 13862 if (predicate(currentNode)) { 13863 return currentNode; 13864 } 13865 currentNode = currentNode.parentNode; 13866 } 13867 return null; 13868 }; 13869 const hasParent$1 = (node, rootNode, predicate) => findParent(node, rootNode, predicate) !== null; 13870 const hasParentWithName = (node, rootNode, name) => hasParent$1(node, rootNode, (node) => node.nodeName === name); 13871 const isCeFalseCaretContainer = (node, rootNode) => isCaretContainer$2(node) && !hasParent$1(node, rootNode, isCaretNode); 13872 const hasBrBeforeAfter = (dom, node, left) => { 13873 const parentNode = node.parentNode; 13874 if (parentNode) { 13875 const walker = new DomTreeWalker(node, dom.getParent(parentNode, dom.isBlock) || dom.getRoot()); 13876 let currentNode; 13877 while ((currentNode = walker[left ? 'prev' : 'next']())) { 13878 if (isBr$7(currentNode)) { 13879 return true; 13880 } 13881 } 13882 } 13883 return false; 13884 }; 13885 const isPrevNode = (node, name) => node.previousSibling?.nodeName === name; 13886 const hasContentEditableFalseParent = (root, node) => { 13887 let currentNode = node; 13888 while (currentNode && currentNode !== root) { 13889 if (isContentEditableFalse$a(currentNode)) { 13890 return true; 13891 } 13892 currentNode = currentNode.parentNode; 13893 } 13894 return false; 13895 }; 13896 // Walks the dom left/right to find a suitable text node to move the endpoint into 13897 // It will only walk within the current parent block or body and will stop if it hits a block or a BR/IMG 13898 const findTextNodeRelative = (dom, isAfterNode, collapsed, left, startNode) => { 13899 const body = dom.getRoot(); 13900 const nonEmptyElementsMap = dom.schema.getNonEmptyElements(); 13901 const parentNode = startNode.parentNode; 13902 let lastInlineElement; 13903 let node; 13904 if (!parentNode) { 13905 return Optional.none(); 13906 } 13907 const parentBlockContainer = dom.getParent(parentNode, dom.isBlock) || body; 13908 // Lean left before the BR element if it's the only BR within a block element. Gecko bug: #6680 13909 // This: <p><br>|</p> becomes <p>|<br></p> 13910 if (left && isBr$7(startNode) && isAfterNode && dom.isEmpty(parentBlockContainer)) { 13911 return Optional.some(CaretPosition(parentNode, dom.nodeIndex(startNode))); 13912 } 13913 // Walk left until we hit a text node we can move to or a block/br/img 13914 const walker = new DomTreeWalker(startNode, parentBlockContainer); 13915 while ((node = walker[left ? 'prev' : 'next']())) { 13916 // Break if we hit a non content editable node 13917 if (dom.getContentEditableParent(node) === 'false' || isCeFalseCaretContainer(node, body)) { 13918 return Optional.none(); 13919 } 13920 // Found text node that has a length 13921 if (isText$b(node) && node.data.length > 0) { 13922 if (!hasParentWithName(node, body, 'A')) { 13923 return Optional.some(CaretPosition(node, left ? node.data.length : 0)); 13924 } 13925 return Optional.none(); 13926 } 13927 // Break if we find a block or a BR/IMG/INPUT etc 13928 if (dom.isBlock(node) || nonEmptyElementsMap[node.nodeName.toLowerCase()]) { 13929 return Optional.none(); 13930 } 13931 lastInlineElement = node; 13932 } 13933 if (isComment(lastInlineElement)) { 13934 return Optional.none(); 13935 } 13936 // Only fetch the last inline element when in caret mode for now 13937 if (collapsed && lastInlineElement) { 13938 return Optional.some(CaretPosition(lastInlineElement, 0)); 13939 } 13940 return Optional.none(); 13941 }; 13942 const normalizeEndPoint = (dom, collapsed, start, rng) => { 13943 const body = dom.getRoot(); 13944 let node; 13945 let normalized = false; 13946 let container = start ? rng.startContainer : rng.endContainer; 13947 let offset = start ? rng.startOffset : rng.endOffset; 13948 const isAfterNode = isElement$7(container) && offset === container.childNodes.length; 13949 const nonEmptyElementsMap = dom.schema.getNonEmptyElements(); 13950 let directionLeft = start; 13951 if (isCaretContainer$2(container)) { 13952 return Optional.none(); 13953 } 13954 if (isElement$7(container) && offset > container.childNodes.length - 1) { 13955 directionLeft = false; 13956 } 13957 // If the container is a document move it to the body element 13958 if (isDocument$1(container)) { 13959 container = body; 13960 offset = 0; 13961 } 13962 // If the container is body try move it into the closest text node or position 13963 if (container === body) { 13964 // If start is before/after a image, table etc 13965 if (directionLeft) { 13966 node = container.childNodes[offset > 0 ? offset - 1 : 0]; 13967 if (node) { 13968 if (isCaretContainer$2(node)) { 13969 return Optional.none(); 13970 } 13971 if (nonEmptyElementsMap[node.nodeName] || isTable$2(node)) { 13972 return Optional.none(); 13973 } 13974 } 13975 } 13976 // Resolve the index 13977 if (container.hasChildNodes()) { 13978 offset = Math.min(!directionLeft && offset > 0 ? offset - 1 : offset, container.childNodes.length - 1); 13979 container = container.childNodes[offset]; 13980 offset = isText$b(container) && isAfterNode ? container.data.length : 0; 13981 // Don't normalize non collapsed selections like <p>[a</p><table></table>] 13982 if (!collapsed && container === body.lastChild && isTable$2(container)) { 13983 return Optional.none(); 13984 } 13985 if (hasContentEditableFalseParent(body, container) || isCaretContainer$2(container)) { 13986 return Optional.none(); 13987 } 13988 if (isDetails(container)) { 13989 return Optional.none(); 13990 } 13991 // Don't walk into elements that doesn't have any child nodes like a IMG 13992 if (container.hasChildNodes() && !isTable$2(container)) { 13993 // Walk the DOM to find a text node to place the caret at or a BR 13994 node = container; 13995 const walker = new DomTreeWalker(container, body); 13996 do { 13997 if (isContentEditableFalse$a(node) || isCaretContainer$2(node)) { 13998 normalized = false; 13999 break; 14000 } 14001 // Found a text node use that position 14002 if (isText$b(node) && node.data.length > 0) { 14003 offset = directionLeft ? 0 : node.data.length; 14004 container = node; 14005 normalized = true; 14006 break; 14007 } 14008 // Found a BR/IMG/PRE element that we can place the caret before 14009 if (nonEmptyElementsMap[node.nodeName.toLowerCase()] && !isTableCellOrCaption(node)) { 14010 offset = dom.nodeIndex(node); 14011 container = node.parentNode; 14012 // Put caret after image and pre tag when moving the end point 14013 if (!directionLeft) { 14014 offset++; 14015 } 14016 normalized = true; 14017 break; 14018 } 14019 } while ((node = (directionLeft ? walker.next() : walker.prev()))); 14020 } 14021 } 14022 } 14023 // Lean the caret to the left if possible 14024 if (collapsed) { 14025 // So this: <b>x</b><i>|x</i> 14026 // Becomes: <b>x|</b><i>x</i> 14027 // Seems that only gecko has issues with this 14028 if (isText$b(container) && offset === 0) { 14029 findTextNodeRelative(dom, isAfterNode, collapsed, true, container).each((pos) => { 14030 container = pos.container(); 14031 offset = pos.offset(); 14032 normalized = true; 14033 }); 14034 } 14035 // Lean left into empty inline elements when the caret is before a BR 14036 // So this: <i><b></b><i>|<br></i> 14037 // Becomes: <i><b>|</b><i><br></i> 14038 // Seems that only gecko has issues with this. 14039 // Special edge case for <p><a>x</a>|<br></p> since we don't want <p><a>x|</a><br></p> 14040 if (isElement$7(container)) { 14041 node = container.childNodes[offset]; 14042 // Offset is after the containers last child 14043 // then use the previous child for normalization 14044 if (!node) { 14045 node = container.childNodes[offset - 1]; 14046 } 14047 if (node && isBr$7(node) && !isPrevNode(node, 'A') && 14048 !hasBrBeforeAfter(dom, node, false) && !hasBrBeforeAfter(dom, node, true)) { 14049 findTextNodeRelative(dom, isAfterNode, collapsed, true, node).each((pos) => { 14050 container = pos.container(); 14051 offset = pos.offset(); 14052 normalized = true; 14053 }); 14054 } 14055 } 14056 } 14057 // Lean the start of the selection right if possible 14058 // So this: x[<b>x]</b> 14059 // Becomes: x<b>[x]</b> 14060 if (directionLeft && !collapsed && isText$b(container) && offset === container.data.length) { 14061 findTextNodeRelative(dom, isAfterNode, collapsed, false, container).each((pos) => { 14062 container = pos.container(); 14063 offset = pos.offset(); 14064 normalized = true; 14065 }); 14066 } 14067 return normalized && container ? Optional.some(CaretPosition(container, offset)) : Optional.none(); 14068 }; 14069 const normalize$2 = (dom, rng) => { 14070 const collapsed = rng.collapsed, normRng = rng.cloneRange(); 14071 const startPos = CaretPosition.fromRangeStart(rng); 14072 normalizeEndPoint(dom, collapsed, true, normRng).each((pos) => { 14073 // #TINY-1595: Do not move the caret to previous line 14074 if (!collapsed || !CaretPosition.isAbove(startPos, pos)) { 14075 normRng.setStart(pos.container(), pos.offset()); 14076 } 14077 }); 14078 if (!collapsed) { 14079 normalizeEndPoint(dom, collapsed, false, normRng).each((pos) => { 14080 normRng.setEnd(pos.container(), pos.offset()); 14081 }); 14082 } 14083 // If it was collapsed then make sure it still is 14084 if (collapsed) { 14085 normRng.collapse(true); 14086 } 14087 return isEq$4(rng, normRng) ? Optional.none() : Optional.some(normRng); 14088 }; 14089 14090 const splitText = (node, offset) => { 14091 return node.splitText(offset); 14092 }; 14093 const split = (rng) => { 14094 let startContainer = rng.startContainer, startOffset = rng.startOffset, endContainer = rng.endContainer, endOffset = rng.endOffset; 14095 // Handle single text node 14096 if (startContainer === endContainer && isText$b(startContainer)) { 14097 if (startOffset > 0 && startOffset < startContainer.data.length) { 14098 endContainer = splitText(startContainer, startOffset); 14099 startContainer = endContainer.previousSibling; 14100 if (endOffset > startOffset) { 14101 endOffset = endOffset - startOffset; 14102 const newContainer = splitText(endContainer, endOffset).previousSibling; 14103 startContainer = endContainer = newContainer; 14104 endOffset = newContainer.data.length; 14105 startOffset = 0; 14106 } 14107 else { 14108 endOffset = 0; 14109 } 14110 } 14111 } 14112 else { 14113 // Split startContainer text node if needed 14114 if (isText$b(startContainer) && startOffset > 0 && startOffset < startContainer.data.length) { 14115 startContainer = splitText(startContainer, startOffset); 14116 startOffset = 0; 14117 } 14118 // Split endContainer text node if needed 14119 if (isText$b(endContainer) && endOffset > 0 && endOffset < endContainer.data.length) { 14120 const newContainer = splitText(endContainer, endOffset).previousSibling; 14121 endContainer = newContainer; 14122 endOffset = newContainer.data.length; 14123 } 14124 } 14125 return { 14126 startContainer, 14127 startOffset, 14128 endContainer, 14129 endOffset 14130 }; 14131 }; 14132 14133 /** 14134 * This class contains a few utility methods for ranges. 14135 * 14136 * @class tinymce.dom.RangeUtils 14137 */ 14138 const RangeUtils = (dom) => { 14139 /** 14140 * Walks the specified range like object and executes the callback for each sibling collection it finds. 14141 * 14142 * @private 14143 * @method walk 14144 * @param {RangeObject} rng Range like object. 14145 * @param {Function} callback Callback function to execute for each sibling collection. 14146 */ 14147 const walk = (rng, callback) => { 14148 return walk$3(dom, rng, callback); 14149 }; 14150 /** 14151 * Splits the specified range at it's start/end points. 14152 * 14153 * @private 14154 * @param {RangeObject} rng Range to split. 14155 * @return {RangeObject} Range position object. 14156 */ 14157 const split$1 = split; 14158 /** 14159 * Normalizes the specified range by finding the closest best suitable caret location. 14160 * 14161 * @private 14162 * @param {Range} rng Range to normalize. 14163 * @return {Boolean} True or false if the specified range was normalized or not. 14164 */ 14165 const normalize = (rng) => { 14166 return normalize$2(dom, rng).fold(never, (normalizedRng) => { 14167 rng.setStart(normalizedRng.startContainer, normalizedRng.startOffset); 14168 rng.setEnd(normalizedRng.endContainer, normalizedRng.endOffset); 14169 return true; 14170 }); 14171 }; 14172 /** 14173 * Returns a range expanded around the entire word the provided selection was collapsed within. 14174 * 14175 * @method expand 14176 * @param {Range} rng The initial range to work from. 14177 * @param {Object} options Optional options provided to the expansion. Defaults to { type: 'word' } 14178 * @return {Range} Returns the expanded range. 14179 */ 14180 const expand = (rng, options = { type: 'word' }) => { 14181 if (options.type === 'word') { 14182 const rangeLike = expandRng(dom, rng, [{ inline: 'span' }], { includeTrailingSpace: false, expandToBlock: false }); 14183 const newRange = dom.createRng(); 14184 newRange.setStart(rangeLike.startContainer, rangeLike.startOffset); 14185 newRange.setEnd(rangeLike.endContainer, rangeLike.endOffset); 14186 return newRange; 14187 } 14188 return rng; 14189 }; 14190 return { 14191 walk, 14192 split: split$1, 14193 expand, 14194 normalize 14195 }; 14196 }; 14197 /** 14198 * Compares two ranges and checks if they are equal. 14199 * 14200 * @static 14201 * @method compareRanges 14202 * @param {RangeObject} rng1 First range to compare. 14203 * @param {RangeObject} rng2 First range to compare. 14204 * @return {Boolean} True or false if the ranges are equal. 14205 */ 14206 RangeUtils.compareRanges = isEq$4; 14207 /** 14208 * Gets the caret range for the given x/y location. 14209 * 14210 * @static 14211 * @method getCaretRangeFromPoint 14212 * @param {Number} clientX X coordinate for range 14213 * @param {Number} clientY Y coordinate for range 14214 * @param {Document} doc Document that the x and y coordinates are relative to 14215 * @returns {Range} Caret range 14216 */ 14217 RangeUtils.getCaretRangeFromPoint = fromPoint; 14218 RangeUtils.getSelectedNode = getSelectedNode; 14219 RangeUtils.getNode = getNode$1; 14220 14221 const walkUp = (navigation, doc) => { 14222 const frame = navigation.view(doc); 14223 return frame.fold(constant([]), (f) => { 14224 const parent = navigation.owner(f); 14225 const rest = walkUp(navigation, parent); 14226 return [f].concat(rest); 14227 }); 14228 }; 14229 const pathTo = (element, navigation) => { 14230 const d = navigation.owner(element); 14231 return walkUp(navigation, d); 14232 }; 14233 14234 const view = (doc) => { 14235 // Only walk up to the document this script is defined in. 14236 // This prevents walking up to the parent window when the editor is in an iframe. 14237 const element = doc.dom === document ? Optional.none() : Optional.from(doc.dom.defaultView?.frameElement); 14238 return element.map(SugarElement.fromDom); 14239 }; 14240 const owner = (element) => documentOrOwner(element); 14241 14242 var Navigation = /*#__PURE__*/Object.freeze({ 14243 __proto__: null, 14244 view: view, 14245 owner: owner 14246 }); 14247 14248 const find = (element) => { 14249 const doc = getDocument(); 14250 const scroll = get$
vendor: 2,893 bytes, lines 14250-14323
142505(doc); 14251 const frames = pathTo(element, Navigation); 14252 const offset = viewport(element); 14253 const r = foldr(frames, (b, a) => { 14254 const loc = viewport(a); 14255 return { 14256 left: b.left + loc.left, 14257 top: b.top + loc.top 14258 }; 14259 }, { left: 0, top: 0 }); 14260 return SugarPosition(r.left + offset.left + scroll.left, r.top + offset.top + scroll.top); 14261 }; 14262 14263 const excludeFromDescend = (element) => name(element) === 'textarea'; 14264 const fireScrollIntoViewEvent = (editor, data) => { 14265 const scrollEvent = editor.dispatch('ScrollIntoView', data); 14266 return scrollEvent.isDefaultPrevented(); 14267 }; 14268 const fireAfterScrollIntoViewEvent = (editor, data) => { 14269 editor.dispatch('AfterScrollIntoView', data); 14270 }; 14271 const descend = (element, offset) => { 14272 const children = children$1(element); 14273 if (children.length === 0 || excludeFromDescend(element)) { 14274 return { element, offset }; 14275 } 14276 else if (offset < children.length && !excludeFromDescend(children[offset])) { 14277 return { element: children[offset], offset: 0 }; 14278 } 14279 else { 14280 const last = children[children.length - 1]; 14281 if (excludeFromDescend(last)) { 14282 return { element, offset }; 14283 } 14284 else { 14285 if (name(last) === 'img') { 14286 return { element: last, offset: 1 }; 14287 } 14288 else if (isText$c(last)) { 14289 return { element: last, offset: get$4(last).length }; 14290 } 14291 else { 14292 return { element: last, offset: children$1(last).length }; 14293 } 14294 } 14295 } 14296 }; 14297 const markerInfo = (element, cleanupFun) => { 14298 const pos = absolute(element); 14299 const height = get$6(element); 14300 return { 14301 element, 14302 bottom: pos.top + height, 14303 height, 14304 pos, 14305 cleanup: cleanupFun 14306 }; 14307 }; 14308 const createMarker$1 = (element, offset) => { 14309 const startPoint = descend(element, offset); 14310 const span = SugarElement.fromHtml('<span data-mce-bogus="all" style="display: inline-block;">' + ZWSP$1 + '</span>'); 14311 before$4(startPoint.element, span); 14312 return markerInfo(span, () => remove$8(span)); 14313 }; 14314 const elementMarker = (element) => markerInfo(SugarElement.fromDom(element), noop); 14315 const withMarker = (editor, f, rng, alignToTop) => { 14316 preserveWith(editor, (_s, _e) => applyWithMarker(editor, f, rng, alignToTop), rng); 14317 }; 14318 const withScrollEvents = (editor, doc, f, marker, alignToTop) => { 14319 const data = { elm: marker.element.dom, alignToTop }; 14320 if (fireScrollIntoViewEvent(editor, data)) { 14321 return; 14322 } 14323 const scrollTop = get$5
vendor: 27,895 bytes, lines 14323-15006
14323(doc).top; 14324 f(editor, doc, scrollTop, marker, alignToTop); 14325 fireAfterScrollIntoViewEvent(editor, data); 14326 }; 14327 const applyWithMarker = (editor, f, rng, alignToTop) => { 14328 const body = SugarElement.fromDom(editor.getBody()); 14329 const doc = SugarElement.fromDom(editor.getDoc()); 14330 reflow(body); 14331 const marker = createMarker$1(SugarElement.fromDom(rng.startContainer), rng.startOffset); 14332 withScrollEvents(editor, doc, f, marker, alignToTop); 14333 marker.cleanup(); 14334 }; 14335 const withElement = (editor, element, f, alignToTop) => { 14336 const doc = SugarElement.fromDom(editor.getDoc()); 14337 withScrollEvents(editor, doc, f, elementMarker(element), alignToTop); 14338 }; 14339 const preserveWith = (editor, f, rng) => { 14340 const startElement = rng.startContainer; 14341 const startOffset = rng.startOffset; 14342 const endElement = rng.endContainer; 14343 const endOffset = rng.endOffset; 14344 f(SugarElement.fromDom(startElement), SugarElement.fromDom(endElement)); 14345 const newRng = editor.dom.createRng(); 14346 newRng.setStart(startElement, startOffset); 14347 newRng.setEnd(endElement, endOffset); 14348 editor.selection.setRng(rng); 14349 }; 14350 const scrollToMarker = (editor, marker, viewHeight, alignToTop, doc) => { 14351 const pos = marker.pos; 14352 // with default font size 16px font and 1.3 line height (~21px per line), 14353 // adding roughly 50% extra space gives about 30px of breathing room ensuring comfortable spacing. 14354 const scrollMargin = 30; 14355 if (alignToTop) { 14356 // When scrolling to top, add margin to the top position 14357 to(pos.left, Math.max(0, pos.top - scrollMargin), doc); 14358 } 14359 else { 14360 // The position we want to scroll to is the... 14361 // (absolute position of the marker, minus the view height) plus (the height of the marker) 14362 // When scrolling to bottom, add margin to ensure content isn't at the very bottom 14363 const y = (pos.top - viewHeight) + marker.height + scrollMargin; 14364 to(-editor.getBody().getBoundingClientRect().left, y, doc); 14365 } 14366 }; 14367 const intoWindowIfNeeded = (editor, doc, scrollTop, viewHeight, marker, alignToTop) => { 14368 const viewportBottom = viewHeight + scrollTop; 14369 const markerTop = marker.pos.top; 14370 const markerBottom = marker.bottom; 14371 const largerThanViewport = markerBottom - markerTop >= viewHeight; 14372 // above the screen, scroll to top by default 14373 if (markerTop < scrollTop) { 14374 scrollToMarker(editor, marker, viewHeight, alignToTop !== false, doc); 14375 // completely below the screen. Default scroll to the top if element height is larger 14376 // than the viewport, otherwise default to scrolling to the bottom 14377 } 14378 else if (markerTop > viewportBottom) { 14379 const align = largerThanViewport ? alignToTop !== false : alignToTop === true; 14380 scrollToMarker(editor, marker, viewHeight, align, doc); 14381 // partially below the bottom, only scroll if element height is less than viewport 14382 } 14383 else if (markerBottom > viewportBottom && !largerThanViewport) { 14384 scrollToMarker(editor, marker, viewHeight, alignToTop === true, doc); 14385 } 14386 }; 14387 const intoWindow = (editor, doc, scrollTop, marker, alignToTop) => { 14388 const viewHeight = defaultView(doc).dom.innerHeight; 14389 intoWindowIfNeeded(editor, doc, scrollTop, viewHeight, marker, alignToTop); 14390 }; 14391 const intoFrame = (editor, doc, scrollTop, marker, alignToTop) => { 14392 const frameViewHeight = defaultView(doc).dom.innerHeight; // height of iframe container 14393 // If the position is outside the iframe viewport, scroll to it 14394 intoWindowIfNeeded(editor, doc, scrollTop, frameViewHeight, marker, alignToTop); 14395 // If the new position is outside the window viewport, scroll to it 14396 const op = find(marker.element); 14397 const viewportBounds = getBounds(window); 14398 if (op.top < viewportBounds.y) { 14399 intoView(marker.element, alignToTop !== false); 14400 } 14401 else if (op.top > viewportBounds.bottom) { 14402 intoView(marker.element, alignToTop === true); 14403 } 14404 }; 14405 const rangeIntoWindow = (editor, rng, alignToTop) => withMarker(editor, intoWindow, rng, alignToTop); 14406 const elementIntoWindow = (editor, element, alignToTop) => withElement(editor, element, intoWindow, alignToTop); 14407 const rangeIntoFrame = (editor, rng, alignToTop) => withMarker(editor, intoFrame, rng, alignToTop); 14408 const elementIntoFrame = (editor, element, alignToTop) => withElement(editor, element, intoFrame, alignToTop); 14409 const scrollElementIntoView = (editor, element, alignToTop) => { 14410 const scroller = editor.inline ? elementIntoWindow : elementIntoFrame; 14411 scroller(editor, element, alignToTop); 14412 }; 14413 // This method is made to deal with the user pressing enter, it is not useful 14414 // if we want for example scroll in content after a paste event. 14415 const scrollRangeIntoView = (editor, rng, alignToTop) => { 14416 const scroller = editor.inline ? rangeIntoWindow : rangeIntoFrame; 14417 scroller(editor, rng, alignToTop); 14418 }; 14419 14420 const isEditableRange = (dom, rng) => { 14421 if (rng.collapsed) { 14422 return dom.isEditable(rng.startContainer); 14423 } 14424 else { 14425 return dom.isEditable(rng.startContainer) && dom.isEditable(rng.endContainer); 14426 } 14427 }; 14428 14429 const getEndpointElement = (root, rng, start, real, resolve) => { 14430 const container = start ? rng.startContainer : rng.endContainer; 14431 const offset = start ? rng.startOffset : rng.endOffset; 14432 return Optional.from(container) 14433 .map(SugarElement.fromDom) 14434 .map((elm) => !real || !rng.collapsed ? child$1(elm, resolve(elm, offset)).getOr(elm) : elm) 14435 .bind((elm) => isElement$8(elm) ? Optional.some(elm) : parent(elm).filter(isElement$8)) 14436 .map((elm) => elm.dom) 14437 .getOr(root); 14438 }; 14439 const getStart = (root, rng, real = false) => getEndpointElement(root, rng, true, real, (elm, offset) => Math.min(childNodesCount(elm), offset)); 14440 const getEnd = (root, rng, real = false) => getEndpointElement(root, rng, false, real, (elm, offset) => offset > 0 ? offset - 1 : offset); 14441 const skipEmptyTextNodes = (node, forwards) => { 14442 const orig = node; 14443 while (node && isText$b(node) && node.length === 0) { 14444 node = forwards ? node.nextSibling : node.previousSibling; 14445 } 14446 return node || orig; 14447 }; 14448 const getNode = (root, rng) => { 14449 // Range maybe lost after the editor is made visible again 14450 if (!rng) { 14451 return root; 14452 } 14453 let startContainer = rng.startContainer; 14454 let endContainer = rng.endContainer; 14455 const startOffset = rng.startOffset; 14456 const endOffset = rng.endOffset; 14457 let node = rng.commonAncestorContainer; 14458 // Handle selection a image or other control like element such as anchors 14459 if (!rng.collapsed) { 14460 if (startContainer === endContainer) { 14461 if (endOffset - startOffset < 2) { 14462 if (startContainer.hasChildNodes()) { 14463 node = startContainer.childNodes[startOffset]; 14464 } 14465 } 14466 } 14467 // If the anchor node is a element instead of a text node then return this element 14468 // if (tinymce.isWebKit && sel.anchorNode && sel.anchorNode.nodeType == 1) 14469 // return sel.anchorNode.childNodes[sel.anchorOffset]; 14470 // Handle cases where the selection is immediately wrapped around a node and return that node instead of it's parent. 14471 // This happens when you double click an underlined word in FireFox. 14472 if (isText$b(startContainer) && isText$b(endContainer)) { 14473 if (startContainer.length === startOffset) { 14474 startContainer = skipEmptyTextNodes(startContainer.nextSibling, true); 14475 } 14476 else { 14477 startContainer = startContainer.parentNode; 14478 } 14479 if (endOffset === 0) { 14480 endContainer = skipEmptyTextNodes(endContainer.previousSibling, false); 14481 } 14482 else { 14483 endContainer = endContainer.parentNode; 14484 } 14485 if (startContainer && startContainer === endContainer) { 14486 node = startContainer; 14487 } 14488 } 14489 } 14490 const elm = isText$b(node) ? node.parentNode : node; 14491 return isHTMLElement(elm) ? elm : root; 14492 }; 14493 const getSelectedBlocks = (dom, rng, startElm, endElm) => { 14494 const selectedBlocks = []; 14495 const root = dom.getRoot(); 14496 const start = dom.getParent(startElm || getStart(root, rng, rng.collapsed), dom.isBlock); 14497 const end = dom.getParent(endElm || getEnd(root, rng, rng.collapsed), dom.isBlock); 14498 if (start && start !== root) { 14499 selectedBlocks.push(start); 14500 } 14501 if (start && end && start !== end) { 14502 let node; 14503 const walker = new DomTreeWalker(start, root); 14504 while ((node = walker.next()) && node !== end) { 14505 if (dom.isBlock(node)) { 14506 selectedBlocks.push(node); 14507 } 14508 } 14509 } 14510 if (end && start !== end && end !== root) { 14511 selectedBlocks.push(end); 14512 } 14513 return selectedBlocks; 14514 }; 14515 const select = (dom, node, content) => Optional.from(node).bind((node) => Optional.from(node.parentNode).map((parent) => { 14516 const idx = dom.nodeIndex(node); 14517 const rng = dom.createRng(); 14518 rng.setStart(parent, idx); 14519 rng.setEnd(parent, idx + 1); 14520 // Find first/last text node or BR element 14521 if (content) { 14522 moveEndPoint(dom, rng, node, true); 14523 moveEndPoint(dom, rng, node, false); 14524 } 14525 return rng; 14526 })); 14527 14528 const processRanges = (editor, ranges) => map$3(ranges, (range) => { 14529 const evt = editor.dispatch('GetSelectionRange', { range }); 14530 return evt.range !== range ? evt.range : range; 14531 }); 14532 14533 const typeLookup = { 14534 '#text': 3, 14535 '#comment': 8, 14536 '#cdata': 4, 14537 '#pi': 7, 14538 '#doctype': 10, 14539 '#document-fragment': 11 14540 }; 14541 // Walks the tree left/right 14542 const walk$2 = (node, root, prev) => { 14543 const startName = prev ? 'lastChild' : 'firstChild'; 14544 const siblingName = prev ? 'prev' : 'next'; 14545 // Walk into nodes if it has a start 14546 if (node[startName]) { 14547 return node[startName]; 14548 } 14549 // Return the sibling if it has one 14550 if (node !== root) { 14551 let sibling = node[siblingName]; 14552 if (sibling) { 14553 return sibling; 14554 } 14555 // Walk up the parents to look for siblings 14556 for (let parent = node.parent; parent && parent !== root; parent = parent.parent) { 14557 sibling = parent[siblingName]; 14558 if (sibling) { 14559 return sibling; 14560 } 14561 } 14562 } 14563 return undefined; 14564 }; 14565 const isEmptyTextNode = (node) => { 14566 const text = node.value ?? ''; 14567 // Non whitespace content 14568 if (!isWhitespaceText(text)) { 14569 return false; 14570 } 14571 // Parent is not a span and only spaces or is a span but has styles 14572 const parentNode = node.parent; 14573 if (parentNode && (parentNode.name !== 'span' || parentNode.attr('style')) && /^[ ]+$/.test(text)) { 14574 return false; 14575 } 14576 return true; 14577 }; 14578 // Check if node contains data-bookmark attribute, name attribute, id attribute or is a named anchor 14579 const isNonEmptyElement = (node) => { 14580 const isNamedAnchor = node.name === 'a' && !node.attr('href') && node.attr('id'); 14581 return (node.attr('name') || (node.attr('id') && !node.firstChild) || node.attr('data-mce-bookmark') || isNamedAnchor); 14582 }; 14583 /** 14584 * This class is a minimalistic implementation of a DOM like node used by the DomParser class. 14585 * 14586 * @class tinymce.html.Node 14587 * @version 3.4 14588 * @example 14589 * const node = new tinymce.html.Node('strong', 1); 14590 * someRoot.append(node); 14591 */ 14592 class AstNode { 14593 /** 14594 * Creates a node of a specific type. 14595 * 14596 * @static 14597 * @method create 14598 * @param {String} name Name of the node type to create for example "b" or "#text". 14599 * @param {Object} attrs Name/value collection of attributes that will be applied to elements. 14600 */ 14601 static create(name, attrs) { 14602 // Create node 14603 const node = new AstNode(name, typeLookup[name] || 1); 14604 // Add attributes if needed 14605 if (attrs) { 14606 each$d(attrs, (value, attrName) => { 14607 node.attr(attrName, value); 14608 }); 14609 } 14610 return node; 14611 } 14612 name; 14613 type; 14614 attributes; 14615 value; 14616 parent; 14617 firstChild; 14618 lastChild; 14619 next; 14620 prev; 14621 raw; 14622 /** 14623 * Constructs a new Node instance. 14624 * 14625 * @constructor 14626 * @method Node 14627 * @param {String} name Name of the node type. 14628 * @param {Number} type Numeric type representing the node. 14629 */ 14630 constructor(name, type) { 14631 this.name = name; 14632 this.type = type; 14633 if (type === 1) { 14634 this.attributes = []; 14635 this.attributes.map = {}; // Should be considered internal 14636 } 14637 } 14638 /** 14639 * Replaces the current node with the specified one. 14640 * 14641 * @method replace 14642 * @param {tinymce.html.Node} node Node to replace the current node with. 14643 * @return {tinymce.html.Node} The old node that got replaced. 14644 * @example 14645 * someNode.replace(someNewNode); 14646 */ 14647 replace(node) { 14648 const self = this; 14649 if (node.parent) { 14650 node.remove(); 14651 } 14652 self.insert(node, self); 14653 self.remove(); 14654 return self; 14655 } 14656 attr(name, value) { 14657 const self = this; 14658 if (!isString(name)) { 14659 if (isNonNullable(name)) { 14660 each$d(name, (value, key) => { 14661 self.attr(key, value); 14662 }); 14663 } 14664 return self; 14665 } 14666 const attrs = self.attributes; 14667 if (attrs) { 14668 if (value !== undefined) { 14669 // Remove attribute 14670 if (value === null) { 14671 if (name in attrs.map) { 14672 delete attrs.map[name]; 14673 let i = attrs.length; 14674 while (i--) { 14675 if (attrs[i].name === name) { 14676 attrs.splice(i, 1); 14677 return self; 14678 } 14679 } 14680 } 14681 return self; 14682 } 14683 // Set attribute 14684 if (name in attrs.map) { 14685 // Set attribute 14686 let i = attrs.length; 14687 while (i--) { 14688 if (attrs[i].name === name) { 14689 attrs[i].value = value; 14690 break; 14691 } 14692 } 14693 } 14694 else { 14695 attrs.push({ name, value }); 14696 } 14697 attrs.map[name] = value; 14698 return self; 14699 } 14700 return attrs.map[name]; 14701 } 14702 return undefined; 14703 } 14704 /** 14705 * Does a shallow clones the node into a new node. It will also exclude id attributes since 14706 * there should only be one id per document. 14707 * 14708 * @method clone 14709 * @return {tinymce.html.Node} New copy of the original node. 14710 * @example 14711 * const clonedNode = node.clone(); 14712 */ 14713 clone() { 14714 const self = this; 14715 const clone = new AstNode(self.name, self.type); 14716 const selfAttrs = self.attributes; 14717 // Clone element attributes 14718 if (selfAttrs) { 14719 const cloneAttrs = []; 14720 cloneAttrs.map = {}; 14721 for (let i = 0, l = selfAttrs.length; i < l; i++) { 14722 const selfAttr = selfAttrs[i]; 14723 // Clone everything except id 14724 if (selfAttr.name !== 'id') { 14725 cloneAttrs[cloneAttrs.length] = { name: selfAttr.name, value: selfAttr.value }; 14726 cloneAttrs.map[selfAttr.name] = selfAttr.value; 14727 } 14728 } 14729 clone.attributes = cloneAttrs; 14730 } 14731 clone.value = self.value; 14732 return clone; 14733 } 14734 /** 14735 * Wraps the node in in another node. 14736 * 14737 * @method wrap 14738 * @example 14739 * node.wrap(wrapperNode); 14740 */ 14741 wrap(wrapper) { 14742 const self = this; 14743 if (self.parent) { 14744 self.parent.insert(wrapper, self); 14745 wrapper.append(self); 14746 } 14747 return self; 14748 } 14749 /** 14750 * Unwraps the node in other words it removes the node but keeps the children. 14751 * 14752 * @method unwrap 14753 * @example 14754 * node.unwrap(); 14755 */ 14756 unwrap() { 14757 const self = this; 14758 for (let node = self.firstChild; node;) { 14759 const next = node.next; 14760 self.insert(node, self, true); 14761 node = next; 14762 } 14763 self.remove(); 14764 } 14765 /** 14766 * Removes the node from it's parent. 14767 * 14768 * @method remove 14769 * @return {tinymce.html.Node} Current node that got removed. 14770 * @example 14771 * node.remove(); 14772 */ 14773 remove() { 14774 const self = this, parent = self.parent, next = self.next, prev = self.prev; 14775 if (parent) { 14776 if (parent.firstChild === self) { 14777 parent.firstChild = next; 14778 if (next) { 14779 next.prev = null; 14780 } 14781 } 14782 else if (prev) { 14783 prev.next = next; 14784 } 14785 if (parent.lastChild === self) { 14786 parent.lastChild = prev; 14787 if (prev) { 14788 prev.next = null; 14789 } 14790 } 14791 else if (next) { 14792 next.prev = prev; 14793 } 14794 self.parent = self.next = self.prev = null; 14795 } 14796 return self; 14797 } 14798 /** 14799 * Appends a new node as a child of the current node. 14800 * 14801 * @method append 14802 * @param {tinymce.html.Node} node Node to append as a child of the current one. 14803 * @return {tinymce.html.Node} The node that got appended. 14804 * @example 14805 * node.append(someNode); 14806 */ 14807 append(node) { 14808 const self = this; 14809 if (node.parent) { 14810 node.remove(); 14811 } 14812 const last = self.lastChild; 14813 if (last) { 14814 last.next = node; 14815 node.prev = last; 14816 self.lastChild = node; 14817 } 14818 else { 14819 self.lastChild = self.firstChild = node; 14820 } 14821 node.parent = self; 14822 return node; 14823 } 14824 /** 14825 * Inserts a node at a specific position as a child of this node. 14826 * 14827 * @method insert 14828 * @param {tinymce.html.Node} node Node to insert as a child of this node. 14829 * @param {tinymce.html.Node} refNode Reference node to set node before/after. 14830 * @param {Boolean} before Optional state to insert the node before the reference node. 14831 * @return {tinymce.html.Node} The node that got inserted. 14832 * @example 14833 * parentNode.insert(newChildNode, oldChildNode); 14834 */ 14835 insert(node, refNode, before) { 14836 if (node.parent) { 14837 node.remove(); 14838 } 14839 const parent = refNode.parent || this; 14840 if (before) { 14841 if (refNode === parent.firstChild) { 14842 parent.firstChild = node; 14843 } 14844 else if (refNode.prev) { 14845 refNode.prev.next = node; 14846 } 14847 node.prev = refNode.prev; 14848 node.next = refNode; 14849 refNode.prev = node; 14850 } 14851 else { 14852 if (refNode === parent.lastChild) { 14853 parent.lastChild = node; 14854 } 14855 else if (refNode.next) { 14856 refNode.next.prev = node; 14857 } 14858 node.next = refNode.next; 14859 node.prev = refNode; 14860 refNode.next = node; 14861 } 14862 node.parent = parent; 14863 return node; 14864 } 14865 /** 14866 * Get all descendants by name. 14867 * 14868 * @method getAll 14869 * @param {String} name Name of the descendant nodes to collect. 14870 * @return {Array} Array with descendant nodes matching the specified name. 14871 */ 14872 getAll(name) { 14873 const self = this; 14874 const collection = []; 14875 for (let node = self.firstChild; node; node = walk$2(node, self)) { 14876 if (node.name === name) { 14877 collection.push(node); 14878 } 14879 } 14880 return collection; 14881 } 14882 /** 14883 * Get all children of this node. 14884 * 14885 * @method children 14886 * @return {Array} Array containing child nodes. 14887 */ 14888 children() { 14889 const self = this; 14890 const collection = []; 14891 for (let node = self.firstChild; node; node = node.next) { 14892 collection.push(node); 14893 } 14894 return collection; 14895 } 14896 /** 14897 * Removes all children of the current node. 14898 * 14899 * @method empty 14900 * @return {tinymce.html.Node} The current node that got cleared. 14901 */ 14902 empty() { 14903 const self = this; 14904 // Remove all children 14905 if (self.firstChild) { 14906 const nodes = []; 14907 // Collect the children 14908 for (let node = self.firstChild; node; node = walk$2(node, self)) { 14909 nodes.push(node); 14910 } 14911 // Remove the children 14912 let i = nodes.length; 14913 while (i--) { 14914 const node = nodes[i]; 14915 node.parent = node.firstChild = node.lastChild = node.next = node.prev = null; 14916 } 14917 } 14918 self.firstChild = self.lastChild = null; 14919 return self; 14920 } 14921 /** 14922 * Returns true/false if the node is to be considered empty or not. 14923 * 14924 * @method isEmpty 14925 * @param {Object} elements Name/value object with elements that are automatically treated as non empty elements. 14926 * @param {Object} whitespace Name/value object with elements that are automatically treated whitespace preservables. 14927 * @param {Function} predicate Optional predicate that gets called after the other rules determine that the node is empty. Should return true if the node is a content node. 14928 * @return {Boolean} true/false if the node is empty or not. 14929 * @example 14930 * node.isEmpty({ img: true }); 14931 */ 14932 isEmpty(elements, whitespace = {}, predicate) { 14933 const self = this; 14934 let node = self.firstChild; 14935 if (isNonEmptyElement(self)) { 14936 return false; 14937 } 14938 if (node) { 14939 do { 14940 if (node.type === 1) { 14941 // Ignore bogus elements 14942 if (node.attr('data-mce-bogus')) { 14943 continue; 14944 } 14945 // Keep empty elements like <img /> 14946 if (elements[node.name]) { 14947 return false; 14948 } 14949 if (isNonEmptyElement(node)) { 14950 return false; 14951 } 14952 } 14953 // Keep comments 14954 if (node.type === 8) { 14955 return false; 14956 } 14957 // Keep non whitespace text nodes 14958 if (node.type === 3 && !isEmptyTextNode(node)) { 14959 return false; 14960 } 14961 // Keep whitespace preserve elements 14962 if (node.type === 3 && node.parent && whitespace[node.parent.name] && isWhitespaceText(node.value ?? '')) { 14963 return false; 14964 } 14965 // Predicate tells that the node is contents 14966 if (predicate && predicate(node)) { 14967 return false; 14968 } 14969 } while ((node = walk$2(node, self))); 14970 } 14971 return true; 14972 } 14973 /** 14974 * Walks to the next or previous node and returns that node or null if it wasn't found. 14975 * 14976 * @method walk 14977 * @param {Boolean} prev Optional previous node state defaults to false. 14978 * @return {tinymce.html.Node} Node that is next to or previous of the current node. 14979 */ 14980 walk(prev) { 14981 return walk$2(this, null, prev); 14982 } 14983 } 14984 14985 // TINY-10305: Map over array for faster lookup. 14986 const unescapedTextParents = Tools.makeMap('NOSCRIPT STYLE SCRIPT XMP IFRAME NOEMBED NOFRAMES PLAINTEXT', ' '); 14987 const containsZwsp = (node) => isString(node.nodeValue) && node.nodeValue.includes(ZWSP$1); 14988 const getTemporaryNodeSelector = (tempAttrs) => `${tempAttrs.length === 0 ? '' : `${map$3(tempAttrs, (attr) => `[${attr}]`).join(',')},`}[data-mce-bogus="all"]`; 14989 const getTemporaryNodes = (tempAttrs, body) => body.querySelectorAll(getTemporaryNodeSelector(tempAttrs)); 14990 const createZwspCommentWalker = (body) => document.createTreeWalker(body, NodeFilter.SHOW_COMMENT, (node) => containsZwsp(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP); 14991 const createUnescapedZwspTextWalker = (body) => document.createTreeWalker(body, NodeFilter.SHOW_TEXT, (node) => { 14992 if (containsZwsp(node)) { 14993 const parent = node.parentNode; 14994 return parent && has$2(unescapedTextParents, parent.nodeName) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP; 14995 } 14996 else { 14997 return NodeFilter.FILTER_SKIP; 14998 } 14999 }); 15000 const hasZwspComment = (body) => createZwspCommentWalker(body).nextNode() !== null; 15001 const hasUnescapedZwspText = (body) => createUnescapedZwspTextWalker(body).nextNode() !== null; 15002 const hasTemporaryNode = (tempAttrs, body) => body.querySelector(getTemporaryNodeSelector(tempAttrs)) !== null; 15003 const trimTemporaryNodes = (tempAttrs, body) => { 15004 each$e(getTemporaryNodes(tempAttrs, body), (elm) => { 15005 const element = SugarElement.fromDom(elm); 15006 if (get$
vendor: 4,122 bytes, lines 15006-15117
150069(element, 'data-mce-bogus') === 'all') { 15007 remove$8(element); 15008 } 15009 else { 15010 each$e(tempAttrs, (attr) => { 15011 if (has$1(element, attr)) { 15012 remove$9(element, attr); 15013 } 15014 }); 15015 } 15016 }); 15017 }; 15018 const emptyAllNodeValuesInWalker = (walker) => { 15019 let curr = walker.nextNode(); 15020 while (curr !== null) { 15021 curr.nodeValue = null; 15022 curr = walker.nextNode(); 15023 } 15024 }; 15025 const emptyZwspComments = compose(emptyAllNodeValuesInWalker, createZwspCommentWalker); 15026 const emptyUnescapedZwspTexts = compose(emptyAllNodeValuesInWalker, createUnescapedZwspTextWalker); 15027 const trim$1 = (body, tempAttrs) => { 15028 const conditionalTrims = [ 15029 { 15030 condition: curry(hasTemporaryNode, tempAttrs), 15031 action: curry(trimTemporaryNodes, tempAttrs) 15032 }, 15033 { 15034 condition: hasZwspComment, 15035 action: emptyZwspComments 15036 }, 15037 { 15038 condition: hasUnescapedZwspText, 15039 action: emptyUnescapedZwspTexts 15040 } 15041 ]; 15042 let trimmed = body; 15043 let cloned = false; 15044 each$e(conditionalTrims, ({ condition, action }) => { 15045 if (condition(trimmed)) { 15046 if (!cloned) { 15047 trimmed = body.cloneNode(true); 15048 cloned = true; 15049 } 15050 action(trimmed); 15051 } 15052 }); 15053 return trimmed; 15054 }; 15055 15056 const cleanupBogusElements = (parent) => { 15057 const bogusElements = descendants(parent, '[data-mce-bogus]'); 15058 each$e(bogusElements, (elem) => { 15059 const bogusValue = get$9(elem, 'data-mce-bogus'); 15060 if (bogusValue === 'all') { 15061 remove$8(elem); 15062 } 15063 else if (isBr$6(elem)) { 15064 // Need to keep bogus padding brs represented as a zero-width space so that they aren't collapsed by the browser 15065 before$4(elem, SugarElement.fromText(zeroWidth)); 15066 remove$8(elem); 15067 } 15068 else { 15069 unwrap(elem); 15070 } 15071 }); 15072 }; 15073 const cleanupInputNames = (parent) => { 15074 const inputs = descendants(parent, 'input'); 15075 each$e(inputs, (input) => { 15076 remove$9(input, 'name'); 15077 }); 15078 }; 15079 15080 const trimEmptyContents = (editor, html) => { 15081 const blockName = getForcedRootBlock(editor); 15082 const emptyRegExp = new RegExp(`^(<${blockName}[^>]*>( | |\\s|\u00a0|<br \\/>|)<\\/${blockName}>[\r\n]*|<br \\/>[\r\n]*)$`); 15083 return html.replace(emptyRegExp, ''); 15084 }; 15085 const getPlainTextContent = (editor, body) => { 15086 const doc = editor.getDoc(); 15087 const dos = getRootNode(SugarElement.fromDom(editor.getBody())); 15088 const offscreenDiv = SugarElement.fromTag('div', doc); 15089 set$4(offscreenDiv, 'data-mce-bogus', 'all'); 15090 setAll(offscreenDiv, { 15091 position: 'fixed', 15092 left: '-9999999px', 15093 top: '0' 15094 }); 15095 set$3(offscreenDiv, body.innerHTML); 15096 cleanupBogusElements(offscreenDiv); 15097 cleanupInputNames(offscreenDiv); 15098 // Append the wrapper element so that the browser will evaluate styles when getting the `innerText` 15099 const root = getContentContainer(dos); 15100 append$1(root, offscreenDiv); 15101 const content = trim$2(offscreenDiv.dom.innerText); 15102 remove$8(offscreenDiv); 15103 return content; 15104 }; 15105 const getContentFromBody = (editor, args, body) => { 15106 let content; 15107 if (args.format === 'raw') { 15108 content = Tools.trim(trim$2(trim$1(body, editor.serializer.getTempAttrs()).innerHTML)); 15109 } 15110 else if (args.format === 'text') { 15111 content = getPlainTextContent(editor, body); 15112 } 15113 else if (args.format === 'tree') { 15114 content = editor.serializer.serialize(body, args); 15115 } 15116 else { 15117 content = trimEmptyContents(editor
vendor: 17,073 bytes, lines 15117-15487
15117, editor.serializer.serialize(body, args)); 15118 } 15119 // Trim if not using a whitespace preserve format/element 15120 const shouldTrim = args.format !== 'text' && !isWsPreserveElement(SugarElement.fromDom(body)); 15121 return shouldTrim && isString(content) ? Tools.trim(content) : content; 15122 }; 15123 const getContentInternal = (editor, args) => Optional.from(editor.getBody()) 15124 .fold(constant(args.format === 'tree' ? new AstNode('body', 11) : ''), (body) => getContentFromBody(editor, args, body)); 15125 15126 /** 15127 * This class is used to write HTML tags out it can be used with the Serializer. 15128 * 15129 * @class tinymce.html.Writer 15130 * @version 3.4 15131 * @example 15132 * const writer = tinymce.html.Writer({ indent: true }); 15133 * writer.start('node', { attr: 'value' }); 15134 * writer.end('node'); 15135 * console.log(writer.getContent()); 15136 */ 15137 const makeMap$1 = Tools.makeMap; 15138 const Writer = (settings) => { 15139 const html = []; 15140 settings = settings || {}; 15141 const indent = settings.indent; 15142 const indentBefore = makeMap$1(settings.indent_before || ''); 15143 const indentAfter = makeMap$1(settings.indent_after || ''); 15144 const encode = Entities.getEncodeFunc(settings.entity_encoding || 'raw', settings.entities); 15145 const htmlOutput = settings.element_format !== 'xhtml'; 15146 return { 15147 /** 15148 * Writes a start element, such as `<p id="a">`. 15149 * 15150 * @method start 15151 * @param {String} name Name of the element. 15152 * @param {Array} attrs Optional array of objects containing an attribute name and value, or undefined if the element has no attributes. 15153 * @param {Boolean} empty Optional empty state if the tag should serialize as a void element. For example: `<img />` 15154 */ 15155 start: (name, attrs, empty) => { 15156 if (indent && indentBefore[name] && html.length > 0) { 15157 const value = html[html.length - 1]; 15158 if (value.length > 0 && value !== '\n') { 15159 html.push('\n'); 15160 } 15161 } 15162 html.push('<', name); 15163 if (attrs) { 15164 for (let i = 0, l = attrs.length; i < l; i++) { 15165 const attr = attrs[i]; 15166 html.push(' ', attr.name, '="', encode(attr.value, true), '"'); 15167 } 15168 } 15169 if (!empty || htmlOutput) { 15170 html[html.length] = '>'; 15171 } 15172 else { 15173 html[html.length] = ' />'; 15174 } 15175 if (empty && indent && indentAfter[name] && html.length > 0) { 15176 const value = html[html.length - 1]; 15177 if (value.length > 0 && value !== '\n') { 15178 html.push('\n'); 15179 } 15180 } 15181 }, 15182 /** 15183 * Writes an end element, such as `</p>`. 15184 * 15185 * @method end 15186 * @param {String} name Name of the element. 15187 */ 15188 end: (name) => { 15189 let value; 15190 /* if (indent && indentBefore[name] && html.length > 0) { 15191 value = html[html.length - 1]; 15192 15193 if (value.length > 0 && value !== '\n') 15194 html.push('\n'); 15195 }*/ 15196 html.push('</', name, '>'); 15197 if (indent && indentAfter[name] && html.length > 0) { 15198 value = html[html.length - 1]; 15199 if (value.length > 0 && value !== '\n') { 15200 html.push('\n'); 15201 } 15202 } 15203 }, 15204 /** 15205 * Writes a text node. 15206 * 15207 * @method text 15208 * @param {String} text String to write out. 15209 * @param {Boolean} raw Optional raw state. If true, the contents won't get encoded. 15210 */ 15211 text: (text, raw) => { 15212 if (text.length > 0) { 15213 html[html.length] = raw ? text : encode(text); 15214 } 15215 }, 15216 /** 15217 * Writes a cdata node, such as `<![CDATA[data]]>`. 15218 * 15219 * @method cdata 15220 * @param {String} text String to write out inside the cdata. 15221 */ 15222 cdata: (text) => { 15223 html.push('<![CDATA[', text, ']]>'); 15224 }, 15225 /** 15226 * Writes a comment node, such as `<!-- Comment -->`. 15227 * 15228 * @method comment 15229 * @param {String} text String to write out inside the comment. 15230 */ 15231 comment: (text) => { 15232 html.push('<!--', text, '-->'); 15233 }, 15234 /** 15235 * Writes a processing instruction (PI) node, such as `<?xml attr="value" ?>`. 15236 * 15237 * @method pi 15238 * @param {String} name Name of the pi. 15239 * @param {String} text String to write out inside the pi. 15240 */ 15241 pi: (name, text) => { 15242 if (text) { 15243 html.push('<?', name, ' ', encode(text), '?>'); 15244 } 15245 else { 15246 html.push('<?', name, '?>'); 15247 } 15248 if (indent) { 15249 html.push('\n'); 15250 } 15251 }, 15252 /** 15253 * Writes a doctype node, such as `<!DOCTYPE data>`. 15254 * 15255 * @method doctype 15256 * @param {String} text String to write out inside the doctype. 15257 */ 15258 doctype: (text) => { 15259 html.push('<!DOCTYPE', text, '>', indent ? '\n' : ''); 15260 }, 15261 /** 15262 * Resets the internal buffer. For example, if one wants to reuse the writer. 15263 * 15264 * @method reset 15265 */ 15266 reset: () => { 15267 html.length = 0; 15268 }, 15269 /** 15270 * Returns the contents that was serialized. 15271 * 15272 * @method getContent 15273 * @return {String} HTML contents that got written down. 15274 */ 15275 getContent: () => { 15276 return html.join('').replace(/\n$/, ''); 15277 } 15278 }; 15279 }; 15280 15281 /** 15282 * This class is used to serialize down the DOM tree into a string using a Writer instance. 15283 * 15284 * @class tinymce.html.Serializer 15285 * @version 3.4 15286 * @example 15287 * tinymce.html.Serializer().serialize(tinymce.html.DomParser().parse('<p>text</p>')); 15288 */ 15289 const HtmlSerializer = (settings = {}, schema = Schema()) => { 15290 const writer = Writer(settings); 15291 settings.validate = 'validate' in settings ? settings.validate : true; 15292 /** 15293 * Serializes the specified node into a string. 15294 * 15295 * @method serialize 15296 * @param {tinymce.html.Node} node Node instance to serialize. 15297 * @return {String} String with HTML based on the DOM tree. 15298 * @example 15299 * tinymce.html.Serializer().serialize(tinymce.html.DomParser().parse('<p>text</p>')); 15300 */ 15301 const serialize = (node) => { 15302 const validate = settings.validate; 15303 const handlers = { 15304 // #text 15305 3: (node) => { 15306 writer.text(node.value ?? '', node.raw); 15307 }, 15308 // #comment 15309 8: (node) => { 15310 writer.comment(node.value ?? ''); 15311 }, 15312 // Processing instruction 15313 7: (node) => { 15314 writer.pi(node.name, node.value); 15315 }, 15316 // Doctype 15317 10: (node) => { 15318 writer.doctype(node.value ?? ''); 15319 }, 15320 // CDATA 15321 4: (node) => { 15322 writer.cdata(node.value ?? ''); 15323 }, 15324 // Document fragment 15325 11: (node) => { 15326 let tempNode = node; 15327 if ((tempNode = tempNode.firstChild)) { 15328 do { 15329 walk(tempNode); 15330 } while ((tempNode = tempNode.next)); 15331 } 15332 } 15333 }; 15334 writer.reset(); 15335 const walk = (node) => { 15336 const handler = handlers[node.type]; 15337 if (!handler) { 15338 const name = node.name; 15339 const isEmpty = name in schema.getVoidElements(); 15340 let attrs = node.attributes; 15341 // Sort attributes 15342 if (validate && attrs && attrs.length > 1) { 15343 const sortedAttrs = []; 15344 sortedAttrs.map = {}; 15345 const elementRule = schema.getElementRule(node.name); 15346 if (elementRule) { 15347 for (let i = 0, l = elementRule.attributesOrder.length; i < l; i++) { 15348 const attrName = elementRule.attributesOrder[i]; 15349 if (attrName in attrs.map) { 15350 const attrValue = attrs.map[attrName]; 15351 sortedAttrs.map[attrName] = attrValue; 15352 sortedAttrs.push({ name: attrName, value: attrValue }); 15353 } 15354 } 15355 for (let i = 0, l = attrs.length; i < l; i++) { 15356 const attrName = attrs[i].name; 15357 if (!(attrName in sortedAttrs.map)) { 15358 const attrValue = attrs.map[attrName]; 15359 sortedAttrs.map[attrName] = attrValue; 15360 sortedAttrs.push({ name: attrName, value: attrValue }); 15361 } 15362 } 15363 attrs = sortedAttrs; 15364 } 15365 } 15366 writer.start(name, attrs, isEmpty); 15367 if (isNonHtmlElementRootName(name)) { 15368 if (isString(node.value)) { 15369 writer.text(node.value, true); 15370 } 15371 writer.end(name); 15372 } 15373 else { 15374 if (!isEmpty) { 15375 let child = node.firstChild; 15376 if (child) { 15377 // Pre and textarea elements treat the first newline character as optional and will omit it. As such, if the content starts 15378 // with a newline we need to add in an additional newline to prevent the current newline in the value being treated as optional 15379 // See https://html.spec.whatwg.org/multipage/syntax.html#element-restrictions 15380 if ((name === 'pre' || name === 'textarea') && child.type === 3 && child.value?.[0] === '\n') { 15381 writer.text('\n', true); 15382 } 15383 do { 15384 walk(child); 15385 } while ((child = child.next)); 15386 } 15387 writer.end(name); 15388 } 15389 } 15390 } 15391 else { 15392 handler(node); 15393 } 15394 }; 15395 // Serialize element or text nodes and treat all other nodes as fragments 15396 if (node.type === 1 && !settings.inner) { 15397 walk(node); 15398 } 15399 else if (node.type === 3) { 15400 handlers[3](node); 15401 } 15402 else { 15403 handlers[11](node); 15404 } 15405 return writer.getContent(); 15406 }; 15407 return { 15408 serialize 15409 }; 15410 }; 15411 15412 const nonInheritableStyles = new Set(); 15413 (() => { 15414 // TODO: TINY-7326 Figure out what else should go in the nonInheritableStyles list 15415 const nonInheritableStylesArr = [ 15416 'margin', 'margin-left', 'margin-right', 'margin-top', 'margin-bottom', 15417 'padding', 'padding-left', 'padding-right', 'padding-top', 'padding-bottom', 15418 'border', 'border-width', 'border-style', 'border-color', 15419 'background', 'background-attachment', 'background-clip', 15420 'background-image', 'background-origin', 'background-position', 'background-repeat', 'background-size', 15421 'float', 'position', 'left', 'right', 'top', 'bottom', 15422 'z-index', 'display', 'transform', 15423 'width', 'max-width', 'min-width', 'height', 'max-height', 'min-height', 15424 'overflow', 'overflow-x', 'overflow-y', 'text-overflow', 'vertical-align', 15425 'transition', 'transition-delay', 'transition-duration', 'transition-property', 'transition-timing-function' 15426 ]; 15427 each$e(nonInheritableStylesArr, (style) => { 15428 nonInheritableStyles.add(style); 15429 }); 15430 })(); 15431 const conditionalNonInheritableStyles = new Set(); 15432 (() => { 15433 // These styles are only noninheritable when applied to an element with a noninheritable style 15434 // For example, background-color is visible on an element with padding, even when children have background-color; 15435 // however, when the element has no padding, background-color is either visible or overridden by children 15436 const conditionalNonInheritableStylesArr = [ 15437 'background-color' 15438 ]; 15439 each$e(conditionalNonInheritableStylesArr, (style) => { 15440 conditionalNonInheritableStyles.add(style); 15441 }); 15442 })(); 15443 // TODO: TINY-7326 Figure out what else should be added to the shorthandStyleProps list 15444 // Does not include non-inherited shorthand style properties 15445 const shorthandStyleProps = ['font', 'text-decoration', 'text-emphasis']; 15446 const getStyles$1 = (dom, node) => dom.parseStyle(dom.getAttrib(node, 'style')); 15447 const getStyleProps = (dom, node) => keys(getStyles$1(dom, node)); 15448 const isNonInheritableStyle = (style) => nonInheritableStyles.has(style); 15449 const isConditionalNonInheritableStyle = (style) => conditionalNonInheritableStyles.has(style); 15450 const hasNonInheritableStyles = (dom, node) => exists(getStyleProps(dom, node), (style) => isNonInheritableStyle(style)); 15451 const hasConditionalNonInheritableStyles = (dom, node) => hasNonInheritableStyles(dom, node) && 15452 exists(getStyleProps(dom, node), (style) => isConditionalNonInheritableStyle(style)); 15453 const getLonghandStyleProps = (styles) => filter$5(styles, (style) => exists(shorthandStyleProps, (prop) => startsWith(style, prop))); 15454 const hasStyleConflict = (dom, node, parentNode) => { 15455 const nodeStyleProps = getStyleProps(dom, node); 15456 const parentNodeStyleProps = getStyleProps(dom, parentNode); 15457 const valueMismatch = (prop) => { 15458 const nodeValue = dom.getStyle(node, prop) ?? ''; 15459 const parentValue = dom.getStyle(parentNode, prop) ?? ''; 15460 return isNotEmpty(nodeValue) && isNotEmpty(parentValue) && nodeValue !== parentValue; 15461 }; 15462 return exists(nodeStyleProps, (nodeStyleProp) => { 15463 const propExists = (props) => exists(props, (prop) => prop === nodeStyleProp); 15464 // If parent has a longhand property e.g. margin-left but the child (node) style is margin, need to get the margin-left value of node to be able to do a proper comparison 15465 // This is because getting the style using the key of 'margin' on a 'margin-left' parent would give a string of space separated values or empty string depending on the browser 15466 if (!propExists(parentNodeStyleProps) && propExists(shorthandStyleProps)) { 15467 const longhandProps = getLonghandStyleProps(parentNodeStyleProps); 15468 return exists(longhandProps, valueMismatch); 15469 } 15470 else { 15471 return valueMismatch(nodeStyleProp); 15472 } 15473 }); 15474 }; 15475 15476 const isChar = (forward, predicate, pos) => Optional.from(pos.container()).filter(isText$b).exists((text) => { 15477 const delta = forward ? 0 : -1; 15478 return predicate(text.data.charAt(pos.offset() + delta)); 15479 }); 15480 const isBeforeSpace = curry(isChar, true, isWhiteSpace); 15481 const isAfterSpace = curry(isChar, false, isWhiteSpace); 15482 const isEmptyText = (pos) => { 15483 const container = pos.container(); 15484 return isText$b(container) && (container.data.length === 0 || isZwsp(container.data) && BookmarkManager.isBookmarkNode(container.parentNode)); 15485 }; 15486 const matchesElementPosition = (before, predicate) => (pos) => getChildNodeAtRelativeOffset(before ? 0 : -1, pos).filter(predicate).isSome(); 15487 const isImageBlock = (node) => isImg(node) && get$
vendor: 4,368 bytes, lines 15487-15558
154877(SugarElement.fromDom(node), 'display') === 'block'; 15488 const isCefNode = (node) => isContentEditableFalse$a(node) && !isBogusAll(node); 15489 const isBeforeImageBlock = matchesElementPosition(true, isImageBlock); 15490 const isAfterImageBlock = matchesElementPosition(false, isImageBlock); 15491 const isBeforeMedia = matchesElementPosition(true, isMedia$2); 15492 const isAfterMedia = matchesElementPosition(false, isMedia$2); 15493 const isBeforeTable = matchesElementPosition(true, isTable$2); 15494 const isAfterTable = matchesElementPosition(false, isTable$2); 15495 const isBeforeContentEditableFalse = matchesElementPosition(true, isCefNode); 15496 const isAfterContentEditableFalse = matchesElementPosition(false, isCefNode); 15497 15498 const dropLast = (xs) => xs.slice(0, -1); 15499 const parentsUntil = (start, root, predicate) => { 15500 if (contains(root, start)) { 15501 return dropLast(parents$1(start, (elm) => { 15502 return predicate(elm) || eq(elm, root); 15503 })); 15504 } 15505 else { 15506 return []; 15507 } 15508 }; 15509 const parents = (start, root) => parentsUntil(start, root, never); 15510 const parentsAndSelf = (start, root) => [start].concat(parents(start, root)); 15511 15512 const navigateIgnoreEmptyTextNodes = (forward, root, from) => navigateIgnore(forward, root, from, isEmptyText); 15513 const isBlock$2 = (schema) => (el) => schema.isBlock(name(el)); 15514 const getClosestBlock$1 = (root, pos, schema) => find$2(parentsAndSelf(SugarElement.fromDom(pos.container()), root), isBlock$2(schema)); 15515 const isAtBeforeAfterBlockBoundary = (forward, root, pos, schema) => navigateIgnoreEmptyTextNodes(forward, root.dom, pos) 15516 .forall((newPos) => getClosestBlock$1(root, pos, schema).fold(() => !isInSameBlock(newPos, pos, root.dom), (fromBlock) => !isInSameBlock(newPos, pos, root.dom) && contains(fromBlock, SugarElement.fromDom(newPos.container())))); 15517 const isAtBlockBoundary = (forward, root, pos, schema) => getClosestBlock$1(root, pos, schema).fold(() => navigateIgnoreEmptyTextNodes(forward, root.dom, pos).forall((newPos) => !isInSameBlock(newPos, pos, root.dom)), (parent) => navigateIgnoreEmptyTextNodes(forward, parent.dom, pos).isNone()); 15518 const isAtStartOfBlock = curry(isAtBlockBoundary, false); 15519 const isAtEndOfBlock = curry(isAtBlockBoundary, true); 15520 const isBeforeBlock = curry(isAtBeforeAfterBlockBoundary, false); 15521 const isAfterBlock = curry(isAtBeforeAfterBlockBoundary, true); 15522 15523 const isBr$2 = (pos) => getElementFromPosition(pos).exists(isBr$6); 15524 const findBr = (forward, root, pos, schema) => { 15525 const parentBlocks = filter$5(parentsAndSelf(SugarElement.fromDom(pos.container()), root), (el) => schema.isBlock(name(el))); 15526 const scope = head(parentBlocks).getOr(root); 15527 return fromPosition(forward, scope.dom, pos).filter(isBr$2); 15528 }; 15529 const isBeforeBr$1 = (root, pos, schema) => getElementFromPosition(pos).exists(isBr$6) || findBr(true, root, pos, schema).isSome(); 15530 const isAfterBr = (root, pos, schema) => getElementFromPrevPosition(pos).exists(isBr$6) || findBr(false, root, pos, schema).isSome(); 15531 const findPreviousBr = curry(findBr, false); 15532 const findNextBr = curry(findBr, true); 15533 15534 const isInMiddleOfText = (pos) => CaretPosition.isTextPosition(pos) && !pos.isAtStart() && !pos.isAtEnd(); 15535 const getClosestBlock = (root, pos, schema) => { 15536 const parentBlocks = filter$5(parentsAndSelf(SugarElement.fromDom(pos.container()), root), (el) => schema.isBlock(name(el))); 15537 return head(parentBlocks).getOr(root); 15538 }; 15539 const hasSpaceBefore = (root, pos, schema) => { 15540 if (isInMiddleOfText(pos)) { 15541 return isAfterSpace(pos); 15542 } 15543 else { 15544 return isAfterSpace(pos) || prevPosition(getClosestBlock(root, pos, schema).dom, pos).exists(isAfterSpace); 15545 } 15546 }; 15547 const hasSpaceAfter = (root, pos, schema) => { 15548 if (isInMiddleOfText(pos)) { 15549 return isBeforeSpace(pos); 15550 } 15551 else { 15552 return isBeforeSpace(pos) || nextPosition(getClosestBlock(root, pos, schema).dom, pos).exists(isBeforeSpace); 15553 } 15554 }; 15555 const isPreValue = (value) => contains$2(['pre', 'pre-wrap'], value); 15556 const isInPre = (pos) => getElementFromPosition(pos) 15557 .bind((elm) => closest$5(elm, isElement$8)) 15558 .exists((elm) => isPreValue(get$7
vendor: 11,575 bytes, lines 15558-15796
15558(elm, 'white-space'))); 15559 const isAtBeginningOfBody = (root, pos) => prevPosition(root.dom, pos).isNone(); 15560 const isAtEndOfBody = (root, pos) => nextPosition(root.dom, pos).isNone(); 15561 const isAtLineBoundary = (root, pos, schema) => (isAtBeginningOfBody(root, pos) || 15562 isAtEndOfBody(root, pos) || 15563 isAtStartOfBlock(root, pos, schema) || 15564 isAtEndOfBlock(root, pos, schema) || 15565 isAfterBr(root, pos, schema) || 15566 isBeforeBr$1(root, pos, schema)); 15567 const isCefBlock = (node) => isNonNullable(node) && isContentEditableFalse$a(node) && isBlockLike(node); 15568 // Check the next/previous element in case it is a cef and the next/previous caret position then would skip it, then check 15569 // the next next/previous caret position ( for example in case the next element is a strong, containing a cef ). 15570 const isSiblingCefBlock = (root, direction) => (container) => { 15571 return isCefBlock(new DomTreeWalker(container, root)[direction]()); 15572 }; 15573 const isBeforeCefBlock = (root, pos) => { 15574 const nextPos = nextPosition(root.dom, pos).getOr(pos); 15575 const isNextCefBlock = isSiblingCefBlock(root.dom, 'next'); 15576 return pos.isAtEnd() && (isNextCefBlock(pos.container()) || isNextCefBlock(nextPos.container())); 15577 }; 15578 const isAfterCefBlock = (root, pos) => { 15579 const prevPos = prevPosition(root.dom, pos).getOr(pos); 15580 const isPrevCefBlock = isSiblingCefBlock(root.dom, 'prev'); 15581 return pos.isAtStart() && (isPrevCefBlock(pos.container()) || isPrevCefBlock(prevPos.container())); 15582 }; 15583 const needsToHaveNbsp = (root, pos, schema) => { 15584 if (isInPre(pos)) { 15585 return false; 15586 } 15587 else { 15588 return isAtLineBoundary(root, pos, schema) || hasSpaceBefore(root, pos, schema) || hasSpaceAfter(root, pos, schema); 15589 } 15590 }; 15591 const needsToBeNbspLeft = (root, pos, schema) => { 15592 if (isInPre(pos)) { 15593 return false; 15594 } 15595 else { 15596 return isAtStartOfBlock(root, pos, schema) || isBeforeBlock(root, pos, schema) || isAfterBr(root, pos, schema) || hasSpaceBefore(root, pos, schema) || isAfterCefBlock(root, pos); 15597 } 15598 }; 15599 const leanRight = (pos) => { 15600 const container = pos.container(); 15601 const offset = pos.offset(); 15602 if (isText$b(container) && offset < container.data.length) { 15603 return CaretPosition(container, offset + 1); 15604 } 15605 else { 15606 return pos; 15607 } 15608 }; 15609 const needsToBeNbspRight = (root, pos, schema) => { 15610 if (isInPre(pos)) { 15611 return false; 15612 } 15613 else { 15614 return isAtEndOfBlock(root, pos, schema) || isAfterBlock(root, pos, schema) || isBeforeBr$1(root, pos, schema) || hasSpaceAfter(root, pos, schema) || isBeforeCefBlock(root, pos); 15615 } 15616 }; 15617 const needsToBeNbsp = (root, pos, schema) => needsToBeNbspLeft(root, pos, schema) || needsToBeNbspRight(root, leanRight(pos), schema); 15618 const isNbspAt = (text, offset) => isNbsp(text.charAt(offset)); 15619 const isWhiteSpaceAt = (text, offset) => isWhiteSpace(text.charAt(offset)); 15620 const hasNbsp = (pos) => { 15621 const container = pos.container(); 15622 return isText$b(container) && contains$1(container.data, nbsp); 15623 }; 15624 const normalizeNbspMiddle = (text) => { 15625 const chars = text.split(''); 15626 return map$3(chars, (chr, i) => { 15627 if (isNbsp(chr) && i > 0 && i < chars.length - 1 && isContent(chars[i - 1]) && isContent(chars[i + 1])) { 15628 return ' '; 15629 } 15630 else { 15631 return chr; 15632 } 15633 }).join(''); 15634 }; 15635 const normalizeNbspAtStart = (root, node, makeNbsp, schema) => { 15636 const text = node.data; 15637 const firstPos = CaretPosition(node, 0); 15638 if (!makeNbsp && isNbspAt(text, 0) && !needsToBeNbsp(root, firstPos, schema)) { 15639 node.data = ' ' + text.slice(1); 15640 return true; 15641 } 15642 else if (makeNbsp && isWhiteSpaceAt(text, 0) && needsToBeNbspLeft(root, firstPos, schema)) { 15643 node.data = nbsp + text.slice(1); 15644 return true; 15645 } 15646 else { 15647 return false; 15648 } 15649 }; 15650 const normalizeNbspInMiddleOfTextNode = (node) => { 15651 const text = node.data; 15652 const newText = normalizeNbspMiddle(text); 15653 if (newText !== text) { 15654 node.data = newText; 15655 return true; 15656 } 15657 else { 15658 return false; 15659 } 15660 }; 15661 const normalizeNbspAtEnd = (root, node, makeNbsp, schema) => { 15662 const text = node.data; 15663 const lastPos = CaretPosition(node, text.length - 1); 15664 if (!makeNbsp && isNbspAt(text, text.length - 1) && !needsToBeNbsp(root, lastPos, schema)) { 15665 node.data = text.slice(0, -1) + ' '; 15666 return true; 15667 } 15668 else if (makeNbsp && isWhiteSpaceAt(text, text.length - 1) && needsToBeNbspRight(root, lastPos, schema)) { 15669 node.data = text.slice(0, -1) + nbsp; 15670 return true; 15671 } 15672 else { 15673 return false; 15674 } 15675 }; 15676 const normalizeNbsps$1 = (root, pos, schema) => { 15677 const container = pos.container(); 15678 if (!isText$b(container)) { 15679 return Optional.none(); 15680 } 15681 if (hasNbsp(pos)) { 15682 const normalized = normalizeNbspAtStart(root, container, false, schema) || normalizeNbspInMiddleOfTextNode(container) || normalizeNbspAtEnd(root, container, false, schema); 15683 return someIf(normalized, pos); 15684 } 15685 else if (needsToBeNbsp(root, pos, schema)) { 15686 const normalized = normalizeNbspAtStart(root, container, true, schema) || normalizeNbspAtEnd(root, container, true, schema); 15687 return someIf(normalized, pos); 15688 } 15689 else { 15690 return Optional.none(); 15691 } 15692 }; 15693 const normalizeNbspsInEditor = (editor) => { 15694 const root = SugarElement.fromDom(editor.getBody()); 15695 if (editor.selection.isCollapsed()) { 15696 normalizeNbsps$1(root, CaretPosition.fromRangeStart(editor.selection.getRng()), editor.schema).each((pos) => { 15697 editor.selection.setRng(pos.toRange()); 15698 }); 15699 } 15700 }; 15701 15702 const normalize$1 = (node, offset, count, schema) => { 15703 if (count === 0) { 15704 return; 15705 } 15706 const elm = SugarElement.fromDom(node); 15707 const root = ancestor$5(elm, (el) => schema.isBlock(name(el))).getOr(elm); 15708 // Get the whitespace 15709 const whitespace = node.data.slice(offset, offset + count); 15710 // Determine if we're at the end or start of the content 15711 const isEndOfContent = offset + count >= node.data.length && needsToBeNbspRight(root, CaretPosition(node, node.data.length), schema); 15712 const isStartOfContent = offset === 0 && needsToBeNbspLeft(root, CaretPosition(node, 0), schema); 15713 // Replace the original whitespace with the normalized whitespace content 15714 node.replaceData(offset, count, normalize$4(whitespace, 4, isStartOfContent, isEndOfContent)); 15715 }; 15716 const normalizeWhitespaceAfter = (node, offset, schema) => { 15717 const content = node.data.slice(offset); 15718 const whitespaceCount = content.length - lTrim(content).length; 15719 normalize$1(node, offset, whitespaceCount, schema); 15720 }; 15721 const normalizeWhitespaceBefore = (node, offset, schema) => { 15722 const content = node.data.slice(0, offset); 15723 const whitespaceCount = content.length - rTrim(content).length; 15724 normalize$1(node, offset - whitespaceCount, whitespaceCount, schema); 15725 }; 15726 const mergeTextNodes = (prevNode, nextNode, schema, normalizeWhitespace, mergeToPrev = true) => { 15727 const whitespaceOffset = rTrim(prevNode.data).length; 15728 const newNode = mergeToPrev ? prevNode : nextNode; 15729 const removeNode = mergeToPrev ? nextNode : prevNode; 15730 // Merge the elements 15731 if (mergeToPrev) { 15732 newNode.appendData(removeNode.data); 15733 } 15734 else { 15735 newNode.insertData(0, removeNode.data); 15736 } 15737 remove$8(SugarElement.fromDom(removeNode)); 15738 // Normalize the whitespace around the merged elements, to ensure it doesn't get lost 15739 if (normalizeWhitespace) { 15740 normalizeWhitespaceAfter(newNode, whitespaceOffset, schema); 15741 } 15742 return newNode; 15743 }; 15744 15745 const needsReposition = (pos, elm) => { 15746 const container = pos.container(); 15747 const offset = pos.offset(); 15748 return !CaretPosition.isTextPosition(pos) && container === elm.parentNode && offset > CaretPosition.before(elm).offset(); 15749 }; 15750 const reposition = (elm, pos) => needsReposition(pos, elm) ? CaretPosition(pos.container(), pos.offset() - 1) : pos; 15751 const beforeOrStartOf = (node) => isText$b(node) ? CaretPosition(node, 0) : CaretPosition.before(node); 15752 const afterOrEndOf = (node) => isText$b(node) ? CaretPosition(node, node.data.length) : CaretPosition.after(node); 15753 const getPreviousSiblingCaretPosition = (elm) => { 15754 if (isCaretCandidate$3(elm.previousSibling)) { 15755 return Optional.some(afterOrEndOf(elm.previousSibling)); 15756 } 15757 else { 15758 return elm.previousSibling ? lastPositionIn(elm.previousSibling) : Optional.none(); 15759 } 15760 }; 15761 const getNextSiblingCaretPosition = (elm) => { 15762 if (isCaretCandidate$3(elm.nextSibling)) { 15763 return Optional.some(beforeOrStartOf(elm.nextSibling)); 15764 } 15765 else { 15766 return elm.nextSibling ? firstPositionIn(elm.nextSibling) : Optional.none(); 15767 } 15768 }; 15769 const findCaretPositionBackwardsFromElm = (rootElement, elm) => { 15770 return Optional.from(elm.previousSibling ? elm.previousSibling : elm.parentNode) 15771 .bind((node) => prevPosition(rootElement, CaretPosition.before(node))) 15772 .orThunk(() => nextPosition(rootElement, CaretPosition.after(elm))); 15773 }; 15774 const findCaretPositionForwardsFromElm = (rootElement, elm) => nextPosition(rootElement, CaretPosition.after(elm)).orThunk(() => prevPosition(rootElement, CaretPosition.before(elm))); 15775 const findCaretPositionBackwards = (rootElement, elm) => getPreviousSiblingCaretPosition(elm).orThunk(() => getNextSiblingCaretPosition(elm)) 15776 .orThunk(() => findCaretPositionBackwardsFromElm(rootElement, elm)); 15777 const findCaretPositionForward = (rootElement, elm) => getNextSiblingCaretPosition(elm) 15778 .orThunk(() => getPreviousSiblingCaretPosition(elm)) 15779 .orThunk(() => findCaretPositionForwardsFromElm(rootElement, elm)); 15780 const findCaretPosition = (forward, rootElement, elm) => forward ? findCaretPositionForward(rootElement, elm) : findCaretPositionBackwards(rootElement, elm); 15781 const findCaretPosOutsideElmAfterDelete = (forward, rootElement, elm) => findCaretPosition(forward, rootElement, elm).map(curry(reposition, elm)); 15782 const setSelection$1 = (editor, forward, pos) => { 15783 pos.fold(() => { 15784 editor.focus(); 15785 }, (pos) => { 15786 editor.selection.setRng(pos.toRange(), forward); 15787 }); 15788 }; 15789 const eqRawNode = (rawNode) => (elm) => elm.dom === rawNode; 15790 const isBlock$1 = (editor, elm) => elm && has$2(editor.schema.getBlockElements(), name(elm)); 15791 const paddEmptyBlock = (schema, elm, preserveEmptyCaret) => { 15792 if (isEmpty$4(schema, elm)) { 15793 const br = SugarElement.fromHtml('<br data-mce-bogus="1">'); 15794 // Remove all bogus elements except caret 15795 if (preserveEmptyCaret) { 15796 each$e(children$
vendor: 8,698 bytes, lines 15796-15968
157961(elm), (node) => { 15797 if (!isEmptyCaretFormatElement(node)) { 15798 remove$8(node); 15799 } 15800 }); 15801 } 15802 else { 15803 empty(elm); 15804 } 15805 append$1(elm, br); 15806 return Optional.some(CaretPosition.before(br.dom)); 15807 } 15808 else { 15809 return Optional.none(); 15810 } 15811 }; 15812 const deleteNormalized = (elm, afterDeletePosOpt, schema, normalizeWhitespace) => { 15813 const prevTextOpt = prevSibling(elm).filter(isText$c); 15814 const nextTextOpt = nextSibling(elm).filter(isText$c); 15815 // Delete the element 15816 remove$8(elm); 15817 // Merge and normalize any prev/next text nodes, so that they are merged and don't lose meaningful whitespace 15818 // eg. <p>a <span></span> b</p> -> <p>a &nsbp;b</p> or <p><span></span> a</p> -> <p> a</a> 15819 return lift3(prevTextOpt, nextTextOpt, afterDeletePosOpt, (prev, next, pos) => { 15820 const prevNode = prev.dom, nextNode = next.dom; 15821 const offset = prevNode.data.length; 15822 mergeTextNodes(prevNode, nextNode, schema, normalizeWhitespace); 15823 // Update the cursor position if required 15824 return pos.container() === nextNode ? CaretPosition(prevNode, offset) : pos; 15825 }).orThunk(() => { 15826 if (normalizeWhitespace) { 15827 prevTextOpt.each((elm) => normalizeWhitespaceBefore(elm.dom, elm.dom.length, schema)); 15828 nextTextOpt.each((elm) => normalizeWhitespaceAfter(elm.dom, 0, schema)); 15829 } 15830 return afterDeletePosOpt; 15831 }); 15832 }; 15833 const isInlineElement = (editor, element) => has$2(editor.schema.getTextInlineElements(), name(element)); 15834 const deleteElement$2 = (editor, forward, elm, moveCaret = true, preserveEmptyCaret = false) => { 15835 // Existing delete logic 15836 const afterDeletePos = findCaretPosOutsideElmAfterDelete(forward, editor.getBody(), elm.dom); 15837 const parentBlock = ancestor$5(elm, curry(isBlock$1, editor), eqRawNode(editor.getBody())); 15838 const normalizedAfterDeletePos = deleteNormalized(elm, afterDeletePos, editor.schema, isInlineElement(editor, elm)); 15839 if (editor.dom.isEmpty(editor.getBody())) { 15840 editor.setContent(''); 15841 editor.selection.setCursorLocation(); 15842 } 15843 else { 15844 parentBlock.bind((elm) => paddEmptyBlock(editor.schema, elm, preserveEmptyCaret)).fold(() => { 15845 if (moveCaret) { 15846 setSelection$1(editor, forward, normalizedAfterDeletePos); 15847 } 15848 }, (paddPos) => { 15849 if (moveCaret) { 15850 setSelection$1(editor, forward, Optional.some(paddPos)); 15851 } 15852 }); 15853 } 15854 }; 15855 15856 const strongRtl = /[\u0591-\u07FF\uFB1D-\uFDFF\uFE70-\uFEFC]/; 15857 const hasStrongRtl = (text) => strongRtl.test(text); 15858 15859 const isInlineTarget = (editor, elm) => is$2(SugarElement.fromDom(elm), getInlineBoundarySelector(editor)) 15860 && !isTransparentBlock(editor.schema, elm) 15861 && editor.dom.isEditable(elm); 15862 const isRtl = (element) => DOMUtils.DOM.getStyle(element, 'direction', true) === 'rtl' || hasStrongRtl(element.textContent ?? ''); 15863 const findInlineParents = (isInlineTarget, rootNode, pos) => filter$5(DOMUtils.DOM.getParents(pos.container(), '*', rootNode), isInlineTarget); 15864 const findRootInline = (isInlineTarget, rootNode, pos) => { 15865 const parents = findInlineParents(isInlineTarget, rootNode, pos); 15866 return Optional.from(parents[parents.length - 1]); 15867 }; 15868 const hasSameParentBlock = (rootNode, node1, node2) => { 15869 const block1 = getParentBlock$3(node1, rootNode); 15870 const block2 = getParentBlock$3(node2, rootNode); 15871 return isNonNullable(block1) && block1 === block2; 15872 }; 15873 const isAtZwsp = (pos) => isBeforeInline(pos) || isAfterInline(pos); 15874 const normalizePosition = (forward, pos) => { 15875 const container = pos.container(), offset = pos.offset(); 15876 if (forward) { 15877 if (isCaretContainerInline(container)) { 15878 if (isText$b(container.nextSibling)) { 15879 return CaretPosition(container.nextSibling, 0); 15880 } 15881 else { 15882 return CaretPosition.after(container); 15883 } 15884 } 15885 else { 15886 return isBeforeInline(pos) ? CaretPosition(container, offset + 1) : pos; 15887 } 15888 } 15889 else { 15890 if (isCaretContainerInline(container)) { 15891 if (isText$b(container.previousSibling)) { 15892 return CaretPosition(container.previousSibling, container.previousSibling.data.length); 15893 } 15894 else { 15895 return CaretPosition.before(container); 15896 } 15897 } 15898 else { 15899 return isAfterInline(pos) ? CaretPosition(container, offset - 1) : pos; 15900 } 15901 } 15902 }; 15903 const normalizeForwards = curry(normalizePosition, true); 15904 const normalizeBackwards = curry(normalizePosition, false); 15905 15906 const execCommandIgnoreInputEvents = (editor, command) => { 15907 // We need to prevent the input events from being fired by execCommand when delete is used internally 15908 const inputBlocker = (e) => e.stopImmediatePropagation(); 15909 editor.on('beforeinput input', inputBlocker, true); 15910 editor.getDoc().execCommand(command); 15911 editor.off('beforeinput input', inputBlocker); 15912 }; 15913 // ASSUMPTION: The editor command 'delete' doesn't have any `beforeinput` and `input` trapping 15914 // because those events are only triggered by native contenteditable behaviour. 15915 const execEditorDeleteCommand = (editor) => { 15916 editor.execCommand('delete'); 15917 }; 15918 const execNativeDeleteCommand = (editor) => execCommandIgnoreInputEvents(editor, 'Delete'); 15919 const execNativeForwardDeleteCommand = (editor) => execCommandIgnoreInputEvents(editor, 'ForwardDelete'); 15920 const isBeforeRoot = (rootNode) => (elm) => is$4(parent(elm), rootNode, eq); 15921 const isTextBlockOrListItem = (element) => isTextBlock$3(element) || isListItem$2(element); 15922 const getParentBlock$2 = (rootNode, elm) => { 15923 if (contains(rootNode, elm)) { 15924 return closest$5(elm, isTextBlockOrListItem, isBeforeRoot(rootNode)); 15925 } 15926 else { 15927 return Optional.none(); 15928 } 15929 }; 15930 const paddEmptyBody = (editor, moveSelection = true) => { 15931 if (editor.dom.isEmpty(editor.getBody())) { 15932 editor.setContent('', { no_selection: !moveSelection }); 15933 } 15934 }; 15935 const willDeleteLastPositionInElement = (forward, fromPos, elm) => lift2(firstPositionIn(elm), lastPositionIn(elm), (firstPos, lastPos) => { 15936 const normalizedFirstPos = normalizePosition(true, firstPos); 15937 const normalizedLastPos = normalizePosition(false, lastPos); 15938 const normalizedFromPos = normalizePosition(false, fromPos); 15939 if (forward) { 15940 return nextPosition(elm, normalizedFromPos).exists((nextPos) => nextPos.isEqual(normalizedLastPos) && fromPos.isEqual(normalizedFirstPos)); 15941 } 15942 else { 15943 return prevPosition(elm, normalizedFromPos).exists((prevPos) => prevPos.isEqual(normalizedFirstPos) && fromPos.isEqual(normalizedLastPos)); 15944 } 15945 }).getOr(true); 15946 const freefallRtl = (root) => { 15947 const child = isComment$1(root) ? prevSibling(root) : lastChild(root); 15948 return child.bind(freefallRtl).orThunk(() => Optional.some(root)); 15949 }; 15950 const deleteRangeContents = (editor, rng, root, moveSelection = true) => { 15951 rng.deleteContents(); 15952 // Pad the last block node 15953 const lastNode = freefallRtl(root).getOr(root); 15954 const lastBlock = SugarElement.fromDom(editor.dom.getParent(lastNode.dom, editor.dom.isBlock) ?? root.dom); 15955 // If the block is the editor body then we need to insert the root block as well 15956 if (lastBlock.dom === editor.getBody()) { 15957 paddEmptyBody(editor, moveSelection); 15958 } 15959 else if (isEmpty$4(editor.schema, lastBlock, { checkRootAsContent: false })) { 15960 fillWithPaddingBr(lastBlock); 15961 if (moveSelection) { 15962 editor.selection.setCursorLocation(lastBlock.dom, 0); 15963 } 15964 } 15965 // Clean up any additional leftover nodes. If the last block wasn't a direct child, then we also need to clean up siblings 15966 if (!eq(root, lastBlock)) { 15967 const additionalCleanupNodes = is$4(parent(lastBlock), root) ? [] : siblings(lastBlock); 15968 each$e(additionalCleanupNodes.concat(children$1
vendor: 11,084 bytes, lines 15968-16181
15968(root)), (node) => { 15969 if (!eq(node, lastBlock) && !contains(node, lastBlock) && isEmpty$4(editor.schema, node)) { 15970 remove$8(node); 15971 } 15972 }); 15973 } 15974 }; 15975 15976 const isRootFromElement = (root) => (cur) => eq(root, cur); 15977 const getTableCells = (table) => descendants(table, 'td,th'); 15978 const getTable$1 = (node, isRoot) => getClosestTable(SugarElement.fromDom(node), isRoot); 15979 const selectionInTableWithNestedTable = (details) => { 15980 return lift2(details.startTable, details.endTable, (startTable, endTable) => { 15981 const isStartTableParentOfEndTable = descendant(startTable, (t) => eq(t, endTable)); 15982 const isEndTableParentOfStartTable = descendant(endTable, (t) => eq(t, startTable)); 15983 return !isStartTableParentOfEndTable && !isEndTableParentOfStartTable ? details : { 15984 ...details, 15985 startTable: isStartTableParentOfEndTable ? Optional.none() : details.startTable, 15986 endTable: isEndTableParentOfStartTable ? Optional.none() : details.endTable, 15987 isSameTable: false, 15988 isMultiTable: false 15989 }; 15990 }).getOr(details); 15991 }; 15992 const adjustQuirksInDetails = (details) => { 15993 return selectionInTableWithNestedTable(details); 15994 }; 15995 const getTableDetailsFromRange = (rng, isRoot) => { 15996 const startTable = getTable$1(rng.startContainer, isRoot); 15997 const endTable = getTable$1(rng.endContainer, isRoot); 15998 const isStartInTable = startTable.isSome(); 15999 const isEndInTable = endTable.isSome(); 16000 // Partial selection - selection is not within the same table 16001 const isSameTable = lift2(startTable, endTable, eq).getOr(false); 16002 const isMultiTable = !isSameTable && isStartInTable && isEndInTable; 16003 return adjustQuirksInDetails({ 16004 startTable, 16005 endTable, 16006 isStartInTable, 16007 isEndInTable, 16008 isSameTable, 16009 isMultiTable 16010 }); 16011 }; 16012 16013 const tableCellRng = (start, end) => ({ 16014 start, 16015 end, 16016 }); 16017 const tableSelection = (rng, table, cells) => ({ 16018 rng, 16019 table, 16020 cells 16021 }); 16022 const deleteAction = Adt.generate([ 16023 { singleCellTable: ['rng', 'cell'] }, 16024 { fullTable: ['table'] }, 16025 { partialTable: ['cells', 'outsideDetails'] }, 16026 { multiTable: ['startTableCells', 'endTableCells', 'betweenRng'] }, 16027 ]); 16028 const getClosestCell$1 = (container, isRoot) => closest$4(SugarElement.fromDom(container), 'td,th', isRoot); 16029 const isExpandedCellRng = (cellRng) => !eq(cellRng.start, cellRng.end); 16030 const getTableFromCellRng = (cellRng, isRoot) => getClosestTable(cellRng.start, isRoot) 16031 .bind((startParentTable) => getClosestTable(cellRng.end, isRoot) 16032 .bind((endParentTable) => someIf(eq(startParentTable, endParentTable), startParentTable))); 16033 const isSingleCellTable = (cellRng, isRoot) => !isExpandedCellRng(cellRng) && 16034 getTableFromCellRng(cellRng, isRoot).exists((table) => { 16035 const rows = table.dom.rows; 16036 return rows.length === 1 && rows[0].cells.length === 1; 16037 }); 16038 const getCellRng = (rng, isRoot) => { 16039 const startCell = getClosestCell$1(rng.startContainer, isRoot); 16040 const endCell = getClosestCell$1(rng.endContainer, isRoot); 16041 return lift2(startCell, endCell, tableCellRng); 16042 }; 16043 const getCellRangeFromStartTable = (isRoot) => (startCell) => getClosestTable(startCell, isRoot).bind((table) => last$2(getTableCells(table)).map((endCell) => tableCellRng(startCell, endCell))); 16044 const getCellRangeFromEndTable = (isRoot) => (endCell) => getClosestTable(endCell, isRoot).bind((table) => head(getTableCells(table)).map((startCell) => tableCellRng(startCell, endCell))); 16045 const getTableSelectionFromCellRng = (isRoot) => (cellRng) => getTableFromCellRng(cellRng, isRoot).map((table) => tableSelection(cellRng, table, getTableCells(table))); 16046 const getTableSelections = (cellRng, selectionDetails, rng, isRoot) => { 16047 if (rng.collapsed || !cellRng.forall(isExpandedCellRng)) { 16048 return Optional.none(); 16049 } 16050 else if (selectionDetails.isSameTable) { 16051 const sameTableSelection = cellRng.bind(getTableSelectionFromCellRng(isRoot)); 16052 return Optional.some({ 16053 start: sameTableSelection, 16054 end: sameTableSelection 16055 }); 16056 } 16057 else { 16058 // Covers partial table selection (either start or end will have a tableSelection) and multitable selection (both start and end will have a tableSelection) 16059 const startCell = getClosestCell$1(rng.startContainer, isRoot); 16060 const endCell = getClosestCell$1(rng.endContainer, isRoot); 16061 const startTableSelection = startCell 16062 .bind(getCellRangeFromStartTable(isRoot)) 16063 .bind(getTableSelectionFromCellRng(isRoot)); 16064 const endTableSelection = endCell 16065 .bind(getCellRangeFromEndTable(isRoot)) 16066 .bind(getTableSelectionFromCellRng(isRoot)); 16067 return Optional.some({ 16068 start: startTableSelection, 16069 end: endTableSelection 16070 }); 16071 } 16072 }; 16073 const getCellIndex = (cells, cell) => findIndex$2(cells, (x) => eq(x, cell)); 16074 const getSelectedCells = (tableSelection) => lift2(getCellIndex(tableSelection.cells, tableSelection.rng.start), getCellIndex(tableSelection.cells, tableSelection.rng.end), (startIndex, endIndex) => tableSelection.cells.slice(startIndex, endIndex + 1)); 16075 const isSingleCellTableContentSelected = (optCellRng, rng, isRoot) => optCellRng.exists((cellRng) => isSingleCellTable(cellRng, isRoot) && hasAllContentsSelected(cellRng.start, rng)); 16076 const unselectCells = (rng, selectionDetails) => { 16077 const { startTable, endTable } = selectionDetails; 16078 const otherContentRng = rng.cloneRange(); 16079 // If the table is some, it should be unselected (works for single table and multitable cases) 16080 startTable.each((table) => otherContentRng.setStartAfter(table.dom)); 16081 endTable.each((table) => otherContentRng.setEndBefore(table.dom)); 16082 return otherContentRng; 16083 }; 16084 const handleSingleTable = (cellRng, selectionDetails, rng, isRoot) => getTableSelections(cellRng, selectionDetails, rng, isRoot) 16085 .bind(({ start, end }) => start.or(end)) 16086 .bind((tableSelection) => { 16087 const { isSameTable } = selectionDetails; 16088 const selectedCells = getSelectedCells(tableSelection).getOr([]); 16089 if (isSameTable && tableSelection.cells.length === selectedCells.length) { 16090 return Optional.some(deleteAction.fullTable(tableSelection.table)); 16091 } 16092 else if (selectedCells.length > 0) { 16093 if (isSameTable) { 16094 return Optional.some(deleteAction.partialTable(selectedCells, Optional.none())); 16095 } 16096 else { 16097 const otherContentRng = unselectCells(rng, selectionDetails); 16098 return Optional.some(deleteAction.partialTable(selectedCells, Optional.some({ 16099 ...selectionDetails, 16100 rng: otherContentRng 16101 }))); 16102 } 16103 } 16104 else { 16105 return Optional.none(); 16106 } 16107 }); 16108 const handleMultiTable = (cellRng, selectionDetails, rng, isRoot) => getTableSelections(cellRng, selectionDetails, rng, isRoot) 16109 .bind(({ start, end }) => { 16110 const startTableSelectedCells = start.bind(getSelectedCells).getOr([]); 16111 const endTableSelectedCells = end.bind(getSelectedCells).getOr([]); 16112 if (startTableSelectedCells.length > 0 && endTableSelectedCells.length > 0) { 16113 const otherContentRng = unselectCells(rng, selectionDetails); 16114 return Optional.some(deleteAction.multiTable(startTableSelectedCells, endTableSelectedCells, otherContentRng)); 16115 } 16116 else { 16117 return Optional.none(); 16118 } 16119 }); 16120 const getActionFromRange = (root, rng) => { 16121 const isRoot = isRootFromElement(root); 16122 const optCellRng = getCellRng(rng, isRoot); 16123 const selectionDetails = getTableDetailsFromRange(rng, isRoot); 16124 if (isSingleCellTableContentSelected(optCellRng, rng, isRoot)) { 16125 // SingleCellTable 16126 return optCellRng.map((cellRng) => deleteAction.singleCellTable(rng, cellRng.start)); 16127 } 16128 else if (selectionDetails.isMultiTable) { 16129 // MultiTable 16130 return handleMultiTable(optCellRng, selectionDetails, rng, isRoot); 16131 } 16132 else { 16133 // FullTable, PartialTable with no rng or PartialTable with outside rng 16134 return handleSingleTable(optCellRng, selectionDetails, rng, isRoot); 16135 } 16136 }; 16137 16138 // Reset the contenteditable state and fill the content with a padding br 16139 const cleanCells = (cells) => each$e(cells, (cell) => { 16140 remove$9(cell, 'contenteditable'); 16141 fillWithPaddingBr(cell); 16142 }); 16143 const getOutsideBlock = (editor, container) => Optional.from(editor.dom.getParent(container, editor.dom.isBlock)).map(SugarElement.fromDom); 16144 const handleEmptyBlock = (editor, startInTable, emptyBlock) => { 16145 emptyBlock.each((block) => { 16146 if (startInTable) { 16147 // Note that we don't need to set the selection as it'll be within the table 16148 remove$8(block); 16149 } 16150 else { 16151 // Set the cursor location as it'll move when filling with padding 16152 fillWithPaddingBr(block); 16153 editor.selection.setCursorLocation(block.dom, 0); 16154 } 16155 }); 16156 }; 16157 const deleteContentInsideCell = (editor, cell, rng, isFirstCellInSelection) => { 16158 const insideTableRng = rng.cloneRange(); 16159 if (isFirstCellInSelection) { 16160 insideTableRng.setStart(rng.startContainer, rng.startOffset); 16161 insideTableRng.setEndAfter(cell.dom.lastChild); 16162 } 16163 else { 16164 insideTableRng.setStartBefore(cell.dom.firstChild); 16165 insideTableRng.setEnd(rng.endContainer, rng.endOffset); 16166 } 16167 deleteCellContents(editor, insideTableRng, cell, false).each((action) => action()); 16168 }; 16169 const collapseAndRestoreCellSelection = (editor) => { 16170 const selectedCells = getCellsFromEditor(editor); 16171 const selectedNode = SugarElement.fromDom(editor.selection.getNode()); 16172 if (isTableCell$3(selectedNode.dom) && isEmpty$4(editor.schema, selectedNode)) { 16173 editor.selection.setCursorLocation(selectedNode.dom, 0); 16174 } 16175 else { 16176 editor.selection.collapse(true); 16177 } 16178 // Restore the data-mce-selected attribute if multiple cells were selected, as if it was a cef element 16179 // then selection overrides would remove it as it was using an offscreen selection clone. 16180 if (selectedCells.length > 1 && exists(selectedCells, (cell) => eq(cell, selectedNode))) { 16181 set$
161814
vendor: 23,653 bytes, lines 16181-16679
16181(selectedNode, 'data-mce-selected', '1'); 16182 } 16183 }; 16184 /* 16185 * Runs when 16186 * - the start and end of the selection is contained within the same table (called directly from deleteRange) 16187 * - part of a table and content outside is selected 16188 */ 16189 const emptySingleTableCells = (editor, cells, outsideDetails) => Optional.some(() => { 16190 const editorRng = editor.selection.getRng(); 16191 const cellsToClean = outsideDetails.bind(({ rng, isStartInTable }) => { 16192 /* 16193 * Delete all content outside of the table that is in the selection 16194 * - Get the outside block before deleting the contents 16195 * - Delete the contents outside 16196 * - Handle the block outside the table if it is empty since rng.deleteContents leaves it 16197 */ 16198 const outsideBlock = getOutsideBlock(editor, isStartInTable ? rng.endContainer : rng.startContainer); 16199 rng.deleteContents(); 16200 handleEmptyBlock(editor, isStartInTable, outsideBlock.filter(curry(isEmpty$4, editor.schema))); 16201 /* 16202 * The only time we can have only part of the cell contents selected is when part of the selection 16203 * is outside the table (otherwise we use the Darwin fake selection, which always selects entire cells), 16204 * in which case we need to delete the contents inside and check if the entire contents of the cell have been deleted. 16205 * 16206 * Note: The endPointCell is the only cell which may have only part of its contents selected. 16207 */ 16208 const endPointCell = isStartInTable ? cells[0] : cells[cells.length - 1]; 16209 deleteContentInsideCell(editor, endPointCell, editorRng, isStartInTable); 16210 if (!isEmpty$4(editor.schema, endPointCell)) { 16211 return Optional.some(isStartInTable ? cells.slice(1) : cells.slice(0, -1)); 16212 } 16213 else { 16214 return Optional.none(); 16215 } 16216 }).getOr(cells); 16217 // Remove content from cells we need to clean 16218 cleanCells(cellsToClean); 16219 // Collapse the original selection after deleting everything 16220 collapseAndRestoreCellSelection(editor); 16221 }); 16222 /* 16223 * Runs when the start of the selection is in a table and the end of the selection is in another table 16224 */ 16225 const emptyMultiTableCells = (editor, startTableCells, endTableCells, betweenRng) => Optional.some(() => { 16226 const rng = editor.selection.getRng(); 16227 const startCell = startTableCells[0]; 16228 const endCell = endTableCells[endTableCells.length - 1]; 16229 deleteContentInsideCell(editor, startCell, rng, true); 16230 deleteContentInsideCell(editor, endCell, rng, false); 16231 // Only clean empty cells, the first and last cells have the potential to still have content 16232 const startTableCellsToClean = isEmpty$4(editor.schema, startCell) ? startTableCells : startTableCells.slice(1); 16233 const endTableCellsToClean = isEmpty$4(editor.schema, endCell) ? endTableCells : endTableCells.slice(0, -1); 16234 cleanCells(startTableCellsToClean.concat(endTableCellsToClean)); 16235 // Delete all content in between the start table and end table 16236 betweenRng.deleteContents(); 16237 // This will collapse the selection into the cell of the start table 16238 collapseAndRestoreCellSelection(editor); 16239 }); 16240 // Delete the contents of a range inside a cell. Runs on tables that are a single cell or partial selections that need to be cleaned up. 16241 const deleteCellContents = (editor, rng, cell, moveSelection = true) => Optional.some(() => { 16242 deleteRangeContents(editor, rng, cell, moveSelection); 16243 }); 16244 const deleteTableElement = (editor, table) => Optional.some(() => deleteElement$2(editor, false, table)); 16245 const deleteCellRange = (editor, rootElm, rng) => getActionFromRange(rootElm, rng) 16246 .bind((action) => action.fold(curry(deleteCellContents, editor), curry(deleteTableElement, editor), curry(emptySingleTableCells, editor), curry(emptyMultiTableCells, editor))); 16247 const deleteCaptionRange = (editor, caption) => emptyElement(editor, caption); 16248 const deleteTableRange = (editor, rootElm, rng, startElm) => getParentCaption(rootElm, startElm).fold(() => deleteCellRange(editor, rootElm, rng), (caption) => deleteCaptionRange(editor, caption)); 16249 const deleteRange$4 = (editor, startElm, selectedCells) => { 16250 const rootNode = SugarElement.fromDom(editor.getBody()); 16251 const rng = editor.selection.getRng(); 16252 return selectedCells.length !== 0 ? 16253 emptySingleTableCells(editor, selectedCells, Optional.none()) : 16254 deleteTableRange(editor, rootNode, rng, startElm); 16255 }; 16256 const getParentCell = (rootElm, elm) => find$2(parentsAndSelf(elm, rootElm), isTableCell$2); 16257 const getParentCaption = (rootElm, elm) => find$2(parentsAndSelf(elm, rootElm), isTag('caption')); 16258 const deleteBetweenCells = (editor, rootElm, forward, fromCell, from) => 16259 // TODO: TINY-8865 - This may not be safe to cast as Node below and alternative solutions need to be looked into 16260 navigate(forward, editor.getBody(), from) 16261 .bind((to) => getParentCell(rootElm, SugarElement.fromDom(to.getNode())) 16262 .bind((toCell) => eq(toCell, fromCell) ? Optional.none() : Optional.some(noop))); 16263 const emptyElement = (editor, elm) => Optional.some(() => { 16264 fillWithPaddingBr(elm); 16265 editor.selection.setCursorLocation(elm.dom, 0); 16266 }); 16267 const isDeleteOfLastCharPos = (fromCaption, forward, from, to) => firstPositionIn(fromCaption.dom).bind((first) => lastPositionIn(fromCaption.dom).map((last) => forward ? 16268 from.isEqual(first) && to.isEqual(last) : 16269 from.isEqual(last) && to.isEqual(first))).getOr(true); 16270 const emptyCaretCaption = (editor, elm) => emptyElement(editor, elm); 16271 const validateCaretCaption = (rootElm, fromCaption, to) => 16272 // TODO: TINY-8865 - This may not be safe to cast as Node below and alternative solutions need to be looked into 16273 getParentCaption(rootElm, SugarElement.fromDom(to.getNode())) 16274 .fold(() => Optional.some(noop), (toCaption) => someIf(!eq(toCaption, fromCaption), noop)); 16275 const deleteCaretInsideCaption = (editor, rootElm, forward, fromCaption, from) => navigate(forward, editor.getBody(), from).fold(() => Optional.some(noop), (to) => isDeleteOfLastCharPos(fromCaption, forward, from, to) ? 16276 emptyCaretCaption(editor, fromCaption) : 16277 validateCaretCaption(rootElm, fromCaption, to)); 16278 const deleteCaretCells = (editor, forward, rootElm, startElm) => { 16279 const from = CaretPosition.fromRangeStart(editor.selection.getRng()); 16280 return getParentCell(rootElm, startElm).bind((fromCell) => isEmpty$4(editor.schema, fromCell, { checkRootAsContent: false }) ? 16281 emptyElement(editor, fromCell) : 16282 deleteBetweenCells(editor, rootElm, forward, fromCell, from)); 16283 }; 16284 const deleteCaretCaption = (editor, forward, rootElm, fromCaption) => { 16285 const from = CaretPosition.fromRangeStart(editor.selection.getRng()); 16286 return isEmpty$4(editor.schema, fromCaption) ? 16287 emptyElement(editor, fromCaption) : 16288 deleteCaretInsideCaption(editor, rootElm, forward, fromCaption, from); 16289 }; 16290 const isNearTable = (forward, pos) => forward ? isBeforeTable(pos) : isAfterTable(pos); 16291 const isBeforeOrAfterTable = (editor, forward) => { 16292 const fromPos = CaretPosition.fromRangeStart(editor.selection.getRng()); 16293 return isNearTable(forward, fromPos) || fromPosition(forward, editor.getBody(), fromPos) 16294 .exists((pos) => isNearTable(forward, pos)); 16295 }; 16296 const deleteCaret$3 = (editor, forward, startElm) => { 16297 const rootElm = SugarElement.fromDom(editor.getBody()); 16298 return getParentCaption(rootElm, startElm).fold(() => deleteCaretCells(editor, forward, rootElm, startElm) 16299 .orThunk(() => someIf(isBeforeOrAfterTable(editor, forward), noop)), (fromCaption) => deleteCaretCaption(editor, forward, rootElm, fromCaption)); 16300 }; 16301 const backspaceDelete$d = (editor, forward) => { 16302 const startElm = SugarElement.fromDom(editor.selection.getStart(true)); 16303 const cells = getCellsFromEditor(editor); 16304 return editor.selection.isCollapsed() && cells.length === 0 ? 16305 deleteCaret$3(editor, forward, startElm) : 16306 deleteRange$4(editor, startElm, cells); 16307 }; 16308 16309 const getContentEditableRoot$1 = (root, node) => { 16310 let tempNode = node; 16311 while (tempNode && tempNode !== root) { 16312 if (isContentEditableTrue$3(tempNode) || isContentEditableFalse$a(tempNode)) { 16313 return tempNode; 16314 } 16315 tempNode = tempNode.parentNode; 16316 } 16317 return null; 16318 }; 16319 16320 const internalAttributesPrefixes = [ 16321 'data-ephox-', 16322 'data-mce-', 16323 'data-alloy-', 16324 'data-snooker-', 16325 '_' 16326 ]; 16327 /** 16328 * Utility class for various element specific functions. 16329 * 16330 * @private 16331 * @class tinymce.dom.ElementUtils 16332 */ 16333 const each$9 = Tools.each; 16334 const ElementUtils = (editor) => { 16335 const dom = editor.dom; 16336 const internalAttributes = new Set(editor.serializer.getTempAttrs()); 16337 /** 16338 * Compares two nodes and checks if it's attributes and styles matches. 16339 * This doesn't compare classes as items since their order is significant. 16340 * 16341 * @method compare 16342 * @param {Node} node1 First node to compare with. 16343 * @param {Node} node2 Second node to compare with. 16344 * @return {Boolean} True/false if the nodes are the same or not. 16345 */ 16346 const compare = (node1, node2) => { 16347 // Not the same name or type 16348 if (node1.nodeName !== node2.nodeName || node1.nodeType !== node2.nodeType) { 16349 return false; 16350 } 16351 /** 16352 * Returns all the nodes attributes excluding internal ones, styles and classes. 16353 * 16354 * @private 16355 * @param {Node} node Node to get attributes from. 16356 * @return {Object} Name/value object with attributes and attribute values. 16357 */ 16358 const getAttribs = (node) => { 16359 const attribs = {}; 16360 each$9(dom.getAttribs(node), (attr) => { 16361 const name = attr.nodeName.toLowerCase(); 16362 // Don't compare internal attributes or style 16363 if (name !== 'style' && !isAttributeInternal(name)) { 16364 attribs[name] = dom.getAttrib(node, name); 16365 } 16366 }); 16367 return attribs; 16368 }; 16369 /** 16370 * Compares two objects checks if it's key + value exists in the other one. 16371 * 16372 * @private 16373 * @param {Object} obj1 First object to compare. 16374 * @param {Object} obj2 Second object to compare. 16375 * @return {Boolean} True/false if the objects matches or not. 16376 */ 16377 const compareObjects = (obj1, obj2) => { 16378 for (const name in obj1) { 16379 // Obj1 has item obj2 doesn't have 16380 if (has$2(obj1, name)) { 16381 const value = obj2[name]; 16382 // Obj2 doesn't have obj1 item 16383 if (isUndefined(value)) { 16384 return false; 16385 } 16386 // Obj2 item has a different value 16387 if (obj1[name] !== value) { 16388 return false; 16389 } 16390 // Delete similar value 16391 delete obj2[name]; 16392 } 16393 } 16394 // Check if obj 2 has something obj 1 doesn't have 16395 for (const name in obj2) { 16396 // Obj2 has item obj1 doesn't have 16397 if (has$2(obj2, name)) { 16398 return false; 16399 } 16400 } 16401 return true; 16402 }; 16403 if (isElement$7(node1) && isElement$7(node2)) { 16404 // Attribs are not the same 16405 if (!compareObjects(getAttribs(node1), getAttribs(node2))) { 16406 return false; 16407 } 16408 // Styles are not the same 16409 if (!compareObjects(dom.parseStyle(dom.getAttrib(node1, 'style')), dom.parseStyle(dom.getAttrib(node2, 'style')))) { 16410 return false; 16411 } 16412 } 16413 return !isBookmarkNode$1(node1) && !isBookmarkNode$1(node2); 16414 }; 16415 const isAttributeInternal = (attributeName) => exists(internalAttributesPrefixes, (value) => startsWith(attributeName, value)) || internalAttributes.has(attributeName); 16416 return { 16417 compare, 16418 isAttributeInternal 16419 }; 16420 }; 16421 16422 const getNormalizedPoint$1 = (container, offset) => { 16423 if (isText$b(container)) { 16424 return { container, offset }; 16425 } 16426 const node = RangeUtils.getNode(container, offset); 16427 if (isText$b(node)) { 16428 return { 16429 container: node, 16430 offset: offset >= container.childNodes.length ? node.data.length : 0 16431 }; 16432 } 16433 else if (node.previousSibling && isText$b(node.previousSibling)) { 16434 return { 16435 container: node.previousSibling, 16436 offset: node.previousSibling.data.length 16437 }; 16438 } 16439 else if (node.nextSibling && isText$b(node.nextSibling)) { 16440 return { 16441 container: node.nextSibling, 16442 offset: 0 16443 }; 16444 } 16445 return { container, offset }; 16446 }; 16447 const normalizeRange$1 = (rng) => { 16448 const outRng = rng.cloneRange(); 16449 const rangeStart = getNormalizedPoint$1(rng.startContainer, rng.startOffset); 16450 outRng.setStart(rangeStart.container, rangeStart.offset); 16451 const rangeEnd = getNormalizedPoint$1(rng.endContainer, rng.endOffset); 16452 outRng.setEnd(rangeEnd.container, rangeEnd.offset); 16453 return outRng; 16454 }; 16455 16456 const DOM$b = DOMUtils.DOM; 16457 const defaultMarker = () => DOM$b.create('span', { 'data-mce-type': 'bookmark' }); 16458 const setupEndPoint = (container, offset, createMarker) => { 16459 if (isElement$7(container)) { 16460 const offsetNode = createMarker(); 16461 if (container.hasChildNodes()) { 16462 if (offset === container.childNodes.length) { 16463 container.appendChild(offsetNode); 16464 } 16465 else { 16466 container.insertBefore(offsetNode, container.childNodes[offset]); 16467 } 16468 } 16469 else { 16470 container.appendChild(offsetNode); 16471 } 16472 return { container: offsetNode, offset: 0 }; 16473 } 16474 else { 16475 return { container, offset }; 16476 } 16477 }; 16478 const restoreEndPoint = (container, offset) => { 16479 const nodeIndex = (container) => { 16480 let node = container.parentNode?.firstChild; 16481 let idx = 0; 16482 while (node) { 16483 if (node === container) { 16484 return idx; 16485 } 16486 // Skip data-mce-type=bookmark nodes 16487 if (!isElement$7(node) || node.getAttribute('data-mce-type') !== 'bookmark') { 16488 idx++; 16489 } 16490 node = node.nextSibling; 16491 } 16492 return -1; 16493 }; 16494 if (isElement$7(container) && isNonNullable(container.parentNode)) { 16495 const node = container; 16496 offset = nodeIndex(container); 16497 container = container.parentNode; 16498 DOM$b.remove(node); 16499 if (!container.hasChildNodes() && DOM$b.isBlock(container)) { 16500 container.appendChild(DOM$b.create('br')); 16501 } 16502 } 16503 return { container, offset }; 16504 }; 16505 const createNormalizedRange = (startContainer, startOffset, endContainer, endOffset) => { 16506 const rng = DOM$b.createRng(); 16507 rng.setStart(startContainer, startOffset); 16508 rng.setEnd(endContainer, endOffset); 16509 return normalizeRange$1(rng); 16510 }; 16511 /** 16512 * Returns a range bookmark. This will convert indexed bookmarks into temporary span elements with 16513 * index 0 so that they can be restored properly after the DOM has been modified. Text bookmarks will not have spans 16514 * added to them since they can be restored after a dom operation. 16515 * 16516 * So this: <p><b>|</b><b>|</b></p> 16517 * becomes: <p><b><span data-mce-type="bookmark">|</span></b><b data-mce-type="bookmark">|</span></b></p> 16518 */ 16519 const createBookmark = (rng, createMarker = defaultMarker) => { 16520 const { container: startContainer, offset: startOffset } = setupEndPoint(rng.startContainer, rng.startOffset, createMarker); 16521 if (rng.collapsed) { 16522 return { startContainer, startOffset }; 16523 } 16524 else { 16525 const { container: endContainer, offset: endOffset } = setupEndPoint(rng.endContainer, rng.endOffset, createMarker); 16526 return { startContainer, startOffset, endContainer, endOffset }; 16527 } 16528 }; 16529 const resolveBookmark = (bookmark) => { 16530 const { container: startContainer, offset: startOffset } = restoreEndPoint(bookmark.startContainer, bookmark.startOffset); 16531 if (!isUndefined(bookmark.endContainer) && !isUndefined(bookmark.endOffset)) { 16532 const { container: endContainer, offset: endOffset } = restoreEndPoint(bookmark.endContainer, bookmark.endOffset); 16533 return createNormalizedRange(startContainer, startOffset, endContainer, endOffset); 16534 } 16535 else { 16536 return createNormalizedRange(startContainer, startOffset, startContainer, startOffset); 16537 } 16538 }; 16539 16540 const applyStyles = (dom, elm, format, vars) => { 16541 Tools.each(format.styles, (value, name) => { 16542 dom.setStyle(elm, name, replaceVars(value, vars)); 16543 }); 16544 // Needed for the WebKit span spam bug 16545 // TODO: Remove this once WebKit/Blink fixes this 16546 if (format.styles) { 16547 const styleVal = dom.getAttrib(elm, 'style'); 16548 if (styleVal) { 16549 dom.setAttrib(elm, 'data-mce-style', styleVal); 16550 } 16551 } 16552 }; 16553 const setElementFormat = (ed, elm, fmt, vars, node) => { 16554 const dom = ed.dom; 16555 if (isFunction(fmt.onformat)) { 16556 fmt.onformat(elm, fmt, vars, node); 16557 } 16558 applyStyles(dom, elm, fmt, vars); 16559 Tools.each(fmt.attributes, (value, name) => { 16560 dom.setAttrib(elm, name, replaceVars(value, vars)); 16561 }); 16562 Tools.each(fmt.classes, (value) => { 16563 const newValue = replaceVars(value, vars); 16564 if (!dom.hasClass(elm, newValue)) { 16565 dom.addClass(elm, newValue); 16566 } 16567 }); 16568 }; 16569 16570 const isApplyFormat = (format) => !isArray$1(format.attributes) && !isArray$1(format.styles); 16571 16572 const isEq$3 = isEq$5; 16573 const matchesUnInheritedFormatSelector = (ed, node, name) => { 16574 const formatList = ed.formatter.get(name); 16575 if (formatList) { 16576 for (let i = 0; i < formatList.length; i++) { 16577 const format = formatList[i]; 16578 if (isSelectorFormat(format) && format.inherit === false && ed.dom.is(node, format.selector)) { 16579 return true; 16580 } 16581 } 16582 } 16583 return false; 16584 }; 16585 const matchParents = (editor, node, name, vars, similar) => { 16586 const root = editor.dom.getRoot(); 16587 if (node === root) { 16588 return false; 16589 } 16590 // Find first node with similar format settings 16591 const matchedNode = editor.dom.getParent(node, (elm) => { 16592 if (matchesUnInheritedFormatSelector(editor, elm, name)) { 16593 return true; 16594 } 16595 return elm.parentNode === root || !!matchNode$1(editor, elm, name, vars, true); 16596 }); 16597 // Do an exact check on the similar format element 16598 return !!matchNode$1(editor, matchedNode, name, vars, similar); 16599 }; 16600 const matchName = (dom, node, format) => { 16601 // Check for inline match 16602 if (isInlineFormat(format) && isEq$3(node, format.inline)) { 16603 return true; 16604 } 16605 // Check for block match 16606 if (isBlockFormat(format) && isEq$3(node, format.block)) { 16607 return true; 16608 } 16609 // Check for selector match 16610 if (isSelectorFormat(format)) { 16611 return isElement$7(node) && dom.is(node, format.selector); 16612 } 16613 return false; 16614 }; 16615 const matchItems = (dom, node, format, itemName, similar, vars) => { 16616 const items = format[itemName]; 16617 const matchAttributes = itemName === 'attributes'; 16618 // Custom match 16619 if (isFunction(format.onmatch)) { 16620 // onmatch is generic in a way that we can't really express without casting 16621 return format.onmatch(node, format, itemName); 16622 } 16623 // Check all items 16624 if (items) { 16625 // Non indexed object 16626 if (!isArrayLike(items)) { 16627 for (const key in items) { 16628 if (has$2(items, key)) { 16629 const value = matchAttributes ? dom.getAttrib(node, key) : getStyle(dom, node, key); 16630 const expectedValue = replaceVars(items[key], vars); 16631 const isEmptyValue = isNullable(value) || isEmpty$5(value); 16632 if (isEmptyValue && isNullable(expectedValue)) { 16633 continue; 16634 } 16635 if (similar && isEmptyValue && !format.exact) { 16636 return false; 16637 } 16638 if ((!similar || format.exact) && !isEq$3(value, normalizeStyleValue(expectedValue, key))) { 16639 return false; 16640 } 16641 } 16642 } 16643 } 16644 else { 16645 // Only one match needed for indexed arrays 16646 for (let i = 0; i < items.length; i++) { 16647 if (matchAttributes ? dom.getAttrib(node, items[i]) : getStyle(dom, node, items[i])) { 16648 return true; 16649 } 16650 } 16651 } 16652 } 16653 return true; 16654 }; 16655 const matchNode$1 = (ed, node, name, vars, similar) => { 16656 const formatList = ed.formatter.get(name); 16657 const dom = ed.dom; 16658 if (formatList && isElement$7(node)) { 16659 // Check each format in list 16660 for (let i = 0; i < formatList.length; i++) { 16661 const format = formatList[i]; 16662 // Name name, attributes, styles and classes 16663 if (matchName(ed.dom, node, format) && matchItems(dom, node, format, 'attributes', similar, vars) && matchItems(dom, node, format, 'styles', similar, vars)) { 16664 // Match classes 16665 const classes = format.classes; 16666 if (classes) { 16667 for (let x = 0; x < classes.length; x++) { 16668 if (!ed.dom.hasClass(node, replaceVars(classes[x], vars))) { 16669 return; 16670 } 16671 } 16672 } 16673 return format; 16674 } 16675 } 16676 } 16677 return undefined; 16678 }; 16679 const
16679match$2 = (editor, name, vars, node, similar) => { 16680 // Check specified node 16681 if (node) { 16682 return matchParents(editor, node, name, vars, similar); 16683 }
vendor: 8,525 bytes, lines 16684-16880
16684 // Check selected node 16685 node = editor.selection.getNode(); 16686 if (matchParents(editor, node, name, vars, similar)) { 16687 return true; 16688 } 16689 // Check start node if it's different 16690 const startNode = editor.selection.getStart(); 16691 if (startNode !== node) { 16692 if (matchParents(editor, startNode, name, vars, similar)) { 16693 return true; 16694 } 16695 } 16696 return false; 16697 }; 16698 const matchAll = (editor, names, vars) => { 16699 const matchedFormatNames = []; 16700 const checkedMap = {}; 16701 // Check start of selection for formats 16702 const startElement = editor.selection.getStart(); 16703 editor.dom.getParent(startElement, (node) => { 16704 for (let i = 0; i < names.length; i++) { 16705 const name = names[i]; 16706 if (!checkedMap[name] && matchNode$1(editor, node, name, vars)) { 16707 checkedMap[name] = true; 16708 matchedFormatNames.push(name); 16709 } 16710 } 16711 }, editor.dom.getRoot()); 16712 return matchedFormatNames; 16713 }; 16714 const closest = (editor, names) => { 16715 const isRoot = (elm) => eq(elm, SugarElement.fromDom(editor.getBody())); 16716 const match = (elm, name) => matchNode$1(editor, elm.dom, name) ? Optional.some(name) : Optional.none(); 16717 return Optional.from(editor.selection.getStart(true)).bind((rawElm) => closest$1(SugarElement.fromDom(rawElm), (elm) => findMap(names, (name) => match(elm, name)), isRoot)).getOrNull(); 16718 }; 16719 const canApply = (editor, name) => { 16720 const formatList = editor.formatter.get(name); 16721 const dom = editor.dom; 16722 if (formatList && editor.selection.isEditable()) { 16723 const startNode = editor.selection.getStart(); 16724 const parents = getParents$2(dom, startNode); 16725 for (let x = formatList.length - 1; x >= 0; x--) { 16726 const format = formatList[x]; 16727 // Format is not selector based then always return TRUE 16728 if (!isSelectorFormat(format)) { 16729 return true; 16730 } 16731 for (let i = parents.length - 1; i >= 0; i--) { 16732 if (dom.is(parents[i], format.selector)) { 16733 return true; 16734 } 16735 } 16736 } 16737 } 16738 return false; 16739 }; 16740 /** 16741 * Get all of the format names present on the specified node 16742 */ 16743 const matchAllOnNode = (editor, node, formatNames) => foldl(formatNames, (acc, name) => { 16744 const matchSimilar = isVariableFormatName(editor, name); 16745 if (editor.formatter.matchNode(node, name, {}, matchSimilar)) { 16746 return acc.concat([name]); 16747 } 16748 else { 16749 return acc; 16750 } 16751 }, []); 16752 16753 const ZWSP = ZWSP$1; 16754 const importNode = (ownerDocument, node) => { 16755 return ownerDocument.importNode(node, true); 16756 }; 16757 const findFirstTextNode = (node) => { 16758 if (node) { 16759 const walker = new DomTreeWalker(node, node); 16760 for (let tempNode = walker.current(); tempNode; tempNode = walker.next()) { 16761 if (isText$b(tempNode)) { 16762 return tempNode; 16763 } 16764 } 16765 } 16766 return null; 16767 }; 16768 const createCaretContainer = (fill) => { 16769 const caretContainer = SugarElement.fromTag('span'); 16770 setAll$1(caretContainer, { 16771 // style: 'color:red', 16772 'id': CARET_ID, 16773 'data-mce-bogus': '1', 16774 'data-mce-type': 'format-caret' 16775 }); 16776 if (fill) { 16777 append$1(caretContainer, SugarElement.fromText(ZWSP)); 16778 } 16779 return caretContainer; 16780 }; 16781 const trimZwspFromCaretContainer = (caretContainerNode) => { 16782 const textNode = findFirstTextNode(caretContainerNode); 16783 if (textNode && textNode.data.charAt(0) === ZWSP) { 16784 textNode.deleteData(0, 1); 16785 } 16786 return textNode; 16787 }; 16788 const removeCaretContainerNode = (editor, node, moveCaret) => { 16789 const dom = editor.dom, selection = editor.selection; 16790 if (isCaretContainerEmpty(node)) { 16791 deleteElement$2(editor, false, SugarElement.fromDom(node), moveCaret, true); 16792 } 16793 else { 16794 const rng = selection.getRng(); 16795 const block = dom.getParent(node, dom.isBlock); 16796 // Store the current selection offsets 16797 const startContainer = rng.startContainer; 16798 const startOffset = rng.startOffset; 16799 const endContainer = rng.endContainer; 16800 const endOffset = rng.endOffset; 16801 const textNode = trimZwspFromCaretContainer(node); 16802 dom.remove(node, true); 16803 // Restore the selection after unwrapping the node and removing the zwsp 16804 if (startContainer === textNode && startOffset > 0) { 16805 rng.setStart(textNode, startOffset - 1); 16806 } 16807 if (endContainer === textNode && endOffset > 0) { 16808 rng.setEnd(textNode, endOffset - 1); 16809 } 16810 if (block && dom.isEmpty(block)) { 16811 fillWithPaddingBr(SugarElement.fromDom(block)); 16812 } 16813 selection.setRng(rng); 16814 } 16815 }; 16816 // Removes the caret container for the specified node or all on the current document 16817 const removeCaretContainer = (editor, node, moveCaret) => { 16818 const dom = editor.dom, selection = editor.selection; 16819 if (!node) { 16820 node = getParentCaretContainer(editor.getBody(), selection.getStart()); 16821 if (!node) { 16822 while ((node = dom.get(CARET_ID))) { 16823 removeCaretContainerNode(editor, node, moveCaret); 16824 } 16825 } 16826 } 16827 else { 16828 removeCaretContainerNode(editor, node, moveCaret); 16829 } 16830 }; 16831 const insertCaretContainerNode = (editor, caretContainer, formatNode) => { 16832 const dom = editor.dom; 16833 const block = dom.getParent(formatNode, curry(isTextBlock$2, editor.schema)); 16834 if (block && dom.isEmpty(block)) { 16835 // Replace formatNode with caretContainer when removing format from empty block like <p><b>|</b></p> 16836 formatNode.parentNode?.replaceChild(caretContainer, formatNode); 16837 } 16838 else { 16839 removeTrailingBr(SugarElement.fromDom(formatNode)); 16840 if (dom.isEmpty(formatNode)) { 16841 formatNode.parentNode?.replaceChild(caretContainer, formatNode); 16842 } 16843 else { 16844 dom.insertAfter(caretContainer, formatNode); 16845 } 16846 } 16847 }; 16848 const appendNode = (parentNode, node) => { 16849 parentNode.appendChild(node); 16850 return node; 16851 }; 16852 const insertFormatNodesIntoCaretContainer = (formatNodes, caretContainer) => { 16853 const innerMostFormatNode = foldr(formatNodes, (parentNode, formatNode) => { 16854 return appendNode(parentNode, formatNode.cloneNode(false)); 16855 }, caretContainer); 16856 const doc = innerMostFormatNode.ownerDocument ?? document; 16857 return appendNode(innerMostFormatNode, doc.createTextNode(ZWSP)); 16858 }; 16859 const cleanFormatNode = (editor, caretContainer, formatNode, name, vars, similar) => { 16860 const formatter = editor.formatter; 16861 const dom = editor.dom; 16862 // Find all formats present on the format node 16863 const validFormats = filter$5(keys(formatter.get()), (formatName) => formatName !== name && !contains$1(formatName, 'removeformat')); 16864 const matchedFormats = matchAllOnNode(editor, formatNode, validFormats); 16865 // Filter out any matched formats that are 'visually' equivalent to the 'name' format since they are not unique formats on the node 16866 const uniqueFormats = filter$5(matchedFormats, (fmtName) => !areSimilarFormats(editor, fmtName, name)); 16867 // If more than one format is present, then there's additional formats that should be retained. So clone the node, 16868 // remove the format and then return cleaned format node 16869 if (uniqueFormats.length > 0) { 16870 const clonedFormatNode = formatNode.cloneNode(false); 16871 dom.add(caretContainer, clonedFormatNode); 16872 formatter.remove(name, vars, clonedFormatNode, similar); 16873 dom.remove(clonedFormatNode); 16874 return Optional.some(clonedFormatNode); 16875 } 16876 else { 16877 return Optional.none(); 16878 } 16879 }; 16880 const normalizeNbsps = (node) => set$1(node, get$4
vendor: 10,188 bytes, lines 16880-17091
16880(node).replace(new RegExp(`${nbsp}$`), ' ')); 16881 const normalizeNbspsBetween = (editor, caretContainer) => { 16882 const handler = () => { 16883 if (caretContainer !== null && !editor.dom.isEmpty(caretContainer)) { 16884 prevSibling(SugarElement.fromDom(caretContainer)).each((node) => { 16885 if (isText$c(node)) { 16886 normalizeNbsps(node); 16887 } 16888 else { 16889 descendant$2(node, (e) => isText$c(e)).each((textNode) => { 16890 if (isText$c(textNode)) { 16891 normalizeNbsps(textNode); 16892 } 16893 }); 16894 } 16895 }); 16896 } 16897 }; 16898 editor.once('input', (e) => { 16899 if (e.data && !isWhiteSpace(e.data)) { 16900 if (!e.isComposing) { 16901 handler(); 16902 } 16903 else { 16904 editor.once('compositionend', () => { 16905 handler(); 16906 }); 16907 } 16908 } 16909 }); 16910 }; 16911 const applyCaretFormat = (editor, name, vars) => { 16912 let caretContainer; 16913 const selection = editor.selection; 16914 const formatList = editor.formatter.get(name); 16915 if (!formatList) { 16916 return; 16917 } 16918 const selectionRng = selection.getRng(); 16919 let offset = selectionRng.startOffset; 16920 const container = selectionRng.startContainer; 16921 const text = container.nodeValue; 16922 caretContainer = getParentCaretContainer(editor.getBody(), selection.getStart()); 16923 // Expand to word if caret is in the middle of a text node and the char before/after is a alpha numeric character 16924 const wordcharRegex = /[^\s\u00a0\u00ad\u200b\ufeff]/; 16925 if (text && offset > 0 && offset < text.length && 16926 wordcharRegex.test(text.charAt(offset)) && wordcharRegex.test(text.charAt(offset - 1))) { 16927 // Get bookmark of caret position 16928 const bookmark = selection.getBookmark(); 16929 // Collapse bookmark range (WebKit) 16930 selectionRng.collapse(true); 16931 // Expand the range to the closest word and split it at those points 16932 let rng = expandRng(editor.dom, selectionRng, formatList); 16933 rng = split(rng); 16934 // Apply the format to the range 16935 editor.formatter.apply(name, vars, rng); 16936 // Move selection back to caret position 16937 selection.moveToBookmark(bookmark); 16938 } 16939 else { 16940 let textNode = caretContainer ? findFirstTextNode(caretContainer) : null; 16941 if (!caretContainer || textNode?.data !== ZWSP) { 16942 // Need to import the node into the document on IE or we get a lovely WrongDocument exception 16943 caretContainer = importNode(editor.getDoc(), createCaretContainer(true).dom); 16944 textNode = caretContainer.firstChild; 16945 selectionRng.insertNode(caretContainer); 16946 offset = 1; 16947 normalizeNbspsBetween(editor, caretContainer); 16948 editor.formatter.apply(name, vars, caretContainer); 16949 } 16950 else { 16951 editor.formatter.apply(name, vars, caretContainer); 16952 } 16953 // Move selection to text node 16954 selection.setCursorLocation(textNode, offset); 16955 } 16956 }; 16957 const removeCaretFormat = (editor, name, vars, similar) => { 16958 const dom = editor.dom; 16959 const selection = editor.selection; 16960 let hasContentAfter = false; 16961 const formatList = editor.formatter.get(name); 16962 if (!formatList) { 16963 return; 16964 } 16965 const rng = selection.getRng(); 16966 const container = rng.startContainer; 16967 const offset = rng.startOffset; 16968 let node = container; 16969 if (isText$b(container)) { 16970 if (offset !== container.data.length) { 16971 hasContentAfter = true; 16972 } 16973 node = node.parentNode; 16974 } 16975 const parents = []; 16976 let formatNode; 16977 while (node) { 16978 if (matchNode$1(editor, node, name, vars, similar)) { 16979 formatNode = node; 16980 break; 16981 } 16982 if (node.nextSibling) { 16983 hasContentAfter = true; 16984 } 16985 parents.push(node); 16986 node = node.parentNode; 16987 } 16988 // Node doesn't have the specified format 16989 if (!formatNode) { 16990 return; 16991 } 16992 // Is there contents after the caret then remove the format on the element 16993 if (hasContentAfter) { 16994 const bookmark = selection.getBookmark(); 16995 // Collapse bookmark range (WebKit) 16996 rng.collapse(true); 16997 // Expand the range to the closest word and split it at those points 16998 let expandedRng = expandRng(dom, rng, formatList, { includeTrailingSpace: true }); 16999 expandedRng = split(expandedRng); 17000 // TODO: Figure out how on earth this works, as it shouldn't since remove format 17001 // definitely seems to require an actual Range 17002 editor.formatter.remove(name, vars, expandedRng, similar); 17003 selection.moveToBookmark(bookmark); 17004 } 17005 else { 17006 const caretContainer = getParentCaretContainer(editor.getBody(), formatNode); 17007 const parentsAfter = isNonNullable(caretContainer) ? dom.getParents(formatNode.parentNode, always, caretContainer) : []; 17008 const newCaretContainer = createCaretContainer(false).dom; 17009 insertCaretContainerNode(editor, newCaretContainer, caretContainer ?? formatNode); 17010 const cleanedFormatNode = cleanFormatNode(editor, newCaretContainer, formatNode, name, vars, similar); 17011 const caretTextNode = insertFormatNodesIntoCaretContainer([ 17012 ...parents, 17013 ...cleanedFormatNode.toArray(), 17014 ...parentsAfter 17015 ], newCaretContainer); 17016 if (caretContainer) { 17017 removeCaretContainerNode(editor, caretContainer, isNonNullable(caretContainer)); 17018 } 17019 selection.setCursorLocation(caretTextNode, 1); 17020 normalizeNbspsBetween(editor, newCaretContainer); 17021 if (dom.isEmpty(formatNode)) { 17022 dom.remove(formatNode); 17023 } 17024 } 17025 }; 17026 const disableCaretContainer = (editor, keyCode, moveCaret) => { 17027 const selection = editor.selection, body = editor.getBody(); 17028 removeCaretContainer(editor, null, moveCaret); 17029 // Remove caret container if it's empty 17030 if ((keyCode === 8 || keyCode === 46) && selection.isCollapsed() && selection.getStart().innerHTML === ZWSP) { 17031 removeCaretContainer(editor, getParentCaretContainer(body, selection.getStart()), true); 17032 } 17033 // Remove caret container on keydown and it's left/right arrow keys 17034 if (keyCode === 37 || keyCode === 39) { 17035 removeCaretContainer(editor, getParentCaretContainer(body, selection.getStart()), true); 17036 } 17037 }; 17038 const endsWithNbsp = (element) => isText$b(element) && endsWith(element.data, nbsp); 17039 const setup$B = (editor) => { 17040 editor.on('mouseup keydown', (e) => { 17041 disableCaretContainer(editor, e.keyCode, endsWithNbsp(editor.selection.getRng().endContainer)); 17042 }); 17043 }; 17044 const createCaretFormat = (formatNodes) => { 17045 const caretContainer = createCaretContainer(false); 17046 const innerMost = insertFormatNodesIntoCaretContainer(formatNodes, caretContainer.dom); 17047 return { caretContainer, caretPosition: CaretPosition(innerMost, 0) }; 17048 }; 17049 const replaceWithCaretFormat = (targetNode, formatNodes) => { 17050 const { caretContainer, caretPosition } = createCaretFormat(formatNodes); 17051 before$4(SugarElement.fromDom(targetNode), caretContainer); 17052 remove$8(SugarElement.fromDom(targetNode)); 17053 return caretPosition; 17054 }; 17055 const createCaretFormatAtStart$1 = (rng, formatNodes) => { 17056 const { caretContainer, caretPosition } = createCaretFormat(formatNodes); 17057 rng.insertNode(caretContainer.dom); 17058 return caretPosition; 17059 }; 17060 const isFormatElement = (editor, element) => { 17061 if (isCaretNode(element.dom)) { 17062 return false; 17063 } 17064 const inlineElements = editor.schema.getTextInlineElements(); 17065 return has$2(inlineElements, name(element)) && !isCaretNode(element.dom) && !isBogus$1(element.dom); 17066 }; 17067 17068 const listItemStyles = ['fontWeight', 'fontStyle', 'color', 'fontSize', 'fontFamily']; 17069 const hasListStyles = (fmt) => isObject(fmt.styles) && exists(keys(fmt.styles), (name) => contains$2(listItemStyles, name)); 17070 const findExpandedListItemFormat = (formats) => find$2(formats, (fmt) => isInlineFormat(fmt) && fmt.inline === 'span' && hasListStyles(fmt)); 17071 const getExpandedListItemFormat = (formatter, format) => { 17072 const formatList = formatter.get(format); 17073 return isArray$1(formatList) ? findExpandedListItemFormat(formatList) : Optional.none(); 17074 }; 17075 const isRngStartAtStartOfElement = (rng, elm) => prevPosition(elm, CaretPosition.fromRangeStart(rng)).isNone(); 17076 const isRngEndAtEndOfElement = (rng, elm) => { 17077 return nextPosition(elm, CaretPosition.fromRangeEnd(rng)) 17078 .exists((pos) => !isBr$7(pos.getNode()) || nextPosition(elm, pos).isSome()) === false; 17079 }; 17080 const isEditableListItem = (dom) => (elm) => isListItem$3(elm) && dom.isEditable(elm); 17081 // TINY-13197: If the content is wrapped inside a block element, the first block returned by getSelectedBlocks() is not LI, even when the content is fully selected. 17082 // However, the second and subsequent do return LI as the selected block so only the first block needs to be adjusted 17083 const getAndOnlyNormalizeFirstBlockIf = (selection, pred) => map$3(selection.getSelectedBlocks(), (block, i) => { 17084 if (i === 0 && pred(block)) { 17085 return selection.dom.getParent(block, isListItem$3) ?? block; 17086 } 17087 else { 17088 return block; 17089 } 17090 }); 17091
vendor: 1,100 bytes, lines 17091-17108
17091const getFullySelectedBlocks = (selection) => { 17092 if (selection.isCollapsed()) { 17093 return []; 17094 } 17095 const rng = selection.getRng(); 17096 const blocks = getAndOnlyNormalizeFirstBlockIf(selection, (el) => isRngStartAtStartOfElement(rng, el) && !isListItem$3(el)); 17097 if (blocks.length === 1) { 17098 return isRngStartAtStartOfElement(rng, blocks[0]) && isRngEndAtEndOfElement(rng, blocks[0]) ? blocks : []; 17099 } 17100 else { 17101 const first = head(blocks).filter((elm) => isRngStartAtStartOfElement(rng, elm)).toArray(); 17102 const last = last$2(blocks).filter((elm) => isRngEndAtEndOfElement(rng, elm)).toArray(); 17103 const middle = blocks.slice(1, -1); 17104 return first.concat(middle).concat(last); 17105 } 17106 }; 17107 const getFullySelectedListItems = (selection) => filter$5(getFullySelectedBlocks(selection), isEditableListItem(selection.dom)); 17108 const getPartiallySelectedListItems = (selection) => filter$5(getAndOnlyNormalizeFirstBlockIf(selection, (el) => !isListItem$3(el)), isEditableListItem(selection.dom))
vendor: 10,081 bytes, lines 17108-17350
17108; 17109 17110 const each$8 = Tools.each; 17111 const isElementNode = (node) => isElement$7(node) && !isBookmarkNode$1(node) && !isCaretNode(node) && !isBogus$1(node); 17112 const findElementSibling = (node, siblingName) => { 17113 for (let sibling = node; sibling; sibling = sibling[siblingName]) { 17114 if (isText$b(sibling) && isNotEmpty(sibling.data)) { 17115 return node; 17116 } 17117 if (isElement$7(sibling) && !isBookmarkNode$1(sibling)) { 17118 return sibling; 17119 } 17120 } 17121 return node; 17122 }; 17123 const mergeSiblingsNodes = (editor, prev, next) => { 17124 const elementUtils = ElementUtils(editor); 17125 const isPrevEditable = isHTMLElement(prev) && editor.dom.isEditable(prev); 17126 const isNextEditable = isHTMLElement(next) && editor.dom.isEditable(next); 17127 // Check if next/prev exists and that they are elements 17128 if (isPrevEditable && isNextEditable) { 17129 // If previous sibling is empty then jump over it 17130 const prevSibling = findElementSibling(prev, 'previousSibling'); 17131 const nextSibling = findElementSibling(next, 'nextSibling'); 17132 // Compare next and previous nodes 17133 if (elementUtils.compare(prevSibling, nextSibling)) { 17134 // Append nodes between 17135 for (let sibling = prevSibling.nextSibling; sibling && sibling !== nextSibling;) { 17136 const tmpSibling = sibling; 17137 sibling = sibling.nextSibling; 17138 prevSibling.appendChild(tmpSibling); 17139 } 17140 editor.dom.remove(nextSibling); 17141 Tools.each(Tools.grep(nextSibling.childNodes), (node) => { 17142 prevSibling.appendChild(node); 17143 }); 17144 return prevSibling; 17145 } 17146 } 17147 return next; 17148 }; 17149 const mergeSiblings = (editor, format, vars, node) => { 17150 // Merge next and previous siblings if they are similar <b>text</b><b>text</b> becomes <b>texttext</b> 17151 // Note: mergeSiblingNodes attempts to not merge sibilings if they are noneditable 17152 if (node && format.merge_siblings !== false) { 17153 // Previous sibling 17154 const newNode = mergeSiblingsNodes(editor, getNonWhiteSpaceSibling(node), node) ?? node; 17155 // Next sibling 17156 mergeSiblingsNodes(editor, newNode, getNonWhiteSpaceSibling(newNode, true)); 17157 } 17158 }; 17159 const clearChildStyles = (dom, format, node) => { 17160 if (format.clear_child_styles) { 17161 const selector = format.links ? '*:not(a)' : '*'; 17162 each$8(dom.select(selector, node), (childNode) => { 17163 if (isElementNode(childNode) && dom.isEditable(childNode)) { 17164 each$8(format.styles, (_value, name) => { 17165 dom.setStyle(childNode, name, ''); 17166 }); 17167 } 17168 }); 17169 } 17170 }; 17171 const processChildElements = (node, filter, process) => { 17172 each$8(node.childNodes, (node) => { 17173 if (isElementNode(node)) { 17174 if (filter(node)) { 17175 process(node); 17176 } 17177 if (node.hasChildNodes()) { 17178 processChildElements(node, filter, process); 17179 } 17180 } 17181 }); 17182 }; 17183 const unwrapEmptySpan = (dom, node) => { 17184 if (node.nodeName === 'SPAN' && dom.getAttribs(node).length === 0) { 17185 dom.remove(node, true); 17186 } 17187 }; 17188 const hasStyle = (dom, name) => (node) => !!(node && getStyle(dom, node, name)); 17189 const applyStyle = (dom, name, value) => (node) => { 17190 dom.setStyle(node, name, value); 17191 if (node.getAttribute('style') === '') { 17192 node.removeAttribute('style'); 17193 } 17194 unwrapEmptySpan(dom, node); 17195 }; 17196 17197 const removeResult = Adt.generate([ 17198 { keep: [] }, 17199 { rename: ['name'] }, 17200 { removed: [] } 17201 ]); 17202 const MCE_ATTR_RE = /^(src|href|style)$/; 17203 const each$7 = Tools.each; 17204 const isEq$2 = isEq$5; 17205 const isTableCellOrRow = (node) => /^(TR|TH|TD)$/.test(node.nodeName); 17206 const isChildOfInlineParent = (dom, node, parent) => dom.isChildOf(node, parent) && node !== parent && !dom.isBlock(parent); 17207 const getContainer = (ed, rng, start) => { 17208 let container = rng[start ? 'startContainer' : 'endContainer']; 17209 let offset = rng[start ? 'startOffset' : 'endOffset']; 17210 if (isElement$7(container)) { 17211 const lastIdx = container.childNodes.length - 1; 17212 if (!start && offset) { 17213 offset--; 17214 } 17215 container = container.childNodes[offset > lastIdx ? lastIdx : offset]; 17216 } 17217 // If start text node is excluded then walk to the next node 17218 if (isText$b(container) && start && offset >= container.data.length) { 17219 container = new DomTreeWalker(container, ed.getBody()).next() || container; 17220 } 17221 // If end text node is excluded then walk to the previous node 17222 if (isText$b(container) && !start && offset === 0) { 17223 container = new DomTreeWalker(container, ed.getBody()).prev() || container; 17224 } 17225 return container; 17226 }; 17227 const normalizeTableSelection = (node, start) => { 17228 const prop = start ? 'firstChild' : 'lastChild'; 17229 const childNode = node[prop]; 17230 if (isTableCellOrRow(node) && childNode) { 17231 if (node.nodeName === 'TR') { 17232 return childNode[prop] || childNode; 17233 } 17234 else { 17235 return childNode; 17236 } 17237 } 17238 return node; 17239 }; 17240 const wrap$1 = (dom, node, name, attrs) => { 17241 const wrapper = dom.create(name, attrs); 17242 node.parentNode?.insertBefore(wrapper, node); 17243 wrapper.appendChild(node); 17244 return wrapper; 17245 }; 17246 const wrapWithSiblings = (dom, node, next, name, attrs) => { 17247 const start = SugarElement.fromDom(node); 17248 const wrapper = SugarElement.fromDom(dom.create(name, attrs)); 17249 const siblings = next ? nextSiblings(start) : prevSiblings(start); 17250 append(wrapper, siblings); 17251 if (next) { 17252 before$4(start, wrapper); 17253 prepend(wrapper, start); 17254 } 17255 else { 17256 after$4(start, wrapper); 17257 append$1(wrapper, start); 17258 } 17259 return wrapper.dom; 17260 }; 17261 const isColorFormatAndAnchor = (node, format) => format.links && node.nodeName === 'A'; 17262 /** 17263 * Removes the node and wrap it's children in paragraphs before doing so or 17264 * appends BR elements to the beginning/end of the block element if forcedRootBlocks is disabled. 17265 * 17266 * If the div in the node below gets removed: 17267 * text<div>text</div>text 17268 * 17269 * Output becomes: 17270 * text<div><br />text<br /></div>text 17271 * 17272 * So when the div is removed the result is: 17273 * text<br />text<br />text 17274 * 17275 * @private 17276 * @param {Node} node Node to remove + apply BR/P elements to. 17277 * @param {Object} format Format rule. 17278 * @return {Node} Input node. 17279 */ 17280 const removeNode = (ed, node, format) => { 17281 const parentNode = node.parentNode; 17282 let rootBlockElm; 17283 const dom = ed.dom; 17284 const forcedRootBlock = getForcedRootBlock(ed); 17285 if (isBlockFormat(format)) { 17286 // Wrap the block in a forcedRootBlock if we are at the root of document 17287 if (parentNode === dom.getRoot()) { 17288 if (!format.list_block || !isEq$2(node, format.list_block)) { 17289 each$e(from(node.childNodes), (node) => { 17290 if (isValid(ed, forcedRootBlock, node.nodeName.toLowerCase())) { 17291 if (!rootBlockElm) { 17292 rootBlockElm = wrap$1(dom, node, forcedRootBlock); 17293 dom.setAttribs(rootBlockElm, getForcedRootBlockAttrs(ed)); 17294 } 17295 else { 17296 rootBlockElm.appendChild(node); 17297 } 17298 } 17299 else { 17300 rootBlockElm = null; 17301 } 17302 }); 17303 } 17304 } 17305 } 17306 // Never remove nodes that aren't the specified inline element if a selector is specified too 17307 if (isMixedFormat(format) && !isEq$2(format.inline, node)) { 17308 return; 17309 } 17310 dom.remove(node, true); 17311 }; 17312 // Attributes or styles can be either an array of names or an object containing name/value pairs 17313 const processFormatAttrOrStyle = (name, value, vars) => { 17314 // Indexed array 17315 if (isNumber(name)) { 17316 return { 17317 name: value, 17318 value: null 17319 }; 17320 } 17321 else { 17322 return { 17323 name, 17324 value: replaceVars(value, vars) 17325 }; 17326 } 17327 }; 17328 const removeEmptyStyleAttributeIfNeeded = (dom, elm) => { 17329 if (dom.getAttrib(elm, 'style') === '') { 17330 elm.removeAttribute('style'); 17331 elm.removeAttribute('data-mce-style'); 17332 } 17333 }; 17334 const removeStyles$1 = (dom, elm, format, vars, compareNode) => { 17335 let stylesModified = false; 17336 each$7(format.styles, (value, name) => { 17337 const { name: styleName, value: styleValue } = processFormatAttrOrStyle(name, value, vars); 17338 const normalizedStyleValue = normalizeStyleValue(styleValue, styleName); 17339 if (format.remove_similar || isNull(styleValue) || !isElement$7(compareNode) || isEq$2(getStyle(dom, compareNode, styleName), normalizedStyleValue)) { 17340 dom.setStyle(elm, styleName, ''); 17341 } 17342 stylesModified = true; 17343 }); 17344 if (stylesModified) { 17345 removeEmptyStyleAttributeIfNeeded(dom, elm); 17346 } 17347 }; 17348 const removeListStyleFormats = (editor, name, vars) => { 17349 if (name === 'removeformat') { 17350 each$e(getPartiallySelectedListItems(editor
17350.selection), (li) => { 17351 each$e(listItemStyles, (name) => editor.dom.setStyle(li, name, '')); 17352 removeEmptyStyleAttributeIfNeeded(editor.dom, li); 17353 }); 17354 } 17355 else { 17356 getExpandedListItemFormat(editor.formatter, name).each((liFmt) => { 17357 each$e(getPartiallySelectedListItems(editor.selection
vendor: 20,508 bytes, lines 17357-17741
17357), (li) => removeStyles$1(editor.dom, li, liFmt, vars, null)); 17358 }); 17359 } 17360 }; 17361 const removeNodeFormatInternal = (ed, format, vars, node, compareNode) => { 17362 const dom = ed.dom; 17363 const elementUtils = ElementUtils(ed); 17364 const schema = ed.schema; 17365 // Root level block transparents should get converted into regular text blocks 17366 if (isInlineFormat(format) && isTransparentElementName(schema, format.inline) && isTransparentBlock(schema, node) && node.parentElement === ed.getBody()) { 17367 removeNode(ed, node, format); 17368 return removeResult.removed(); 17369 } 17370 // Check if node is noneditable and can have the format removed from it 17371 if (!format.ceFalseOverride && node && dom.getContentEditableParent(node) === 'false') { 17372 return removeResult.keep(); 17373 } 17374 // Check if node matches format 17375 if (node && !matchName(dom, node, format) && !isColorFormatAndAnchor(node, format)) { 17376 return removeResult.keep(); 17377 } 17378 // "matchName" will made sure we're dealing with an element, so cast as one 17379 const elm = node; 17380 // Applies to styling elements like strong, em, i, u, etc. so that if they have styling attributes, the attributes can be kept but the styling element is removed 17381 const preserveAttributes = format.preserve_attributes; 17382 if (isInlineFormat(format) && format.remove === 'all' && isArray$1(preserveAttributes)) { 17383 // Remove all attributes except for the attributes specified in preserve_attributes 17384 const attrsToPreserve = filter$5(dom.getAttribs(elm), (attr) => contains$2(preserveAttributes, attr.name.toLowerCase())); 17385 dom.removeAllAttribs(elm); 17386 each$e(attrsToPreserve, (attr) => dom.setAttrib(elm, attr.name, attr.value)); 17387 // Note: If there are no attributes left, the element will be removed as normal at the end of the function 17388 if (attrsToPreserve.length > 0) { 17389 // Convert inline element to span if necessary 17390 return removeResult.rename('span'); 17391 } 17392 } 17393 // Should we compare with format attribs and styles 17394 if (format.remove !== 'all') { 17395 removeStyles$1(dom, elm, format, vars, compareNode); 17396 // Remove attributes 17397 each$7(format.attributes, (value, name) => { 17398 const { name: attrName, value: attrValue } = processFormatAttrOrStyle(name, value, vars); 17399 if (format.remove_similar || isNull(attrValue) || !isElement$7(compareNode) || isEq$2(dom.getAttrib(compareNode, attrName), attrValue)) { 17400 // Keep internal classes 17401 if (attrName === 'class') { 17402 const currentValue = dom.getAttrib(elm, attrName); 17403 if (currentValue) { 17404 // Build new class value where everything is removed except the internal prefixed classes 17405 let valueOut = ''; 17406 each$e(currentValue.split(/\s+/), (cls) => { 17407 if (/mce\-\w+/.test(cls)) { 17408 valueOut += (valueOut ? ' ' : '') + cls; 17409 } 17410 }); 17411 // We got some internal classes left 17412 if (valueOut) { 17413 dom.setAttrib(elm, attrName, valueOut); 17414 return; 17415 } 17416 } 17417 } 17418 // Remove mce prefixed attributes (must clean before short circuit operations) 17419 if (MCE_ATTR_RE.test(attrName)) { 17420 elm.removeAttribute('data-mce-' + attrName); 17421 } 17422 // keep style="list-style-type: none" on <li>s 17423 if (attrName === 'style' && matchNodeNames$1(['li'])(elm) && dom.getStyle(elm, 'list-style-type') === 'none') { 17424 elm.removeAttribute(attrName); 17425 dom.setStyle(elm, 'list-style-type', 'none'); 17426 return; 17427 } 17428 // IE6 has a bug where the attribute doesn't get removed correctly 17429 if (attrName === 'class') { 17430 elm.removeAttribute('className'); 17431 } 17432 elm.removeAttribute(attrName); 17433 } 17434 }); 17435 // Remove classes 17436 each$7(format.classes, (value) => { 17437 value = replaceVars(value, vars); 17438 if (!isElement$7(compareNode) || dom.hasClass(compareNode, value)) { 17439 dom.removeClass(elm, value); 17440 } 17441 }); 17442 // Check for non internal attributes 17443 const attrs = dom.getAttribs(elm); 17444 for (let i = 0; i < attrs.length; i++) { 17445 const attrName = attrs[i].nodeName; 17446 if (!elementUtils.isAttributeInternal(attrName)) { 17447 return removeResult.keep(); 17448 } 17449 } 17450 } 17451 // Remove the inline child if it's empty for example <b> or <span> 17452 if (format.remove !== 'none') { 17453 removeNode(ed, elm, format); 17454 return removeResult.removed(); 17455 } 17456 return removeResult.keep(); 17457 }; 17458 const findFormatRoot = (editor, container, name, vars, similar) => { 17459 let formatRoot; 17460 if (container.parentNode) { 17461 // Find format root 17462 each$e(getParents$2(editor.dom, container.parentNode).reverse(), (parent) => { 17463 // Find format root element 17464 if (!formatRoot && isElement$7(parent) && parent.id !== '_start' && parent.id !== '_end') { 17465 // Is the node matching the format we are looking for 17466 const format = matchNode$1(editor, parent, name, vars, similar); 17467 if (format && format.split !== false) { 17468 formatRoot = parent; 17469 } 17470 } 17471 }); 17472 } 17473 return formatRoot; 17474 }; 17475 const removeNodeFormatFromClone = (editor, format, vars, clone) => removeNodeFormatInternal(editor, format, vars, clone, clone).fold(constant(clone), (newName) => { 17476 // To rename a node, it needs to be a child of another node 17477 const fragment = editor.dom.createFragment(); 17478 fragment.appendChild(clone); 17479 // If renaming we are guaranteed this is a Element, so cast 17480 return editor.dom.rename(clone, newName); 17481 }, constant(null)); 17482 const wrapAndSplit = (editor, formatList, formatRoot, container, target, split, format, vars) => { 17483 let lastClone; 17484 let firstClone; 17485 const dom = editor.dom; 17486 // Format root found then clone formats and split it 17487 if (formatRoot) { 17488 const formatRootParent = formatRoot.parentNode; 17489 for (let parent = container.parentNode; parent && parent !== formatRootParent; parent = parent.parentNode) { 17490 let clone = dom.clone(parent, false); 17491 for (let i = 0; i < formatList.length; i++) { 17492 clone = removeNodeFormatFromClone(editor, formatList[i], vars, clone); 17493 if (clone === null) { 17494 break; 17495 } 17496 } 17497 // Build wrapper node 17498 if (clone) { 17499 if (lastClone) { 17500 clone.appendChild(lastClone); 17501 } 17502 if (!firstClone) { 17503 firstClone = clone; 17504 } 17505 lastClone = clone; 17506 } 17507 } 17508 // Never split block elements if the format is mixed 17509 if (split && (!format.mixed || !dom.isBlock(formatRoot))) { 17510 container = dom.split(formatRoot, container) ?? container; 17511 } 17512 // Wrap container in cloned formats 17513 if (lastClone && firstClone) { 17514 target.parentNode?.insertBefore(lastClone, target); 17515 firstClone.appendChild(target); 17516 // After splitting the nodes may match with other siblings so we need to attempt to merge them 17517 // Note: We can't use MergeFormats, as that'd create a circular dependency 17518 if (isInlineFormat(format)) { 17519 mergeSiblings(editor, format, vars, lastClone); 17520 } 17521 } 17522 } 17523 return container; 17524 }; 17525 const removeFormatInternal = (ed, name, vars, node, similar) => { 17526 const formatList = ed.formatter.get(name); 17527 const format = formatList[0]; 17528 const dom = ed.dom; 17529 const selection = ed.selection; 17530 const splitToFormatRoot = (container) => { 17531 const formatRoot = findFormatRoot(ed, container, name, vars, similar); 17532 return wrapAndSplit(ed, formatList, formatRoot, container, container, true, format, vars); 17533 }; 17534 // Make sure to only check for bookmarks created here (eg _start or _end) 17535 // as there maybe nested bookmarks 17536 const isRemoveBookmarkNode = (node) => isBookmarkNode$1(node) && isElement$7(node) && (node.id === '_start' || node.id === '_end'); 17537 const removeFormatOnNode = (node) => exists(formatList, (fmt) => removeNodeFormat(ed, fmt, vars, node, node)); 17538 // Merges the styles for each node 17539 const process = (node) => { 17540 // Grab the children first since the nodelist might be changed 17541 const children = from(node.childNodes); 17542 // Process current node 17543 const removed = removeFormatOnNode(node); 17544 // TINY-6567/TINY-7393: Include the parent if using an expanded selector format and no match was found for the current node 17545 const currentNodeMatches = removed || exists(formatList, (f) => matchName(dom, node, f)); 17546 const parentNode = node.parentNode; 17547 if (!currentNodeMatches && isNonNullable(parentNode) && shouldExpandToSelector(format)) { 17548 removeFormatOnNode(parentNode); 17549 } 17550 // Process the children 17551 if (format.deep) { 17552 if (children.length) { 17553 for (let i = 0; i < children.length; i++) { 17554 process(children[i]); 17555 } 17556 } 17557 } 17558 // Note: Assists with cleaning up any stray text decorations that may been applied when text decorations 17559 // and text colors were merged together from an applied format 17560 // Remove child span if it only contains text-decoration and a parent node also has the same text decoration. 17561 const textDecorations = ['underline', 'line-through', 'overline']; 17562 each$e(textDecorations, (decoration) => { 17563 if (isElement$7(node) && ed.dom.getStyle(node, 'text-decoration') === decoration && 17564 node.parentNode && getTextDecoration(dom, node.parentNode) === decoration) { 17565 removeNodeFormat(ed, { 17566 deep: false, 17567 exact: true, 17568 inline: 'span', 17569 styles: { 17570 textDecoration: decoration 17571 } 17572 }, undefined, node); 17573 } 17574 }); 17575 }; 17576 const unwrap = (start) => { 17577 const node = dom.get(start ? '_start' : '_end'); 17578 if (node) { 17579 let out = node[start ? 'firstChild' : 'lastChild']; 17580 // If the end is placed within the start the result will be removed 17581 // So this checks if the out node is a bookmark node if it is it 17582 // checks for another more suitable node 17583 if (isRemoveBookmarkNode(out)) { 17584 out = out[start ? 'firstChild' : 'lastChild']; 17585 } 17586 // Since dom.remove removes empty text nodes then we need to try to find a better node 17587 if (isText$b(out) && out.data.length === 0) { 17588 out = start ? node.previousSibling || node.nextSibling : node.nextSibling || node.previousSibling; 17589 } 17590 dom.remove(node, true); 17591 return out; 17592 } 17593 else { 17594 return null; 17595 } 17596 }; 17597 const removeRngStyle = (rng) => { 17598 let startContainer; 17599 let endContainer; 17600 let expandedRng = expandRng(dom, rng, formatList, { includeTrailingSpace: rng.collapsed }); 17601 if (format.split) { 17602 // Split text nodes 17603 expandedRng = split(expandedRng); 17604 startContainer = getContainer(ed, expandedRng, true); 17605 endContainer = getContainer(ed, expandedRng); 17606 if (startContainer !== endContainer) { 17607 // WebKit will render the table incorrectly if we wrap a TH or TD in a SPAN 17608 // so let's see if we can use the first/last child instead 17609 // This will happen if you triple click a table cell and use remove formatting 17610 startContainer = normalizeTableSelection(startContainer, true); 17611 endContainer = normalizeTableSelection(endContainer, false); 17612 // Wrap and split if nested 17613 if (isChildOfInlineParent(dom, startContainer, endContainer)) { 17614 const marker = Optional.from(startContainer.firstChild).getOr(startContainer); 17615 splitToFormatRoot(wrapWithSiblings(dom, marker, true, 'span', { 'id': '_start', 'data-mce-type': 'bookmark' })); 17616 unwrap(true); 17617 return; 17618 } 17619 // Wrap and split if nested 17620 if (isChildOfInlineParent(dom, endContainer, startContainer)) { 17621 const marker = Optional.from(endContainer.lastChild).getOr(endContainer); 17622 splitToFormatRoot(wrapWithSiblings(dom, marker, false, 'span', { 'id': '_end', 'data-mce-type': 'bookmark' })); 17623 unwrap(false); 17624 return; 17625 } 17626 // Wrap start/end nodes in span element since these might be cloned/moved 17627 startContainer = wrap$1(dom, startContainer, 'span', { 'id': '_start', 'data-mce-type': 'bookmark' }); 17628 endContainer = wrap$1(dom, endContainer, 'span', { 'id': '_end', 'data-mce-type': 'bookmark' }); 17629 // Split start/end and anything in between 17630 const newRng = dom.createRng(); 17631 newRng.setStartAfter(startContainer); 17632 newRng.setEndBefore(endContainer); 17633 walk$3(dom, newRng, (nodes) => { 17634 each$e(nodes, (n) => { 17635 if (!isBookmarkNode$1(n) && !isBookmarkNode$1(n.parentNode)) { 17636 splitToFormatRoot(n); 17637 } 17638 }); 17639 }); 17640 splitToFormatRoot(startContainer); 17641 splitToFormatRoot(endContainer); 17642 // Unwrap start/end to get real elements again 17643 // Note that the return value should always be a node since it's wrapped above 17644 startContainer = unwrap(true); 17645 endContainer = unwrap(); 17646 } 17647 else { 17648 startContainer = endContainer = splitToFormatRoot(startContainer); 17649 } 17650 // Update range positions since they might have changed after the split operations 17651 expandedRng.startContainer = startContainer.parentNode ? startContainer.parentNode : startContainer; 17652 expandedRng.startOffset = dom.nodeIndex(startContainer); 17653 expandedRng.endContainer = endContainer.parentNode ? endContainer.parentNode : endContainer; 17654 expandedRng.endOffset = dom.nodeIndex(endContainer) + 1; 17655 } 17656 // Remove items between start/end 17657 walk$3(dom, expandedRng, (nodes) => { 17658 each$e(nodes, process); 17659 }); 17660 }; 17661 // Handle node 17662 if (node) { 17663 if (isNode(node)) { 17664 const rng = dom.createRng(); 17665 rng.setStartBefore(node); 17666 rng.setEndAfter(node); 17667 removeRngStyle(rng); 17668 } 17669 else { 17670 removeRngStyle(node); 17671 } 17672 fireFormatRemove(ed, name, node, vars); 17673 return; 17674 } 17675 if (!selection.isCollapsed() || !isInlineFormat(format) || getCellsFromEditor(ed).length) { 17676 // Remove formatting on the selection 17677 preserveSelection(ed, () => runOnRanges(ed, removeRngStyle), 17678 // Before trying to move the start of the selection, check if start element still has formatting then we are at: "<b>text|</b>text" 17679 // and need to move the start into the next text node 17680 (startNode) => isInlineFormat(format) && match$2(ed, name, vars, startNode)); 17681 ed.nodeChanged(); 17682 } 17683 else { 17684 removeCaretFormat(ed, name, vars, similar); 17685 } 17686 removeListStyleFormats(ed, name, vars); 17687 fireFormatRemove(ed, name, node, vars); 17688 }; 17689 const removeFormat$1 = (ed, name, vars, node, similar) => { 17690 if (node || ed.selection.isEditable()) { 17691 removeFormatInternal(ed, name, vars, node, similar); 17692 } 17693 }; 17694 const removeFormatOnElement = (editor, format, vars, node) => { 17695 return removeNodeFormatInternal(editor, format, vars, node).fold(() => Optional.some(node), (newName) => Optional.some(editor.dom.rename(node, newName)), Optional.none); 17696 }; 17697 /** 17698 * Removes the specified format for the specified node. It will also remove the node if it doesn't have 17699 * any attributes if the format specifies it to do so. 17700 * 17701 * @private 17702 * @param {Object} format Format object with items to remove from node. 17703 * @param {Object} vars Name/value object with variables to apply to format. 17704 * @param {Node} node Node to remove the format styles on. 17705 * @param {Node} compareNode Optional compare node, if specified the styles will be compared to that node. 17706 * @return {Boolean} True/false if the node was removed or not. 17707 */ 17708 const removeNodeFormat = (editor, format, vars, node, compareNode) => { 17709 return removeNodeFormatInternal(editor, format, vars, node, compareNode).fold(never, (newName) => { 17710 // If renaming we are guaranteed this is a Element, so cast 17711 editor.dom.rename(node, newName); 17712 return true; 17713 }, always); 17714 }; 17715 17716 const fontSizeAlteringFormats = ['fontsize', 'subscript', 'superscript']; 17717 const formatsToActOn = ['strikethrough', ...fontSizeAlteringFormats]; 17718 const hasFormat = (formatter, el, format) => isNonNullable(formatter.matchNode(el.dom, format, {}, format === 'fontsize')); 17719 const isFontSizeAlteringElement = (formatter, el) => exists(fontSizeAlteringFormats, (format) => hasFormat(formatter, el, format)); 17720 const isNormalizingFormat = (format) => contains$2(formatsToActOn, format); 17721 const gatherWrapperData = (isRoot, scope, hasFormat, createFormatElement, removeFormatFromElement) => { 17722 const parents = parents$1(scope, isRoot).filter(isElement$8); 17723 return findLastIndex(parents, hasFormat).map((index) => { 17724 const container = parents[index]; 17725 const innerWrapper = createFormatElement(container); 17726 const outerWrappers = [ 17727 ...removeFormatFromElement(shallow(container)).toArray(), 17728 ...bind$3(parents.slice(0, index), (wrapper) => { 17729 if (hasFormat(wrapper)) { 17730 return removeFormatFromElement(wrapper).toArray(); 17731 } 17732 else { 17733 return [shallow(wrapper)]; 17734 } 17735 }) 17736 ]; 17737 return { container, innerWrapper, outerWrappers }; 17738 }); 17739 }; 17740 const wrapChildrenInInnerWrapper = (target, wrapper, hasFormat, removeFormatFromElement) => { 17741 each$e(children$
177411(target), (child) => { 17742 if (isElement$8(child) && hasFormat(child)) { 17743 if (removeFormatFromElement(child).isNone()) { 17744 unwrap(child); 17745 } 17746 } 17747 }); 17748 each$e(children$1
vendor: 24,488 bytes, lines 17748-18317
17748(target), (child) => append$1(wrapper, child)); 17749 prepend(target, wrapper); 17750 }; 17751 const wrapInOuterWrappers = (target, wrappers) => { 17752 if (wrappers.length > 0) { 17753 const outermost = wrappers[wrappers.length - 1]; 17754 before$4(target, outermost); 17755 const innerMost = foldl(wrappers.slice(0, wrappers.length - 1), (acc, wrapper) => { 17756 append$1(acc, wrapper); 17757 return wrapper; 17758 }, outermost); 17759 append$1(innerMost, target); 17760 } 17761 }; 17762 const normalizeFontSizeElementsInternal = (domUtils, fontSizeElements, hasFormat, createFormatElement, removeFormatFromElement) => { 17763 const isRoot = (el) => eq(SugarElement.fromDom(domUtils.getRoot()), el) || domUtils.isBlock(el.dom); 17764 each$e(fontSizeElements, (fontSizeElement) => { 17765 gatherWrapperData(isRoot, fontSizeElement, hasFormat, createFormatElement, removeFormatFromElement).each(({ container, innerWrapper, outerWrappers }) => { 17766 domUtils.split(container.dom, fontSizeElement.dom); 17767 wrapChildrenInInnerWrapper(fontSizeElement, innerWrapper, hasFormat, removeFormatFromElement); 17768 wrapInOuterWrappers(fontSizeElement, outerWrappers); 17769 }); 17770 }); 17771 }; 17772 const normalizeFontSizeElementsWithFormat = (editor, formatName, fontSizeElements) => { 17773 const hasFormat = (el) => isNonNullable(matchNode$1(editor, el.dom, formatName)); 17774 const createFormatElement = (el) => { 17775 const newEl = SugarElement.fromTag(name(el)); 17776 const format = matchNode$1(editor, el.dom, formatName, {}); 17777 if (isNonNullable(format) && isApplyFormat(format)) { 17778 setElementFormat(editor, newEl.dom, format); 17779 } 17780 return newEl; 17781 }; 17782 const removeFormatFromElement = (el) => { 17783 const format = matchNode$1(editor, el.dom, formatName, {}); 17784 if (isNonNullable(format)) { 17785 return removeFormatOnElement(editor, format, {}, el.dom).map(SugarElement.fromDom); 17786 } 17787 else { 17788 return Optional.some(el); 17789 } 17790 }; 17791 const bookmark = createBookmark(editor.selection.getRng()); 17792 normalizeFontSizeElementsInternal(editor.dom, fontSizeElements, hasFormat, createFormatElement, removeFormatFromElement); 17793 editor.selection.setRng(resolveBookmark(bookmark)); 17794 }; 17795 const collectFontSizeElements = (formatter, wrappers) => bind$3(wrappers, (wrapper) => { 17796 const fontSizeDescendants = descendants$1(wrapper, (el) => isFontSizeAlteringElement(formatter, el)); 17797 return isFontSizeAlteringElement(formatter, wrapper) ? [wrapper, ...fontSizeDescendants] : fontSizeDescendants; 17798 }); 17799 const normalizeFontSizeElementsAfterApply = (editor, appliedFormat, wrappers) => { 17800 if (isNormalizingFormat(appliedFormat)) { 17801 const fontSizeElements = collectFontSizeElements(editor.formatter, wrappers); 17802 normalizeFontSizeElementsWithFormat(editor, 'strikethrough', fontSizeElements); 17803 } 17804 }; 17805 const normalizeElements = (editor, elements) => { 17806 const fontSizeElements = filter$5(elements, (el) => isFontSizeAlteringElement(editor.formatter, el)); 17807 normalizeFontSizeElementsWithFormat(editor, 'strikethrough', fontSizeElements); 17808 }; 17809 17810 const isHeading = (node) => ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(node.name); 17811 const isSummary = (node) => node.name === 'summary'; 17812 17813 const traverse = (root, fn) => { 17814 let node = root; 17815 while ((node = node.walk())) { 17816 fn(node); 17817 } 17818 }; 17819 // Test a single node against the current filters, and add it to any match lists if necessary 17820 const matchNode = (nodeFilters, attributeFilters, node, matches) => { 17821 const name = node.name; 17822 // Match node filters 17823 for (let ni = 0, nl = nodeFilters.length; ni < nl; ni++) { 17824 const filter = nodeFilters[ni]; 17825 if (filter.name === name) { 17826 const match = matches.nodes[name]; 17827 if (match) { 17828 match.nodes.push(node); 17829 } 17830 else { 17831 matches.nodes[name] = { filter, nodes: [node] }; 17832 } 17833 } 17834 } 17835 // Match attribute filters 17836 if (node.attributes) { 17837 for (let ai = 0, al = attributeFilters.length; ai < al; ai++) { 17838 const filter = attributeFilters[ai]; 17839 const attrName = filter.name; 17840 if (attrName in node.attributes.map) { 17841 const match = matches.attributes[attrName]; 17842 if (match) { 17843 match.nodes.push(node); 17844 } 17845 else { 17846 matches.attributes[attrName] = { filter, nodes: [node] }; 17847 } 17848 } 17849 } 17850 } 17851 }; 17852 const findMatchingNodes = (nodeFilters, attributeFilters, node) => { 17853 const matches = { nodes: {}, attributes: {} }; 17854 if (node.firstChild) { 17855 traverse(node, (childNode) => { 17856 matchNode(nodeFilters, attributeFilters, childNode, matches); 17857 }); 17858 } 17859 return matches; 17860 }; 17861 // Run all necessary node filters and attribute filters, based on a match set 17862 const runFilters = (matches, args) => { 17863 const run = (matchRecord, filteringAttributes) => { 17864 each$d(matchRecord, (match) => { 17865 // in theory we don't need to copy the array, it was created purely for this filtering, but the method is exported so we can't guarantee that 17866 const nodes = from(match.nodes); 17867 each$e(match.filter.callbacks, (callback) => { 17868 // very very carefully mutate the nodes array based on whether the filter still matches them 17869 for (let i = nodes.length - 1; i >= 0; i--) { 17870 const node = nodes[i]; 17871 // Remove already removed children, and nodes that no longer match the filter 17872 const valueMatches = filteringAttributes ? node.attr(match.filter.name) !== undefined : node.name === match.filter.name; 17873 if (!valueMatches || isNullable(node.parent)) { 17874 nodes.splice(i, 1); 17875 } 17876 } 17877 if (nodes.length > 0) { 17878 callback(nodes, match.filter.name, args); 17879 } 17880 }); 17881 }); 17882 }; 17883 run(matches.nodes, false); 17884 run(matches.attributes, true); 17885 }; 17886 const filter$1 = (nodeFilters, attributeFilters, node, args = {}) => { 17887 const matches = findMatchingNodes(nodeFilters, attributeFilters, node); 17888 runFilters(matches, args); 17889 }; 17890 17891 const paddEmptyNode = (settings, args, isBlock, node) => { 17892 const brPreferred = settings.pad_empty_with_br || args.insert; 17893 if (brPreferred && isBlock(node)) { 17894 const astNode = new AstNode('br', 1); 17895 if (args.insert) { 17896 astNode.attr('data-mce-bogus', '1'); 17897 } 17898 node.empty().append(astNode); 17899 } 17900 else { 17901 node.empty().append(new AstNode('#text', 3)).value = nbsp; 17902 } 17903 }; 17904 const isPaddedWithNbsp = (node) => hasOnlyChild(node, '#text') && node?.firstChild?.value === nbsp; 17905 const hasOnlyChild = (node, name) => { 17906 const firstChild = node?.firstChild; 17907 return isNonNullable(firstChild) && firstChild === node.lastChild && firstChild.name === name; 17908 }; 17909 const isPadded = (schema, node) => { 17910 const rule = schema.getElementRule(node.name); 17911 return rule?.paddEmpty === true; 17912 }; 17913 const isEmpty$2 = (schema, nonEmptyElements, whitespaceElements, node) => node.isEmpty(nonEmptyElements, whitespaceElements, (node) => isPadded(schema, node)); 17914 const isLineBreakNode = (node, isBlock) => isNonNullable(node) && (isBlock(node) || node.name === 'br'); 17915 const findClosestEditingHost = (scope) => { 17916 let editableNode; 17917 for (let node = scope; node; node = node.parent) { 17918 const contentEditable = node.attr('contenteditable'); 17919 if (contentEditable === 'false') { 17920 break; 17921 } 17922 else if (contentEditable === 'true') { 17923 editableNode = node; 17924 } 17925 } 17926 return Optional.from(editableNode); 17927 }; 17928 const getAllDescendants = (scope) => { 17929 const collection = []; 17930 for (let node = scope.firstChild; isNonNullable(node); node = node.walk()) { 17931 collection.push(node); 17932 } 17933 return collection; 17934 }; 17935 17936 const removeOrUnwrapInvalidNode = (node, schema, originalNodeParent = node.parent) => { 17937 if (schema.getSpecialElements()[node.name]) { 17938 node.empty().remove(); 17939 } 17940 else { 17941 // are the children of `node` valid children of the top level parent? 17942 // if not, remove or unwrap them too 17943 const children = node.children(); 17944 for (const childNode of children) { 17945 if (originalNodeParent && !schema.isValidChild(originalNodeParent.name, childNode.name)) { 17946 removeOrUnwrapInvalidNode(childNode, schema, originalNodeParent); 17947 } 17948 } 17949 node.unwrap(); 17950 } 17951 }; 17952 const cleanInvalidNodes = (nodes, schema, rootNode, onCreate = noop) => { 17953 const textBlockElements = schema.getTextBlockElements(); 17954 const nonEmptyElements = schema.getNonEmptyElements(); 17955 const whitespaceElements = schema.getWhitespaceElements(); 17956 const nonSplittableElements = Tools.makeMap('tr,td,th,tbody,thead,tfoot,table,summary'); 17957 const fixed = new Set(); 17958 const isSplittableElement = (node) => node !== rootNode && !nonSplittableElements[node.name]; 17959 for (let ni = 0; ni < nodes.length; ni++) { 17960 const node = nodes[ni]; 17961 let parent; 17962 let newParent; 17963 let tempNode; 17964 // Don't bother if it's detached from the tree 17965 if (!node.parent || fixed.has(node)) { 17966 continue; 17967 } 17968 // If the invalid element is a text block, and the text block is within a parent LI element 17969 // Then unwrap the first text block and convert other sibling text blocks to LI elements similar to Word/Open Office 17970 if (textBlockElements[node.name] && node.parent.name === 'li') { 17971 // Move sibling text blocks after LI element 17972 let sibling = node.next; 17973 while (sibling) { 17974 if (textBlockElements[sibling.name]) { 17975 sibling.name = 'li'; 17976 fixed.add(sibling); 17977 node.parent.insert(sibling, node.parent); 17978 } 17979 else { 17980 break; 17981 } 17982 sibling = sibling.next; 17983 } 17984 // Unwrap current text block 17985 node.unwrap(); 17986 continue; 17987 } 17988 // Get list of all parent nodes until we find a valid parent to stick the child into 17989 const parents = [node]; 17990 for (parent = node.parent; parent && !schema.isValidChild(parent.name, node.name) && isSplittableElement(parent); parent = parent.parent) { 17991 parents.push(parent); 17992 } 17993 // Found a suitable parent 17994 if (parent && parents.length > 1) { 17995 // If the node is a valid child of the parent, then try to move it. Otherwise unwrap it 17996 if (!isInvalid(schema, node, parent)) { 17997 // Reverse the array since it makes looping easier 17998 parents.reverse(); 17999 // Clone the related parent and insert that after the moved node 18000 newParent = parents[0].clone(); 18001 onCreate(newParent); 18002 // Start cloning and moving children on the left side of the target node 18003 let currentNode = newParent; 18004 for (let i = 0; i < parents.length - 1; i++) { 18005 if (schema.isValidChild(currentNode.name, parents[i].name) && i > 0) { 18006 tempNode = parents[i].clone(); 18007 onCreate(tempNode); 18008 currentNode.append(tempNode); 18009 } 18010 else { 18011 tempNode = currentNode; 18012 } 18013 for (let childNode = parents[i].firstChild; childNode && childNode !== parents[i + 1];) { 18014 const nextNode = childNode.next; 18015 tempNode.append(childNode); 18016 childNode = nextNode; 18017 } 18018 currentNode = tempNode; 18019 } 18020 if (!isEmpty$2(schema, nonEmptyElements, whitespaceElements, newParent)) { 18021 parent.insert(newParent, parents[0], true); 18022 parent.insert(node, newParent); 18023 } 18024 else { 18025 parent.insert(node, parents[0], true); 18026 } 18027 // Check if the element is empty by looking through its contents, with special treatment for <p><br /></p> 18028 parent = parents[0]; 18029 if (isEmpty$2(schema, nonEmptyElements, whitespaceElements, parent) || hasOnlyChild(parent, 'br')) { 18030 parent.empty().remove(); 18031 } 18032 } 18033 else { 18034 removeOrUnwrapInvalidNode(node, schema); 18035 } 18036 } 18037 else if (node.parent) { 18038 // If it's an LI try to find a UL/OL for it or wrap it 18039 if (node.name === 'li') { 18040 let sibling = node.prev; 18041 if (sibling && (sibling.name === 'ul' || sibling.name === 'ol')) { 18042 sibling.append(node); 18043 continue; 18044 } 18045 sibling = node.next; 18046 if (sibling && (sibling.name === 'ul' || sibling.name === 'ol') && sibling.firstChild) { 18047 sibling.insert(node, sibling.firstChild, true); 18048 continue; 18049 } 18050 const wrapper = new AstNode('ul', 1); 18051 onCreate(wrapper); 18052 node.wrap(wrapper); 18053 continue; 18054 } 18055 // Try wrapping the element in a DIV 18056 if (schema.isValidChild(node.parent.name, 'div') && schema.isValidChild('div', node.name)) { 18057 const wrapper = new AstNode('div', 1); 18058 onCreate(wrapper); 18059 node.wrap(wrapper); 18060 } 18061 else { 18062 // We failed wrapping it, remove or unwrap it 18063 removeOrUnwrapInvalidNode(node, schema); 18064 } 18065 } 18066 } 18067 }; 18068 const hasClosest = (node, parentName) => { 18069 let tempNode = node; 18070 while (tempNode) { 18071 if (tempNode.name === parentName) { 18072 return true; 18073 } 18074 tempNode = tempNode.parent; 18075 } 18076 return false; 18077 }; 18078 // The `parent` parameter of `isInvalid` function represents the closest valid parent 18079 // under which the `node` is intended to be moved. 18080 const isInvalid = (schema, node, parent = node.parent) => { 18081 if (!parent) { 18082 return false; 18083 } 18084 // Check if the node is a valid child of the parent node. If the child is 18085 // unknown we don't collect it since it's probably a custom element 18086 if (schema.children[node.name] && !schema.isValidChild(parent.name, node.name)) { 18087 return true; 18088 } 18089 // Anchors are a special case and cannot be nested 18090 if (node.name === 'a' && hasClosest(parent, 'a')) { 18091 return true; 18092 } 18093 // heading element is valid if it is the only one child of summary 18094 if (isSummary(parent) && isHeading(node)) { 18095 return !(parent?.firstChild === node && parent?.lastChild === node); 18096 } 18097 return false; 18098 }; 18099 18100 const createRange = (sc, so, ec, eo) => { 18101 const rng = document.createRange(); 18102 rng.setStart(sc, so); 18103 rng.setEnd(ec, eo); 18104 return rng; 18105 }; 18106 // If you triple click a paragraph in this case: 18107 // <blockquote><p>a</p></blockquote><p>b</p> 18108 // It would become this range in webkit: 18109 // <blockquote><p>[a</p></blockquote><p>]b</p> 18110 // We would want it to be: 18111 // <blockquote><p>[a]</p></blockquote><p>b</p> 18112 // Since it would otherwise produces spans out of thin air on insertContent for example. 18113 const normalizeBlockSelectionRange = (rng) => { 18114 const startPos = CaretPosition.fromRangeStart(rng); 18115 const endPos = CaretPosition.fromRangeEnd(rng); 18116 const rootNode = rng.commonAncestorContainer; 18117 return fromPosition(false, rootNode, endPos) 18118 .map((newEndPos) => { 18119 if (!isInSameBlock(startPos, endPos, rootNode) && isInSameBlock(startPos, newEndPos, rootNode)) { 18120 return createRange(startPos.container(), startPos.offset(), newEndPos.container(), newEndPos.offset()); 18121 } 18122 else { 18123 return rng; 18124 } 18125 }).getOr(rng); 18126 }; 18127 const normalize = (rng) => rng.collapsed ? rng : normalizeBlockSelectionRange(rng); 18128 18129 const explode$1 = Tools.explode; 18130 const create$8 = () => { 18131 const filters = {}; 18132 const addFilter = (name, callback) => { 18133 each$e(explode$1(name), (name) => { 18134 if (!has$2(filters, name)) { 18135 filters[name] = { name, callbacks: [] }; 18136 } 18137 filters[name].callbacks.push(callback); 18138 }); 18139 }; 18140 const getFilters = () => values(filters); 18141 const removeFilter = (name, callback) => { 18142 each$e(explode$1(name), (name) => { 18143 if (has$2(filters, name)) { 18144 if (isNonNullable(callback)) { 18145 const filter = filters[name]; 18146 const newCallbacks = filter$5(filter.callbacks, (c) => c !== callback); 18147 // If all callbacks have been removed then remove the filter reference 18148 if (newCallbacks.length > 0) { 18149 filter.callbacks = newCallbacks; 18150 } 18151 else { 18152 delete filters[name]; 18153 } 18154 } 18155 else { 18156 delete filters[name]; 18157 } 18158 } 18159 }); 18160 }; 18161 return { 18162 addFilter, 18163 getFilters, 18164 removeFilter 18165 }; 18166 }; 18167 18168 const encodeData = (data) => data.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); 18169 const decodeData$1 = (data) => data.replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&'); 18170 18171 const removeAttrs = (node, names) => { 18172 each$e(names, (name) => { 18173 node.attr(name, null); 18174 }); 18175 }; 18176 const addFontToSpansFilter = (domParser, styles, fontSizes) => { 18177 domParser.addNodeFilter('font', (nodes) => { 18178 each$e(nodes, (node) => { 18179 const props = styles.parse(node.attr('style')); 18180 const color = node.attr('color'); 18181 const face = node.attr('face'); 18182 const size = node.attr('size'); 18183 if (color) { 18184 props.color = color; 18185 } 18186 if (face) { 18187 props['font-family'] = face; 18188 } 18189 if (size) { 18190 toInt(size).each((num) => { 18191 props['font-size'] = fontSizes[num - 1]; 18192 }); 18193 } 18194 node.name = 'span'; 18195 node.attr('style', styles.serialize(props)); 18196 removeAttrs(node, ['color', 'face', 'size']); 18197 }); 18198 }); 18199 }; 18200 const addStrikeFilter = (domParser, schema, styles) => { 18201 domParser.addNodeFilter('strike', (nodes) => { 18202 const convertToSTag = schema.type !== 'html4'; 18203 each$e(nodes, (node) => { 18204 if (convertToSTag) { 18205 node.name = 's'; 18206 } 18207 else { 18208 const props = styles.parse(node.attr('style')); 18209 props['text-decoration'] = 'line-through'; 18210 node.name = 'span'; 18211 node.attr('style', styles.serialize(props)); 18212 } 18213 }); 18214 }); 18215 }; 18216 const addFilters = (domParser, settings, schema) => { 18217 const styles = Styles(); 18218 if (settings.convert_fonts_to_spans) { 18219 addFontToSpansFilter(domParser, styles, Tools.explode(settings.font_size_legacy_values ?? '')); 18220 } 18221 addStrikeFilter(domParser, schema, styles); 18222 }; 18223 const register$5 = (domParser, settings, schema) => { 18224 if (settings.inline_styles) { 18225 addFilters(domParser, settings, schema); 18226 } 18227 }; 18228 18229 const blobUriToBlob = (url) => fetch(url) 18230 .then((res) => res.ok ? res.blob() : Promise.reject()) 18231 .catch(() => Promise.reject({ 18232 message: `Cannot convert ${url} to Blob. Resource might not exist or is inaccessible.`, 18233 uriType: 'blob' 18234 })); 18235 const extractBase64Data = (data) => { 18236 const matches = /([a-z0-9+\/=\s]+)/i.exec(data); 18237 return matches ? matches[1] : ''; 18238 }; 18239 const decodeData = (data) => { 18240 try { 18241 return decodeURIComponent(data); 18242 } 18243 catch { 18244 return data; 18245 } 18246 }; 18247 const parseDataUri = (uri) => { 18248 const [type, ...rest] = uri.split(','); 18249 const data = rest.join(','); 18250 const matches = /data:([^/]+\/[^;]+)(;.+)?/.exec(type); 18251 if (matches) { 18252 const base64Encoded = matches[2] === ';base64'; 18253 const decodedData = decodeData(data); 18254 const extractedData = base64Encoded ? extractBase64Data(decodedData) : decodedData; 18255 return Optional.some({ 18256 type: matches[1], 18257 data: extractedData, 18258 base64Encoded 18259 }); 18260 } 18261 else { 18262 return Optional.none(); 18263 } 18264 }; 18265 const buildBlob = (type, data, base64Encoded = true) => { 18266 let str = data; 18267 if (base64Encoded) { 18268 // Might throw error if data isn't proper base64 18269 try { 18270 str = atob(data); 18271 } 18272 catch { 18273 return Optional.none(); 18274 } 18275 } 18276 const arr = new Uint8Array(str.length); 18277 for (let i = 0; i < arr.length; i++) { 18278 arr[i] = str.charCodeAt(i); 18279 } 18280 return Optional.some(new Blob([arr], { type })); 18281 }; 18282 const dataUriToBlob = (uri) => { 18283 return new Promise((resolve, reject) => { 18284 parseDataUri(uri) 18285 .bind(({ type, data, base64Encoded }) => buildBlob(type, data, base64Encoded)) 18286 .fold(() => reject('Invalid data URI'), resolve); 18287 }); 18288 }; 18289 const uriToBlob = (url) => { 18290 if (startsWith(url, 'blob:')) { 18291 return blobUriToBlob(url); 18292 } 18293 else if (startsWith(url, 'data:')) { 18294 return dataUriToBlob(url); 18295 } 18296 else { 18297 return Promise.reject('Unknown URI format'); 18298 } 18299 }; 18300 const blobToDataUri = (blob) => { 18301 return new Promise((resolve, reject) => { 18302 const reader = new FileReader(); 18303 reader.onloadend = () => { 18304 resolve(reader.result); 18305 }; 18306 reader.onerror = () => { 18307 reject(reader.error?.message); 18308 }; 18309 reader.readAsDataURL(blob); 18310 }); 18311 }; 18312 18313 let count$1 = 0; 18314 const uniqueId$1 = (prefix) => { 18315 return (prefix || 'blobid') + (count$1++); 18316 }; 18317 const
vendor: 13,363 bytes, lines 18317-18620
18317processDataUri = (dataUri, base64Only, generateBlobInfo) => { 18318 return parseDataUri(dataUri).bind(({ data, type, base64Encoded }) => { 18319 if (base64Only && !base64Encoded) { 18320 return Optional.none(); 18321 } 18322 else { 18323 const base64 = base64Encoded ? data : btoa(data); 18324 return generateBlobInfo(base64, type); 18325 } 18326 }); 18327 }; 18328 const createBlobInfo$1 = (blobCache, blob, base64) => { 18329 const blobInfo = blobCache.create(uniqueId$1(), blob, base64); 18330 blobCache.add(blobInfo); 18331 return blobInfo; 18332 }; 18333 const dataUriToBlobInfo = (blobCache, dataUri, base64Only = false) => { 18334 return processDataUri(dataUri, base64Only, (base64, type) => Optional.from(blobCache.getByData(base64, type)).orThunk(() => buildBlob(type, base64).map((blob) => createBlobInfo$1(blobCache, blob, base64)))); 18335 }; 18336 const imageToBlobInfo = (blobCache, imageSrc) => { 18337 const invalidDataUri = () => Promise.reject('Invalid data URI'); 18338 if (startsWith(imageSrc, 'blob:')) { 18339 const blobInfo = blobCache.getByUri(imageSrc); 18340 if (isNonNullable(blobInfo)) { 18341 return Promise.resolve(blobInfo); 18342 } 18343 else { 18344 return uriToBlob(imageSrc).then((blob) => { 18345 return blobToDataUri(blob).then((dataUri) => { 18346 return processDataUri(dataUri, false, (base64) => { 18347 return Optional.some(createBlobInfo$1(blobCache, blob, base64)); 18348 }).getOrThunk(invalidDataUri); 18349 }); 18350 }); 18351 } 18352 } 18353 else if (startsWith(imageSrc, 'data:')) { 18354 return dataUriToBlobInfo(blobCache, imageSrc).fold(invalidDataUri, (blobInfo) => Promise.resolve(blobInfo)); 18355 } 18356 else { 18357 // Not a blob or data URI so the image isn't a local image and isn't something that can be processed 18358 return Promise.reject('Unknown image data format'); 18359 } 18360 }; 18361 18362 // TINY-10350: A modification of the Regexes.link regex to specifically capture host. 18363 // eslint-disable-next-line max-len 18364 const hostCaptureRegex = /^(?:(?:(?:[A-Za-z][A-Za-z\d.+-]{0,14}:\/\/(?:[-.~*+=!&;:'%@?^${}(),\w]+@)?|www\.|[-;:&=+$,.\w]+@)([A-Za-z\d-]+(?:\.[A-Za-z\d-]+)*))(?::\d+)?(?:\/(?:[-.~*+=!;:'%@$(),\/\w]*[-~*+=%@$()\/\w])?)?(?:\?(?:[-.~*+=!&;:'%@?^${}(),\/\w]+)?)?(?:#(?:[-.~*+=!&;:'%@?^${}(),\/\w]+)?)?)$/; 18365 const extractHost = (url) => Optional.from(url.match(hostCaptureRegex)).bind((ms) => get$b(ms, 1)).map((h) => startsWith(h, 'www.') ? h.substring(4) : h); 18366 18367 const sandboxIframe = (iframeNode, exclusions) => { 18368 if (Optional.from(iframeNode.attr('src')).bind(extractHost).forall((host) => !contains$2(exclusions, host))) { 18369 iframeNode.attr('sandbox', ''); 18370 } 18371 }; 18372 const isMimeType = (mime, type) => startsWith(mime, `${type}/`); 18373 const getEmbedType = (type) => { 18374 if (isUndefined(type)) { 18375 return 'iframe'; 18376 } 18377 else if (isMimeType(type, 'image')) { 18378 return 'img'; 18379 } 18380 else if (isMimeType(type, 'video')) { 18381 return 'video'; 18382 } 18383 else if (isMimeType(type, 'audio')) { 18384 return 'audio'; 18385 } 18386 else { 18387 return 'iframe'; 18388 } 18389 }; 18390 const createSafeEmbed = ({ type, src, width, height } = {}, sandboxIframes, sandboxIframesExclusions) => { 18391 const name = getEmbedType(type); 18392 const embed = new AstNode(name, 1); 18393 embed.attr(name === 'audio' ? { src } : { src, width, height }); 18394 // TINY-10349: Show controls for audio and video so the replaced embed is visible in editor. 18395 if (name === 'audio' || name === 'video') { 18396 embed.attr('controls', ''); 18397 } 18398 if (name === 'iframe' && sandboxIframes) { 18399 sandboxIframe(embed, sandboxIframesExclusions); 18400 } 18401 return embed; 18402 }; 18403 18404 const isBogusImage = (img) => isNonNullable(img.attr('data-mce-bogus')); 18405 const isInternalImageSource = (img) => img.attr('src') === Env.transparentSrc || isNonNullable(img.attr('data-mce-placeholder')); 18406 const registerBase64ImageFilter = (parser, settings) => { 18407 const { blob_cache: blobCache } = settings; 18408 if (blobCache) { 18409 const processImage = (img) => { 18410 const inputSrc = img.attr('src'); 18411 if (isInternalImageSource(img) || isBogusImage(img) || isNullable(inputSrc)) { 18412 return; 18413 } 18414 dataUriToBlobInfo(blobCache, inputSrc, true).each((blobInfo) => { 18415 img.attr('src', blobInfo.blobUri()); 18416 }); 18417 }; 18418 parser.addAttributeFilter('src', (nodes) => each$e(nodes, processImage)); 18419 } 18420 }; 18421 const register$4 = (parser, settings) => { 18422 const schema = parser.schema; 18423 parser.addAttributeFilter('href', (nodes) => { 18424 let i = nodes.length; 18425 const appendRel = (rel) => { 18426 const parts = rel.split(' ').filter((p) => p.length > 0); 18427 return parts.concat(['noopener']).sort().join(' '); 18428 }; 18429 const addNoOpener = (rel) => { 18430 const newRel = rel ? Tools.trim(rel) : ''; 18431 if (!/\b(noopener)\b/g.test(newRel)) { 18432 return appendRel(newRel); 18433 } 18434 else { 18435 return newRel; 18436 } 18437 }; 18438 if (!settings.allow_unsafe_link_target) { 18439 while (i--) { 18440 const node = nodes[i]; 18441 if (node.name === 'a' && node.attr('target') === '_blank') { 18442 node.attr('rel', addNoOpener(node.attr('rel'))); 18443 } 18444 } 18445 } 18446 }); 18447 // Force anchor names closed, unless the setting "allow_html_in_named_anchor" is explicitly included. 18448 if (!settings.allow_html_in_named_anchor) { 18449 parser.addAttributeFilter('id,name', (nodes) => { 18450 let i = nodes.length, sibling, prevSibling, parent, node; 18451 while (i--) { 18452 node = nodes[i]; 18453 if (node.name === 'a' && node.firstChild && !node.attr('href')) { 18454 parent = node.parent; 18455 // Move children after current node 18456 sibling = node.lastChild; 18457 while (sibling && parent) { 18458 prevSibling = sibling.prev; 18459 parent.insert(sibling, node); 18460 sibling = prevSibling; 18461 } 18462 } 18463 } 18464 }); 18465 } 18466 if (settings.fix_list_elements) { 18467 parser.addNodeFilter('ul,ol', (nodes) => { 18468 let i = nodes.length, node, parentNode; 18469 while (i--) { 18470 node = nodes[i]; 18471 parentNode = node.parent; 18472 if (parentNode && (parentNode.name === 'ul' || parentNode.name === 'ol')) { 18473 if (node.prev && node.prev.name === 'li') { 18474 node.prev.append(node); 18475 } 18476 else { 18477 const li = new AstNode('li', 1); 18478 li.attr('style', 'list-style-type: none'); 18479 node.wrap(li); 18480 } 18481 } 18482 } 18483 }); 18484 } 18485 const validClasses = schema.getValidClasses(); 18486 if (settings.validate && validClasses) { 18487 parser.addAttributeFilter('class', (nodes) => { 18488 let i = nodes.length; 18489 while (i--) { 18490 const node = nodes[i]; 18491 const clazz = node.attr('class') ?? ''; 18492 const classList = Tools.explode(clazz, ' '); 18493 let classValue = ''; 18494 for (let ci = 0; ci < classList.length; ci++) { 18495 const className = classList[ci]; 18496 let valid = false; 18497 let validClassesMap = validClasses['*']; 18498 if (validClassesMap && validClassesMap[className]) { 18499 valid = true; 18500 } 18501 validClassesMap = validClasses[node.name]; 18502 if (!valid && validClassesMap && validClassesMap[className]) { 18503 valid = true; 18504 } 18505 if (valid) { 18506 if (classValue) { 18507 classValue += ' '; 18508 } 18509 classValue += className; 18510 } 18511 } 18512 if (!classValue.length) { 18513 classValue = null; 18514 } 18515 node.attr('class', classValue); 18516 } 18517 }); 18518 } 18519 registerBase64ImageFilter(parser, settings); 18520 const shouldSandboxIframes = settings.sandbox_iframes ?? false; 18521 const sandboxIframesExclusions = unique$1(settings.sandbox_iframes_exclusions ?? []); 18522 if (settings.convert_unsafe_embeds) { 18523 parser.addNodeFilter('object,embed', (nodes) => each$e(nodes, (node) => { 18524 node.replace(createSafeEmbed({ 18525 type: node.attr('type'), 18526 src: node.name === 'object' ? node.attr('data') : node.attr('src'), 18527 width: node.attr('width'), 18528 height: node.attr('height'), 18529 }, shouldSandboxIframes, sandboxIframesExclusions)); 18530 })); 18531 } 18532 if (shouldSandboxIframes) { 18533 parser.addNodeFilter('iframe', (nodes) => each$e(nodes, (node) => sandboxIframe(node, sandboxIframesExclusions))); 18534 } 18535 }; 18536 18537 /*! @license DOMPurify 3.3.2 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.3.2/LICENSE */ 18538 18539 const { 18540 entries, 18541 setPrototypeOf, 18542 isFrozen, 18543 getPrototypeOf, 18544 getOwnPropertyDescriptor 18545 } = Object; 18546 let { 18547 freeze, 18548 seal, 18549 create: create$7 18550 } = Object; // eslint-disable-line import/no-mutable-exports 18551 let { 18552 apply, 18553 construct 18554 } = typeof Reflect !== 'undefined' && Reflect; 18555 if (!freeze) { 18556 freeze = function freeze(x) { 18557 return x; 18558 }; 18559 } 18560 if (!seal) { 18561 seal = function seal(x) { 18562 return x; 18563 }; 18564 } 18565 if (!apply) { 18566 apply = function apply(func, thisArg) { 18567 for (var _len = arguments.length, args = new Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) { 18568 args[_key - 2] = arguments[_key]; 18569 } 18570 return func.apply(thisArg, args); 18571 }; 18572 } 18573 if (!construct) { 18574 construct = function construct(Func) { 18575 for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { 18576 args[_key2 - 1] = arguments[_key2]; 18577 } 18578 return new Func(...args); 18579 }; 18580 } 18581 const arrayForEach = unapply(Array.prototype.forEach); 18582 const arrayLastIndexOf = unapply(Array.prototype.lastIndexOf); 18583 const arrayPop = unapply(Array.prototype.pop); 18584 const arrayPush = unapply(Array.prototype.push); 18585 const arraySplice = unapply(Array.prototype.splice); 18586 const stringToLowerCase = unapply(String.prototype.toLowerCase); 18587 const stringToString = unapply(String.prototype.toString); 18588 const stringMatch = unapply(String.prototype.match); 18589 const stringReplace = unapply(String.prototype.replace); 18590 const stringIndexOf = unapply(String.prototype.indexOf); 18591 const stringTrim = unapply(String.prototype.trim); 18592 const objectHasOwnProperty = unapply(Object.prototype.hasOwnProperty); 18593 const regExpTest = unapply(RegExp.prototype.test); 18594 const typeErrorCreate = unconstruct(TypeError); 18595 /** 18596 * Creates a new function that calls the given function with a specified thisArg and arguments. 18597 * 18598 * @param func - The function to be wrapped and called. 18599 * @returns A new function that calls the given function with a specified thisArg and arguments. 18600 */ 18601 function unapply(func) { 18602 return function (thisArg) { 18603 if (thisArg instanceof RegExp) { 18604 thisArg.lastIndex = 0; 18605 } 18606 for (var _len3 = arguments.length, args = new Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) { 18607 args[_key3 - 1] = arguments[_key3]; 18608 } 18609 return apply(func, thisArg, args); 18610 }; 18611 } 18612 /** 18613 * Creates a new function that constructs an instance of the given constructor function with the provided arguments. 18614 * 18615 * @param func - The constructor function to be wrapped and called. 18616 * @returns A new function that constructs an instance of the given constructor function with the provided arguments. 18617 */ 18618 function unconstruct(Func) { 18619 return function () { 18620 for (var _len4 = arguments.length, args = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++) {
vendor: 6,587 bytes, lines 18621-18733
18621 args[_key4] = arguments[_key4]; 18622 } 18623 return construct(Func, args); 18624 }; 18625 } 18626 /** 18627 * Add properties to a lookup table 18628 * 18629 * @param set - The set to which elements will be added. 18630 * @param array - The array containing elements to be added to the set. 18631 * @param transformCaseFunc - An optional function to transform the case of each element before adding to the set. 18632 * @returns The modified set with added elements. 18633 */ 18634 function addToSet(set, array) { 18635 let transformCaseFunc = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : stringToLowerCase; 18636 if (setPrototypeOf) { 18637 // Make 'in' and truthy checks like Boolean(set.constructor) 18638 // independent of any properties defined on Object.prototype. 18639 // Prevent prototype setters from intercepting set as a this value. 18640 setPrototypeOf(set, null); 18641 } 18642 let l = array.length; 18643 while (l--) { 18644 let element = array[l]; 18645 if (typeof element === 'string') { 18646 const lcElement = transformCaseFunc(element); 18647 if (lcElement !== element) { 18648 // Config presets (e.g. tags.js, attrs.js) are immutable. 18649 if (!isFrozen(array)) { 18650 array[l] = lcElement; 18651 } 18652 element = lcElement; 18653 } 18654 } 18655 set[element] = true; 18656 } 18657 return set; 18658 } 18659 /** 18660 * Clean up an array to harden against CSPP 18661 * 18662 * @param array - The array to be cleaned. 18663 * @returns The cleaned version of the array 18664 */ 18665 function cleanArray(array) { 18666 for (let index = 0; index < array.length; index++) { 18667 const isPropertyExist = objectHasOwnProperty(array, index); 18668 if (!isPropertyExist) { 18669 array[index] = null; 18670 } 18671 } 18672 return array; 18673 } 18674 /** 18675 * Shallow clone an object 18676 * 18677 * @param object - The object to be cloned. 18678 * @returns A new object that copies the original. 18679 */ 18680 function clone(object) { 18681 const newObject = create$7(null); 18682 for (const [property, value] of entries(object)) { 18683 const isPropertyExist = objectHasOwnProperty(object, property); 18684 if (isPropertyExist) { 18685 if (Array.isArray(value)) { 18686 newObject[property] = cleanArray(value); 18687 } else if (value && typeof value === 'object' && value.constructor === Object) { 18688 newObject[property] = clone(value); 18689 } else { 18690 newObject[property] = value; 18691 } 18692 } 18693 } 18694 return newObject; 18695 } 18696 /** 18697 * This method automatically checks if the prop is function or getter and behaves accordingly. 18698 * 18699 * @param object - The object to look up the getter function in its prototype chain. 18700 * @param prop - The property name for which to find the getter function. 18701 * @returns The getter function found in the prototype chain or a fallback function. 18702 */ 18703 function lookupGetter(object, prop) { 18704 while (object !== null) { 18705 const desc = getOwnPropertyDescriptor(object, prop); 18706 if (desc) { 18707 if (desc.get) { 18708 return unapply(desc.get); 18709 } 18710 if (typeof desc.value === 'function') { 18711 return unapply(desc.value); 18712 } 18713 } 18714 object = getPrototypeOf(object); 18715 } 18716 function fallbackValue() { 18717 return null; 18718 } 18719 return fallbackValue; 18720 } 18721 18722 const html$1 = freeze(['a', 'abbr', 'acronym', 'address', 'area', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo', 'big', 'blink', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'content', 'data', 'datalist', 'dd', 'decorator', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'element', 'em', 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'main', 'map', 'mark', 'marquee', 'menu', 'menuitem', 'meter', 'nav', 'nobr', 'ol', 'optgroup', 'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'search', 'section', 'select', 'shadow', 'slot', 'small', 'source', 'spacer', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'wbr']); 18723 const svg$1 = freeze(['svg', 'a', 'altglyph', 'altglyphdef', 'altglyphitem', 'animatecolor', 'animatemotion', 'animatetransform', 'circle', 'clippath', 'defs', 'desc', 'ellipse', 'enterkeyhint', 'exportparts', 'filter', 'font', 'g', 'glyph', 'glyphref', 'hkern', 'image', 'inputmode', 'line', 'lineargradient', 'marker', 'mask', 'metadata', 'mpath', 'part', 'path', 'pattern', 'polygon', 'polyline', 'radialgradient', 'rect', 'stop', 'style', 'switch', 'symbol', 'text', 'textpath', 'title', 'tref', 'tspan', 'view', 'vkern']); 18724 const svgFilters = freeze(['feBlend', 'feColorMatrix', 'feComponentTransfer', 'feComposite', 'feConvolveMatrix', 'feDiffuseLighting', 'feDisplacementMap', 'feDistantLight', 'feDropShadow', 'feFlood', 'feFuncA', 'feFuncB', 'feFuncG', 'feFuncR', 'feGaussianBlur', 'feImage', 'feMerge', 'feMergeNode', 'feMorphology', 'feOffset', 'fePointLight', 'feSpecularLighting', 'feSpotLight', 'feTile', 'feTurbulence']); 18725 // List of SVG elements that are disallowed by default. 18726 // We still need to know them so that we can do namespace 18727 // checks properly in case one wants to add them to 18728 // allow-list. 18729 const svgDisallowed = freeze(['animate', 'color-profile', 'cursor', 'discard', 'font-face', 'font-face-format', 'font-face-name', 'font-face-src', 'font-face-uri', 'foreignobject', 'hatch', 'hatchpath', 'mesh', 'meshgradient', 'meshpatch', 'meshrow', 'missing-glyph', 'script', 'set', 'solidcolor', 'unknown', 'use']); 18730 const mathMl$1 = freeze(['math', 'menclose', 'merror', 'mfenced', 'mfrac', 'mglyph', 'mi', 'mlabeledtr', 'mmultiscripts', 'mn', 'mo', 'mover', 'mpadded', 'mphantom', 'mroot', 'mrow', 'ms', 'mspace', 'msqrt', 'mstyle', 'msub', 'msup', 'msubsup', 'mtable', 'mtd', 'mtext', 'mtr', 'munder', 'munderover', 'mprescripts']); 18731 // Similarly to SVG, we want to know all MathML elements, 18732 // even those that we disallow by default. 18733 const mathMlDisallowed = freeze(['maction', 'maligngroup', 'malignmark', 'mlongdiv', 'mscarries', 'mscarry', 'msgroup', 'mstack', 'msline', 'msrow', 'semantics', 'annotation', 'annotation-xml', 'mprescripts', 'n
18733one']); 18734 const text = freeze(['#text']); 18735 18736 const html = freeze(['accept', 'action', 'align', 'alt', 'autocapitalize', 'autocomplete', 'autopictureinpicture', 'autoplay', 'background', 'bgcolor', 'border', 'capture', 'cellpadding', 'cellspacing', 'checked', 'cite', 'class', 'clear', 'color', 'cols', 'colspan', 'controls', 'controlslist', 'coords', 'crossorigin', 'datetime', 'decoding', 'default', 'dir', 'disabled', 'disablepictureinpicture', 'disableremoteplayback', 'download', 'draggable', 'enctype', 'enterkeyhint', 'exportparts', 'face', 'for', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'id', 'inert', 'inputmode', 'integrity', 'ismap', 'kind', 'label', 'lang', 'list', 'loading', 'loop', 'low', 'max', 'maxlength', 'media', 'method', 'min', 'minlength', 'multiple', 'muted', 'name', 'nonce', 'noshade', 'novalidate', 'nowrap', 'open', 'optimum', 'part', 'pattern', 'placeholder', 'playsinline', 'popover', 'popovertarget', 'popovertargetaction', 'poster', 'preload', 'pubdate', 'radiogroup', 'readonly', 'rel', 'required', 'rev', 'reversed', 'role', 'rows', 'rowspan', 'spellcheck', 'scope', 'selected', 'shape', 'size', 'sizes', 'slot', 'span', 'srclang', 'start', 'src', 'srcset', 'step', 'style', 'summary', 'tabindex', 'title', 'translate', 'type', 'usemap', 'valign', 'value', 'width', 'wrap', 'xmlns', 'slot']); 18737 const svg = freeze(['accent-height', 'accumulate', 'additive', 'alignment-baseline', 'amplitude', 'ascent', 'attributename', 'attributetype', 'azimuth', 'basefrequency', 'baseline-shift', 'begin', 'bias', 'by', 'class', 'clip', 'clippathunits', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cx', 'cy', 'd', 'dx', 'dy', 'diffuseconstant', 'direction', 'display', 'divisor', 'dur', 'edgemode', 'elevation', 'end', 'exponent', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'filterunits', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'glyphref', 'gradientunits', 'gradienttransform', 'height', 'href', 'id', 'image-rendering', 'in', 'in2', 'intercept', 'k', 'k1', 'k2', 'k3', 'k4', 'kerning', 'keypoints', 'keysplines', 'keytimes', 'lang', 'lengthadjust', 'letter-spacing', 'kernelmatrix', 'kernelunitlength', 'lighting-color', 'local', 'marker-end', 'marker-mid', 'marker-start', 'markerheight', 'markerunits', 'markerwidth', 'maskcontentunits', 'maskunits', 'max', 'mask', 'mask-type', 'media', 'method', 'mode', 'min', 'name', 'numoctaves', 'offset', 'operator', 'opacity', 'order', 'orient', 'orientation', 'origin', 'overflow', 'paint-order', 'path', 'pathlength', 'patterncontentunits', 'patterntransform', 'patternunits', 'points', 'preservealpha', 'preserveaspectratio', 'primitiveunits', 'r', 'rx', 'ry', 'radius', 'refx', 'refy', 'repeatcount', 'repeatdur', 'restart', 'result', 'rotate', 'scale', 'seed', 'shape-rendering', 'slope', 'specularconstant', 'specularexponent', 'spreadmethod', 'startoffset', 'stddeviation', 'stitchtiles', 'stop-color', 'stop-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke', 'stroke-width', 'style', 'surfacescale', 'systemlanguage', 'tabindex', 'tablevalues', 'targetx', 'targety', 'transform', 'transform-origin', 'text-anchor', 'text-decoration', 'text-rendering', 'textlength', 'type', 'u1', 'u2', 'unicode', 'values', 'viewbox', 'visibility', 'version', 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', 'width', 'word-spacing', 'wrap', 'writing-mode', 'xchannelselector', 'ychannelselector', 'x', 'x1', 'x2', 'xmlns', 'y', 'y1', 'y2', 'z', 'zoomandpan']); 18738 const mathMl = freeze(['accent', 'accentunder', 'align', 'bevelled', 'close', 'columnsalign', 'columnlines', 'columnspan', 'denomalign', 'depth', 'dir', 'display', 'displaystyle', 'encoding', 'fence', 'frame', 'height', 'href', 'id', 'largeop', 'length', 'linethickness', 'lspace', 'lquote', 'mathbackground', 'mathcolor', 'mathsize', 'mathvariant', 'maxsize', 'minsize', 'movablelimits', 'n
vendor: 9,941 bytes, lines 18738-18975
18738otation', 'numalign', 'open', 'rowalign', 'rowlines', 'rowspacing', 'rowspan', 'rspace', 'rquote', 'scriptlevel', 'scriptminsize', 'scriptsizemultiplier', 'selection', 'separator', 'separators', 'stretchy', 'subscriptshift', 'supscriptshift', 'symmetric', 'voffset', 'width', 'xmlns']); 18739 const xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']); 18740 18741 // eslint-disable-next-line unicorn/better-regex 18742 const MUSTACHE_EXPR = seal(/\{\{[\w\W]*|[\w\W]*\}\}/gm); // Specify template detection regex for SAFE_FOR_TEMPLATES mode 18743 const ERB_EXPR = seal(/<%[\w\W]*|[\w\W]*%>/gm); 18744 const TMPLIT_EXPR = seal(/\$\{[\w\W]*/gm); // eslint-disable-line unicorn/better-regex 18745 const DATA_ATTR = seal(/^data-[\-\w.\u00B7-\uFFFF]+$/); // eslint-disable-line no-useless-escape 18746 const ARIA_ATTR = seal(/^aria-[\-\w]+$/); // eslint-disable-line no-useless-escape 18747 const IS_ALLOWED_URI = seal(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i // eslint-disable-line no-useless-escape 18748 ); 18749 const IS_SCRIPT_OR_DATA = seal(/^(?:\w+script|data):/i); 18750 const ATTR_WHITESPACE = seal(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g // eslint-disable-line no-control-regex 18751 ); 18752 const DOCTYPE_NAME = seal(/^html$/i); 18753 const CUSTOM_ELEMENT = seal(/^[a-z][.\w]*(-[.\w]+)+$/i); 18754 18755 var EXPRESSIONS = /*#__PURE__*/Object.freeze({ 18756 __proto__: null, 18757 ARIA_ATTR: ARIA_ATTR, 18758 ATTR_WHITESPACE: ATTR_WHITESPACE, 18759 CUSTOM_ELEMENT: CUSTOM_ELEMENT, 18760 DATA_ATTR: DATA_ATTR, 18761 DOCTYPE_NAME: DOCTYPE_NAME, 18762 ERB_EXPR: ERB_EXPR, 18763 IS_ALLOWED_URI: IS_ALLOWED_URI, 18764 IS_SCRIPT_OR_DATA: IS_SCRIPT_OR_DATA, 18765 MUSTACHE_EXPR: MUSTACHE_EXPR, 18766 TMPLIT_EXPR: TMPLIT_EXPR 18767 }); 18768 18769 /* eslint-disable @typescript-eslint/indent */ 18770 // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType 18771 const NODE_TYPE = { 18772 element: 1, 18773 attribute: 2, 18774 text: 3, 18775 cdataSection: 4, 18776 entityReference: 5, 18777 // Deprecated 18778 entityNode: 6, 18779 // Deprecated 18780 progressingInstruction: 7, 18781 comment: 8, 18782 document: 9, 18783 documentType: 10, 18784 documentFragment: 11, 18785 notation: 12 // Deprecated 18786 }; 18787 const getGlobal = function getGlobal() { 18788 return typeof window === 'undefined' ? null : window; 18789 }; 18790 /** 18791 * Creates a no-op policy for internal use only. 18792 * Don't export this function outside this module! 18793 * @param trustedTypes The policy factory. 18794 * @param purifyHostElement The Script element used to load DOMPurify (to determine policy name suffix). 18795 * @return The policy created (or null, if Trusted Types 18796 * are not supported or creating the policy failed). 18797 */ 18798 const _createTrustedTypesPolicy = function _createTrustedTypesPolicy(trustedTypes, purifyHostElement) { 18799 if (typeof trustedTypes !== 'object' || typeof trustedTypes.createPolicy !== 'function') { 18800 return null; 18801 } 18802 // Allow the callers to control the unique policy name 18803 // by adding a data-tt-policy-suffix to the script element with the DOMPurify. 18804 // Policy creation with duplicate names throws in Trusted Types. 18805 let suffix = null; 18806 const ATTR_NAME = 'data-tt-policy-suffix'; 18807 if (purifyHostElement && purifyHostElement.hasAttribute(ATTR_NAME)) { 18808 suffix = purifyHostElement.getAttribute(ATTR_NAME); 18809 } 18810 const policyName = 'dompurify' + (suffix ? '#' + suffix : ''); 18811 try { 18812 return trustedTypes.createPolicy(policyName, { 18813 createHTML(html) { 18814 return html; 18815 }, 18816 createScriptURL(scriptUrl) { 18817 return scriptUrl; 18818 } 18819 }); 18820 } catch (_) { 18821 // Policy creation failed (most likely another DOMPurify script has 18822 // already run). Skip creating the policy, as this will only cause errors 18823 // if TT are enforced. 18824 console.warn('TrustedTypes policy ' + policyName + ' could not be created.'); 18825 return null; 18826 } 18827 }; 18828 const _createHooksMap = function _createHooksMap() { 18829 return { 18830 afterSanitizeAttributes: [], 18831 afterSanitizeElements: [], 18832 afterSanitizeShadowDOM: [], 18833 beforeSanitizeAttributes: [], 18834 beforeSanitizeElements: [], 18835 beforeSanitizeShadowDOM: [], 18836 uponSanitizeAttribute: [], 18837 uponSanitizeElement: [], 18838 uponSanitizeShadowNode: [] 18839 }; 18840 }; 18841 function createDOMPurify() { 18842 let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal(); 18843 const DOMPurify = root => createDOMPurify(root); 18844 DOMPurify.version = '3.3.2'; 18845 DOMPurify.removed = []; 18846 if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) { 18847 // Not running in a browser, provide a factory function 18848 // so that you can pass your own Window 18849 DOMPurify.isSupported = false; 18850 return DOMPurify; 18851 } 18852 let { 18853 document 18854 } = window; 18855 const originalDocument = document; 18856 const currentScript = originalDocument.currentScript; 18857 const { 18858 DocumentFragment, 18859 HTMLTemplateElement, 18860 Node, 18861 Element, 18862 NodeFilter, 18863 NamedNodeMap = window.NamedNodeMap || window.MozNamedAttrMap, 18864 HTMLFormElement, 18865 DOMParser, 18866 trustedTypes 18867 } = window; 18868 const ElementPrototype = Element.prototype; 18869 const cloneNode = lookupGetter(ElementPrototype, 'cloneNode'); 18870 const remove = lookupGetter(ElementPrototype, 'remove'); 18871 const getNextSibling = lookupGetter(ElementPrototype, 'nextSibling'); 18872 const getChildNodes = lookupGetter(ElementPrototype, 'childNodes'); 18873 const getParentNode = lookupGetter(ElementPrototype, 'parentNode'); 18874 // As per issue #47, the web-components registry is inherited by a 18875 // new document created via createHTMLDocument. As per the spec 18876 // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries) 18877 // a new empty registry is used when creating a template contents owner 18878 // document, so we use that as our parent document to ensure nothing 18879 // is inherited. 18880 if (typeof HTMLTemplateElement === 'function') { 18881 const template = document.createElement('template'); 18882 if (template.content && template.content.ownerDocument) { 18883 document = template.content.ownerDocument; 18884 } 18885 } 18886 let trustedTypesPolicy; 18887 let emptyHTML = ''; 18888 const { 18889 implementation, 18890 createNodeIterator, 18891 createDocumentFragment, 18892 getElementsByTagName 18893 } = document; 18894 const { 18895 importNode 18896 } = originalDocument; 18897 let hooks = _createHooksMap(); 18898 /** 18899 * Expose whether this browser supports running the full DOMPurify. 18900 */ 18901 DOMPurify.isSupported = typeof entries === 'function' && typeof getParentNode === 'function' && implementation && implementation.createHTMLDocument !== undefined; 18902 const { 18903 MUSTACHE_EXPR, 18904 ERB_EXPR, 18905 TMPLIT_EXPR, 18906 DATA_ATTR, 18907 ARIA_ATTR, 18908 IS_SCRIPT_OR_DATA, 18909 ATTR_WHITESPACE, 18910 CUSTOM_ELEMENT 18911 } = EXPRESSIONS; 18912 let { 18913 IS_ALLOWED_URI: IS_ALLOWED_URI$1 18914 } = EXPRESSIONS; 18915 /** 18916 * We consider the elements and attributes below to be safe. Ideally 18917 * don't add any new ones but feel free to remove unwanted ones. 18918 */ 18919 /* allowed element names */ 18920 let ALLOWED_TAGS = null; 18921 const DEFAULT_ALLOWED_TAGS = addToSet({}, [...html$1, ...svg$1, ...svgFilters, ...mathMl$1, ...text]); 18922 /* Allowed attribute names */ 18923 let ALLOWED_ATTR = null; 18924 const DEFAULT_ALLOWED_ATTR = addToSet({}, [...html, ...svg, ...mathMl, ...xml]); 18925 /* 18926 * Configure how DOMPurify should handle custom elements and their attributes as well as customized built-in elements. 18927 * @property {RegExp|Function|null} tagNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any custom elements) 18928 * @property {RegExp|Function|null} attributeNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any attributes not on the allow list) 18929 * @property {boolean} allowCustomizedBuiltInElements allow custom elements derived from built-ins if they pass CUSTOM_ELEMENT_HANDLING.tagNameCheck. Default: `false`. 18930 */ 18931 let CUSTOM_ELEMENT_HANDLING = Object.seal(create$7(null, { 18932 tagNameCheck: { 18933 writable: true, 18934 configurable: false, 18935 enumerable: true, 18936 value: null 18937 }, 18938 attributeNameCheck: { 18939 writable: true, 18940 configurable: false, 18941 enumerable: true, 18942 value: null 18943 }, 18944 allowCustomizedBuiltInElements: { 18945 writable: true, 18946 configurable: false, 18947 enumerable: true, 18948 value: false 18949 } 18950 })); 18951 /* Explicitly forbidden tags (overrides ALLOWED_TAGS/ADD_TAGS) */ 18952 let FORBID_TAGS = null; 18953 /* Explicitly forbidden attributes (overrides ALLOWED_ATTR/ADD_ATTR) */ 18954 let FORBID_ATTR = null; 18955 /* Config object to store ADD_TAGS/ADD_ATTR functions (when used as functions) */ 18956 const EXTRA_ELEMENT_HANDLING = Object.seal(create$7(null, { 18957 tagCheck: { 18958 writable: true, 18959 configurable: false, 18960 enumerable: true, 18961 value: null 18962 }, 18963 attributeCheck: { 18964 writable: true, 18965 configurable: false, 18966 enumerable: true, 18967 value: null 18968 } 18969 })); 18970 /* Decide if ARIA attributes are okay */ 18971 let ALLOW_ARIA_ATTR = true; 18972 /* Decide if custom data attributes are okay */ 18973 let ALLOW_DATA_ATTR = true; 18974 /* Decide if unknown protocols are okay */ 18975 let ALLOW_UNKNOWN_PROTOCOLS = false;
18976 /* Decide if self-closing tags in attributes are allowed. 18977 * Usually removed due to a mXSS issue in jQuery 3.0 */ 18978 let ALLOW_SELF_CLOSE_IN_ATTR = true; 18979 /* Output should be safe for common template engines. 18980 * This means, DOMPurify removes data attributes, mustaches and ERB 18981 */ 18982 let SAFE_FOR_TEMPLATES = false; 18983 /* Output should be safe even for XML used within HTML and alike. 18984 * This means, DOMPurify removes comments when containing risky content. 18985 */ 18986 let SAFE_FOR_XML = true; 18987 /* Decide if document with <html>... should be returned */ 18988 let WHOLE_DOCUMENT = false; 18989 /* Track whether config is already set on this instance of DOMPurify. */ 18990 let SET_CONFIG = false; 18991 /* Decide if all elements (e.g. style, script) must be children of 18992 * document.body. By default, browsers might move them to document.head */ 18993 let FORCE_BODY = false; 18994 /* Decide if a DOM `HTMLBodyElement` should be returned, instead of a html 18995 * string (or a TrustedHTML object if Trusted Types are supported). 18996 * If `WHOLE_DOCUMENT` is enabled a `HTMLHtmlElement` will be returned instead 18997 */ 18998 let RETURN_DOM = false; 18999 /* Decide if a DOM `DocumentFragment` should be returned, instead of a html 19000 * string (or a TrustedHTML object if Trusted Types are supported) */ 19001 let RETURN_DOM_FRAGMENT = false; 19002 /* Try to return a Trusted Type object instead of a string, return a string in 19003 * case Trusted Types are not supported */ 19004 let RETURN_TRUSTED_TYPE = false; 19005 /* Output should be free from DOM clobbering attacks? 19006 * This sanitizes markups named with colliding, clobberable built-in DOM APIs. 19007 */ 19008 let SANITIZE_DOM = true; 19009 /* Achieve full DOM Clobbering protection by isolating the namespace of named 19010 * properties and JS variables, mitigating attacks that abuse the HTML/DOM spec rules. 19011 * 19012 * HTML/DOM spec rules that enable DOM Clobbering: 19013 * - Named Access on Window (§7.3.3) 19014 * - DOM Tree Accessors (§3.1.5) 19015 * - Form Element Parent-Child Relations (§4.10.3) 19016 * - Iframe srcdoc / Nested WindowProxies (§4.8.5) 19017 * - HTMLCollection (§4.2.10.2) 19018 * 19019 * Namespace isolation is implemented by prefixing `id` and `name` attributes 19020 * with a constant string, i.e., `user-content-` 19021 */ 19022 let SANITIZE_NAMED_PROPS = false; 19023 const SANITIZE_NAMED_PROPS_PREFIX = 'user-content-'; 19024 /* Keep element content when removing element? */ 19025 let KEEP_CONTENT = true; 19026 /* If a `Node` is passed to sanitize(), then performs sanitization in-place instead 19027 * of importing it into a new Document and returning a sanitized copy */ 19028 let IN_PLACE = false; 19029 /* Allow usage of profiles like html, svg and mathMl */ 19030 let USE_PROFILES = {}; 19031 /* Tags to ignore content of when KEEP_CONTENT is true */ 19032 let FORBID_CONTENTS = null; 19033 const DEFAULT_FORBID_CONTENTS = addToSet({}, ['annotation-xml', 'audio', 'colgroup', 'desc', 'foreignobject', 'head', 'iframe', 'math', 'mi', 'mn', 'mo', 'ms', 'mtext', 'noembed', 'noframes', 'n
vendor: 42,766 bytes, lines 19033-19922
19033oscript', 'plaintext', 'script', 'style', 'svg', 'template', 'thead', 'title', 'video', 'xmp']); 19034 /* Tags that are safe for data: URIs */ 19035 let DATA_URI_TAGS = null; 19036 const DEFAULT_DATA_URI_TAGS = addToSet({}, ['audio', 'video', 'img', 'source', 'image', 'track']); 19037 /* Attributes safe for values like "javascript:" */ 19038 let URI_SAFE_ATTRIBUTES = null; 19039 const DEFAULT_URI_SAFE_ATTRIBUTES = addToSet({}, ['alt', 'class', 'for', 'id', 'label', 'name', 'pattern', 'placeholder', 'role', 'summary', 'title', 'value', 'style', 'xmlns']); 19040 const MATHML_NAMESPACE = 'http://www.w3.org/1998/Math/MathML'; 19041 const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'; 19042 const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml'; 19043 /* Document namespace */ 19044 let NAMESPACE = HTML_NAMESPACE; 19045 let IS_EMPTY_INPUT = false; 19046 /* Allowed XHTML+XML namespaces */ 19047 let ALLOWED_NAMESPACES = null; 19048 const DEFAULT_ALLOWED_NAMESPACES = addToSet({}, [MATHML_NAMESPACE, SVG_NAMESPACE, HTML_NAMESPACE], stringToString); 19049 let MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, ['mi', 'mo', 'mn', 'ms', 'mtext']); 19050 let HTML_INTEGRATION_POINTS = addToSet({}, ['annotation-xml']); 19051 // Certain elements are allowed in both SVG and HTML 19052 // namespace. We need to specify them explicitly 19053 // so that they don't get erroneously deleted from 19054 // HTML namespace. 19055 const COMMON_SVG_AND_HTML_ELEMENTS = addToSet({}, ['title', 'style', 'font', 'a', 'script']); 19056 /* Parsing of strict XHTML documents */ 19057 let PARSER_MEDIA_TYPE = null; 19058 const SUPPORTED_PARSER_MEDIA_TYPES = ['application/xhtml+xml', 'text/html']; 19059 const DEFAULT_PARSER_MEDIA_TYPE = 'text/html'; 19060 let transformCaseFunc = null; 19061 /* Keep a reference to config to pass to hooks */ 19062 let CONFIG = null; 19063 /* Ideally, do not touch anything below this line */ 19064 /* ______________________________________________ */ 19065 const formElement = document.createElement('form'); 19066 const isRegexOrFunction = function isRegexOrFunction(testValue) { 19067 return testValue instanceof RegExp || testValue instanceof Function; 19068 }; 19069 /** 19070 * _parseConfig 19071 * 19072 * @param cfg optional config literal 19073 */ 19074 // eslint-disable-next-line complexity 19075 const _parseConfig = function _parseConfig() { 19076 let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 19077 if (CONFIG && CONFIG === cfg) { 19078 return; 19079 } 19080 /* Shield configuration object from tampering */ 19081 if (!cfg || typeof cfg !== 'object') { 19082 cfg = {}; 19083 } 19084 /* Shield configuration object from prototype pollution */ 19085 cfg = clone(cfg); 19086 PARSER_MEDIA_TYPE = 19087 // eslint-disable-next-line unicorn/prefer-includes 19088 SUPPORTED_PARSER_MEDIA_TYPES.indexOf(cfg.PARSER_MEDIA_TYPE) === -1 ? DEFAULT_PARSER_MEDIA_TYPE : cfg.PARSER_MEDIA_TYPE; 19089 // HTML tags and attributes are not case-sensitive, converting to lowercase. Keeping XHTML as is. 19090 transformCaseFunc = PARSER_MEDIA_TYPE === 'application/xhtml+xml' ? stringToString : stringToLowerCase; 19091 /* Set configuration parameters */ 19092 ALLOWED_TAGS = objectHasOwnProperty(cfg, 'ALLOWED_TAGS') ? addToSet({}, cfg.ALLOWED_TAGS, transformCaseFunc) : DEFAULT_ALLOWED_TAGS; 19093 ALLOWED_ATTR = objectHasOwnProperty(cfg, 'ALLOWED_ATTR') ? addToSet({}, cfg.ALLOWED_ATTR, transformCaseFunc) : DEFAULT_ALLOWED_ATTR; 19094 ALLOWED_NAMESPACES = objectHasOwnProperty(cfg, 'ALLOWED_NAMESPACES') ? addToSet({}, cfg.ALLOWED_NAMESPACES, stringToString) : DEFAULT_ALLOWED_NAMESPACES; 19095 URI_SAFE_ATTRIBUTES = objectHasOwnProperty(cfg, 'ADD_URI_SAFE_ATTR') ? addToSet(clone(DEFAULT_URI_SAFE_ATTRIBUTES), cfg.ADD_URI_SAFE_ATTR, transformCaseFunc) : DEFAULT_URI_SAFE_ATTRIBUTES; 19096 DATA_URI_TAGS = objectHasOwnProperty(cfg, 'ADD_DATA_URI_TAGS') ? addToSet(clone(DEFAULT_DATA_URI_TAGS), cfg.ADD_DATA_URI_TAGS, transformCaseFunc) : DEFAULT_DATA_URI_TAGS; 19097 FORBID_CONTENTS = objectHasOwnProperty(cfg, 'FORBID_CONTENTS') ? addToSet({}, cfg.FORBID_CONTENTS, transformCaseFunc) : DEFAULT_FORBID_CONTENTS; 19098 FORBID_TAGS = objectHasOwnProperty(cfg, 'FORBID_TAGS') ? addToSet({}, cfg.FORBID_TAGS, transformCaseFunc) : clone({}); 19099 FORBID_ATTR = objectHasOwnProperty(cfg, 'FORBID_ATTR') ? addToSet({}, cfg.FORBID_ATTR, transformCaseFunc) : clone({}); 19100 USE_PROFILES = objectHasOwnProperty(cfg, 'USE_PROFILES') ? cfg.USE_PROFILES : false; 19101 ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; // Default true 19102 ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true 19103 ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; // Default false 19104 ALLOW_SELF_CLOSE_IN_ATTR = cfg.ALLOW_SELF_CLOSE_IN_ATTR !== false; // Default true 19105 SAFE_FOR_TEMPLATES = cfg.SAFE_FOR_TEMPLATES || false; // Default false 19106 SAFE_FOR_XML = cfg.SAFE_FOR_XML !== false; // Default true 19107 WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT || false; // Default false 19108 RETURN_DOM = cfg.RETURN_DOM || false; // Default false 19109 RETURN_DOM_FRAGMENT = cfg.RETURN_DOM_FRAGMENT || false; // Default false 19110 RETURN_TRUSTED_TYPE = cfg.RETURN_TRUSTED_TYPE || false; // Default false 19111 FORCE_BODY = cfg.FORCE_BODY || false; // Default false 19112 SANITIZE_DOM = cfg.SANITIZE_DOM !== false; // Default true 19113 SANITIZE_NAMED_PROPS = cfg.SANITIZE_NAMED_PROPS || false; // Default false 19114 KEEP_CONTENT = cfg.KEEP_CONTENT !== false; // Default true 19115 IN_PLACE = cfg.IN_PLACE || false; // Default false 19116 IS_ALLOWED_URI$1 = cfg.ALLOWED_URI_REGEXP || IS_ALLOWED_URI; 19117 NAMESPACE = cfg.NAMESPACE || HTML_NAMESPACE; 19118 MATHML_TEXT_INTEGRATION_POINTS = cfg.MATHML_TEXT_INTEGRATION_POINTS || MATHML_TEXT_INTEGRATION_POINTS; 19119 HTML_INTEGRATION_POINTS = cfg.HTML_INTEGRATION_POINTS || HTML_INTEGRATION_POINTS; 19120 CUSTOM_ELEMENT_HANDLING = cfg.CUSTOM_ELEMENT_HANDLING || {}; 19121 if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck)) { 19122 CUSTOM_ELEMENT_HANDLING.tagNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck; 19123 } 19124 if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)) { 19125 CUSTOM_ELEMENT_HANDLING.attributeNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck; 19126 } 19127 if (cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements === 'boolean') { 19128 CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements; 19129 } 19130 if (SAFE_FOR_TEMPLATES) { 19131 ALLOW_DATA_ATTR = false; 19132 } 19133 if (RETURN_DOM_FRAGMENT) { 19134 RETURN_DOM = true; 19135 } 19136 /* Parse profile info */ 19137 if (USE_PROFILES) { 19138 ALLOWED_TAGS = addToSet({}, text); 19139 ALLOWED_ATTR = create$7(null); 19140 if (USE_PROFILES.html === true) { 19141 addToSet(ALLOWED_TAGS, html$1); 19142 addToSet(ALLOWED_ATTR, html); 19143 } 19144 if (USE_PROFILES.svg === true) { 19145 addToSet(ALLOWED_TAGS, svg$1); 19146 addToSet(ALLOWED_ATTR, svg); 19147 addToSet(ALLOWED_ATTR, xml); 19148 } 19149 if (USE_PROFILES.svgFilters === true) { 19150 addToSet(ALLOWED_TAGS, svgFilters); 19151 addToSet(ALLOWED_ATTR, svg); 19152 addToSet(ALLOWED_ATTR, xml); 19153 } 19154 if (USE_PROFILES.mathMl === true) { 19155 addToSet(ALLOWED_TAGS, mathMl$1); 19156 addToSet(ALLOWED_ATTR, mathMl); 19157 addToSet(ALLOWED_ATTR, xml); 19158 } 19159 } 19160 /* Prevent function-based ADD_ATTR / ADD_TAGS from leaking across calls */ 19161 if (!objectHasOwnProperty(cfg, 'ADD_TAGS')) { 19162 EXTRA_ELEMENT_HANDLING.tagCheck = null; 19163 } 19164 if (!objectHasOwnProperty(cfg, 'ADD_ATTR')) { 19165 EXTRA_ELEMENT_HANDLING.attributeCheck = null; 19166 } 19167 /* Merge configuration parameters */ 19168 if (cfg.ADD_TAGS) { 19169 if (typeof cfg.ADD_TAGS === 'function') { 19170 EXTRA_ELEMENT_HANDLING.tagCheck = cfg.ADD_TAGS; 19171 } else { 19172 if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) { 19173 ALLOWED_TAGS = clone(ALLOWED_TAGS); 19174 } 19175 addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc); 19176 } 19177 } 19178 if (cfg.ADD_ATTR) { 19179 if (typeof cfg.ADD_ATTR === 'function') { 19180 EXTRA_ELEMENT_HANDLING.attributeCheck = cfg.ADD_ATTR; 19181 } else { 19182 if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) { 19183 ALLOWED_ATTR = clone(ALLOWED_ATTR); 19184 } 19185 addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc); 19186 } 19187 } 19188 if (cfg.ADD_URI_SAFE_ATTR) { 19189 addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc); 19190 } 19191 if (cfg.FORBID_CONTENTS) { 19192 if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) { 19193 FORBID_CONTENTS = clone(FORBID_CONTENTS); 19194 } 19195 addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc); 19196 } 19197 if (cfg.ADD_FORBID_CONTENTS) { 19198 if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) { 19199 FORBID_CONTENTS = clone(FORBID_CONTENTS); 19200 } 19201 addToSet(FORBID_CONTENTS, cfg.ADD_FORBID_CONTENTS, transformCaseFunc); 19202 } 19203 /* Add #text in case KEEP_CONTENT is set to true */ 19204 if (KEEP_CONTENT) { 19205 ALLOWED_TAGS['#text'] = true; 19206 } 19207 /* Add html, head and body to ALLOWED_TAGS in case WHOLE_DOCUMENT is true */ 19208 if (WHOLE_DOCUMENT) { 19209 addToSet(ALLOWED_TAGS, ['html', 'head', 'body']); 19210 } 19211 /* Add tbody to ALLOWED_TAGS in case tables are permitted, see #286, #365 */ 19212 if (ALLOWED_TAGS.table) { 19213 addToSet(ALLOWED_TAGS, ['tbody']); 19214 delete FORBID_TAGS.tbody; 19215 } 19216 if (cfg.TRUSTED_TYPES_POLICY) { 19217 if (typeof cfg.TRUSTED_TYPES_POLICY.createHTML !== 'function') { 19218 throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.'); 19219 } 19220 if (typeof cfg.TRUSTED_TYPES_POLICY.createScriptURL !== 'function') { 19221 throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.'); 19222 } 19223 // Overwrite existing TrustedTypes policy. 19224 trustedTypesPolicy = cfg.TRUSTED_TYPES_POLICY; 19225 // Sign local variables required by `sanitize`. 19226 emptyHTML = trustedTypesPolicy.createHTML(''); 19227 } else { 19228 // Uninitialized policy, attempt to initialize the internal dompurify policy. 19229 if (trustedTypesPolicy === undefined) { 19230 trustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript); 19231 } 19232 // If creating the internal policy succeeded sign internal variables. 19233 if (trustedTypesPolicy !== null && typeof emptyHTML === 'string') { 19234 emptyHTML = trustedTypesPolicy.createHTML(''); 19235 } 19236 } 19237 // Prevent further manipulation of configuration. 19238 // Not available in IE8, Safari 5, etc. 19239 if (freeze) { 19240 freeze(cfg); 19241 } 19242 CONFIG = cfg; 19243 }; 19244 /* Keep track of all possible SVG and MathML tags 19245 * so that we can perform the namespace checks 19246 * correctly. */ 19247 const ALL_SVG_TAGS = addToSet({}, [...svg$1, ...svgFilters, ...svgDisallowed]); 19248 const ALL_MATHML_TAGS = addToSet({}, [...mathMl$1, ...mathMlDisallowed]); 19249 /** 19250 * @param element a DOM element whose namespace is being checked 19251 * @returns Return false if the element has a 19252 * namespace that a spec-compliant parser would never 19253 * return. Return true otherwise. 19254 */ 19255 const _checkValidNamespace = function _checkValidNamespace(element) { 19256 let parent = getParentNode(element); 19257 // In JSDOM, if we're inside shadow DOM, then parentNode 19258 // can be null. We just simulate parent in this case. 19259 if (!parent || !parent.tagName) { 19260 parent = { 19261 namespaceURI: NAMESPACE, 19262 tagName: 'template' 19263 }; 19264 } 19265 const tagName = stringToLowerCase(element.tagName); 19266 const parentTagName = stringToLowerCase(parent.tagName); 19267 if (!ALLOWED_NAMESPACES[element.namespaceURI]) { 19268 return false; 19269 } 19270 if (element.namespaceURI === SVG_NAMESPACE) { 19271 // The only way to switch from HTML namespace to SVG 19272 // is via <svg>. If it happens via any other tag, then 19273 // it should be killed. 19274 if (parent.namespaceURI === HTML_NAMESPACE) { 19275 return tagName === 'svg'; 19276 } 19277 // The only way to switch from MathML to SVG is via` 19278 // svg if parent is either <annotation-xml> or MathML 19279 // text integration points. 19280 if (parent.namespaceURI === MATHML_NAMESPACE) { 19281 return tagName === 'svg' && (parentTagName === 'annotation-xml' || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]); 19282 } 19283 // We only allow elements that are defined in SVG 19284 // spec. All others are disallowed in SVG namespace. 19285 return Boolean(ALL_SVG_TAGS[tagName]); 19286 } 19287 if (element.namespaceURI === MATHML_NAMESPACE) { 19288 // The only way to switch from HTML namespace to MathML 19289 // is via <math>. If it happens via any other tag, then 19290 // it should be killed. 19291 if (parent.namespaceURI === HTML_NAMESPACE) { 19292 return tagName === 'math'; 19293 } 19294 // The only way to switch from SVG to MathML is via 19295 // <math> and HTML integration points 19296 if (parent.namespaceURI === SVG_NAMESPACE) { 19297 return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName]; 19298 } 19299 // We only allow elements that are defined in MathML 19300 // spec. All others are disallowed in MathML namespace. 19301 return Boolean(ALL_MATHML_TAGS[tagName]); 19302 } 19303 if (element.namespaceURI === HTML_NAMESPACE) { 19304 // The only way to switch from SVG to HTML is via 19305 // HTML integration points, and from MathML to HTML 19306 // is via MathML text integration points 19307 if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) { 19308 return false; 19309 } 19310 if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) { 19311 return false; 19312 } 19313 // We disallow tags that are specific for MathML 19314 // or SVG and should never appear in HTML namespace 19315 return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]); 19316 } 19317 // For XHTML and XML documents that support custom namespaces 19318 if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && ALLOWED_NAMESPACES[element.namespaceURI]) { 19319 return true; 19320 } 19321 // The code should never reach this place (this means 19322 // that the element somehow got namespace that is not 19323 // HTML, SVG, MathML or allowed via ALLOWED_NAMESPACES). 19324 // Return false just in case. 19325 return false; 19326 }; 19327 /** 19328 * _forceRemove 19329 * 19330 * @param node a DOM node 19331 */ 19332 const _forceRemove = function _forceRemove(node) { 19333 arrayPush(DOMPurify.removed, { 19334 element: node 19335 }); 19336 try { 19337 // eslint-disable-next-line unicorn/prefer-dom-node-remove 19338 getParentNode(node).removeChild(node); 19339 } catch (_) { 19340 remove(node); 19341 } 19342 }; 19343 /** 19344 * _removeAttribute 19345 * 19346 * @param name an Attribute name 19347 * @param element a DOM node 19348 */ 19349 const _removeAttribute = function _removeAttribute(name, element) { 19350 try { 19351 arrayPush(DOMPurify.removed, { 19352 attribute: element.getAttributeNode(name), 19353 from: element 19354 }); 19355 } catch (_) { 19356 arrayPush(DOMPurify.removed, { 19357 attribute: null, 19358 from: element 19359 }); 19360 } 19361 element.removeAttribute(name); 19362 // We void attribute values for unremovable "is" attributes 19363 if (name === 'is') { 19364 if (RETURN_DOM || RETURN_DOM_FRAGMENT) { 19365 try { 19366 _forceRemove(element); 19367 } catch (_) {} 19368 } else { 19369 try { 19370 element.setAttribute(name, ''); 19371 } catch (_) {} 19372 } 19373 } 19374 }; 19375 /** 19376 * _initDocument 19377 * 19378 * @param dirty - a string of dirty markup 19379 * @return a DOM, filled with the dirty markup 19380 */ 19381 const _initDocument = function _initDocument(dirty) { 19382 /* Create a HTML document */ 19383 let doc = null; 19384 let leadingWhitespace = null; 19385 if (FORCE_BODY) { 19386 dirty = '<remove></remove>' + dirty; 19387 } else { 19388 /* If FORCE_BODY isn't used, leading whitespace needs to be preserved manually */ 19389 const matches = stringMatch(dirty, /^[\r\n\t ]+/); 19390 leadingWhitespace = matches && matches[0]; 19391 } 19392 if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && NAMESPACE === HTML_NAMESPACE) { 19393 // Root of XHTML doc must contain xmlns declaration (see https://www.w3.org/TR/xhtml1/normative.html#strict) 19394 dirty = '<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>' + dirty + '</body></html>'; 19395 } 19396 const dirtyPayload = trustedTypesPolicy ? trustedTypesPolicy.createHTML(dirty) : dirty; 19397 /* 19398 * Use the DOMParser API by default, fallback later if needs be 19399 * DOMParser not work for svg when has multiple root element. 19400 */ 19401 if (NAMESPACE === HTML_NAMESPACE) { 19402 try { 19403 doc = new DOMParser().parseFromString(dirtyPayload, PARSER_MEDIA_TYPE); 19404 } catch (_) {} 19405 } 19406 /* Use createHTMLDocument in case DOMParser is not available */ 19407 if (!doc || !doc.documentElement) { 19408 doc = implementation.createDocument(NAMESPACE, 'template', null); 19409 try { 19410 doc.documentElement.innerHTML = IS_EMPTY_INPUT ? emptyHTML : dirtyPayload; 19411 } catch (_) { 19412 // Syntax error if dirtyPayload is invalid xml 19413 } 19414 } 19415 const body = doc.body || doc.documentElement; 19416 if (dirty && leadingWhitespace) { 19417 body.insertBefore(document.createTextNode(leadingWhitespace), body.childNodes[0] || null); 19418 } 19419 /* Work on whole document or just its body */ 19420 if (NAMESPACE === HTML_NAMESPACE) { 19421 return getElementsByTagName.call(doc, WHOLE_DOCUMENT ? 'html' : 'body')[0]; 19422 } 19423 return WHOLE_DOCUMENT ? doc.documentElement : body; 19424 }; 19425 /** 19426 * Creates a NodeIterator object that you can use to traverse filtered lists of nodes or elements in a document. 19427 * 19428 * @param root The root element or node to start traversing on. 19429 * @return The created NodeIterator 19430 */ 19431 const _createNodeIterator = function _createNodeIterator(root) { 19432 return createNodeIterator.call(root.ownerDocument || root, root, 19433 // eslint-disable-next-line no-bitwise 19434 NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_CDATA_SECTION, null); 19435 }; 19436 /** 19437 * _isClobbered 19438 * 19439 * @param element element to check for clobbering attacks 19440 * @return true if clobbered, false if safe 19441 */ 19442 const _isClobbered = function _isClobbered(element) { 19443 return element instanceof HTMLFormElement && (typeof element.nodeName !== 'string' || typeof element.textContent !== 'string' || typeof element.removeChild !== 'function' || !(element.attributes instanceof NamedNodeMap) || typeof element.removeAttribute !== 'function' || typeof element.setAttribute !== 'function' || typeof element.namespaceURI !== 'string' || typeof element.insertBefore !== 'function' || typeof element.hasChildNodes !== 'function'); 19444 }; 19445 /** 19446 * Checks whether the given object is a DOM node. 19447 * 19448 * @param value object to check whether it's a DOM node 19449 * @return true is object is a DOM node 19450 */ 19451 const _isNode = function _isNode(value) { 19452 return typeof Node === 'function' && value instanceof Node; 19453 }; 19454 function _executeHooks(hooks, currentNode, data) { 19455 arrayForEach(hooks, hook => { 19456 hook.call(DOMPurify, currentNode, data, CONFIG); 19457 }); 19458 } 19459 /** 19460 * _sanitizeElements 19461 * 19462 * @protect nodeName 19463 * @protect textContent 19464 * @protect removeChild 19465 * @param currentNode to check for permission to exist 19466 * @return true if node was killed, false if left alive 19467 */ 19468 const _sanitizeElements = function _sanitizeElements(currentNode) { 19469 let content = null; 19470 /* Execute a hook if present */ 19471 _executeHooks(hooks.beforeSanitizeElements, currentNode, null); 19472 /* Check if element is clobbered or can clobber */ 19473 if (_isClobbered(currentNode)) { 19474 _forceRemove(currentNode); 19475 return true; 19476 } 19477 /* Now let's check the element's type and name */ 19478 const tagName = transformCaseFunc(currentNode.nodeName); 19479 /* Execute a hook if present */ 19480 _executeHooks(hooks.uponSanitizeElement, currentNode, { 19481 tagName, 19482 allowedTags: ALLOWED_TAGS 19483 }); 19484 /* Detect mXSS attempts abusing namespace confusion */ 19485 if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(/<[/\w!]/g, currentNode.innerHTML) && regExpTest(/<[/\w!]/g, currentNode.textContent)) { 19486 _forceRemove(currentNode); 19487 return true; 19488 } 19489 /* Remove any occurrence of processing instructions */ 19490 if (currentNode.nodeType === NODE_TYPE.progressingInstruction) { 19491 _forceRemove(currentNode); 19492 return true; 19493 } 19494 /* Remove any kind of possibly harmful comments */ 19495 if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(/<[/\w]/g, currentNode.data)) { 19496 _forceRemove(currentNode); 19497 return true; 19498 } 19499 /* Remove element if anything forbids its presence */ 19500 if (!(EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function && EXTRA_ELEMENT_HANDLING.tagCheck(tagName)) && (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName])) { 19501 /* Check if we have a custom element to handle */ 19502 if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) { 19503 if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) { 19504 return false; 19505 } 19506 if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) { 19507 return false; 19508 } 19509 } 19510 /* Keep content except for bad-listed elements */ 19511 if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) { 19512 const parentNode = getParentNode(currentNode) || currentNode.parentNode; 19513 const childNodes = getChildNodes(currentNode) || currentNode.childNodes; 19514 if (childNodes && parentNode) { 19515 const childCount = childNodes.length; 19516 for (let i = childCount - 1; i >= 0; --i) { 19517 const childClone = cloneNode(childNodes[i], true); 19518 childClone.__removalCount = (currentNode.__removalCount || 0) + 1; 19519 parentNode.insertBefore(childClone, getNextSibling(currentNode)); 19520 } 19521 } 19522 } 19523 _forceRemove(currentNode); 19524 return true; 19525 } 19526 /* Check whether element has a valid namespace */ 19527 if (currentNode instanceof Element && !_checkValidNamespace(currentNode)) { 19528 _forceRemove(currentNode); 19529 return true; 19530 } 19531 /* Make sure that older browsers don't get fallback-tag mXSS */ 19532 if ((tagName === 'noscript' || tagName === 'noembed' || tagName === 'noframes') && regExpTest(/<\/no(script|embed|frames)/i, currentNode.innerHTML)) { 19533 _forceRemove(currentNode); 19534 return true; 19535 } 19536 /* Sanitize element content to be template-safe */ 19537 if (SAFE_FOR_TEMPLATES && currentNode.nodeType === NODE_TYPE.text) { 19538 /* Get the element's text content */ 19539 content = currentNode.textContent; 19540 arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => { 19541 content = stringReplace(content, expr, ' '); 19542 }); 19543 if (currentNode.textContent !== content) { 19544 arrayPush(DOMPurify.removed, { 19545 element: currentNode.cloneNode() 19546 }); 19547 currentNode.textContent = content; 19548 } 19549 } 19550 /* Execute a hook if present */ 19551 _executeHooks(hooks.afterSanitizeElements, currentNode, null); 19552 return false; 19553 }; 19554 /** 19555 * _isValidAttribute 19556 * 19557 * @param lcTag Lowercase tag name of containing element. 19558 * @param lcName Lowercase attribute name. 19559 * @param value Attribute value. 19560 * @return Returns true if `value` is valid, otherwise false. 19561 */ 19562 // eslint-disable-next-line complexity 19563 const _isValidAttribute = function _isValidAttribute(lcTag, lcName, value) { 19564 /* FORBID_ATTR must always win, even if ADD_ATTR predicate would allow it */ 19565 if (FORBID_ATTR[lcName]) { 19566 return false; 19567 } 19568 /* Make sure attribute cannot clobber */ 19569 if (SANITIZE_DOM && (lcName === 'id' || lcName === 'name') && (value in document || value in formElement)) { 19570 return false; 19571 } 19572 /* Allow valid data-* attributes: At least one character after "-" 19573 (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes) 19574 XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804) 19575 We don't need to check the value; it's always URI safe. */ 19576 if (ALLOW_DATA_ATTR && !FORBID_ATTR[lcName] && regExpTest(DATA_ATTR, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR, lcName)) ; else if (EXTRA_ELEMENT_HANDLING.attributeCheck instanceof Function && EXTRA_ELEMENT_HANDLING.attributeCheck(lcName, lcTag)) ; else if (!ALLOWED_ATTR[lcName] || FORBID_ATTR[lcName]) { 19577 if ( 19578 // First condition does a very basic check if a) it's basically a valid custom element tagname AND 19579 // b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck 19580 // and c) if the attribute name passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.attributeNameCheck 19581 _isBasicCustomElement(lcTag) && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, lcTag) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(lcTag)) && (CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.attributeNameCheck, lcName) || CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.attributeNameCheck(lcName, lcTag)) || 19582 // Alternative, second condition checks if it's an `is`-attribute, AND 19583 // the value passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck 19584 lcName === 'is' && CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, value) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(value))) ; else { 19585 return false; 19586 } 19587 /* Check value is safe. First, is attr inert? If so, is safe */ 19588 } else if (URI_SAFE_ATTRIBUTES[lcName]) ; else if (regExpTest(IS_ALLOWED_URI$1, stringReplace(value, ATTR_WHITESPACE, ''))) ; else if ((lcName === 'src' || lcName === 'xlink:href' || lcName === 'href') && lcTag !== 'script' && stringIndexOf(value, 'data:') === 0 && DATA_URI_TAGS[lcTag]) ; else if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA, stringReplace(value, ATTR_WHITESPACE, ''))) ; else if (value) { 19589 return false; 19590 } else ; 19591 return true; 19592 }; 19593 /** 19594 * _isBasicCustomElement 19595 * checks if at least one dash is included in tagName, and it's not the first char 19596 * for more sophisticated checking see https://github.com/sindresorhus/validate-element-name 19597 * 19598 * @param tagName name of the tag of the node to sanitize 19599 * @returns Returns true if the tag name meets the basic criteria for a custom element, otherwise false. 19600 */ 19601 const _isBasicCustomElement = function _isBasicCustomElement(tagName) { 19602 return tagName !== 'annotation-xml' && stringMatch(tagName, CUSTOM_ELEMENT); 19603 }; 19604 /** 19605 * _sanitizeAttributes 19606 * 19607 * @protect attributes 19608 * @protect nodeName 19609 * @protect removeAttribute 19610 * @protect setAttribute 19611 * 19612 * @param currentNode to sanitize 19613 */ 19614 const _sanitizeAttributes = function _sanitizeAttributes(currentNode) { 19615 /* Execute a hook if present */ 19616 _executeHooks(hooks.beforeSanitizeAttributes, currentNode, null); 19617 const { 19618 attributes 19619 } = currentNode; 19620 /* Check if we have attributes; if not we might have a text node */ 19621 if (!attributes || _isClobbered(currentNode)) { 19622 return; 19623 } 19624 const hookEvent = { 19625 attrName: '', 19626 attrValue: '', 19627 keepAttr: true, 19628 allowedAttributes: ALLOWED_ATTR, 19629 forceKeepAttr: undefined 19630 }; 19631 let l = attributes.length; 19632 /* Go backwards over all attributes; safely remove bad ones */ 19633 while (l--) { 19634 const attr = attributes[l]; 19635 const { 19636 name, 19637 namespaceURI, 19638 value: attrValue 19639 } = attr; 19640 const lcName = transformCaseFunc(name); 19641 const initValue = attrValue; 19642 let value = name === 'value' ? initValue : stringTrim(initValue); 19643 /* Execute a hook if present */ 19644 hookEvent.attrName = lcName; 19645 hookEvent.attrValue = value; 19646 hookEvent.keepAttr = true; 19647 hookEvent.forceKeepAttr = undefined; // Allows developers to see this is a property they can set 19648 _executeHooks(hooks.uponSanitizeAttribute, currentNode, hookEvent); 19649 value = hookEvent.attrValue; 19650 /* Full DOM Clobbering protection via namespace isolation, 19651 * Prefix id and name attributes with `user-content-` 19652 */ 19653 if (SANITIZE_NAMED_PROPS && (lcName === 'id' || lcName === 'name')) { 19654 // Remove the attribute with this value 19655 _removeAttribute(name, currentNode); 19656 // Prefix the value and later re-create the attribute with the sanitized value 19657 value = SANITIZE_NAMED_PROPS_PREFIX + value; 19658 } 19659 /* Work around a security issue with comments inside attributes */ 19660 if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i, value)) { 19661 _removeAttribute(name, currentNode); 19662 continue; 19663 } 19664 /* Make sure we cannot easily use animated hrefs, even if animations are allowed */ 19665 if (lcName === 'attributename' && stringMatch(value, 'href')) { 19666 _removeAttribute(name, currentNode); 19667 continue; 19668 } 19669 /* Did the hooks approve of the attribute? */ 19670 if (hookEvent.forceKeepAttr) { 19671 continue; 19672 } 19673 /* Did the hooks approve of the attribute? */ 19674 if (!hookEvent.keepAttr) { 19675 _removeAttribute(name, currentNode); 19676 continue; 19677 } 19678 /* Work around a security issue in jQuery 3.0 */ 19679 if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(/\/>/i, value)) { 19680 _removeAttribute(name, currentNode); 19681 continue; 19682 } 19683 /* Sanitize attribute content to be template-safe */ 19684 if (SAFE_FOR_TEMPLATES) { 19685 arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => { 19686 value = stringReplace(value, expr, ' '); 19687 }); 19688 } 19689 /* Is `value` valid for this attribute? */ 19690 const lcTag = transformCaseFunc(currentNode.nodeName); 19691 if (!_isValidAttribute(lcTag, lcName, value)) { 19692 _removeAttribute(name, currentNode); 19693 continue; 19694 } 19695 /* Handle attributes that require Trusted Types */ 19696 if (trustedTypesPolicy && typeof trustedTypes === 'object' && typeof trustedTypes.getAttributeType === 'function') { 19697 if (namespaceURI) ; else { 19698 switch (trustedTypes.getAttributeType(lcTag, lcName)) { 19699 case 'TrustedHTML': 19700 { 19701 value = trustedTypesPolicy.createHTML(value); 19702 break; 19703 } 19704 case 'TrustedScriptURL': 19705 { 19706 value = trustedTypesPolicy.createScriptURL(value); 19707 break; 19708 } 19709 } 19710 } 19711 } 19712 /* Handle invalid data-* attribute set by try-catching it */ 19713 if (value !== initValue) { 19714 try { 19715 if (namespaceURI) { 19716 currentNode.setAttributeNS(namespaceURI, name, value); 19717 } else { 19718 /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */ 19719 currentNode.setAttribute(name, value); 19720 } 19721 if (_isClobbered(currentNode)) { 19722 _forceRemove(currentNode); 19723 } else { 19724 arrayPop(DOMPurify.removed); 19725 } 19726 } catch (_) { 19727 _removeAttribute(name, currentNode); 19728 } 19729 } 19730 } 19731 /* Execute a hook if present */ 19732 _executeHooks(hooks.afterSanitizeAttributes, currentNode, null); 19733 }; 19734 /** 19735 * _sanitizeShadowDOM 19736 * 19737 * @param fragment to iterate over recursively 19738 */ 19739 const _sanitizeShadowDOM = function _sanitizeShadowDOM(fragment) { 19740 let shadowNode = null; 19741 const shadowIterator = _createNodeIterator(fragment); 19742 /* Execute a hook if present */ 19743 _executeHooks(hooks.beforeSanitizeShadowDOM, fragment, null); 19744 while (shadowNode = shadowIterator.nextNode()) { 19745 /* Execute a hook if present */ 19746 _executeHooks(hooks.uponSanitizeShadowNode, shadowNode, null); 19747 /* Sanitize tags and elements */ 19748 _sanitizeElements(shadowNode); 19749 /* Check attributes next */ 19750 _sanitizeAttributes(shadowNode); 19751 /* Deep shadow DOM detected */ 19752 if (shadowNode.content instanceof DocumentFragment) { 19753 _sanitizeShadowDOM(shadowNode.content); 19754 } 19755 } 19756 /* Execute a hook if present */ 19757 _executeHooks(hooks.afterSanitizeShadowDOM, fragment, null); 19758 }; 19759 // eslint-disable-next-line complexity 19760 DOMPurify.sanitize = function (dirty) { 19761 let cfg = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 19762 let body = null; 19763 let importedNode = null; 19764 let currentNode = null; 19765 let returnNode = null; 19766 /* Make sure we have a string to sanitize. 19767 DO NOT return early, as this will return the wrong type if 19768 the user has requested a DOM object rather than a string */ 19769 IS_EMPTY_INPUT = !dirty; 19770 if (IS_EMPTY_INPUT) { 19771 dirty = '<!-->'; 19772 } 19773 /* Stringify, in case dirty is an object */ 19774 if (typeof dirty !== 'string' && !_isNode(dirty)) { 19775 if (typeof dirty.toString === 'function') { 19776 dirty = dirty.toString(); 19777 if (typeof dirty !== 'string') { 19778 throw typeErrorCreate('dirty is not a string, aborting'); 19779 } 19780 } else { 19781 throw typeErrorCreate('toString is not a function'); 19782 } 19783 } 19784 /* Return dirty HTML if DOMPurify cannot run */ 19785 if (!DOMPurify.isSupported) { 19786 return dirty; 19787 } 19788 /* Assign config vars */ 19789 if (!SET_CONFIG) { 19790 _parseConfig(cfg); 19791 } 19792 /* Clean up removed elements */ 19793 DOMPurify.removed = []; 19794 /* Check if dirty is correctly typed for IN_PLACE */ 19795 if (typeof dirty === 'string') { 19796 IN_PLACE = false; 19797 } 19798 if (IN_PLACE) { 19799 /* Do some early pre-sanitization to avoid unsafe root nodes */ 19800 if (dirty.nodeName) { 19801 const tagName = transformCaseFunc(dirty.nodeName); 19802 if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) { 19803 throw typeErrorCreate('root node is forbidden and cannot be sanitized in-place'); 19804 } 19805 } 19806 } else if (dirty instanceof Node) { 19807 /* If dirty is a DOM element, append to an empty document to avoid 19808 elements being stripped by the parser */ 19809 body = _initDocument('<!---->'); 19810 importedNode = body.ownerDocument.importNode(dirty, true); 19811 if (importedNode.nodeType === NODE_TYPE.element && importedNode.nodeName === 'BODY') { 19812 /* Node is already a body, use as is */ 19813 body = importedNode; 19814 } else if (importedNode.nodeName === 'HTML') { 19815 body = importedNode; 19816 } else { 19817 // eslint-disable-next-line unicorn/prefer-dom-node-append 19818 body.appendChild(importedNode); 19819 } 19820 } else { 19821 /* Exit directly if we have nothing to do */ 19822 if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT && 19823 // eslint-disable-next-line unicorn/prefer-includes 19824 dirty.indexOf('<') === -1) { 19825 return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(dirty) : dirty; 19826 } 19827 /* Initialize the document to work on */ 19828 body = _initDocument(dirty); 19829 /* Check we have a DOM node from the data */ 19830 if (!body) { 19831 return RETURN_DOM ? null : RETURN_TRUSTED_TYPE ? emptyHTML : ''; 19832 } 19833 } 19834 /* Remove first element node (ours) if FORCE_BODY is set */ 19835 if (body && FORCE_BODY) { 19836 _forceRemove(body.firstChild); 19837 } 19838 /* Get node iterator */ 19839 const nodeIterator = _createNodeIterator(IN_PLACE ? dirty : body); 19840 /* Now start iterating over the created document */ 19841 while (currentNode = nodeIterator.nextNode()) { 19842 /* Sanitize tags and elements */ 19843 _sanitizeElements(currentNode); 19844 /* Check attributes next */ 19845 _sanitizeAttributes(currentNode); 19846 /* Shadow DOM detected, sanitize it */ 19847 if (currentNode.content instanceof DocumentFragment) { 19848 _sanitizeShadowDOM(currentNode.content); 19849 } 19850 } 19851 /* If we sanitized `dirty` in-place, return it. */ 19852 if (IN_PLACE) { 19853 return dirty; 19854 } 19855 /* Return sanitized string or DOM */ 19856 if (RETURN_DOM) { 19857 if (RETURN_DOM_FRAGMENT) { 19858 returnNode = createDocumentFragment.call(body.ownerDocument); 19859 while (body.firstChild) { 19860 // eslint-disable-next-line unicorn/prefer-dom-node-append 19861 returnNode.appendChild(body.firstChild); 19862 } 19863 } else { 19864 returnNode = body; 19865 } 19866 if (ALLOWED_ATTR.shadowroot || ALLOWED_ATTR.shadowrootmode) { 19867 /* 19868 AdoptNode() is not used because internal state is not reset 19869 (e.g. the past names map of a HTMLFormElement), this is safe 19870 in theory but we would rather not risk another attack vector. 19871 The state that is cloned by importNode() is explicitly defined 19872 by the specs. 19873 */ 19874 returnNode = importNode.call(originalDocument, returnNode, true); 19875 } 19876 return returnNode; 19877 } 19878 let serializedHTML = WHOLE_DOCUMENT ? body.outerHTML : body.innerHTML; 19879 /* Serialize doctype if allowed */ 19880 if (WHOLE_DOCUMENT && ALLOWED_TAGS['!doctype'] && body.ownerDocument && body.ownerDocument.doctype && body.ownerDocument.doctype.name && regExpTest(DOCTYPE_NAME, body.ownerDocument.doctype.name)) { 19881 serializedHTML = '<!DOCTYPE ' + body.ownerDocument.doctype.name + '>\n' + serializedHTML; 19882 } 19883 /* Sanitize final string template-safe */ 19884 if (SAFE_FOR_TEMPLATES) { 19885 arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => { 19886 serializedHTML = stringReplace(serializedHTML, expr, ' '); 19887 }); 19888 } 19889 return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(serializedHTML) : serializedHTML; 19890 }; 19891 DOMPurify.setConfig = function () { 19892 let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 19893 _parseConfig(cfg); 19894 SET_CONFIG = true; 19895 }; 19896 DOMPurify.clearConfig = function () { 19897 CONFIG = null; 19898 SET_CONFIG = false; 19899 }; 19900 DOMPurify.isValidAttribute = function (tag, attr, value) { 19901 /* Initialize shared config vars if necessary. */ 19902 if (!CONFIG) { 19903 _parseConfig({}); 19904 } 19905 const lcTag = transformCaseFunc(tag); 19906 const lcName = transformCaseFunc(attr); 19907 return _isValidAttribute(lcTag, lcName, value); 19908 }; 19909 DOMPurify.addHook = function (entryPoint, hookFunction) { 19910 if (typeof hookFunction !== 'function') { 19911 return; 19912 } 19913 arrayPush(hooks[entryPoint], hookFunction); 19914 }; 19915 DOMPurify.removeHook = function (entryPoint, hookFunction) { 19916 if (hookFunction !== undefined) { 19917 const index = arrayLastIndexOf(hooks[entryPoint], hookFunction); 19918 return index === -1 ? undefined : arraySplice(hooks[entryPoint], index, 1)[0]; 19919 } 19920 return arrayPop(hooks[entryPoint]); 19921 }; 19922 DOMPurify.removeHooks = function (entryPoint) {
vendor: 18,380 bytes, lines 19922-20388
19922 19923 hooks[entryPoint] = []; 19924 }; 19925 DOMPurify.removeAllHooks = function () { 19926 hooks = _createHooksMap(); 19927 }; 19928 return DOMPurify; 19929 } 19930 var purify = createDOMPurify(); 19931 19932 /** 19933 * This class handles parsing, modification and serialization of URI/URL strings. 19934 * @class tinymce.util.URI 19935 */ 19936 const each$6 = Tools.each, trim = Tools.trim; 19937 const queryParts = [ 19938 'source', 'protocol', 'authority', 'userInfo', 'user', 'password', 'host', 19939 'port', 'relative', 'path', 'directory', 'file', 'query', 'anchor' 19940 ]; 19941 const DEFAULT_PORTS = { 19942 ftp: 21, 19943 http: 80, 19944 https: 443, 19945 mailto: 25 19946 }; 19947 const safeSvgDataUrlElements = ['img', 'video']; 19948 const blockSvgDataUris = (allowSvgDataUrls, tagName) => { 19949 if (isNonNullable(allowSvgDataUrls)) { 19950 return !allowSvgDataUrls; 19951 } 19952 else { 19953 // Only allow SVGs by default on images/videos since the browser won't execute scripts on those elements 19954 return isNonNullable(tagName) ? !contains$2(safeSvgDataUrlElements, tagName) : true; 19955 } 19956 }; 19957 const decodeUri = (encodedUri) => { 19958 try { 19959 // Might throw malformed URI sequence 19960 return decodeURIComponent(encodedUri); 19961 } 19962 catch { 19963 // Fallback to non UTF-8 decoder 19964 return unescape(encodedUri); 19965 } 19966 }; 19967 const isInvalidUri = (settings, uri, tagName) => { 19968 // remove all whitespaces from decoded uri to prevent impact on regex matching 19969 const decodedUri = decodeUri(uri).replace(/\s/g, ''); 19970 if (settings.allow_script_urls) { 19971 return false; 19972 // Ensure we don't have a javascript URI, as that is not safe since it allows arbitrary JavaScript execution 19973 } 19974 else if (/((java|vb)script|mhtml):/i.test(decodedUri)) { 19975 return true; 19976 } 19977 else if (settings.allow_html_data_urls) { 19978 return false; 19979 } 19980 else if (/^data:image\//i.test(decodedUri)) { 19981 return blockSvgDataUris(settings.allow_svg_data_urls, tagName) && /^data:image\/svg\+xml/i.test(decodedUri); 19982 } 19983 else { 19984 return /^data:/i.test(decodedUri); 19985 } 19986 }; 19987 class URI { 19988 static parseDataUri(uri) { 19989 let type; 19990 const uriComponents = decodeURIComponent(uri).split(','); 19991 const matches = /data:([^;]+)/.exec(uriComponents[0]); 19992 if (matches) { 19993 type = matches[1]; 19994 } 19995 return { 19996 type, 19997 data: uriComponents[1] 19998 }; 19999 } 20000 /** 20001 * Check to see if a URI is safe to use in the Document Object Model (DOM). This will return 20002 * true if the URI can be used in the DOM without potentially triggering a security issue. 20003 * 20004 * @method isDomSafe 20005 * @static 20006 * @param {String} uri The URI to be validated. 20007 * @param {Object} context An optional HTML tag name where the element is being used. 20008 * @param {Object} options An optional set of options to use when determining if the URI is safe. 20009 * @return {Boolean} True if the URI is safe, otherwise false. 20010 */ 20011 static isDomSafe(uri, context, options = {}) { 20012 if (options.allow_script_urls) { 20013 return true; 20014 } 20015 else { 20016 const decodedUri = Entities.decode(uri).replace(/[\s\u0000-\u001F]+/g, ''); 20017 return !isInvalidUri(options, decodedUri, context); 20018 } 20019 } 20020 static getDocumentBaseUrl(loc) { 20021 let baseUrl; 20022 // Pass applewebdata:// and other non web protocols though 20023 if (loc.protocol.indexOf('http') !== 0 && loc.protocol !== 'file:') { 20024 baseUrl = loc.href ?? ''; 20025 } 20026 else { 20027 baseUrl = loc.protocol + '//' + loc.host + loc.pathname; 20028 } 20029 if (/^[^:]+:\/\/\/?[^\/]+\//.test(baseUrl)) { 20030 baseUrl = baseUrl.replace(/[\?#].*$/, '').replace(/[\/\\][^\/]+$/, ''); 20031 if (!/[\/\\]$/.test(baseUrl)) { 20032 baseUrl += '/'; 20033 } 20034 } 20035 return baseUrl; 20036 } 20037 source; 20038 protocol; 20039 authority; 20040 userInfo; 20041 user; 20042 password; 20043 host; 20044 port; 20045 relative; 20046 path = ''; 20047 directory = ''; 20048 file; 20049 query; 20050 anchor; 20051 settings; 20052 /** 20053 * Constructs a new URI instance. 20054 * 20055 * @constructor 20056 * @method URI 20057 * @param {String} url URI string to parse. 20058 * @param {Object} settings Optional settings object. 20059 */ 20060 constructor(url, settings = {}) { 20061 url = trim(url); 20062 this.settings = settings; 20063 const baseUri = settings.base_uri; 20064 const self = this; 20065 // Strange app protocol that isn't http/https or local anchor 20066 // For example: mailto,skype,tel etc. 20067 if (/^([\w\-]+):([^\/]{2})/i.test(url) || /^\s*#/.test(url)) { 20068 self.source = url; 20069 return; 20070 } 20071 const isProtocolRelative = url.indexOf('//') === 0; 20072 // Absolute path with no host, fake host and protocol 20073 if (url.indexOf('/') === 0 && !isProtocolRelative) { 20074 url = (baseUri ? baseUri.protocol || 'http' : 'http') + '://mce_host' + url; 20075 } 20076 // Relative path http:// or protocol relative //path 20077 if (!/^[\w\-]*:?\/\//.test(url)) { 20078 const baseUrl = baseUri ? baseUri.path : new URI(document.location.href).directory; 20079 if (baseUri?.protocol === '') { 20080 url = '//mce_host' + self.toAbsPath(baseUrl, url); 20081 } 20082 else { 20083 const match = /([^#?]*)([#?]?.*)/.exec(url); 20084 if (match) { 20085 url = ((baseUri && baseUri.protocol) || 'http') + '://mce_host' + self.toAbsPath(baseUrl, match[1]) + match[2]; 20086 } 20087 } 20088 } 20089 // Parse URL (Credits goes to Steave, http://blog.stevenlevithan.com/archives/parseuri) 20090 url = url.replace(/@@/g, '(mce_at)'); // Zope 3 workaround, they use @@something 20091 const urlMatch = /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@\/]*):?([^:@\/]*))?@)?(\[[a-zA-Z0-9:.%]+\]|[^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/.exec(url); 20092 if (urlMatch) { 20093 each$6(queryParts, (v, i) => { 20094 let part = urlMatch[i]; 20095 // Zope 3 workaround, they use @@something 20096 if (part) { 20097 part = part.replace(/\(mce_at\)/g, '@@'); 20098 } 20099 self[v] = part; 20100 }); 20101 } 20102 if (baseUri) { 20103 if (!self.protocol) { 20104 self.protocol = baseUri.protocol; 20105 } 20106 if (!self.userInfo) { 20107 self.userInfo = baseUri.userInfo; 20108 } 20109 if (!self.port && self.host === 'mce_host') { 20110 self.port = baseUri.port; 20111 } 20112 if (!self.host || self.host === 'mce_host') { 20113 self.host = baseUri.host; 20114 } 20115 self.source = ''; 20116 } 20117 if (isProtocolRelative) { 20118 self.protocol = ''; 20119 } 20120 } 20121 /** 20122 * Sets the internal path part of the URI. 20123 * 20124 * @method setPath 20125 * @param {String} path Path string to set. 20126 */ 20127 setPath(path) { 20128 const pathMatch = /^(.*?)\/?(\w+)?$/.exec(path); 20129 // Update path parts 20130 if (pathMatch) { 20131 this.path = pathMatch[0]; 20132 this.directory = pathMatch[1]; 20133 this.file = pathMatch[2]; 20134 } 20135 // Rebuild source 20136 this.source = ''; 20137 this.getURI(); 20138 } 20139 /** 20140 * Converts the specified URI into a relative URI based on the current URI instance location. 20141 * 20142 * @method toRelative 20143 * @param {String} uri URI to convert into a relative path/URI. 20144 * @return {String} Relative URI from the point specified in the current URI instance. 20145 * @example 20146 * // Converts an absolute URL to an relative URL url will be somedir/somefile.htm 20147 * const url = new tinymce.util.URI('http://www.site.com/dir/').toRelative('http://www.site.com/dir/somedir/somefile.htm'); 20148 */ 20149 toRelative(uri) { 20150 if (uri === './') { 20151 return uri; 20152 } 20153 const relativeUri = new URI(uri, { base_uri: this }); 20154 // Not on same domain/port or protocol 20155 if ((relativeUri.host !== 'mce_host' && this.host !== relativeUri.host && relativeUri.host) || this.port !== relativeUri.port || 20156 (this.protocol !== relativeUri.protocol && relativeUri.protocol !== '')) { 20157 return relativeUri.getURI(); 20158 } 20159 const tu = this.getURI(), uu = relativeUri.getURI(); 20160 // Allow usage of the base_uri when relative_urls = true 20161 if (tu === uu || (tu.charAt(tu.length - 1) === '/' && tu.substr(0, tu.length - 1) === uu)) { 20162 return tu; 20163 } 20164 let output = this.toRelPath(this.path, relativeUri.path); 20165 // Add query 20166 if (relativeUri.query) { 20167 output += '?' + relativeUri.query; 20168 } 20169 // Add anchor 20170 if (relativeUri.anchor) { 20171 output += '#' + relativeUri.anchor; 20172 } 20173 return output; 20174 } 20175 /** 20176 * Converts the specified URI into a absolute URI based on the current URI instance location. 20177 * 20178 * @method toAbsolute 20179 * @param {String} uri URI to convert into a relative path/URI. 20180 * @param {Boolean} noHost No host and protocol prefix. 20181 * @return {String} Absolute URI from the point specified in the current URI instance. 20182 * @example 20183 * // Converts an relative URL to an absolute URL url will be http://www.site.com/dir/somedir/somefile.htm 20184 * const url = new tinymce.util.URI('http://www.site.com/dir/').toAbsolute('somedir/somefile.htm'); 20185 */ 20186 toAbsolute(uri, noHost) { 20187 const absoluteUri = new URI(uri, { base_uri: this }); 20188 return absoluteUri.getURI(noHost && this.isSameOrigin(absoluteUri)); 20189 } 20190 /** 20191 * Determine whether the given URI has the same origin as this URI. Based on RFC-6454. 20192 * Supports default ports for protocols listed in DEFAULT_PORTS. Unsupported protocols will fail safe: they 20193 * won't match, if the port specifications differ. 20194 * 20195 * @method isSameOrigin 20196 * @param {tinymce.util.URI} uri Uri instance to compare. 20197 * @returns {Boolean} True if the origins are the same. 20198 */ 20199 isSameOrigin(uri) { 20200 // eslint-disable-next-line eqeqeq 20201 if (this.host == uri.host && this.protocol == uri.protocol) { 20202 // eslint-disable-next-line eqeqeq 20203 if (this.port == uri.port) { 20204 return true; 20205 } 20206 const defaultPort = this.protocol ? DEFAULT_PORTS[this.protocol] : null; 20207 // eslint-disable-next-line eqeqeq 20208 if (defaultPort && ((this.port || defaultPort) == (uri.port || defaultPort))) { 20209 return true; 20210 } 20211 } 20212 return false; 20213 } 20214 /** 20215 * Converts a absolute path into a relative path. 20216 * 20217 * @method toRelPath 20218 * @param {String} base Base point to convert the path from. 20219 * @param {String} path Absolute path to convert into a relative path. 20220 */ 20221 toRelPath(base, path) { 20222 let breakPoint = 0, out = '', i, l; 20223 // Split the paths 20224 const normalizedBase = base.substring(0, base.lastIndexOf('/')).split('/'); 20225 const items = path.split('/'); 20226 if (normalizedBase.length >= items.length) { 20227 for (i = 0, l = normalizedBase.length; i < l; i++) { 20228 if (i >= items.length || normalizedBase[i] !== items[i]) { 20229 breakPoint = i + 1; 20230 break; 20231 } 20232 } 20233 } 20234 if (normalizedBase.length < items.length) { 20235 for (i = 0, l = items.length; i < l; i++) { 20236 if (i >= normalizedBase.length || normalizedBase[i] !== items[i]) { 20237 breakPoint = i + 1; 20238 break; 20239 } 20240 } 20241 } 20242 if (breakPoint === 1) { 20243 return path; 20244 } 20245 for (i = 0, l = normalizedBase.length - (breakPoint - 1); i < l; i++) { 20246 out += '../'; 20247 } 20248 for (i = breakPoint - 1, l = items.length; i < l; i++) { 20249 if (i !== breakPoint - 1) { 20250 out += '/' + items[i]; 20251 } 20252 else { 20253 out += items[i]; 20254 } 20255 } 20256 return out; 20257 } 20258 /** 20259 * Converts a relative path into a absolute path. 20260 * 20261 * @method toAbsPath 20262 * @param {String} base Base point to convert the path from. 20263 * @param {String} path Relative path to convert into an absolute path. 20264 */ 20265 toAbsPath(base, path) { 20266 let nb = 0; 20267 // Split paths 20268 const tr = /\/$/.test(path) ? '/' : ''; 20269 const normalizedBase = base.split('/'); 20270 const normalizedPath = path.split('/'); 20271 // Remove empty chunks 20272 const baseParts = []; 20273 each$6(normalizedBase, (k) => { 20274 if (k) { 20275 baseParts.push(k); 20276 } 20277 }); 20278 // Merge relURLParts chunks 20279 const pathParts = []; 20280 for (let i = normalizedPath.length - 1; i >= 0; i--) { 20281 // Ignore empty or . 20282 if (normalizedPath[i].length === 0 || normalizedPath[i] === '.') { 20283 continue; 20284 } 20285 // Is parent 20286 if (normalizedPath[i] === '..') { 20287 nb++; 20288 continue; 20289 } 20290 // Move up 20291 if (nb > 0) { 20292 nb--; 20293 continue; 20294 } 20295 pathParts.push(normalizedPath[i]); 20296 } 20297 const i = baseParts.length - nb; 20298 // If /a/b/c or / 20299 let outPath; 20300 if (i <= 0) { 20301 outPath = reverse(pathParts).join('/'); 20302 } 20303 else { 20304 outPath = baseParts.slice(0, i).join('/') + '/' + reverse(pathParts).join('/'); 20305 } 20306 // Add front / if it's needed 20307 if (outPath.indexOf('/') !== 0) { 20308 outPath = '/' + outPath; 20309 } 20310 // Add trailing / if it's needed 20311 if (tr && outPath.lastIndexOf('/') !== outPath.length - 1) { 20312 outPath += tr; 20313 } 20314 return outPath; 20315 } 20316 /** 20317 * Returns the full URI of the internal structure. 20318 * 20319 * @method getURI 20320 * @param {Boolean} noProtoHost Optional no host and protocol part. Defaults to false. 20321 */ 20322 getURI(noProtoHost = false) { 20323 let s; 20324 // Rebuild source 20325 if (!this.source || noProtoHost) { 20326 s = ''; 20327 if (!noProtoHost) { 20328 if (this.protocol) { 20329 s += this.protocol + '://'; 20330 } 20331 else { 20332 s += '//'; 20333 } 20334 if (this.userInfo) { 20335 s += this.userInfo + '@'; 20336 } 20337 if (this.host) { 20338 s += this.host; 20339 } 20340 if (this.port) { 20341 s += ':' + this.port; 20342 } 20343 } 20344 if (this.path) { 20345 s += this.path; 20346 } 20347 if (this.query) { 20348 s += '?' + this.query; 20349 } 20350 if (this.anchor) { 20351 s += '#' + this.anchor; 20352 } 20353 this.source = s; 20354 } 20355 return this.source; 20356 } 20357 } 20358 20359 // A list of attributes that should be filtered further based on the parser settings 20360 const filteredUrlAttrs = Tools.makeMap('src,href,data,background,action,formaction,poster,xlink:href'); 20361 const internalElementAttr = 'data-mce-type'; 20362 let uid = 0; 20363 const processNode = (node, settings, schema, scope, evt) => { 20364 const validate = settings.validate; 20365 const specialElements = schema.getSpecialElements(); 20366 if (node.nodeType === COMMENT) { 20367 // Pad conditional comments if they aren't allowed 20368 if (!settings.allow_conditional_comments && /^\[if/i.test(node.nodeValue ?? '')) { 20369 node.nodeValue = ' ' + node.nodeValue; 20370 } 20371 if (settings.sanitize && settings.allow_html_in_comments && isString(node.nodeValue)) { 20372 node.nodeValue = encodeData(node.nodeValue); 20373 } 20374 } 20375 const lcTagName = evt?.tagName ?? node.nodeName.toLowerCase(); 20376 if (scope !== 'html' && schema.isValid(scope)) { 20377 if (isNonNullable(evt)) { 20378 evt.allowedTags[lcTagName] = true; 20379 } 20380 return; 20381 } 20382 // Just leave non-elements such as text and comments up to dompurify 20383 if (node.nodeType !== ELEMENT || lcTagName === 'body') { 20384 return; 20385 } 20386 // Construct the sugar element wrapper 20387 const element = SugarElement.fromDom(node); 20388
vendor: 5,266 bytes, lines 20388-20492
20388// Determine if we're dealing with an internal attribute 20389 const isInternalElement = has$1(element, internalElementAttr); 20390 // Cleanup bogus elements 20391 const bogus = get$9(element, 'data-mce-bogus'); 20392 if (!isInternalElement && isString(bogus)) { 20393 if (bogus === 'all') { 20394 remove$8(element); 20395 } 20396 else { 20397 unwrap(element); 20398 } 20399 return; 20400 } 20401 // Determine if the schema allows the element and either add it or remove it 20402 const rule = schema.getElementRule(lcTagName); 20403 if (validate && !rule) { 20404 // If a special element is invalid, then remove the entire element instead of unwrapping 20405 if (has$2(specialElements, lcTagName)) { 20406 remove$8(element); 20407 } 20408 else { 20409 unwrap(element); 20410 } 20411 return; 20412 } 20413 else { 20414 if (isNonNullable(evt)) { 20415 evt.allowedTags[lcTagName] = true; 20416 } 20417 } 20418 // Validate the element using the attribute rules 20419 if (validate && rule && !isInternalElement) { 20420 // Fix the attributes for the element, unwrapping it if we have to 20421 each$e(rule.attributesForced ?? [], (attr) => { 20422 set$4(element, attr.name, attr.value === '{$uid}' ? `mce_${uid++}` : attr.value); 20423 }); 20424 each$e(rule.attributesDefault ?? [], (attr) => { 20425 if (!has$1(element, attr.name)) { 20426 set$4(element, attr.name, attr.value === '{$uid}' ? `mce_${uid++}` : attr.value); 20427 } 20428 }); 20429 // If none of the required attributes were found then remove 20430 if (rule.attributesRequired && !exists(rule.attributesRequired, (attr) => has$1(element, attr))) { 20431 unwrap(element); 20432 return; 20433 } 20434 // If there are no attributes then remove 20435 if (rule.removeEmptyAttrs && hasNone(element)) { 20436 unwrap(element); 20437 return; 20438 } 20439 // Change the node name if the schema says to 20440 if (rule.outputName && rule.outputName !== lcTagName) { 20441 mutate(element, rule.outputName); 20442 } 20443 } 20444 }; 20445 const processAttr = (ele, settings, schema, scope, evt) => { 20446 const tagName = ele.tagName.toLowerCase(); 20447 const { attrName, attrValue } = evt; 20448 evt.keepAttr = shouldKeepAttribute(settings, schema, scope, tagName, attrName, attrValue); 20449 if (evt.keepAttr) { 20450 evt.allowedAttributes[attrName] = true; 20451 if (isBooleanAttributeOfNonCustomElement(attrName, schema, ele.nodeName)) { 20452 evt.attrValue = attrName; 20453 } 20454 // We need to tell DOMPurify to forcibly keep the attribute if it's an SVG data URI and svg data URIs are allowed 20455 if (settings.allow_svg_data_urls && startsWith(attrValue, 'data:image/svg+xml')) { 20456 evt.forceKeepAttr = true; 20457 } 20458 // For internal elements always keep the attribute if the attribute name is id, class or style 20459 } 20460 else if (isRequiredAttributeOfInternalElement(ele, attrName)) { 20461 evt.forceKeepAttr = true; 20462 } 20463 }; 20464 const shouldKeepAttribute = (settings, schema, scope, tagName, attrName, attrValue) => { 20465 // All attributes within non HTML namespaces elements are considered valid 20466 if (scope !== 'html' && !isNonHtmlElementRootName(tagName)) { 20467 return true; 20468 } 20469 return !(attrName in filteredUrlAttrs && isInvalidUri(settings, attrValue, tagName)) && 20470 (!settings.validate || schema.isValid(tagName, attrName) || startsWith(attrName, 'data-') || startsWith(attrName, 'aria-')); 20471 }; 20472 const isRequiredAttributeOfInternalElement = (ele, attrName) => ele.hasAttribute(internalElementAttr) && (attrName === 'id' || attrName === 'class' || attrName === 'style'); 20473 const isBooleanAttributeOfNonCustomElement = (attrName, schema, nodeName) => attrName in schema.getBoolAttrs() && !has$2(schema.getCustomElements(), nodeName.toLowerCase()); 20474 const filterAttributes = (ele, settings, schema, scope) => { 20475 const { attributes } = ele; 20476 for (let i = attributes.length - 1; i >= 0; i--) { 20477 const attr = attributes[i]; 20478 const attrName = attr.name; 20479 const attrValue = attr.value; 20480 if (!shouldKeepAttribute(settings, schema, scope, ele.tagName.toLowerCase(), attrName, attrValue) && !isRequiredAttributeOfInternalElement(ele, attrName)) { 20481 ele.removeAttribute(attrName); 20482 } 20483 else if (isBooleanAttributeOfNonCustomElement(attrName, schema, ele.nodeName)) { 20484 ele.setAttribute(attrName, attrName); 20485 } 20486 } 20487 }; 20488 const setupPurify = (settings, schema, namespaceTracker) => { 20489 const purify$1 = purify(); 20490 // We use this to add new tags to the allow-list as we parse, if we notice that a tag has been banned but it's still in the schema 20491 purify$1.addHook('uponSanitizeElement', (ele, evt) => { 20492 processNode(ele, settings, schema, namespaceTracker.track(ele), evt
vendor: 67,209 bytes, lines 20492-21898
20492); 20493 }); 20494 // Let's do the same thing for attributes 20495 purify$1.addHook('uponSanitizeAttribute', (ele, evt) => { 20496 processAttr(ele, settings, schema, namespaceTracker.current(), evt); 20497 }); 20498 return purify$1; 20499 }; 20500 const getPurifyConfig = (settings, mimeType) => { 20501 const basePurifyConfig = { 20502 IN_PLACE: true, 20503 ALLOW_UNKNOWN_PROTOCOLS: true, 20504 // Deliberately ban all tags and attributes by default, and then un-ban them on demand in hooks 20505 // #comment and #cdata-section are always allowed as they aren't controlled via the schema 20506 // body is also allowed due to the DOMPurify checking the root node before sanitizing 20507 ALLOWED_TAGS: ['#comment', '#cdata-section', 'body', 'html'], 20508 ALLOWED_ATTR: [] 20509 }; 20510 const config = { ...basePurifyConfig }; 20511 // Set the relevant parser mimetype 20512 config.PARSER_MEDIA_TYPE = mimeType; 20513 // Allow any URI when allowing script urls 20514 if (settings.allow_script_urls) { 20515 config.ALLOWED_URI_REGEXP = /.*/; 20516 // Allow anything except javascript (or similar) URIs if all html data urls are allowed 20517 } 20518 else if (settings.allow_html_data_urls) { 20519 config.ALLOWED_URI_REGEXP = /^(?!(\w+script|mhtml):)/i; 20520 } 20521 return config; 20522 }; 20523 const sanitizeSvgElement = (ele) => { 20524 // xlink:href used to be the way to do links in SVG 1.x https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href 20525 const xlinkAttrs = ['type', 'href', 'role', 'arcrole', 'title', 'show', 'actuate', 'label', 'from', 'to'].map((name) => `xlink:${name}`); 20526 const config = { 20527 IN_PLACE: true, 20528 USE_PROFILES: { 20529 html: true, 20530 svg: true, 20531 svgFilters: true 20532 }, 20533 ALLOWED_ATTR: xlinkAttrs 20534 }; 20535 purify().sanitize(ele, config); 20536 }; 20537 const sanitizeMathmlElement = (node, settings) => { 20538 const config = { 20539 IN_PLACE: true, 20540 USE_PROFILES: { 20541 mathMl: true 20542 }, 20543 }; 20544 const purify$1 = purify(); 20545 const allowedEncodings = settings.allow_mathml_annotation_encodings; 20546 const hasAllowedEncodings = isArray$1(allowedEncodings) && allowedEncodings.length > 0; 20547 const hasValidEncoding = (el) => { 20548 const encoding = el.getAttribute('encoding'); 20549 return hasAllowedEncodings && isString(encoding) && contains$2(allowedEncodings, encoding); 20550 }; 20551 const isValidElementOpt = (node, lcTagName) => { 20552 if (hasAllowedEncodings && lcTagName === 'semantics') { 20553 return Optional.some(true); 20554 } 20555 else if (lcTagName === 'annotation') { 20556 return Optional.some(isElement$7(node) && hasValidEncoding(node)); 20557 } 20558 else if (isArray$1(settings.extended_mathml_elements)) { 20559 if (settings.extended_mathml_elements.includes(lcTagName)) { 20560 return Optional.from(true); 20561 } 20562 else { 20563 return Optional.none(); 20564 } 20565 } 20566 else { 20567 return Optional.none(); 20568 } 20569 }; 20570 purify$1.addHook('uponSanitizeElement', (node, evt) => { 20571 // We know the node is an element as we have 20572 // passed an element to the purify.sanitize function below 20573 const lcTagName = evt.tagName ?? node.nodeName.toLowerCase(); 20574 const keepElementOpt = isValidElementOpt(node, lcTagName); 20575 keepElementOpt.each((keepElement) => { 20576 evt.allowedTags[lcTagName] = keepElement; 20577 if (!keepElement && settings.sanitize) { 20578 if (isElement$7(node)) { 20579 node.remove(); 20580 } 20581 } 20582 }); 20583 }); 20584 purify$1.addHook('uponSanitizeAttribute', (_node, event) => { 20585 if (isArray$1(settings.extended_mathml_attributes)) { 20586 const keepAttribute = settings.extended_mathml_attributes.includes(event.attrName); 20587 if (keepAttribute) { 20588 event.forceKeepAttr = true; 20589 } 20590 } 20591 }); 20592 purify$1.sanitize(node, config); 20593 }; 20594 const mkSanitizeNamespaceElement = (settings) => (ele) => { 20595 const namespaceType = toScopeType(ele); 20596 if (namespaceType === 'svg') { 20597 sanitizeSvgElement(ele); 20598 } 20599 else if (namespaceType === 'math') { 20600 sanitizeMathmlElement(ele, settings); 20601 } 20602 else { 20603 throw new Error('Not a namespace element'); 20604 } 20605 }; 20606 const getSanitizer = (settings, schema) => { 20607 const namespaceTracker = createNamespaceTracker(); 20608 if (settings.sanitize) { 20609 const purify = setupPurify(settings, schema, namespaceTracker); 20610 const sanitizeHtmlElement = (body, mimeType) => { 20611 purify.sanitize(body, getPurifyConfig(settings, mimeType)); 20612 purify.removed = []; 20613 namespaceTracker.reset(); 20614 }; 20615 return { 20616 sanitizeHtmlElement, 20617 sanitizeNamespaceElement: mkSanitizeNamespaceElement(settings) 20618 }; 20619 } 20620 else { 20621 const sanitizeHtmlElement = (body, _mimeType) => { 20622 // eslint-disable-next-line no-bitwise 20623 const nodeIterator = document.createNodeIterator(body, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT); 20624 let node; 20625 while ((node = nodeIterator.nextNode())) { 20626 const currentScope = namespaceTracker.track(node); 20627 processNode(node, settings, schema, currentScope); 20628 if (isElement$7(node)) { 20629 filterAttributes(node, settings, schema, currentScope); 20630 } 20631 } 20632 namespaceTracker.reset(); 20633 }; 20634 const sanitizeNamespaceElement = noop; 20635 return { 20636 sanitizeHtmlElement, 20637 sanitizeNamespaceElement 20638 }; 20639 } 20640 }; 20641 20642 /** 20643 * @summary 20644 * This class parses HTML code into a DOM like structure of nodes it will remove redundant whitespace and make 20645 * sure that the node tree is valid according to the specified schema. 20646 * So for example: `<p>a<p>b</p>c</p>` will become `<p>a</p><p>b</p><p>c</p>`. 20647 * 20648 * @example 20649 * const parser = tinymce.html.DomParser({ validate: true }, schema); 20650 * const rootNode = parser.parse('<h1>content</h1>'); 20651 * 20652 * @class tinymce.html.DomParser 20653 * @version 3.4 20654 */ 20655 const extraBlockLikeElements = ['script', 'style', 'template', 'param', 'meta', 'title', 'link']; 20656 const makeMap = Tools.makeMap, extend$1 = Tools.extend; 20657 const transferChildren = (parent, nativeParent, specialElements, nsSanitizer, decodeComments) => { 20658 const parentName = parent.name; 20659 // Exclude the special elements where the content is RCDATA as their content needs to be parsed instead of being left as plain text 20660 // See: https://html.spec.whatwg.org/multipage/parsing.html#parsing-html-fragments 20661 const isSpecial = parentName in specialElements && parentName !== 'title' && parentName !== 'textarea' && parentName !== 'noscript'; 20662 const childNodes = nativeParent.childNodes; 20663 for (let ni = 0, nl = childNodes.length; ni < nl; ni++) { 20664 const nativeChild = childNodes[ni]; 20665 const child = new AstNode(nativeChild.nodeName.toLowerCase(), nativeChild.nodeType); 20666 if (isElement$7(nativeChild)) { 20667 const attributes = nativeChild.attributes; 20668 for (let ai = 0, al = attributes.length; ai < al; ai++) { 20669 const attr = attributes[ai]; 20670 child.attr(attr.name, attr.value); 20671 } 20672 if (isNonHtmlElementRootName(child.name)) { 20673 nsSanitizer(nativeChild); 20674 child.value = nativeChild.innerHTML; 20675 } 20676 } 20677 else if (isText$b(nativeChild)) { 20678 child.value = nativeChild.data; 20679 if (isSpecial) { 20680 child.raw = true; 20681 } 20682 } 20683 else if (isComment(nativeChild)) { 20684 child.value = decodeComments ? decodeData$1(nativeChild.data) : nativeChild.data; 20685 } 20686 else if (isCData(nativeChild) || isPi(nativeChild)) { 20687 child.value = nativeChild.data; 20688 } 20689 if (isTemplate(nativeChild)) { 20690 const content = AstNode.create('#text'); 20691 content.value = nativeChild.innerHTML; 20692 content.raw = true; 20693 child.append(content); 20694 } 20695 else if (!isNonHtmlElementRootName(child.name)) { 20696 transferChildren(child, nativeChild, specialElements, nsSanitizer, decodeComments); 20697 } 20698 parent.append(child); 20699 } 20700 }; 20701 const walkTree = (root, preprocessors, postprocessors) => { 20702 const traverseOrder = []; 20703 for (let node = root, lastNode = node; node; lastNode = node, node = node.walk()) { 20704 const tempNode = node; 20705 each$e(preprocessors, (preprocess) => preprocess(tempNode)); 20706 if (isNullable(tempNode.parent) && tempNode !== root) { 20707 // The node has been detached, so rewind a little and don't add it to our traversal 20708 node = lastNode; 20709 } 20710 else { 20711 traverseOrder.push(tempNode); 20712 } 20713 } 20714 for (let i = traverseOrder.length - 1; i >= 0; i--) { 20715 const node = traverseOrder[i]; 20716 each$e(postprocessors, (postprocess) => postprocess(node)); 20717 } 20718 }; 20719 // All the dom operations we want to perform, regardless of whether we're trying to properly validate things 20720 // e.g. removing excess whitespace 20721 // e.g. removing empty nodes (or padding them with <br>) 20722 // 20723 // Returns [ preprocess, postprocess ] 20724 const whitespaceCleaner = (root, schema, settings, args) => { 20725 const validate = settings.validate; 20726 const nonEmptyElements = schema.getNonEmptyElements(); 20727 const whitespaceElements = schema.getWhitespaceElements(); 20728 const blockElements = extend$1(makeMap(extraBlockLikeElements), schema.getBlockElements()); 20729 const textRootBlockElements = getTextRootBlockElements(schema); 20730 const allWhiteSpaceRegExp = /[ \t\r\n]+/g; 20731 const startWhiteSpaceRegExp = /^[ \t\r\n]+/; 20732 const endWhiteSpaceRegExp = /[ \t\r\n]+$/; 20733 const hasWhitespaceParent = (node) => { 20734 let tempNode = node.parent; 20735 while (isNonNullable(tempNode)) { 20736 if (tempNode.name in whitespaceElements) { 20737 return true; 20738 } 20739 else { 20740 tempNode = tempNode.parent; 20741 } 20742 } 20743 return false; 20744 }; 20745 const isTextRootBlockEmpty = (node) => { 20746 let tempNode = node; 20747 while (isNonNullable(tempNode)) { 20748 if (tempNode.name in textRootBlockElements) { 20749 return isEmpty$2(schema, nonEmptyElements, whitespaceElements, tempNode); 20750 } 20751 else { 20752 tempNode = tempNode.parent; 20753 } 20754 } 20755 return false; 20756 }; 20757 const isBlock = (node) => node.name in blockElements || isTransparentAstBlock(schema, node) || (isNonHtmlElementRootName(node.name) && node.parent === root); 20758 const isAtEdgeOfBlock = (node, start) => { 20759 const neighbour = start ? node.prev : node.next; 20760 if (isNonNullable(neighbour) || isNullable(node.parent)) { 20761 return false; 20762 } 20763 // Make sure our parent is actually a block, and also make sure it isn't a temporary "context" element 20764 // that we're probably going to unwrap as soon as we insert this content into the editor 20765 return isBlock(node.parent) && (node.parent !== root || args.isRootContent === true); 20766 }; 20767 const preprocess = (node) => { 20768 if (node.type === 3) { 20769 // Remove leading whitespace here, so that all whitespace in nodes to the left of us has already been fixed 20770 if (!hasWhitespaceParent(node)) { 20771 let text = node.value ?? ''; 20772 text = text.replace(allWhiteSpaceRegExp, ' '); 20773 if (isLineBreakNode(node.prev, isBlock) || isAtEdgeOfBlock(node, true)) { 20774 text = text.replace(startWhiteSpaceRegExp, ''); 20775 } 20776 if (text.length === 0) { 20777 node.remove(); 20778 } 20779 else if (text === ' ' && node.prev && node.prev.type === COMMENT && node.next && node.next.type === COMMENT) { 20780 node.remove(); 20781 } 20782 else { 20783 node.value = text; 20784 } 20785 } 20786 } 20787 }; 20788 const postprocess = (node) => { 20789 if (node.type === 1) { 20790 // Check for empty nodes here, because children will have been processed and (if necessary) emptied / removed already 20791 const elementRule = schema.getElementRule(node.name); 20792 if (validate && elementRule) { 20793 const isNodeEmpty = isEmpty$2(schema, nonEmptyElements, whitespaceElements, node); 20794 if (elementRule.paddInEmptyBlock && isNodeEmpty && isTextRootBlockEmpty(node)) { 20795 paddEmptyNode(settings, args, isBlock, node); 20796 } 20797 else if (elementRule.removeEmpty && isNodeEmpty) { 20798 if (isBlock(node)) { 20799 node.remove(); 20800 } 20801 else { 20802 node.unwrap(); 20803 } 20804 } 20805 else if (elementRule.paddEmpty && (isNodeEmpty || isPaddedWithNbsp(node))) { 20806 paddEmptyNode(settings, args, isBlock, node); 20807 } 20808 } 20809 } 20810 else if (node.type === 3) { 20811 // Removing trailing whitespace here, so that all whitespace in nodes to the right of us has already been fixed 20812 if (!hasWhitespaceParent(node)) { 20813 let text = node.value ?? ''; 20814 if (node.next && isBlock(node.next) || isAtEdgeOfBlock(node, false)) { 20815 text = text.replace(endWhiteSpaceRegExp, ''); 20816 } 20817 if (text.length === 0) { 20818 node.remove(); 20819 } 20820 else { 20821 node.value = text; 20822 } 20823 } 20824 } 20825 }; 20826 return [preprocess, postprocess]; 20827 }; 20828 const getRootBlockName = (settings, args) => { 20829 const name = args.forced_root_block ?? settings.forced_root_block; 20830 if (name === false) { 20831 return ''; 20832 } 20833 else if (name === true) { 20834 return 'p'; 20835 } 20836 else { 20837 return name; 20838 } 20839 }; 20840 const xhtmlAttribte = ' xmlns="http://www.w3.org/1999/xhtml"'; 20841 const DomParser = (settings = {}, schema = Schema()) => { 20842 const nodeFilterRegistry = create$8(); 20843 const attributeFilterRegistry = create$8(); 20844 // Apply setting defaults 20845 const defaultedSettings = { 20846 validate: true, 20847 root_name: 'body', 20848 sanitize: true, 20849 allow_html_in_comments: false, 20850 ...settings 20851 }; 20852 const parser = new DOMParser(); 20853 const sanitizer = getSanitizer(defaultedSettings, schema); 20854 const parseAndSanitizeWithContext = (html, rootName, format = 'html', useDocumentNotBody = false) => { 20855 const isxhtml = format === 'xhtml'; 20856 const mimeType = isxhtml ? 'application/xhtml+xml' : 'text/html'; 20857 // Determine the root element to wrap the HTML in when parsing. If we're dealing with a 20858 // special element then we need to wrap it so the internal content is handled appropriately. 20859 const isSpecialRoot = has$2(schema.getSpecialElements(), rootName.toLowerCase()); 20860 const content = isSpecialRoot ? `<${rootName}>${html}</${rootName}>` : html; 20861 const makeWrap = () => { 20862 if (/^[\s]*<head/i.test(html) || /^[\s]*<html/i.test(html) || /^[\s]*<!DOCTYPE/i.test(html)) { 20863 return `<html${isxhtml ? xhtmlAttribte : ''}>${content}</html>`; 20864 } 20865 else { 20866 if (isxhtml) { 20867 return `<html${xhtmlAttribte}><head></head><body>${content}</body></html>`; 20868 } 20869 else { 20870 return `<body>${content}</body>`; 20871 } 20872 } 20873 }; 20874 const document = parser.parseFromString(makeWrap(), mimeType); 20875 const body = useDocumentNotBody ? document.documentElement : document.body; 20876 sanitizer.sanitizeHtmlElement(body, mimeType); 20877 return isSpecialRoot ? body.firstChild : body; 20878 }; 20879 /** 20880 * Adds a node filter function to the parser, the parser will collect the specified nodes by name 20881 * and then execute the callback once it has finished parsing the document. 20882 * 20883 * @method addNodeFilter 20884 * @param {String} name Comma separated list of nodes to collect. 20885 * @param {Function} callback Callback function to execute once it has collected nodes. 20886 * @example 20887 * parser.addNodeFilter('p,h1', (nodes, name) => { 20888 * for (var i = 0; i < nodes.length; i++) { 20889 * console.log(nodes[i].name); 20890 * } 20891 * }); 20892 */ 20893 const addNodeFilter = nodeFilterRegistry.addFilter; 20894 const getNodeFilters = nodeFilterRegistry.getFilters; 20895 /** 20896 * Removes a node filter function or removes all filter functions from the parser for the node names provided. 20897 * 20898 * @method removeNodeFilter 20899 * @param {String} name Comma separated list of node names to remove filters for. 20900 * @param {Function} callback Optional callback function to only remove a specific callback. 20901 * @example 20902 * // Remove a single filter 20903 * parser.removeNodeFilter('p,h1', someCallback); 20904 * 20905 * // Remove all filters 20906 * parser.removeNodeFilter('p,h1'); 20907 */ 20908 const removeNodeFilter = nodeFilterRegistry.removeFilter; 20909 /** 20910 * Adds an attribute filter function to the parser, the parser will collect nodes that has the specified attributes 20911 * and then execute the callback once it has finished parsing the document. 20912 * 20913 * @method addAttributeFilter 20914 * @param {String} name Comma separated list of attributes to collect. 20915 * @param {Function} callback Callback function to execute once it has collected nodes. 20916 * @example 20917 * parser.addAttributeFilter('src,href', (nodes, name) => { 20918 * for (let i = 0; i < nodes.length; i++) { 20919 * console.log(nodes[i].name); 20920 * } 20921 * }); 20922 */ 20923 const addAttributeFilter = attributeFilterRegistry.addFilter; 20924 const getAttributeFilters = attributeFilterRegistry.getFilters; 20925 /** 20926 * Removes an attribute filter function or removes all filter functions from the parser for the attribute names provided. 20927 * 20928 * @method removeAttributeFilter 20929 * @param {String} name Comma separated list of attribute names to remove filters for. 20930 * @param {Function} callback Optional callback function to only remove a specific callback. 20931 * @example 20932 * // Remove a single filter 20933 * parser.removeAttributeFilter('src,href', someCallback); 20934 * 20935 * // Remove all filters 20936 * parser.removeAttributeFilter('src,href'); 20937 */ 20938 const removeAttributeFilter = attributeFilterRegistry.removeFilter; 20939 const findInvalidChildren = (node, invalidChildren) => { 20940 if (isInvalid(schema, node)) { 20941 invalidChildren.push(node); 20942 } 20943 }; 20944 const isWrappableNode = (blockElements, node) => { 20945 const isInternalElement = isString(node.attr(internalElementAttr)); 20946 const isInlineElement = node.type === 1 && (!has$2(blockElements, node.name) && !isTransparentAstBlock(schema, node)) && !isNonHtmlElementRootName(node.name); 20947 return node.type === 3 || (isInlineElement && !isInternalElement); 20948 }; 20949 const addRootBlocks = (rootNode, rootBlockName) => { 20950 const blockElements = extend$1(makeMap(extraBlockLikeElements), schema.getBlockElements()); 20951 const startWhiteSpaceRegExp = /^[ \t\r\n]+/; 20952 const endWhiteSpaceRegExp = /[ \t\r\n]+$/; 20953 let node = rootNode.firstChild, rootBlockNode = null; 20954 // Removes whitespace at beginning and end of block so: 20955 // <p> x </p> -> <p>x</p> 20956 const trim = (rootBlock) => { 20957 if (rootBlock) { 20958 node = rootBlock.firstChild; 20959 if (node && node.type === 3) { 20960 node.value = node.value?.replace(startWhiteSpaceRegExp, ''); 20961 } 20962 node = rootBlock.lastChild; 20963 if (node && node.type === 3) { 20964 node.value = node.value?.replace(endWhiteSpaceRegExp, ''); 20965 } 20966 } 20967 }; 20968 // Check if rootBlock is valid within rootNode for example if P is valid in H1 if H1 is the contentEditable root 20969 if (!schema.isValidChild(rootNode.name, rootBlockName.toLowerCase())) { 20970 return; 20971 } 20972 while (node) { 20973 const next = node.next; 20974 if (isWrappableNode(blockElements, node)) { 20975 if (!rootBlockNode) { 20976 // Create a new root block element 20977 rootBlockNode = new AstNode(rootBlockName, 1); 20978 rootBlockNode.attr(defaultedSettings.forced_root_block_attrs); 20979 rootNode.insert(rootBlockNode, node); 20980 rootBlockNode.append(node); 20981 } 20982 else { 20983 rootBlockNode.append(node); 20984 } 20985 } 20986 else { 20987 trim(rootBlockNode); 20988 rootBlockNode = null; 20989 } 20990 node = next; 20991 } 20992 trim(rootBlockNode); 20993 }; 20994 /** 20995 * Parses the specified HTML string into a DOM like node tree and returns the result. 20996 * 20997 * @method parse 20998 * @param {String} html Html string to sax parse. 20999 * @param {Object} args Optional args object that gets passed to all filter functions. 21000 * @return {tinymce.html.Node} Root node containing the tree. 21001 * @example 21002 * const rootNode = tinymce.html.DomParser({...}).parse('<b>text</b>'); 21003 */ 21004 const parse = (html, args = {}) => { 21005 const validate = defaultedSettings.validate; 21006 const preferFullDocument = (args.context ?? defaultedSettings.root_name) === '#document'; 21007 const rootName = args.context ?? (preferFullDocument ? 'html' : defaultedSettings.root_name); 21008 // Parse and sanitize the content 21009 const element = parseAndSanitizeWithContext(html, rootName, args.format, preferFullDocument); 21010 updateChildren(schema, element); 21011 // Create the AST representation 21012 const rootNode = new AstNode(rootName, 11); 21013 transferChildren(rootNode, element, schema.getSpecialElements(), sanitizer.sanitizeNamespaceElement, defaultedSettings.sanitize && defaultedSettings.allow_html_in_comments); 21014 // This next line is needed to fix a memory leak in chrome and firefox. 21015 // For more information see TINY-9186 21016 element.innerHTML = ''; 21017 // Set up whitespace fixes 21018 const [whitespacePre, whitespacePost] = whitespaceCleaner(rootNode, schema, defaultedSettings, args); 21019 // Find the invalid children in the tree 21020 const invalidChildren = []; 21021 const invalidFinder = validate ? (node) => findInvalidChildren(node, invalidChildren) : noop; 21022 // Set up attribute and node matching 21023 const matches = { nodes: {}, attributes: {} }; 21024 const matchFinder = (node) => matchNode(getNodeFilters(), getAttributeFilters(), node, matches); 21025 // Walk the dom, apply all of the above things 21026 walkTree(rootNode, [whitespacePre, matchFinder], [whitespacePost, invalidFinder]); 21027 // Because we collected invalid children while walking backwards, we need to reverse the list before operating on them 21028 invalidChildren.reverse(); 21029 // Fix invalid children or report invalid children in a contextual parsing 21030 if (validate && invalidChildren.length > 0) { 21031 if (args.context) { 21032 args.invalid = true; 21033 } 21034 else { 21035 cleanInvalidNodes(invalidChildren, schema, rootNode, matchFinder); 21036 } 21037 } 21038 // Wrap nodes in the root into block elements if the root is body 21039 const rootBlockName = getRootBlockName(defaultedSettings, args); 21040 if (rootBlockName && (rootNode.name === 'body' || args.isRootContent)) { 21041 addRootBlocks(rootNode, rootBlockName); 21042 } 21043 // Run filters only when the contents is valid 21044 if (!args.invalid) { 21045 runFilters(matches, args); 21046 } 21047 return rootNode; 21048 }; 21049 const exports = { 21050 schema, 21051 addAttributeFilter, 21052 getAttributeFilters, 21053 removeAttributeFilter, 21054 addNodeFilter, 21055 getNodeFilters, 21056 removeNodeFilter, 21057 parse 21058 }; 21059 register$4(exports, defaultedSettings); 21060 register$5(exports, defaultedSettings, schema); 21061 return exports; 21062 }; 21063 21064 const isTreeNode = (content) => content instanceof AstNode; 21065 21066 const serializeContent = (content) => isTreeNode(content) ? HtmlSerializer({ validate: false }).serialize(content) : content; 21067 const withSerializedContent = (content, fireEvent, parserSettings) => { 21068 const serializedContent = serializeContent(content); 21069 const eventArgs = fireEvent(serializedContent); 21070 if (eventArgs.isDefaultPrevented()) { 21071 return eventArgs; 21072 } 21073 else if (isTreeNode(content)) { 21074 // Restore the content type back to being an AstNode. If the content has changed we need to 21075 // re-parse the new content, otherwise we can return the input. 21076 if (eventArgs.content !== serializedContent) { 21077 const rootNode = DomParser({ validate: false, forced_root_block: false, ...parserSettings }).parse(eventArgs.content, { context: content.name }); 21078 return { ...eventArgs, content: rootNode }; 21079 } 21080 else { 21081 return { ...eventArgs, content }; 21082 } 21083 } 21084 else { 21085 return eventArgs; 21086 } 21087 }; 21088 const makeParserSettings = (editor) => ({ 21089 sanitize: shouldSanitizeXss(editor), 21090 sandbox_iframes: shouldSandboxIframes(editor), 21091 sandbox_iframes_exclusions: getSandboxIframesExclusions(editor) 21092 }); 21093 const preProcessGetContent = (editor, args) => { 21094 if (args.no_events) { 21095 return Result.value(args); 21096 } 21097 else { 21098 const eventArgs = fireBeforeGetContent(editor, args); 21099 if (eventArgs.isDefaultPrevented()) { 21100 return Result.error(fireGetContent(editor, { content: '', ...eventArgs }).content); 21101 } 21102 else { 21103 return Result.value(eventArgs); 21104 } 21105 } 21106 }; 21107 const postProcessGetContent = (editor, content, args) => { 21108 if (args.no_events) { 21109 return content; 21110 } 21111 else { 21112 const processedEventArgs = withSerializedContent(content, (content) => fireGetContent(editor, { ...args, content }), makeParserSettings(editor)); 21113 return processedEventArgs.content; 21114 } 21115 }; 21116 const preProcessSetContent = (editor, args) => { 21117 if (args.no_events) { 21118 return Result.value(args); 21119 } 21120 else { 21121 const processedEventArgs = withSerializedContent(args.content, (content) => fireBeforeSetContent(editor, { ...args, content }), makeParserSettings(editor)); 21122 if (processedEventArgs.isDefaultPrevented()) { 21123 fireSetContent(editor, processedEventArgs); 21124 return Result.error(undefined); 21125 } 21126 else { 21127 return Result.value(processedEventArgs); 21128 } 21129 } 21130 }; 21131 const postProcessSetContent = (editor, content, args) => { 21132 if (!args.no_events) { 21133 fireSetContent(editor, { ...args, content }); 21134 } 21135 }; 21136 21137 const removedOptions = ('autoresize_on_init,content_editable_state,padd_empty_with_br,block_elements,' + 21138 'boolean_attributes,editor_deselector,editor_selector,elements,file_browser_callback_types,filepicker_validator_handler,' + 21139 'force_hex_style_colors,force_p_newlines,gecko_spellcheck,images_dataimg_filter,media_scripts,mode,move_caret_before_on_enter_elements,' + 21140 'non_empty_elements,self_closing_elements,short_ended_elements,special,spellchecker_select_languages,spellchecker_whitelist,' + 21141 'tab_focus,tabfocus_elements,table_responsive_width,text_block_elements,text_inline_elements,toolbar_drawer,types,validate,whitespace_elements,' + 21142 'paste_enable_default_filters,paste_filter_drop,paste_word_valid_elements,paste_retain_style_properties,paste_convert_word_fake_lists,' + 21143 'template_cdate_classes,template_mdate_classes,template_selected_content_classes,template_preview_replace_values,template_replace_values,templates,template_cdate_format,template_mdate_format').split(','); 21144 const deprecatedOptions = ['content_css_cors']; 21145 const removedPlugins = 'bbcode,colorpicker,contextmenu,fullpage,legacyoutput,spellchecker,template,textcolor,rtc'.split(','); 21146 const deprecatedPlugins = [ 21147 { 21148 name: 'export', 21149 replacedWith: 'Export to PDF' 21150 }, 21151 ]; 21152 const getMatchingOptions = (options, searchingFor) => { 21153 const settingNames = filter$5(searchingFor, (setting) => has$2(options, setting)); 21154 return sort(settingNames); 21155 }; 21156 const getRemovedOptions = (options) => { 21157 const settingNames = getMatchingOptions(options, removedOptions); 21158 // Forced root block is a special case whereby only the empty/false value is deprecated 21159 const forcedRootBlock = options.forced_root_block; 21160 // Note: This cast is required for old configurations as forced root block used to allow a boolean 21161 if (forcedRootBlock === false || forcedRootBlock === '') { 21162 settingNames.push('forced_root_block (false only)'); 21163 } 21164 return sort(settingNames); 21165 }; 21166 const getDeprecatedOptions = (options) => getMatchingOptions(options, deprecatedOptions); 21167 const getMatchingPlugins = (options, searchingFor) => { 21168 const plugins = Tools.makeMap(options.plugins, ' '); 21169 const hasPlugin = (plugin) => has$2(plugins, plugin); 21170 const pluginNames = filter$5(searchingFor, hasPlugin); 21171 return sort(pluginNames); 21172 }; 21173 const getRemovedPlugins = (options) => getMatchingPlugins(options, removedPlugins); 21174 const getDeprecatedPlugins = (options) => getMatchingPlugins(options, deprecatedPlugins.map((entry) => entry.name)); 21175 const logRemovedWarnings = (rawOptions, normalizedOptions) => { 21176 // Note: Ensure we use the original user settings, not the final when logging 21177 const removedOptions = getRemovedOptions(rawOptions); 21178 const removedPlugins = getRemovedPlugins(normalizedOptions); 21179 const hasRemovedPlugins = removedPlugins.length > 0; 21180 const hasRemovedOptions = removedOptions.length > 0; 21181 const isLegacyMobileTheme = normalizedOptions.theme === 'mobile'; 21182 if (hasRemovedPlugins || hasRemovedOptions || isLegacyMobileTheme) { 21183 const listJoiner = '\n- '; 21184 const themesMessage = isLegacyMobileTheme ? `\n\nThemes:${listJoiner}mobile` : ''; 21185 const pluginsMessage = hasRemovedPlugins ? `\n\nPlugins:${listJoiner}${removedPlugins.join(listJoiner)}` : ''; 21186 const optionsMessage = hasRemovedOptions ? `\n\nOptions:${listJoiner}${removedOptions.join(listJoiner)}` : ''; 21187 // eslint-disable-next-line no-console 21188 console.warn('The following deprecated features are currently enabled and have been removed in TinyMCE 8.0. These features will no longer work and should be removed from the TinyMCE configuration. ' + 21189 'See https://www.tiny.cloud/docs/tinymce/8/migration-from-7x/ for more information.' + 21190 themesMessage + 21191 pluginsMessage + 21192 optionsMessage); 21193 } 21194 }; 21195 const getPluginDescription = (name) => find$2(deprecatedPlugins, (entry) => entry.name === name).fold(() => name, (entry) => { 21196 if (entry.replacedWith) { 21197 return `${name}, replaced by ${entry.replacedWith}`; 21198 } 21199 else { 21200 return name; 21201 } 21202 }); 21203 const logDeprecatedWarnings = (rawOptions, normalizedOptions) => { 21204 // Note: Ensure we use the original user settings, not the final when logging 21205 const deprecatedOptions = getDeprecatedOptions(rawOptions); 21206 const deprecatedPlugins = getDeprecatedPlugins(normalizedOptions); 21207 const hasDeprecatedPlugins = deprecatedPlugins.length > 0; 21208 const hasDeprecatedOptions = deprecatedOptions.length > 0; 21209 if (hasDeprecatedPlugins || hasDeprecatedOptions) { 21210 const listJoiner = '\n- '; 21211 const pluginsMessage = hasDeprecatedPlugins ? `\n\nPlugins:${listJoiner}${deprecatedPlugins.map(getPluginDescription).join(listJoiner)}` : ''; 21212 const optionsMessage = hasDeprecatedOptions ? `\n\nOptions:${listJoiner}${deprecatedOptions.join(listJoiner)}` : ''; 21213 // eslint-disable-next-line no-console 21214 console.warn('The following deprecated features are currently enabled but will be removed soon.' + 21215 pluginsMessage + 21216 optionsMessage); 21217 } 21218 }; 21219 const logWarnings = (rawOptions, normalizedOptions) => { 21220 logRemovedWarnings(rawOptions, normalizedOptions); 21221 logDeprecatedWarnings(rawOptions, normalizedOptions); 21222 }; 21223 const deprecatedFeatures = { 21224 fire: 'The "fire" event api has been deprecated and will be removed in TinyMCE 9. Use "dispatch" instead.', 21225 selectionSetContent: 'The "editor.selection.setContent" method has been deprecated and will be removed in TinyMCE 9. Use "editor.insertContent" instead.' 21226 }; 21227 const logFeatureDeprecationWarning = (feature) => { 21228 // eslint-disable-next-line no-console 21229 console.warn(deprecatedFeatures[feature], new Error().stack); 21230 }; 21231 21232 const removeEmpty = (text) => { 21233 if (text.dom.length === 0) { 21234 remove$8(text); 21235 return Optional.none(); 21236 } 21237 else { 21238 return Optional.some(text); 21239 } 21240 }; 21241 const walkPastBookmark = (node, start) => node.filter((elm) => BookmarkManager.isBookmarkNode(elm.dom)) 21242 .bind(start ? nextSibling : prevSibling); 21243 const merge = (outer, inner, rng, start, schema) => { 21244 const outerElm = outer.dom; 21245 const innerElm = inner.dom; 21246 const oldLength = start ? outerElm.length : innerElm.length; 21247 if (start) { 21248 mergeTextNodes(outerElm, innerElm, schema, false, !start); 21249 rng.setStart(innerElm, oldLength); 21250 } 21251 else { 21252 mergeTextNodes(innerElm, outerElm, schema, false, !start); 21253 rng.setEnd(innerElm, oldLength); 21254 } 21255 }; 21256 const normalizeTextIfRequired = (inner, start, schema) => { 21257 parent(inner).each((root) => { 21258 const text = inner.dom; 21259 if (start && needsToBeNbspLeft(root, CaretPosition(text, 0), schema)) { 21260 normalizeWhitespaceAfter(text, 0, schema); 21261 } 21262 else if (!start && needsToBeNbspRight(root, CaretPosition(text, text.length), schema)) { 21263 normalizeWhitespaceBefore(text, text.length, schema); 21264 } 21265 }); 21266 }; 21267 const mergeAndNormalizeText = (outerNode, innerNode, rng, start, schema) => { 21268 outerNode.bind((outer) => { 21269 // Normalize the text outside the inserted content 21270 const normalizer = start ? normalizeWhitespaceBefore : normalizeWhitespaceAfter; 21271 normalizer(outer.dom, start ? outer.dom.length : 0, schema); 21272 // Merge the inserted content with other text nodes 21273 return innerNode.filter(isText$c).map((inner) => merge(outer, inner, rng, start, schema)); 21274 }).orThunk(() => { 21275 // Note: Attempt to leave the inserted/inner content as is and only adjust if absolutely required 21276 const innerTextNode = walkPastBookmark(innerNode, start).or(innerNode).filter(isText$c); 21277 return innerTextNode.map((inner) => normalizeTextIfRequired(inner, start, schema)); 21278 }); 21279 }; 21280 const rngSetContent = (rng, fragment, schema) => { 21281 const firstChild = Optional.from(fragment.firstChild).map(SugarElement.fromDom); 21282 const lastChild = Optional.from(fragment.lastChild).map(SugarElement.fromDom); 21283 rng.deleteContents(); 21284 rng.insertNode(fragment); 21285 const prevText = firstChild.bind(prevSibling).filter(isText$c).bind(removeEmpty); 21286 const nextText = lastChild.bind(nextSibling).filter(isText$c).bind(removeEmpty); 21287 // Join and normalize text 21288 mergeAndNormalizeText(prevText, firstChild, rng, true, schema); 21289 mergeAndNormalizeText(nextText, lastChild, rng, false, schema); 21290 rng.collapse(false); 21291 }; 21292 const setupArgs$3 = (args, content) => ({ 21293 format: 'html', 21294 ...args, 21295 set: true, 21296 selection: true, 21297 content 21298 }); 21299 const cleanContent = (editor, args) => { 21300 if (args.format !== 'raw') { 21301 // Find which context to parse the content in 21302 const rng = editor.selection.getRng(); 21303 const contextBlock = editor.dom.getParent(rng.commonAncestorContainer, editor.dom.isBlock); 21304 const contextArgs = contextBlock ? { context: contextBlock.nodeName.toLowerCase() } : {}; 21305 const node = editor.parser.parse(args.content, { forced_root_block: false, ...contextArgs, ...args }); 21306 return HtmlSerializer({ validate: false }, editor.schema).serialize(node); 21307 } 21308 else { 21309 return args.content; 21310 } 21311 }; 21312 const setContentInternal$1 = (editor, content, args = {}) => { 21313 const defaultedArgs = setupArgs$3(args, content); 21314 preProcessSetContent(editor, defaultedArgs).each((updatedArgs) => { 21315 // Sanitize the content 21316 const cleanedContent = cleanContent(editor, updatedArgs); 21317 const rng = editor.selection.getRng(); 21318 rngSetContent(rng, rng.createContextualFragment(cleanedContent), editor.schema); 21319 editor.selection.setRng(rng); 21320 scrollRangeIntoView(editor, rng); 21321 postProcessSetContent(editor, cleanedContent, updatedArgs); 21322 }); 21323 }; 21324 const setContentExternal = (editor, content, args = {}) => { 21325 logFeatureDeprecationWarning('selectionSetContent'); 21326 setContentInternal$1(editor, content, args); 21327 }; 21328 21329 /** 21330 * Handles inserts of lists into the editor instance. 21331 * 21332 * @class tinymce.InsertList 21333 * @private 21334 */ 21335 const hasOnlyOneChild$1 = (node) => { 21336 return isNonNullable(node.firstChild) && node.firstChild === node.lastChild; 21337 }; 21338 const isPaddingNode = (node) => { 21339 return node.name === 'br' || node.value === nbsp; 21340 }; 21341 const isPaddedEmptyBlock = (schema, node) => { 21342 const blockElements = schema.getBlockElements(); 21343 return blockElements[node.name] && hasOnlyOneChild$1(node) && isPaddingNode(node.firstChild); 21344 }; 21345 const isEmptyFragmentElement = (schema, node) => { 21346 const nonEmptyElements = schema.getNonEmptyElements(); 21347 return isNonNullable(node) && (node.isEmpty(nonEmptyElements) || isPaddedEmptyBlock(schema, node)); 21348 }; 21349 const isListFragment = (schema, fragment) => { 21350 let firstChild = fragment.firstChild; 21351 let lastChild = fragment.lastChild; 21352 // Skip meta since it's likely <meta><ul>..</ul> 21353 if (firstChild && firstChild.name === 'meta') { 21354 firstChild = firstChild.next; 21355 } 21356 // Skip mce_marker since it's likely <ul>..</ul><span id="mce_marker"></span> 21357 if (lastChild && lastChild.attr('id') === 'mce_marker') { 21358 lastChild = lastChild.prev; 21359 } 21360 // Skip last child if it's an empty block 21361 if (isEmptyFragmentElement(schema, lastChild)) { 21362 lastChild = lastChild?.prev; 21363 } 21364 if (!firstChild || firstChild !== lastChild) { 21365 return false; 21366 } 21367 return firstChild.name === 'ul' || firstChild.name === 'ol'; 21368 }; 21369 const cleanupDomFragment = (domFragment) => { 21370 const firstChild = domFragment.firstChild; 21371 const lastChild = domFragment.lastChild; 21372 // TODO: remove the meta tag from paste logic 21373 if (firstChild && firstChild.nodeName === 'META') { 21374 firstChild.parentNode?.removeChild(firstChild); 21375 } 21376 if (lastChild && lastChild.id === 'mce_marker') { 21377 lastChild.parentNode?.removeChild(lastChild); 21378 } 21379 return domFragment; 21380 }; 21381 const toDomFragment = (dom, serializer, fragment) => { 21382 const html = serializer.serialize(fragment); 21383 const domFragment = dom.createFragment(html); 21384 return cleanupDomFragment(domFragment); 21385 }; 21386 const listItems = (elm) => { 21387 return filter$5(elm?.childNodes ?? [], (child) => { 21388 return child.nodeName === 'LI'; 21389 }); 21390 }; 21391 const isPadding = (node) => { 21392 return node.data === nbsp || isBr$7(node); 21393 }; 21394 const isListItemPadded = (node) => { 21395 return isNonNullable(node?.firstChild) && node.firstChild === node.lastChild && isPadding(node.firstChild); 21396 }; 21397 const isEmptyOrPadded = (elm) => { 21398 return !elm.firstChild || isListItemPadded(elm); 21399 }; 21400 const trimListItems = (elms) => { 21401 return elms.length > 0 && isEmptyOrPadded(elms[elms.length - 1]) ? elms.slice(0, -1) : elms; 21402 }; 21403 const getParentLi = (dom, node) => { 21404 const parentBlock = dom.getParent(node, dom.isBlock); 21405 return parentBlock && parentBlock.nodeName === 'LI' ? parentBlock : null; 21406 }; 21407 const isParentBlockLi = (dom, node) => { 21408 return !!getParentLi(dom, node); 21409 }; 21410 const getSplit = (parentNode, rng) => { 21411 const beforeRng = rng.cloneRange(); 21412 const afterRng = rng.cloneRange(); 21413 beforeRng.setStartBefore(parentNode); 21414 afterRng.setEndAfter(parentNode); 21415 return [ 21416 beforeRng.cloneContents(), 21417 afterRng.cloneContents() 21418 ]; 21419 }; 21420 const findFirstIn = (node, rootNode) => { 21421 const caretPos = CaretPosition.before(node); 21422 const caretWalker = CaretWalker(rootNode); 21423 const newCaretPos = caretWalker.next(caretPos); 21424 return newCaretPos ? newCaretPos.toRange() : null; 21425 }; 21426 const findLastOf = (node, rootNode) => { 21427 const caretPos = CaretPosition.after(node); 21428 const caretWalker = CaretWalker(rootNode); 21429 const newCaretPos = caretWalker.prev(caretPos); 21430 return newCaretPos ? newCaretPos.toRange() : null; 21431 }; 21432 const insertMiddle = (target, elms, rootNode, rng) => { 21433 const parts = getSplit(target, rng); 21434 const parentElm = target.parentNode; 21435 if (parentElm) { 21436 parentElm.insertBefore(parts[0], target); 21437 Tools.each(elms, (li) => { 21438 parentElm.insertBefore(li, target); 21439 }); 21440 parentElm.insertBefore(parts[1], target); 21441 parentElm.removeChild(target); 21442 } 21443 return findLastOf(elms[elms.length - 1], rootNode); 21444 }; 21445 const insertBefore$2 = (target, elms, rootNode) => { 21446 const parentElm = target.parentNode; 21447 if (parentElm) { 21448 Tools.each(elms, (elm) => { 21449 parentElm.insertBefore(elm, target); 21450 }); 21451 } 21452 return findFirstIn(target, rootNode); 21453 }; 21454 const insertAfter$2 = (target, elms, rootNode, dom) => { 21455 dom.insertAfter(elms.reverse(), target); 21456 return findLastOf(elms[0], rootNode); 21457 }; 21458 const insertAtCaret$1 = (serializer, dom, rng, fragment) => { 21459 const domFragment = toDomFragment(dom, serializer, fragment); 21460 const liTarget = getParentLi(dom, rng.startContainer); 21461 const liElms = trimListItems(listItems(domFragment.firstChild)); 21462 const BEGINNING = 1, END = 2; 21463 const rootNode = dom.getRoot(); 21464 const isAt = (location) => { 21465 const caretPos = CaretPosition.fromRangeStart(rng); 21466 const caretWalker = CaretWalker(dom.getRoot()); 21467 const newPos = location === BEGINNING ? caretWalker.prev(caretPos) : caretWalker.next(caretPos); 21468 const newPosNode = newPos?.getNode(); 21469 return newPosNode ? getParentLi(dom, newPosNode) !== liTarget : true; 21470 }; 21471 if (!liTarget) { 21472 return null; 21473 } 21474 else if (isAt(BEGINNING)) { 21475 return insertBefore$2(liTarget, liElms, rootNode); 21476 } 21477 else if (isAt(END)) { 21478 return insertAfter$2(liTarget, liElms, rootNode, dom); 21479 } 21480 else { 21481 return insertMiddle(liTarget, liElms, rootNode, rng); 21482 } 21483 }; 21484 21485 const mergeableWrappedElements = ['pre']; 21486 const shouldPasteContentOnly = (dom, fragment, parentNode, root) => { 21487 const firstNode = fragment.firstChild; 21488 const lastNode = fragment.lastChild; 21489 const last = lastNode.attr('data-mce-type') === 'bookmark' ? lastNode.prev : lastNode; 21490 const isPastingSingleElement = firstNode === last; 21491 const isWrappedElement = contains$2(mergeableWrappedElements, firstNode.name); 21492 if (isPastingSingleElement && isWrappedElement) { 21493 const isContentEditable = firstNode.attr('contenteditable') !== 'false'; 21494 const isPastingInTheSameBlockTag = dom.getParent(parentNode, dom.isBlock)?.nodeName.toLowerCase() === firstNode.name; 21495 const isPastingInContentEditable = Optional.from(getContentEditableRoot$1(root, parentNode)).forall(isContentEditableTrue$3); 21496 return isContentEditable && isPastingInTheSameBlockTag && isPastingInContentEditable; 21497 } 21498 else { 21499 return false; 21500 } 21501 }; 21502 const isTableCell = isTableCell$3; 21503 const isTableCellContentSelected = (dom, rng, cell) => { 21504 if (isNonNullable(cell)) { 21505 const endCell = dom.getParent(rng.endContainer, isTableCell); 21506 return cell === endCell && hasAllContentsSelected(SugarElement.fromDom(cell), rng); 21507 } 21508 else { 21509 return false; 21510 } 21511 }; 21512 const isEditableEmptyBlock = (dom, node) => { 21513 if (dom.isBlock(node) && dom.isEditable(node)) { 21514 const childNodes = node.childNodes; 21515 return (childNodes.length === 1 && isBr$7(childNodes[0])) || childNodes.length === 0; 21516 } 21517 else { 21518 return false; 21519 } 21520 }; 21521 const validInsertion = (editor, value, parentNode) => { 21522 // Should never insert content into bogus elements, since these can 21523 // be resize handles or similar 21524 if (parentNode.getAttribute('data-mce-bogus') === 'all') { 21525 parentNode.parentNode?.insertBefore(editor.dom.createFragment(value), parentNode); 21526 } 21527 else { 21528 if (isEditableEmptyBlock(editor.dom, parentNode)) { 21529 editor.dom.setHTML(parentNode, value); 21530 } 21531 else { 21532 setContentInternal$1(editor, value, { no_events: true }); 21533 } 21534 } 21535 }; 21536 const trimBrsFromTableCell = (dom, elm, schema) => { 21537 Optional.from(dom.getParent(elm, 'td,th')).map(SugarElement.fromDom).each((el) => trimBlockTrailingBr(el, schema)); 21538 }; 21539 // Remove children nodes that are exactly the same as a parent node - name, attributes, styles 21540 const reduceInlineTextElements = (editor, merge) => { 21541 const textInlineElements = editor.schema.getTextInlineElements(); 21542 const dom = editor.dom; 21543 if (merge) { 21544 const root = editor.getBody(); 21545 const elementUtils = ElementUtils(editor); 21546 const fragmentSelector = '*[data-mce-fragment]'; 21547 const fragments = dom.select(fragmentSelector); 21548 Tools.each(fragments, (node) => { 21549 const isInline = (currentNode) => isNonNullable(textInlineElements[currentNode.nodeName.toLowerCase()]); 21550 const hasOneChild = (currentNode) => currentNode.childNodes.length === 1; 21551 const hasNoNonInheritableStyles = (currentNode) => !(hasNonInheritableStyles(dom, currentNode) || hasConditionalNonInheritableStyles(dom, currentNode)); 21552 if (hasNoNonInheritableStyles(node) && isInline(node) && hasOneChild(node)) { 21553 const styles = getStyleProps(dom, node); 21554 const isOverridden = (oldStyles, newStyles) => forall(oldStyles, (style) => contains$2(newStyles, style)); 21555 const overriddenByAllChildren = (childNode) => hasOneChild(node) && dom.is(childNode, fragmentSelector) && isInline(childNode) && 21556 (childNode.nodeName === node.nodeName && isOverridden(styles, getStyleProps(dom, childNode)) || overriddenByAllChildren(childNode.children[0])); 21557 const identicalToParent = (parentNode) => isNonNullable(parentNode) && parentNode !== root 21558 && (elementUtils.compare(node, parentNode) || identicalToParent(parentNode.parentElement)); 21559 const conflictWithInsertedParent = (parentNode) => isNonNullable(parentNode) && parentNode !== root 21560 && dom.is(parentNode, fragmentSelector) && (hasStyleConflict(dom, node, parentNode) || conflictWithInsertedParent(parentNode.parentElement)); 21561 if (overriddenByAllChildren(node.children[0]) || (identicalToParent(node.parentElement) && !conflictWithInsertedParent(node.parentElement))) { 21562 dom.remove(node, true); 21563 } 21564 } 21565 }); 21566 normalizeElements(editor, fromDom$1(fragments)); 21567 } 21568 }; 21569 const markFragmentElements = (fragment) => { 21570 let node = fragment; 21571 while ((node = node.walk())) { 21572 if (node.type === 1) { 21573 node.attr('data-mce-fragment', '1'); 21574 } 21575 } 21576 }; 21577 const unmarkFragmentElements = (elm) => { 21578 Tools.each(elm.getElementsByTagName('*'), (elm) => { 21579 elm.removeAttribute('data-mce-fragment'); 21580 }); 21581 }; 21582 const isPartOfFragment = (node) => { 21583 return !!node.getAttribute('data-mce-fragment'); 21584 }; 21585 const canHaveChildren = (editor, node) => { 21586 return isNonNullable(node) && !editor.schema.getVoidElements()[node.nodeName]; 21587 }; 21588 const moveSelectionToMarker = (editor, marker) => { 21589 let nextRng; 21590 const dom = editor.dom; 21591 const selection = editor.selection; 21592 if (!marker) { 21593 return; 21594 } 21595 selection.scrollIntoView(marker); 21596 // If marker is in cE=false then move selection to that element instead 21597 const parentEditableElm = getContentEditableRoot$1(editor.getBody(), marker); 21598 if (parentEditableElm && dom.getContentEditable(parentEditableElm) === 'false') { 21599 dom.remove(marker); 21600 selection.select(parentEditableElm); 21601 return; 21602 } 21603 // Move selection before marker and remove it 21604 let rng = dom.createRng(); 21605 // If previous sibling is a text node set the selection to the end of that node 21606 const node = marker.previousSibling; 21607 if (isText$b(node)) { 21608 rng.setStart(node, node.nodeValue?.length ?? 0); 21609 const node2 = marker.nextSibling; 21610 if (isText$b(node2)) { 21611 node.appendData(node2.data); 21612 node2.parentNode?.removeChild(node2); 21613 } 21614 } 21615 else { 21616 // If the previous sibling isn't a text node or doesn't exist set the selection before the marker node 21617 rng.setStartBefore(marker); 21618 rng.setEndBefore(marker); 21619 } 21620 const findNextCaretRng = (rng) => { 21621 let caretPos = CaretPosition.fromRangeStart(rng); 21622 const caretWalker = CaretWalker(editor.getBody()); 21623 caretPos = caretWalker.next(caretPos); 21624 return caretPos?.toRange(); 21625 }; 21626 // Remove the marker node and set the new range 21627 const parentBlock = dom.getParent(marker, dom.isBlock); 21628 dom.remove(marker); 21629 if (parentBlock && dom.isEmpty(parentBlock)) { 21630 const isCell = isTableCell(parentBlock); 21631 empty(SugarElement.fromDom(parentBlock)); 21632 rng.setStart(parentBlock, 0); 21633 rng.setEnd(parentBlock, 0); 21634 if (!isCell && !isPartOfFragment(parentBlock) && (nextRng = findNextCaretRng(rng))) { 21635 rng = nextRng; 21636 dom.remove(parentBlock); 21637 } 21638 else { 21639 // TINY-9860: If parentBlock is a table cell, add a br without 'data-mce-bogus' attribute. 21640 dom.add(parentBlock, dom.create('br', isCell ? {} : { 'data-mce-bogus': '1' })); 21641 } 21642 } 21643 selection.setRng(rng); 21644 }; 21645 const deleteSelectedContent = (editor) => { 21646 const dom = editor.dom; 21647 // Fix for #2595 seems that delete removes one extra character on 21648 // WebKit for some odd reason if you double click select a word 21649 const rng = normalize(editor.selection.getRng()); 21650 editor.selection.setRng(rng); 21651 // TINY-1044: Selecting all content in a single table cell will cause the entire table to be deleted 21652 // when using the native delete command. As such we need to manually delete the cell content instead 21653 const startCell = dom.getParent(rng.startContainer, isTableCell); 21654 if (isTableCellContentSelected(dom, rng, startCell)) { 21655 deleteCellContents(editor, rng, SugarElement.fromDom(startCell)); 21656 // TINY-9193: If the selection is over the whole text node in an element then Firefox incorrectly moves the caret to the previous line 21657 // TINY-11953: If the selection is over the whole anchor node, then Chrome incorrectly removes parent node alongside with it's child - anchor 21658 } 21659 else if (isSelectionOverWholeAnchor(rng) || isSelectionOverWholeTextNode(rng)) { 21660 rng.deleteContents(); 21661 } 21662 else { 21663 editor.getDoc().execCommand('Delete', false); 21664 } 21665 }; 21666 const findMarkerNode = (scope) => { 21667 for (let markerNode = scope; markerNode; markerNode = markerNode.walk()) { 21668 if (markerNode.attr('id') === 'mce_marker') { 21669 return Optional.some(markerNode); 21670 } 21671 } 21672 return Optional.none(); 21673 }; 21674 const notHeadingsInSummary = (dom, node, fragment) => { 21675 return exists(fragment.children(), isHeading) && dom.getParent(node, dom.isBlock)?.nodeName === 'SUMMARY'; 21676 }; 21677 const insertHtmlAtCaret = (editor, value, details) => { 21678 const selection = editor.selection; 21679 const dom = editor.dom; 21680 // Setup parser and serializer 21681 const parser = editor.parser; 21682 const merge = details.merge; 21683 const serializer = HtmlSerializer({ 21684 validate: true 21685 }, editor.schema); 21686 const bookmarkHtml = '<span id="mce_marker" data-mce-type="bookmark"></span>'; 21687 // TINY-10305: Remove all user-input zwsp to avoid impacting caret removal from content. 21688 if (!details.preserve_zwsp) { 21689 value = trim$2(value); 21690 } 21691 // Add caret at end of contents if it's missing 21692 if (value.indexOf('{$caret}') === -1) { 21693 value += '{$caret}'; 21694 } 21695 // Replace the caret marker with a span bookmark element 21696 value = value.replace(/\{\$caret\}/, bookmarkHtml); 21697 // If selection is at <body>|<p></p> then move it into <body><p>|</p> 21698 let rng = selection.getRng(); 21699 const caretElement = rng.startContainer; 21700 const body = editor.getBody(); 21701 if (caretElement === body && selection.isCollapsed()) { 21702 if (dom.isBlock(body.firstChild) && canHaveChildren(editor, body.firstChild) && dom.isEmpty(body.firstChild)) { 21703 rng = dom.createRng(); 21704 rng.setStart(body.firstChild, 0); 21705 rng.setEnd(body.firstChild, 0); 21706 selection.setRng(rng); 21707 } 21708 } 21709 // Insert node maker where we will insert the new HTML and get it's parent 21710 if (!selection.isCollapsed()) { 21711 deleteSelectedContent(editor); 21712 } 21713 const parentNode = selection.getNode(); 21714 // Parse the fragment within the context of the parent node 21715 const parserArgs = { context: parentNode.nodeName.toLowerCase(), data: details.data, insert: true }; 21716 const fragment = parser.parse(value, parserArgs); 21717 // Custom handling of lists 21718 if (details.paste === true && isListFragment(editor.schema, fragment) && isParentBlockLi(dom, parentNode)) { 21719 rng = insertAtCaret$1(serializer, dom, selection.getRng(), fragment); 21720 if (rng) { 21721 selection.setRng(rng); 21722 } 21723 return value; 21724 } 21725 if (details.paste === true && shouldPasteContentOnly(dom, fragment, parentNode, editor.getBody())) { 21726 fragment.firstChild?.unwrap(); 21727 } 21728 markFragmentElements(fragment); 21729 // Move the caret to a more suitable location 21730 let node = fragment.lastChild; 21731 if (node && node.attr('id') === 'mce_marker') { 21732 const marker = node; 21733 for (node = node.prev; node; node = node.walk(true)) { 21734 if (node.name === 'table') { 21735 break; 21736 } 21737 if (node.type === 3 || !dom.isBlock(node.name)) { 21738 if (node.parent && editor.schema.isValidChild(node.parent.name, 'span')) { 21739 node.parent.insert(marker, node, node.name === 'br'); 21740 } 21741 break; 21742 } 21743 } 21744 } 21745 editor._selectionOverrides.showBlockCaretContainer(parentNode); 21746 // If parser says valid we can insert the contents into that parent 21747 if (!parserArgs.invalid && !notHeadingsInSummary(dom, parentNode, fragment)) { 21748 value = serializer.serialize(fragment); 21749 validInsertion(editor, value, parentNode); 21750 } 21751 else { 21752 // If the fragment was invalid within that context then we need 21753 // to parse and process the parent it's inserted into 21754 // Insert bookmark node and get the parent 21755 setContentInternal$1(editor, bookmarkHtml); 21756 let parentNode = selection.getNode(); 21757 let tempNode; 21758 const rootNode = editor.getBody(); 21759 // Opera will return the document node when selection is in root 21760 if (isDocument$1(parentNode)) { 21761 parentNode = tempNode = rootNode; 21762 } 21763 else { 21764 tempNode = parentNode; 21765 } 21766 // Find the ancestor just before the root element 21767 while (tempNode && tempNode !== rootNode) { 21768 parentNode = tempNode; 21769 tempNode = tempNode.parentNode; 21770 } 21771 // Get the outer/inner HTML depending on if we are in the root and parser and serialize that 21772 value = parentNode === rootNode ? rootNode.innerHTML : dom.getOuterHTML(parentNode); 21773 const root = parser.parse(value); 21774 const markerNode = findMarkerNode(root); 21775 const editingHost = markerNode.bind(findClosestEditingHost).getOr(root); 21776 markerNode.each((marker) => marker.replace(fragment)); 21777 const fragmentNodes = getAllDescendants(fragment); 21778 fragment.unwrap(); 21779 const invalidChildren = filter$5(fragmentNodes, (node) => isInvalid(editor.schema, node)); 21780 cleanInvalidNodes(invalidChildren, editor.schema, editingHost); 21781 filter$1(parser.getNodeFilters(), parser.getAttributeFilters(), root); 21782 value = serializer.serialize(root); 21783 // Set the inner/outer HTML depending on if we are in the root or not 21784 if (parentNode === rootNode) { 21785 dom.setHTML(rootNode, value); 21786 } 21787 else { 21788 dom.setOuterHTML(parentNode, value); 21789 } 21790 } 21791 reduceInlineTextElements(editor, merge); 21792 moveSelectionToMarker(editor, dom.get('mce_marker')); 21793 unmarkFragmentElements(editor.getBody()); 21794 trimBrsFromTableCell(dom, selection.getStart(), editor.schema); 21795 updateCaret(editor.schema, editor.getBody(), selection.getStart()); 21796 return value; 21797 }; 21798 21799 const moveSelection = (editor) => { 21800 if (hasFocus(editor)) { 21801 firstPositionIn(editor.getBody()).each((pos) => { 21802 const node = pos.getNode(); 21803 const caretPos = isTable$2(node) ? firstPositionIn(node).getOr(pos) : pos; 21804 editor.selection.setRng(caretPos.toRange()); 21805 }); 21806 } 21807 }; 21808 const setEditorHtml = (editor, html, noSelection) => { 21809 editor.dom.setHTML(editor.getBody(), html); 21810 if (noSelection !== true) { 21811 moveSelection(editor); 21812 } 21813 }; 21814 const setContentString = (editor, body, content, args) => { 21815 // TINY-10305: Remove all user-input zwsp to avoid impacting caret removal from content. 21816 content = trim$2(content); 21817 // Padd empty content in Gecko and Safari. Commands will otherwise fail on the content 21818 // It will also be impossible to place the caret in the editor unless there is a BR element present 21819 if (content.length === 0 || /^\s+$/.test(content)) { 21820 const padd = '<br data-mce-bogus="1">'; 21821 // Todo: There is a lot more root elements that need special padding 21822 // so separate this and add all of them at some point. 21823 if (body.nodeName === 'TABLE') { 21824 content = '<tr><td>' + padd + '</td></tr>'; 21825 } 21826 else if (/^(UL|OL)$/.test(body.nodeName)) { 21827 content = '<li>' + padd + '</li>'; 21828 } 21829 const forcedRootBlockName = getForcedRootBlock(editor); 21830 // Check if forcedRootBlock is a valid child of the body 21831 if (editor.schema.isValidChild(body.nodeName.toLowerCase(), forcedRootBlockName.toLowerCase())) { 21832 content = padd; 21833 content = editor.dom.createHTML(forcedRootBlockName, getForcedRootBlockAttrs(editor), content); 21834 } 21835 else if (!content) { 21836 content = padd; 21837 } 21838 setEditorHtml(editor, content, args.no_selection); 21839 return { content, html: content }; 21840 } 21841 else { 21842 if (args.format !== 'raw') { 21843 content = HtmlSerializer({ validate: false }, editor.schema).serialize(editor.parser.parse(content, { isRootContent: true, insert: true })); 21844 } 21845 const trimmedHtml = isWsPreserveElement(SugarElement.fromDom(body)) ? content : Tools.trim(content); 21846 setEditorHtml(editor, trimmedHtml, args.no_selection); 21847 return { content: trimmedHtml, html: trimmedHtml }; 21848 } 21849 }; 21850 const setContentTree = (editor, body, content, args) => { 21851 filter$1(editor.parser.getNodeFilters(), editor.parser.getAttributeFilters(), content); 21852 const html = HtmlSerializer({ validate: false }, editor.schema).serialize(content); 21853 // TINY-10305: Remove all user-input zwsp to avoid impacting caret removal from content. 21854 const trimmedHtml = trim$2(isWsPreserveElement(SugarElement.fromDom(body)) ? html : Tools.trim(html)); 21855 setEditorHtml(editor, trimmedHtml, args.no_selection); 21856 return { content, html: trimmedHtml }; 21857 }; 21858 const setContentInternal = (editor, content, args) => { 21859 return Optional.from(editor.getBody()).map((body) => { 21860 if (isTreeNode(content)) { 21861 return setContentTree(editor, body, content, args); 21862 } 21863 else { 21864 return setContentString(editor, body, content, args); 21865 } 21866 }).getOr({ content, html: isTreeNode(args.content) ? '' : args.content }); 21867 }; 21868 21869 const postProcessHooks = {}; 21870 const isPre = matchNodeNames$1(['pre']); 21871 const addPostProcessHook = (name, hook) => { 21872 const hooks = postProcessHooks[name]; 21873 if (!hooks) { 21874 postProcessHooks[name] = []; 21875 } 21876 postProcessHooks[name].push(hook); 21877 }; 21878 const postProcess$1 = (name, editor) => { 21879 if (has$2(postProcessHooks, name)) { 21880 each$e(postProcessHooks[name], (hook) => { 21881 hook(editor); 21882 }); 21883 } 21884 }; 21885 addPostProcessHook('pre', (editor) => { 21886 const rng = editor.selection.getRng(); 21887 const hasPreSibling = (blocks) => (pre) => { 21888 const prev = pre.previousSibling; 21889 return isPre(prev) && contains$2(blocks, prev); 21890 }; 21891 const joinPre = (pre1, pre2) => { 21892 const sPre2 = SugarElement.fromDom(pre2); 21893 const doc = documentOrOwner(sPre2).dom; 21894 remove$8(sPre2); 21895 append(SugarElement.fromDom(pre1), [ 21896 SugarElement.fromTag('br', doc), 21897 SugarElement.fromTag('br', doc), 21898 ...children$
vendor: 7,543 bytes, lines 21898-22048
218981(sPre2) 21899 ]); 21900 }; 21901 if (!rng.collapsed) { 21902 const blocks = editor.selection.getSelectedBlocks(); 21903 const preBlocks = filter$5(filter$5(blocks, isPre), hasPreSibling(blocks)); 21904 each$e(preBlocks, (pre) => { 21905 joinPre(pre.previousSibling, pre); 21906 }); 21907 } 21908 }); 21909 21910 const each$5 = Tools.each; 21911 const mergeTextDecorationsAndColor = (dom, format, vars, node) => { 21912 const processTextDecorationsAndColor = (n) => { 21913 if (isHTMLElement(n) && isElement$7(n.parentNode) && dom.isEditable(n)) { 21914 const parentTextDecoration = getTextDecoration(dom, n.parentNode); 21915 if (dom.getStyle(n, 'color') && parentTextDecoration) { 21916 dom.setStyle(n, 'text-decoration', parentTextDecoration); 21917 } 21918 else if (dom.getStyle(n, 'text-decoration') === parentTextDecoration) { 21919 dom.setStyle(n, 'text-decoration', null); 21920 } 21921 } 21922 }; 21923 // Colored nodes should be underlined so that the color of the underline matches the text color. 21924 if (format.styles && (format.styles.color || format.styles.textDecoration)) { 21925 Tools.walk(node, processTextDecorationsAndColor, 'childNodes'); 21926 processTextDecorationsAndColor(node); 21927 } 21928 }; 21929 const mergeBackgroundColorAndFontSize = (dom, format, vars, node) => { 21930 // nodes with font-size should have their own background color as well to fit the line-height (see TINY-882) 21931 if (format.styles && format.styles.backgroundColor) { 21932 const hasFontSize = hasStyle(dom, 'fontSize'); 21933 processChildElements(node, (elm) => hasFontSize(elm) && dom.isEditable(elm), applyStyle(dom, 'backgroundColor', replaceVars(format.styles.backgroundColor, vars))); 21934 } 21935 }; 21936 const mergeSubSup = (dom, format, vars, node) => { 21937 // Remove font size on all descendants of a sub/sup and remove the inverse elements 21938 if (isInlineFormat(format) && (format.inline === 'sub' || format.inline === 'sup')) { 21939 const hasFontSize = hasStyle(dom, 'fontSize'); 21940 processChildElements(node, (elm) => hasFontSize(elm) && dom.isEditable(elm), applyStyle(dom, 'fontSize', '')); 21941 const inverseTagDescendants = filter$5(dom.select(format.inline === 'sup' ? 'sub' : 'sup', node), dom.isEditable); 21942 dom.remove(inverseTagDescendants, true); 21943 } 21944 }; 21945 const mergeWithChildren = (editor, formatList, vars, node) => { 21946 // Remove/merge children 21947 // Note: RemoveFormat.removeFormat will not remove formatting from noneditable nodes 21948 each$5(formatList, (format) => { 21949 // Merge all children of similar type will move styles from child to parent 21950 // this: <span style="color:red"><b><span style="color:red; font-size:10px">text</span></b></span> 21951 // will become: <span style="color:red"><b><span style="font-size:10px">text</span></b></span> 21952 if (isInlineFormat(format)) { 21953 each$5(editor.dom.select(format.inline, node), (child) => { 21954 if (isElementNode(child)) { 21955 removeNodeFormat(editor, format, vars, child, format.exact ? child : null); 21956 } 21957 }); 21958 } 21959 clearChildStyles(editor.dom, format, node); 21960 }); 21961 }; 21962 const mergeWithParents = (editor, format, name, vars, node) => { 21963 // Remove format if direct parent already has the same format 21964 // Note: RemoveFormat.removeFormat will not remove formatting from noneditable nodes 21965 const parentNode = node.parentNode; 21966 if (matchNode$1(editor, parentNode, name, vars)) { 21967 if (removeNodeFormat(editor, format, vars, node)) { 21968 return; 21969 } 21970 } 21971 // Remove format if any ancestor already has the same format 21972 if (format.merge_with_parents && parentNode) { 21973 editor.dom.getParent(parentNode, (parent) => { 21974 if (matchNode$1(editor, parent, name, vars)) { 21975 removeNodeFormat(editor, format, vars, node); 21976 return true; 21977 } 21978 else { 21979 return false; 21980 } 21981 }); 21982 } 21983 }; 21984 21985 const each$4 = Tools.each; 21986 const canFormatBR = (editor, format, node, parentName) => { 21987 // TINY-6483: Can format 'br' if it is contained in a valid empty block and an inline format is being applied 21988 if (canFormatEmptyLines(editor) && isInlineFormat(format) && node.parentNode) { 21989 const validBRParentElements = getTextRootBlockElements(editor.schema); 21990 // If a caret node is present, the format should apply to that, not the br (applicable to collapsed selections) 21991 const hasCaretNodeSibling = sibling(SugarElement.fromDom(node), (sibling) => isCaretNode(sibling.dom)); 21992 return hasNonNullableKey(validBRParentElements, parentName) && isEmptyNode(editor.schema, node.parentNode, { skipBogus: false, includeZwsp: true }) && !hasCaretNodeSibling; 21993 } 21994 else { 21995 return false; 21996 } 21997 }; 21998 const applyFormatAction = (ed, name, vars, node) => { 21999 const formatList = ed.formatter.get(name); 22000 const format = formatList[0]; 22001 const isCollapsed = !node && ed.selection.isCollapsed(); 22002 const dom = ed.dom; 22003 const selection = ed.selection; 22004 const applyNodeStyle = (formatList, node) => { 22005 let found = false; 22006 // Look for matching formats 22007 each$4(formatList, (format) => { 22008 if (!isSelectorFormat(format)) { 22009 return false; 22010 } 22011 // Check if the node is nonediatble and if the format can override noneditable node 22012 if (dom.getContentEditable(node) === 'false' && !format.ceFalseOverride) { 22013 return true; 22014 } 22015 // Check collapsed state if it exists 22016 if (isNonNullable(format.collapsed) && format.collapsed !== isCollapsed) { 22017 return true; 22018 } 22019 if (dom.is(node, format.selector) && !isCaretNode(node)) { 22020 setElementFormat(ed, node, format, vars, node); 22021 found = true; 22022 return false; 22023 } 22024 return true; 22025 }); 22026 return found; 22027 }; 22028 const createWrapElement = (wrapName) => { 22029 if (isString(wrapName)) { 22030 const wrapElm = dom.create(wrapName); 22031 setElementFormat(ed, wrapElm, format, vars, node); 22032 return wrapElm; 22033 } 22034 else { 22035 return null; 22036 } 22037 }; 22038 const applyRngStyle = (dom, rng, nodeSpecific) => { 22039 const newWrappers = []; 22040 let contentEditable = true; 22041 // Setup wrapper element 22042 const wrapName = format.inline || format.block; 22043 const wrapElm = createWrapElement(wrapName); 22044 const isMatchingWrappingBlock = (node) => isWrappingBlockFormat(format) && matchNode$1(ed, node, name, vars); 22045 const canRenameBlock = (node, parentName, isEditableDescendant) => { 22046 const isValidBlockFormatForNode = isNonWrappingBlockFormat(format) && 22047 isTextBlock$2(ed.schema, node) && 22048 isValid(ed, parentName, wrapNam
vendor: 8,487 bytes, lines 22048-22199
22048e); 22049 return isEditableDescendant && isValidBlockFormatForNode; 22050 }; 22051 const canWrapNode = (node, parentName, isEditableDescendant, isWrappableNoneditableElm) => { 22052 const nodeName = node.nodeName.toLowerCase(); 22053 const isValidWrapNode = isValid(ed, wrapName, nodeName) && 22054 isValid(ed, parentName, wrapName); 22055 // If it is not node specific, it means that it was not passed into 'formatter.apply` and is within the editor selection 22056 const isZwsp$1 = !nodeSpecific && isText$b(node) && isZwsp(node.data); 22057 const isCaret = isCaretNode(node); 22058 const isCorrectFormatForNode = !isInlineFormat(format) || !dom.isBlock(node); 22059 return (isEditableDescendant || isWrappableNoneditableElm) && isValidWrapNode && !isZwsp$1 && !isCaret && isCorrectFormatForNode; 22060 }; 22061 walk$3(dom, rng, (nodes) => { 22062 let currentWrapElm; 22063 /** 22064 * Process a list of nodes wrap them. 22065 */ 22066 const process = (node) => { 22067 let hasContentEditableState = false; 22068 let lastContentEditable = contentEditable; 22069 let isWrappableNoneditableElm = false; 22070 const parentNode = node.parentNode; 22071 const parentName = parentNode.nodeName.toLowerCase(); 22072 // Node has a contentEditable value 22073 const contentEditableValue = dom.getContentEditable(node); 22074 if (isNonNullable(contentEditableValue)) { 22075 lastContentEditable = contentEditable; 22076 contentEditable = contentEditableValue === 'true'; 22077 // Unless the noneditable element is wrappable, we don't want to wrap the container, only it's editable children 22078 hasContentEditableState = true; 22079 isWrappableNoneditableElm = isWrappableNoneditable(ed, node); 22080 } 22081 const isEditableDescendant = contentEditable && !hasContentEditableState; 22082 // Stop wrapping on br elements except when valid 22083 if (isBr$7(node) && !canFormatBR(ed, format, node, parentName)) { 22084 currentWrapElm = null; 22085 // Remove any br elements when we wrap things 22086 if (isBlockFormat(format)) { 22087 dom.remove(node); 22088 } 22089 return; 22090 } 22091 if (isMatchingWrappingBlock(node)) { 22092 currentWrapElm = null; 22093 return; 22094 } 22095 if (canRenameBlock(node, parentName, isEditableDescendant)) { 22096 const elm = dom.rename(node, wrapName); 22097 setElementFormat(ed, elm, format, vars, node); 22098 newWrappers.push(elm); 22099 currentWrapElm = null; 22100 return; 22101 } 22102 if (isSelectorFormat(format)) { 22103 let found = applyNodeStyle(formatList, node); 22104 // TINY-6567/TINY-7393: Include the parent if using an expanded selector format and no match was found for the current node 22105 if (!found && isNonNullable(parentNode) && shouldExpandToSelector(format)) { 22106 found = applyNodeStyle(formatList, parentNode); 22107 } 22108 // Continue processing if a selector match wasn't found and a inline element is defined 22109 if (!isInlineFormat(format) || found) { 22110 currentWrapElm = null; 22111 return; 22112 } 22113 } 22114 if (isNonNullable(wrapElm) && canWrapNode(node, parentName, isEditableDescendant, isWrappableNoneditableElm)) { 22115 // Start wrapping 22116 if (!currentWrapElm) { 22117 // Wrap the node 22118 currentWrapElm = dom.clone(wrapElm, false); 22119 parentNode.insertBefore(currentWrapElm, node); 22120 newWrappers.push(currentWrapElm); 22121 } 22122 // Wrappable noneditable element has been handled so go back to previous state 22123 if (isWrappableNoneditableElm && hasContentEditableState) { 22124 contentEditable = lastContentEditable; 22125 } 22126 currentWrapElm.appendChild(node); 22127 } 22128 else { 22129 // Start a new wrapper for possible children 22130 currentWrapElm = null; 22131 each$e(from(node.childNodes), process); 22132 if (hasContentEditableState) { 22133 contentEditable = lastContentEditable; // Restore last contentEditable state from stack 22134 } 22135 // End the last wrapper 22136 currentWrapElm = null; 22137 } 22138 }; 22139 each$e(nodes, process); 22140 }); 22141 // Apply formats to links as well to get the color of the underline to change as well 22142 if (format.links === true) { 22143 each$e(newWrappers, (wrapper) => { 22144 const process = (target) => { 22145 if (target.nodeName === 'A') { 22146 setElementFormat(ed, target, format, vars, node); 22147 } 22148 each$e(from(target.childNodes), process); 22149 }; 22150 process(wrapper); 22151 }); 22152 } 22153 normalizeFontSizeElementsAfterApply(ed, name, fromDom$1(newWrappers)); 22154 // Cleanup 22155 each$e(newWrappers, (node) => { 22156 const getChildCount = (node) => { 22157 let count = 0; 22158 each$e(node.childNodes, (node) => { 22159 if (!isEmptyTextNode$1(node) && !isBookmarkNode$1(node)) { 22160 count++; 22161 } 22162 }); 22163 return count; 22164 }; 22165 const mergeStyles = (node) => { 22166 // Check if a child was found and of the same type as the current node 22167 const childElement = find$2(node.childNodes, isElementNode$1) 22168 .filter((child) => dom.getContentEditable(child) !== 'false' && matchName(dom, child, format)); 22169 return childElement.map((child) => { 22170 const clone = dom.clone(child, false); 22171 setElementFormat(ed, clone, format, vars, node); 22172 dom.replace(clone, node, true); 22173 dom.remove(child, true); 22174 return clone; 22175 }).getOr(node); 22176 }; 22177 const childCount = getChildCount(node); 22178 // Remove empty nodes but only if there is multiple wrappers and they are not block 22179 // elements so never remove single <h1></h1> since that would remove the 22180 // current empty block element where the caret is at 22181 if ((newWrappers.length > 1 || !dom.isBlock(node)) && childCount === 0) { 22182 dom.remove(node, true); 22183 return; 22184 } 22185 if (isInlineFormat(format) || isBlockFormat(format) && format.wrapper) { 22186 // Merges the current node with it's children of similar type to reduce the number of elements 22187 if (!format.exact && childCount === 1) { 22188 node = mergeStyles(node); 22189 } 22190 mergeWithChildren(ed, formatList, vars, node); 22191 mergeWithParents(ed, format, name, vars, node); 22192 mergeBackgroundColorAndFontSize(dom, format, vars, node); 22193 mergeTextDecorationsAndColor(dom, format, vars, node); 22194 mergeSubSup(dom, format, vars, node); 22195 mergeSiblings(ed, format, vars, node); 22196 } 22197 }); 22198 }; 22199
vendor: 7,227 bytes, lines 22199-22358
22199// TODO: TINY-9142: Remove this to make nested noneditable formatting work 22200 const targetNode = isNode(node) ? node : selection.getNode(); 22201 if (dom.getContentEditable(targetNode) === 'false' && !isWrappableNoneditable(ed, targetNode)) { 22202 // node variable is used by other functions above in the same scope so need to set it here 22203 node = targetNode; 22204 applyNodeStyle(formatList, node); 22205 fireFormatApply(ed, name, node, vars); 22206 return; 22207 } 22208 if (format) { 22209 if (node) { 22210 if (isNode(node)) { 22211 if (!applyNodeStyle(formatList, node)) { 22212 const rng = dom.createRng(); 22213 rng.setStartBefore(node); 22214 rng.setEndAfter(node); 22215 applyRngStyle(dom, expandRng(dom, rng, formatList), true); 22216 } 22217 } 22218 else { 22219 applyRngStyle(dom, node, true); 22220 } 22221 } 22222 else { 22223 if (!isCollapsed || !isInlineFormat(format) || getCellsFromEditor(ed).length) { 22224 // Apply formatting to selection 22225 selection.setRng(normalize(selection.getRng())); 22226 preserveSelection(ed, () => { 22227 runOnRanges(ed, (selectionRng, fake) => { 22228 const expandedRng = fake ? selectionRng : expandRng(dom, selectionRng, formatList); 22229 applyRngStyle(dom, expandedRng, false); 22230 }); 22231 }, always); 22232 ed.nodeChanged(); 22233 } 22234 else { 22235 applyCaretFormat(ed, name, vars); 22236 } 22237 getExpandedListItemFormat(ed.formatter, name).each((liFmt) => { 22238 const list = getFullySelectedListItems(ed.selection); 22239 each$e(list, (li) => applyStyles(dom, li, liFmt, vars)); 22240 }); 22241 } 22242 postProcess$1(name, ed); 22243 } 22244 fireFormatApply(ed, name, node, vars); 22245 }; 22246 const applyFormat$1 = (editor, name, vars, node) => { 22247 if (node || editor.selection.isEditable()) { 22248 applyFormatAction(editor, name, vars, node); 22249 } 22250 }; 22251 22252 const hasVars = (value) => has$2(value, 'vars'); 22253 const setup$A = (registeredFormatListeners, editor) => { 22254 registeredFormatListeners.set({}); 22255 editor.on('NodeChange', (e) => { 22256 updateAndFireChangeCallbacks(editor, e.element, registeredFormatListeners.get()); 22257 }); 22258 editor.on('FormatApply FormatRemove', (e) => { 22259 const element = Optional.from(e.node) 22260 .map((nodeOrRange) => isNode(nodeOrRange) ? nodeOrRange : nodeOrRange.startContainer) 22261 .bind((node) => isElement$7(node) ? Optional.some(node) : Optional.from(node.parentElement)) 22262 .getOrThunk(() => fallbackElement(editor)); 22263 updateAndFireChangeCallbacks(editor, element, registeredFormatListeners.get()); 22264 }); 22265 }; 22266 const fallbackElement = (editor) => editor.selection.getStart(); 22267 const matchingNode = (editor, parents, format, similar, vars) => { 22268 const isMatchingNode = (node) => { 22269 const matchingFormat = editor.formatter.matchNode(node, format, vars ?? {}, similar); 22270 return !isUndefined(matchingFormat); 22271 }; 22272 const isUnableToMatch = (node) => { 22273 if (matchesUnInheritedFormatSelector(editor, node, format)) { 22274 return true; 22275 } 22276 else { 22277 if (!similar) { 22278 // If we want to find an exact match, then finding a similar match halfway up the parents tree is bad 22279 return isNonNullable(editor.formatter.matchNode(node, format, vars, true)); 22280 } 22281 else { 22282 return false; 22283 } 22284 } 22285 }; 22286 return findUntil$1(parents, isMatchingNode, isUnableToMatch); 22287 }; 22288 const getParents = (editor, elm) => { 22289 const element = elm ?? fallbackElement(editor); 22290 return filter$5(getParents$2(editor.dom, element), (node) => isElement$7(node) && !isBogus$1(node)); 22291 }; 22292 const updateAndFireChangeCallbacks = (editor, elm, registeredCallbacks) => { 22293 // Ignore bogus nodes like the <a> tag created by moveStart() 22294 const parents = getParents(editor, elm); 22295 each$d(registeredCallbacks, (data, format) => { 22296 const runIfChanged = (spec) => { 22297 const match = matchingNode(editor, parents, format, spec.similar, hasVars(spec) ? spec.vars : undefined); 22298 const isSet = match.isSome(); 22299 if (spec.state.get() !== isSet) { 22300 spec.state.set(isSet); 22301 const node = match.getOr(elm); 22302 if (hasVars(spec)) { 22303 spec.callback(isSet, { node, format, parents }); 22304 } 22305 else { 22306 each$e(spec.callbacks, (callback) => callback(isSet, { node, format, parents })); 22307 } 22308 } 22309 }; 22310 each$e([data.withSimilar, data.withoutSimilar], runIfChanged); 22311 each$e(data.withVars, runIfChanged); 22312 }); 22313 }; 22314 const addListeners = (editor, registeredFormatListeners, formats, callback, similar, vars) => { 22315 const formatChangeItems = registeredFormatListeners.get(); 22316 each$e(formats.split(','), (format) => { 22317 const group = get$a(formatChangeItems, format).getOrThunk(() => { 22318 const base = { 22319 withSimilar: { 22320 state: Cell(false), 22321 similar: true, 22322 callbacks: [] 22323 }, 22324 withoutSimilar: { 22325 state: Cell(false), 22326 similar: false, 22327 callbacks: [] 22328 }, 22329 withVars: [] 22330 }; 22331 formatChangeItems[format] = base; 22332 return base; 22333 }); 22334 const getCurrent = () => { 22335 const parents = getParents(editor); 22336 return matchingNode(editor, parents, format, similar, vars).isSome(); 22337 }; 22338 if (isUndefined(vars)) { 22339 const toAppendTo = similar ? group.withSimilar : group.withoutSimilar; 22340 toAppendTo.callbacks.push(callback); 22341 if (toAppendTo.callbacks.length === 1) { 22342 toAppendTo.state.set(getCurrent()); 22343 } 22344 } 22345 else { 22346 group.withVars.push({ 22347 state: Cell(getCurrent()), 22348 similar, 22349 vars, 22350 callback 22351 }); 22352 } 22353 }); 22354 registeredFormatListeners.set(formatChangeItems); 22355 }; 22356 const removeListeners = (registeredFormatListeners, formats, callback) => { 22357 const formatChangeItems = registeredFormatListeners.get(); 22358 each$e(formats.split(','), (format) => get$a
vendor: 21,855 bytes, lines 22358-22882
22358(formatChangeItems, format).each((group) => { 22359 formatChangeItems[format] = { 22360 withSimilar: { 22361 ...group.withSimilar, 22362 callbacks: filter$5(group.withSimilar.callbacks, (cb) => cb !== callback), 22363 }, 22364 withoutSimilar: { 22365 ...group.withoutSimilar, 22366 callbacks: filter$5(group.withoutSimilar.callbacks, (cb) => cb !== callback), 22367 }, 22368 withVars: filter$5(group.withVars, (item) => item.callback !== callback), 22369 }; 22370 })); 22371 registeredFormatListeners.set(formatChangeItems); 22372 }; 22373 const formatChangedInternal = (editor, registeredFormatListeners, formats, callback, similar, vars) => { 22374 addListeners(editor, registeredFormatListeners, formats, callback, similar, vars); 22375 return { 22376 unbind: () => removeListeners(registeredFormatListeners, formats, callback) 22377 }; 22378 }; 22379 22380 const toggle = (editor, name, vars, node) => { 22381 const fmt = editor.formatter.get(name); 22382 if (fmt) { 22383 if (match$2(editor, name, vars, node) && (!('toggle' in fmt[0]) || fmt[0].toggle)) { 22384 removeFormat$1(editor, name, vars, node); 22385 } 22386 else { 22387 applyFormat$1(editor, name, vars, node); 22388 } 22389 } 22390 }; 22391 22392 const tableModel = (element, width, rows) => ({ 22393 element, 22394 width, 22395 rows 22396 }); 22397 const tableRow = (element, cells) => ({ 22398 element, 22399 cells 22400 }); 22401 const cellPosition = (x, y) => ({ 22402 x, 22403 y 22404 }); 22405 const getSpan = (td, key) => { 22406 return getOpt(td, key).bind(toInt).getOr(1); 22407 }; 22408 const fillout = (table, x, y, tr, td) => { 22409 const rowspan = getSpan(td, 'rowspan'); 22410 const colspan = getSpan(td, 'colspan'); 22411 const rows = table.rows; 22412 for (let y2 = y; y2 < y + rowspan; y2++) { 22413 if (!rows[y2]) { 22414 rows[y2] = tableRow(deep(tr), []); 22415 } 22416 for (let x2 = x; x2 < x + colspan; x2++) { 22417 const cells = rows[y2].cells; 22418 // not filler td:s are purposely not cloned so that we can 22419 // find cells in the model by element object references 22420 cells[x2] = y2 === y && x2 === x ? td : shallow(td); 22421 } 22422 } 22423 }; 22424 const cellExists = (table, x, y) => { 22425 const rows = table.rows; 22426 const cells = rows[y] ? rows[y].cells : []; 22427 return !!cells[x]; 22428 }; 22429 const skipCellsX = (table, x, y) => { 22430 while (cellExists(table, x, y)) { 22431 x++; 22432 } 22433 return x; 22434 }; 22435 const getWidth = (rows) => { 22436 return foldl(rows, (acc, row) => { 22437 return row.cells.length > acc ? row.cells.length : acc; 22438 }, 0); 22439 }; 22440 const findElementPos = (table, element) => { 22441 const rows = table.rows; 22442 for (let y = 0; y < rows.length; y++) { 22443 const cells = rows[y].cells; 22444 for (let x = 0; x < cells.length; x++) { 22445 if (eq(cells[x], element)) { 22446 return Optional.some(cellPosition(x, y)); 22447 } 22448 } 22449 } 22450 return Optional.none(); 22451 }; 22452 const extractRows = (table, sx, sy, ex, ey) => { 22453 const newRows = []; 22454 const rows = table.rows; 22455 for (let y = sy; y <= ey; y++) { 22456 const cells = rows[y].cells; 22457 const slice = sx < ex ? cells.slice(sx, ex + 1) : cells.slice(ex, sx + 1); 22458 newRows.push(tableRow(rows[y].element, slice)); 22459 } 22460 return newRows; 22461 }; 22462 const subTable = (table, startPos, endPos) => { 22463 const sx = startPos.x, sy = startPos.y; 22464 const ex = endPos.x, ey = endPos.y; 22465 const newRows = sy < ey ? extractRows(table, sx, sy, ex, ey) : extractRows(table, sx, ey, ex, sy); 22466 return tableModel(table.element, getWidth(newRows), newRows); 22467 }; 22468 const createDomTable = (table, rows) => { 22469 const tableElement = shallow(table.element); 22470 const tableBody = SugarElement.fromTag('tbody'); 22471 append(tableBody, rows); 22472 append$1(tableElement, tableBody); 22473 return tableElement; 22474 }; 22475 const modelRowsToDomRows = (table) => { 22476 return map$3(table.rows, (row) => { 22477 const cells = map$3(row.cells, (cell) => { 22478 const td = deep(cell); 22479 remove$9(td, 'colspan'); 22480 remove$9(td, 'rowspan'); 22481 return td; 22482 }); 22483 const tr = shallow(row.element); 22484 append(tr, cells); 22485 return tr; 22486 }); 22487 }; 22488 const fromDom = (tableElm) => { 22489 const table = tableModel(shallow(tableElm), 0, []); 22490 each$e(descendants(tableElm, 'tr'), (tr, y) => { 22491 each$e(descendants(tr, 'td,th'), (td, x) => { 22492 fillout(table, skipCellsX(table, x, y), y, tr, td); 22493 }); 22494 }); 22495 return tableModel(table.element, getWidth(table.rows), table.rows); 22496 }; 22497 const toDom = (table) => { 22498 return createDomTable(table, modelRowsToDomRows(table)); 22499 }; 22500 const subsection = (table, startElement, endElement) => { 22501 return findElementPos(table, startElement).bind((startPos) => { 22502 return findElementPos(table, endElement).map((endPos) => { 22503 return subTable(table, startPos, endPos); 22504 }); 22505 }); 22506 }; 22507 22508 const findParentListContainer = (parents) => find$2(parents, (elm) => name(elm) === 'ul' || name(elm) === 'ol'); 22509 const getFullySelectedListWrappers = (parents, rng) => find$2(parents, (elm) => name(elm) === 'li' && hasAllContentsSelected(elm, rng)).fold(constant([]), (_li) => findParentListContainer(parents).map((listCont) => { 22510 const listElm = SugarElement.fromTag(name(listCont)); 22511 // Retain any list-style* styles when generating the new fragment 22512 const listStyles = filter$4(getAllRaw(listCont), (_style, name) => startsWith(name, 'list-style')); 22513 setAll(listElm, listStyles); 22514 return [ 22515 SugarElement.fromTag('li'), 22516 listElm 22517 ]; 22518 }).getOr([])); 22519 const wrap = (innerElm, elms) => { 22520 const wrapped = foldl(elms, (acc, elm) => { 22521 append$1(elm, acc); 22522 return elm; 22523 }, innerElm); 22524 return elms.length > 0 ? fromElements([wrapped]) : wrapped; 22525 }; 22526 const directListWrappers = (commonAnchorContainer) => { 22527 if (isListItem$2(commonAnchorContainer)) { 22528 return parent(commonAnchorContainer).filter(isList$1).fold(constant([]), (listElm) => [commonAnchorContainer, listElm]); 22529 } 22530 else { 22531 return isList$1(commonAnchorContainer) ? [commonAnchorContainer] : []; 22532 } 22533 }; 22534 const getWrapElements = (rootNode, rng, schema) => { 22535 const commonAnchorContainer = SugarElement.fromDom(rng.commonAncestorContainer); 22536 const parents = parentsAndSelf(commonAnchorContainer, rootNode); 22537 const wrapElements = filter$5(parents, (el) => schema.isWrapper(name(el))); 22538 const listWrappers = getFullySelectedListWrappers(parents, rng); 22539 const allWrappers = wrapElements.concat(listWrappers.length ? listWrappers : directListWrappers(commonAnchorContainer)); 22540 return map$3(allWrappers, shallow); 22541 }; 22542 const emptyFragment = () => fromElements([]); 22543 const getFragmentFromRange = (rootNode, rng, schema) => wrap(SugarElement.fromDom(rng.cloneContents()), getWrapElements(rootNode, rng, schema)); 22544 const getParentTable = (rootElm, cell) => ancestor$4(cell, 'table', curry(eq, rootElm)); 22545 const getTableFragment = (rootNode, selectedTableCells) => getParentTable(rootNode, selectedTableCells[0]).bind((tableElm) => { 22546 const firstCell = selectedTableCells[0]; 22547 const lastCell = selectedTableCells[selectedTableCells.length - 1]; 22548 const fullTableModel = fromDom(tableElm); 22549 return subsection(fullTableModel, firstCell, lastCell).map((sectionedTableModel) => fromElements([toDom(sectionedTableModel)])); 22550 }).getOrThunk(emptyFragment); 22551 const getSelectionFragment = (rootNode, ranges, schema) => ranges.length > 0 && ranges[0].collapsed ? emptyFragment() : getFragmentFromRange(rootNode, ranges[0], schema); 22552 const read$3 = (rootNode, ranges, schema) => { 22553 const selectedCells = getCellsFromElementOrRanges(ranges, rootNode); 22554 return selectedCells.length > 0 ? getTableFragment(rootNode, selectedCells) : getSelectionFragment(rootNode, ranges, schema); 22555 }; 22556 22557 const isCollapsibleWhitespace = (text, index) => index >= 0 && index < text.length && isWhiteSpace(text.charAt(index)); 22558 const getInnerText = (bin) => { 22559 return trim$2(bin.innerText); 22560 }; 22561 const getContextNodeName = (parentBlockOpt) => parentBlockOpt.map((block) => block.nodeName).getOr('div').toLowerCase(); 22562 const getTextContent = (editor) => Optional.from(editor.selection.getRng()).map((rng) => { 22563 const parentBlockOpt = Optional.from(editor.dom.getParent(rng.commonAncestorContainer, editor.dom.isBlock)); 22564 const body = editor.getBody(); 22565 const contextNodeName = getContextNodeName(parentBlockOpt); 22566 const rangeContentClone = SugarElement.fromDom(rng.cloneContents()); 22567 cleanupBogusElements(rangeContentClone); 22568 cleanupInputNames(rangeContentClone); 22569 const bin = editor.dom.add(body, contextNodeName, { 22570 'data-mce-bogus': 'all', 22571 'style': 'overflow: hidden; opacity: 0;' 22572 }, rangeContentClone.dom); 22573 const text = getInnerText(bin); 22574 // textContent will not strip leading/trailing spaces since it doesn't consider how it'll render 22575 const nonRenderedText = trim$2(bin.textContent ?? ''); 22576 editor.dom.remove(bin); 22577 if (isCollapsibleWhitespace(nonRenderedText, 0) || isCollapsibleWhitespace(nonRenderedText, nonRenderedText.length - 1)) { 22578 // If the bin contains a trailing/leading space, then we need to inspect the parent block to see if we should include the spaces 22579 const parentBlock = parentBlockOpt.getOr(body); 22580 const parentBlockText = getInnerText(parentBlock); 22581 const textIndex = parentBlockText.indexOf(text); 22582 if (textIndex === -1) { 22583 return text; 22584 } 22585 else { 22586 const hasProceedingSpace = isCollapsibleWhitespace(parentBlockText, textIndex - 1); 22587 const hasTrailingSpace = isCollapsibleWhitespace(parentBlockText, textIndex + text.length); 22588 return (hasProceedingSpace ? ' ' : '') + text + (hasTrailingSpace ? ' ' : ''); 22589 } 22590 } 22591 else { 22592 return text; 22593 } 22594 }).getOr(''); 22595 const getSerializedContent = (editor, args) => { 22596 const rng = editor.selection.getRng(), tmpElm = editor.dom.create('body'); 22597 const sel = editor.selection.getSel(); 22598 const ranges = processRanges(editor, getRanges(sel)); 22599 const fragment = args.contextual ? read$3(SugarElement.fromDom(editor.getBody()), ranges, editor.schema).dom : rng.cloneContents(); 22600 if (fragment) { 22601 tmpElm.appendChild(fragment); 22602 } 22603 return editor.selection.serializer.serialize(tmpElm, args); 22604 }; 22605 const extractSelectedContent = (editor, args) => { 22606 if (args.format === 'text') { 22607 return getTextContent(editor); 22608 } 22609 else { 22610 const content = getSerializedContent(editor, args); 22611 if (args.format === 'tree') { 22612 return content; 22613 } 22614 else { 22615 return editor.selection.isCollapsed() ? '' : content; 22616 } 22617 } 22618 }; 22619 const setupArgs$2 = (args, format) => ({ 22620 ...args, 22621 format, 22622 get: true, 22623 selection: true, 22624 getInner: true 22625 }); 22626 const getSelectedContentInternal = (editor, format, args = {}) => { 22627 const defaultedArgs = setupArgs$2(args, format); 22628 return preProcessGetContent(editor, defaultedArgs).fold(identity, (updatedArgs) => { 22629 const content = extractSelectedContent(editor, updatedArgs); 22630 return postProcessGetContent(editor, content, updatedArgs); 22631 }); 22632 }; 22633 22634 /** 22635 * JS Implementation of the O(ND) Difference Algorithm by Eugene W. Myers. 22636 * 22637 * @class tinymce.undo.Diff 22638 * @private 22639 */ 22640 const KEEP = 0, INSERT = 1, DELETE = 2; 22641 const diff = (left, right) => { 22642 const size = left.length + right.length + 2; 22643 const vDown = new Array(size); 22644 const vUp = new Array(size); 22645 const snake = (start, end, diag) => { 22646 return { 22647 start, 22648 end, 22649 diag 22650 }; 22651 }; 22652 const buildScript = (start1, end1, start2, end2, script) => { 22653 const middle = getMiddleSnake(start1, end1, start2, end2); 22654 if (middle === null || middle.start === end1 && middle.diag === end1 - end2 || 22655 middle.end === start1 && middle.diag === start1 - start2) { 22656 let i = start1; 22657 let j = start2; 22658 while (i < end1 || j < end2) { 22659 if (i < end1 && j < end2 && left[i] === right[j]) { 22660 script.push([KEEP, left[i]]); 22661 ++i; 22662 ++j; 22663 } 22664 else { 22665 if (end1 - start1 > end2 - start2) { 22666 script.push([DELETE, left[i]]); 22667 ++i; 22668 } 22669 else { 22670 script.push([INSERT, right[j]]); 22671 ++j; 22672 } 22673 } 22674 } 22675 } 22676 else { 22677 buildScript(start1, middle.start, start2, middle.start - middle.diag, script); 22678 for (let i2 = middle.start; i2 < middle.end; ++i2) { 22679 script.push([KEEP, left[i2]]); 22680 } 22681 buildScript(middle.end, end1, middle.end - middle.diag, end2, script); 22682 } 22683 }; 22684 const buildSnake = (start, diag, end1, end2) => { 22685 let end = start; 22686 while (end - diag < end2 && end < end1 && left[end] === right[end - diag]) { 22687 ++end; 22688 } 22689 return snake(start, end, diag); 22690 }; 22691 const getMiddleSnake = (start1, end1, start2, end2) => { 22692 // Myers Algorithm 22693 // Initialisations 22694 const m = end1 - start1; 22695 const n = end2 - start2; 22696 if (m === 0 || n === 0) { 22697 return null; 22698 } 22699 const delta = m - n; 22700 const sum = n + m; 22701 const offset = (sum % 2 === 0 ? sum : sum + 1) / 2; 22702 vDown[1 + offset] = start1; 22703 vUp[1 + offset] = end1 + 1; 22704 let d, k, i, x, y; 22705 for (d = 0; d <= offset; ++d) { 22706 // Down 22707 for (k = -d; k <= d; k += 2) { 22708 // First step 22709 i = k + offset; 22710 if (k === -d || k !== d && vDown[i - 1] < vDown[i + 1]) { 22711 vDown[i] = vDown[i + 1]; 22712 } 22713 else { 22714 vDown[i] = vDown[i - 1] + 1; 22715 } 22716 x = vDown[i]; 22717 y = x - start1 + start2 - k; 22718 while (x < end1 && y < end2 && left[x] === right[y]) { 22719 vDown[i] = ++x; 22720 ++y; 22721 } 22722 // Second step 22723 if (delta % 2 !== 0 && delta - d <= k && k <= delta + d) { 22724 if (vUp[i - delta] <= vDown[i]) { 22725 return buildSnake(vUp[i - delta], k + start1 - start2, end1, end2); 22726 } 22727 } 22728 } 22729 // Up 22730 for (k = delta - d; k <= delta + d; k += 2) { 22731 // First step 22732 i = k + offset - delta; 22733 if (k === delta - d || k !== delta + d && vUp[i + 1] <= vUp[i - 1]) { 22734 vUp[i] = vUp[i + 1] - 1; 22735 } 22736 else { 22737 vUp[i] = vUp[i - 1]; 22738 } 22739 x = vUp[i] - 1; 22740 y = x - start1 + start2 - k; 22741 while (x >= start1 && y >= start2 && left[x] === right[y]) { 22742 vUp[i] = x--; 22743 y--; 22744 } 22745 // Second step 22746 if (delta % 2 === 0 && -d <= k && k <= d) { 22747 if (vUp[i] <= vDown[i + delta]) { 22748 return buildSnake(vUp[i], k + start1 - start2, end1, end2); 22749 } 22750 } 22751 } 22752 } 22753 return null; 22754 }; 22755 const script = []; 22756 buildScript(0, left.length, 0, right.length, script); 22757 return script; 22758 }; 22759 22760 /** 22761 * This module reads and applies html fragments from/to dom nodes. 22762 * 22763 * @class tinymce.undo.Fragments 22764 * @private 22765 */ 22766 const getOuterHtml = (elm) => { 22767 if (isElement$7(elm)) { 22768 return elm.outerHTML; 22769 } 22770 else if (isText$b(elm)) { 22771 return Entities.encodeRaw(elm.data, false); 22772 } 22773 else if (isComment(elm)) { 22774 return '<!--' + elm.data + '-->'; 22775 } 22776 return ''; 22777 }; 22778 const createFragment = (html) => { 22779 let node; 22780 const container = document.createElement('div'); 22781 const frag = document.createDocumentFragment(); 22782 if (html) { 22783 container.innerHTML = html; 22784 } 22785 while ((node = container.firstChild)) { 22786 frag.appendChild(node); 22787 } 22788 return frag; 22789 }; 22790 const insertAt = (elm, html, index) => { 22791 const fragment = createFragment(html); 22792 if (elm.hasChildNodes() && index < elm.childNodes.length) { 22793 const target = elm.childNodes[index]; 22794 elm.insertBefore(fragment, target); 22795 } 22796 else { 22797 elm.appendChild(fragment); 22798 } 22799 }; 22800 const removeAt = (elm, index) => { 22801 if (elm.hasChildNodes() && index < elm.childNodes.length) { 22802 const target = elm.childNodes[index]; 22803 elm.removeChild(target); 22804 } 22805 }; 22806 const applyDiff = (diff, elm) => { 22807 let index = 0; 22808 each$e(diff, (action) => { 22809 if (action[0] === KEEP) { 22810 index++; 22811 } 22812 else if (action[0] === INSERT) { 22813 insertAt(elm, action[1], index); 22814 index++; 22815 } 22816 else if (action[0] === DELETE) { 22817 removeAt(elm, index); 22818 } 22819 }); 22820 }; 22821 const read$2 = (elm, trimZwsp) => filter$5(map$3(from(elm.childNodes), trimZwsp ? compose(trim$2, getOuterHtml) : getOuterHtml), (item) => { 22822 return item.length > 0; 22823 }); 22824 const write = (fragments, elm) => { 22825 const currentFragments = map$3(from(elm.childNodes), getOuterHtml); 22826 applyDiff(diff(currentFragments, fragments), elm); 22827 return elm; 22828 }; 22829 22830 // We need to create a temporary document instead of using the global document since 22831 // innerHTML on a detached element will still make http requests to the images 22832 const lazyTempDocument = cached(() => document.implementation.createHTMLDocument('undo')); 22833 const shouldBeFragmented = (body) => body.querySelector(`iframe, ${ucVideoNodeName}`) !== null; 22834 const createFragmentedLevel = (fragments) => { 22835 return { 22836 type: 'fragmented', 22837 fragments, 22838 content: '', 22839 bookmark: null, 22840 beforeBookmark: null 22841 }; 22842 }; 22843 const createCompleteLevel = (content) => { 22844 return { 22845 type: 'complete', 22846 fragments: null, 22847 content, 22848 bookmark: null, 22849 beforeBookmark: null 22850 }; 22851 }; 22852 const createFromEditor = (editor) => { 22853 const tempAttrs = editor.serializer.getTempAttrs(); 22854 const body = trim$1(editor.getBody(), tempAttrs); 22855 return shouldBeFragmented(body) ? createFragmentedLevel(read$2(body, true)) : createCompleteLevel(trim$2(body.innerHTML)); 22856 }; 22857 const applyToEditor = (editor, level, before) => { 22858 const bookmark = before ? level.beforeBookmark : level.bookmark; 22859 if (level.type === 'fragmented') { 22860 write(level.fragments, editor.getBody()); 22861 } 22862 else { 22863 editor.setContent(level.content, { 22864 format: 'raw', 22865 // If we have a path bookmark, we need to check if the bookmark location was a fake caret. 22866 // If the bookmark was not a fake caret, then we need to ensure that setContent does not move the selection 22867 // as this can create a new fake caret - particularly if the first element in the body is contenteditable=false. 22868 // The creation of this new fake caret will cause our path offset to be off by one when restoring the original selection. 22869 no_selection: isNonNullable(bookmark) && isPathBookmark(bookmark) ? !bookmark.isFakeCaret : true 22870 }); 22871 } 22872 if (bookmark) { 22873 editor.selection.moveToBookmark(bookmark); 22874 editor.selection.scrollIntoView(); 22875 } 22876 }; 22877 const getLevelContent = (level) => { 22878 return level.type === 'fragmented' ? level.fragments.join('') : level.content; 22879 }; 22880 const getCleanLevelContent = (isReadonly, level) => { 22881 const elm = SugarElement.fromTag('body', lazyTempDocument()); 22882 set$
228823(elm, getLevelContent(level)); 22883 each$e(descendants(elm, '*[data-mce-bogus]'), unwrap); 22884 if (isReadonly) { 22885 each$e(descendants(elm, 'details[open]'), (element) => remove$9(element, 'open')); 22886 } 22887 return get$8
vendor: 14,311 bytes, lines 22887-23228
22887(elm); 22888 }; 22889 const hasEqualContent = (level1, level2) => getLevelContent(level1) === getLevelContent(level2); 22890 const hasEqualCleanedContent = (isReadonly, level1, level2) => getCleanLevelContent(isReadonly, level1) === getCleanLevelContent(isReadonly, level2); 22891 // Most of the time the contents is equal so it's faster to first check that using strings then fallback to a cleaned dom comparison 22892 const isEq$1 = (isReadonly, level1, level2) => { 22893 if (!level1 || !level2) { 22894 return false; 22895 } 22896 else if (hasEqualContent(level1, level2)) { 22897 return true; 22898 } 22899 else { 22900 return hasEqualCleanedContent(isReadonly, level1, level2); 22901 } 22902 }; 22903 22904 const isUnlocked = (locks) => locks.get() === 0; 22905 22906 const setTyping = (undoManager, typing, locks) => { 22907 if (isUnlocked(locks)) { 22908 undoManager.typing = typing; 22909 } 22910 }; 22911 const endTyping = (undoManager, locks) => { 22912 if (undoManager.typing) { 22913 setTyping(undoManager, false, locks); 22914 undoManager.add(); 22915 } 22916 }; 22917 const endTypingLevelIgnoreLocks = (undoManager) => { 22918 if (undoManager.typing) { 22919 undoManager.typing = false; 22920 undoManager.add(); 22921 } 22922 }; 22923 22924 const beforeChange$1 = (editor, locks, beforeBookmark) => { 22925 if (isUnlocked(locks)) { 22926 beforeBookmark.set(getUndoBookmark(editor.selection)); 22927 } 22928 }; 22929 const addUndoLevel$1 = (editor, undoManager, index, locks, beforeBookmark, level, event) => { 22930 const currentLevel = createFromEditor(editor); 22931 const newLevel = Tools.extend(level || {}, currentLevel); 22932 if (!isUnlocked(locks) || editor.removed) { 22933 return null; 22934 } 22935 const lastLevel = undoManager.data[index.get()]; 22936 if (editor.dispatch('BeforeAddUndo', { level: newLevel, lastLevel, originalEvent: event }).isDefaultPrevented()) { 22937 return null; 22938 } 22939 // Add undo level if needed 22940 if (lastLevel && isEq$1(editor.readonly, lastLevel, newLevel)) { 22941 return null; 22942 } 22943 // Set before bookmark on previous level 22944 if (undoManager.data[index.get()]) { 22945 beforeBookmark.get().each((bm) => { 22946 undoManager.data[index.get()].beforeBookmark = bm; 22947 }); 22948 } 22949 // Time to compress 22950 const customUndoRedoLevels = getCustomUndoRedoLevels(editor); 22951 if (customUndoRedoLevels) { 22952 if (undoManager.data.length > customUndoRedoLevels) { 22953 for (let i = 0; i < undoManager.data.length - 1; i++) { 22954 undoManager.data[i] = undoManager.data[i + 1]; 22955 } 22956 undoManager.data.length--; 22957 index.set(undoManager.data.length); 22958 } 22959 } 22960 // Get a non intrusive normalized bookmark 22961 newLevel.bookmark = getUndoBookmark(editor.selection); 22962 // Crop array if needed 22963 if (index.get() < undoManager.data.length - 1) { 22964 undoManager.data.length = index.get() + 1; 22965 } 22966 undoManager.data.push(newLevel); 22967 index.set(undoManager.data.length - 1); 22968 const args = { level: newLevel, lastLevel, originalEvent: event }; 22969 if (index.get() > 0) { 22970 editor.setDirty(true); 22971 editor.dispatch('AddUndo', args); 22972 editor.dispatch('change', args); 22973 } 22974 else { 22975 editor.dispatch('AddUndo', args); 22976 } 22977 return newLevel; 22978 }; 22979 const clear$1 = (editor, undoManager, index) => { 22980 undoManager.data = []; 22981 index.set(0); 22982 undoManager.typing = false; 22983 editor.dispatch('ClearUndos'); 22984 }; 22985 const extra$1 = (editor, undoManager, index, callback1, callback2) => { 22986 if (undoManager.transact(callback1)) { 22987 const bookmark = undoManager.data[index.get()].bookmark; 22988 const lastLevel = undoManager.data[index.get() - 1]; 22989 applyToEditor(editor, lastLevel, true); 22990 if (undoManager.transact(callback2)) { 22991 undoManager.data[index.get() - 1].beforeBookmark = bookmark; 22992 } 22993 } 22994 }; 22995 const redo$1 = (editor, index, data) => { 22996 let level; 22997 if (index.get() < data.length - 1) { 22998 index.set(index.get() + 1); 22999 level = data[index.get()]; 23000 applyToEditor(editor, level, false); 23001 editor.setDirty(true); 23002 editor.dispatch('Redo', { level }); 23003 } 23004 return level; 23005 }; 23006 const undo$1 = (editor, undoManager, locks, index) => { 23007 let level; 23008 if (undoManager.typing) { 23009 undoManager.add(); 23010 undoManager.typing = false; 23011 setTyping(undoManager, false, locks); 23012 } 23013 if (index.get() > 0) { 23014 index.set(index.get() - 1); 23015 level = undoManager.data[index.get()]; 23016 applyToEditor(editor, level, true); 23017 editor.setDirty(true); 23018 editor.dispatch('Undo', { level }); 23019 } 23020 return level; 23021 }; 23022 const reset$1 = (undoManager) => { 23023 undoManager.clear(); 23024 undoManager.add(); 23025 }; 23026 const hasUndo$1 = (editor, undoManager, index) => 23027 // Has undo levels or typing and content isn't the same as the initial level 23028 index.get() > 0 || (undoManager.typing && undoManager.data[0] && !isEq$1(editor.readonly, createFromEditor(editor), undoManager.data[0])); 23029 const hasRedo$1 = (undoManager, index) => index.get() < undoManager.data.length - 1 && !undoManager.typing; 23030 const transact$1 = (undoManager, locks, callback) => { 23031 endTyping(undoManager, locks); 23032 undoManager.beforeChange(); 23033 undoManager.ignore(callback); 23034 return undoManager.add(); 23035 }; 23036 const ignore$1 = (locks, callback) => { 23037 try { 23038 locks.set(locks.get() + 1); 23039 callback(); 23040 } 23041 finally { 23042 locks.set(locks.get() - 1); 23043 } 23044 }; 23045 23046 const addVisualInternal = (editor, elm) => { 23047 const dom = editor.dom; 23048 const scope = isNonNullable(elm) ? elm : editor.getBody(); 23049 each$e(dom.select('table,a', scope), (matchedElm) => { 23050 switch (matchedElm.nodeName) { 23051 case 'TABLE': 23052 const cls = getVisualAidsTableClass(editor); 23053 const value = dom.getAttrib(matchedElm, 'border'); 23054 if ((!value || value === '0') && editor.hasVisual) { 23055 dom.addClass(matchedElm, cls); 23056 } 23057 else { 23058 dom.removeClass(matchedElm, cls); 23059 } 23060 break; 23061 case 'A': 23062 if (!dom.getAttrib(matchedElm, 'href')) { 23063 const value = dom.getAttrib(matchedElm, 'name') || matchedElm.id; 23064 const cls = getVisualAidsAnchorClass(editor); 23065 if (value && editor.hasVisual) { 23066 dom.addClass(matchedElm, cls); 23067 } 23068 else { 23069 dom.removeClass(matchedElm, cls); 23070 } 23071 } 23072 break; 23073 } 23074 }); 23075 editor.dispatch('VisualAid', { element: elm, hasVisual: editor.hasVisual }); 23076 }; 23077 23078 const makePlainAdaptor = (editor) => ({ 23079 init: { 23080 bindEvents: noop 23081 }, 23082 undoManager: { 23083 beforeChange: (locks, beforeBookmark) => beforeChange$1(editor, locks, beforeBookmark), 23084 add: (undoManager, index, locks, beforeBookmark, level, event) => addUndoLevel$1(editor, undoManager, index, locks, beforeBookmark, level, event), 23085 undo: (undoManager, locks, index) => undo$1(editor, undoManager, locks, index), 23086 redo: (index, data) => redo$1(editor, index, data), 23087 clear: (undoManager, index) => clear$1(editor, undoManager, index), 23088 reset: (undoManager) => reset$1(undoManager), 23089 hasUndo: (undoManager, index) => hasUndo$1(editor, undoManager, index), 23090 hasRedo: (undoManager, index) => hasRedo$1(undoManager, index), 23091 transact: (undoManager, locks, callback) => transact$1(undoManager, locks, callback), 23092 ignore: (locks, callback) => ignore$1(locks, callback), 23093 extra: (undoManager, index, callback1, callback2) => extra$1(editor, undoManager, index, callback1, callback2) 23094 }, 23095 formatter: { 23096 match: (name, vars, node, similar) => match$2(editor, name, vars, node, similar), 23097 matchAll: (names, vars) => matchAll(editor, names, vars), 23098 matchNode: (node, name, vars, similar) => matchNode$1(editor, node, name, vars, similar), 23099 canApply: (name) => canApply(editor, name), 23100 closest: (names) => closest(editor, names), 23101 apply: (name, vars, node) => applyFormat$1(editor, name, vars, node), 23102 remove: (name, vars, node, similar) => removeFormat$1(editor, name, vars, node, similar), 23103 toggle: (name, vars, node) => toggle(editor, name, vars, node), 23104 formatChanged: (registeredFormatListeners, formats, callback, similar, vars) => formatChangedInternal(editor, registeredFormatListeners, formats, callback, similar, vars) 23105 }, 23106 editor: { 23107 getContent: (args) => getContentInternal(editor, args), 23108 setContent: (content, args) => setContentInternal(editor, content, args), 23109 insertContent: (value, details) => insertHtmlAtCaret(editor, value, details), 23110 addVisual: (elm) => addVisualInternal(editor, elm) 23111 }, 23112 selection: { 23113 getContent: (format, args) => getSelectedContentInternal(editor, format, args) 23114 }, 23115 autocompleter: { 23116 addDecoration: noop, // This was never fully implemented in RTC 23117 removeDecoration: noop, // This was never fully implemented in RTC 23118 }, 23119 raw: { 23120 getModel: () => Optional.none() 23121 } 23122 }); 23123 const makeRtcAdaptor = (rtcEditor) => { 23124 const defaultVars = (vars) => isObject(vars) ? vars : {}; 23125 const { init, undoManager, formatter, editor, selection, autocompleter, raw } = rtcEditor; 23126 return { 23127 init: { 23128 bindEvents: init.bindEvents 23129 }, 23130 undoManager: { 23131 beforeChange: undoManager.beforeChange, 23132 add: undoManager.add, 23133 undo: undoManager.undo, 23134 redo: undoManager.redo, 23135 clear: undoManager.clear, 23136 reset: undoManager.reset, 23137 hasUndo: undoManager.hasUndo, 23138 hasRedo: undoManager.hasRedo, 23139 transact: (_undoManager, _locks, fn) => undoManager.transact(fn), 23140 ignore: (_locks, callback) => undoManager.ignore(callback), 23141 extra: (_undoManager, _index, callback1, callback2) => undoManager.extra(callback1, callback2) 23142 }, 23143 formatter: { 23144 match: (name, vars, _node, similar) => formatter.match(name, defaultVars(vars), similar), 23145 matchAll: formatter.matchAll, 23146 matchNode: formatter.matchNode, 23147 canApply: (name) => formatter.canApply(name), 23148 closest: (names) => formatter.closest(names), 23149 apply: (name, vars, _node) => formatter.apply(name, defaultVars(vars)), 23150 remove: (name, vars, _node, _similar) => formatter.remove(name, defaultVars(vars)), 23151 toggle: (name, vars, _node) => formatter.toggle(name, defaultVars(vars)), 23152 formatChanged: (_rfl, formats, callback, similar, vars) => formatter.formatChanged(formats, callback, similar, vars) 23153 }, 23154 editor: { 23155 getContent: (args) => editor.getContent(args), 23156 setContent: (content, args) => { 23157 return { content: editor.setContent(content, args), html: '' }; 23158 }, 23159 insertContent: (content, _details) => { 23160 editor.insertContent(content); 23161 return ''; 23162 }, 23163 addVisual: editor.addVisual 23164 }, 23165 selection: { 23166 getContent: (_format, args) => selection.getContent(args) 23167 }, 23168 autocompleter: { 23169 addDecoration: autocompleter.addDecoration, 23170 removeDecoration: autocompleter.removeDecoration 23171 }, 23172 raw: { 23173 getModel: () => Optional.some(raw.getRawModel()) 23174 } 23175 }; 23176 }; 23177 const makeNoopAdaptor = () => { 23178 // Cast as any since this will never match the implementations 23179 const nul = constant(null); 23180 const empty = constant(''); 23181 return { 23182 init: { 23183 bindEvents: noop 23184 }, 23185 undoManager: { 23186 beforeChange: noop, 23187 add: nul, 23188 undo: nul, 23189 redo: nul, 23190 clear: noop, 23191 reset: noop, 23192 hasUndo: never, 23193 hasRedo: never, 23194 transact: nul, 23195 ignore: noop, 23196 extra: noop 23197 }, 23198 formatter: { 23199 match: never, 23200 matchAll: constant([]), 23201 matchNode: constant(undefined), 23202 canApply: never, 23203 closest: empty, 23204 apply: noop, 23205 remove: noop, 23206 toggle: noop, 23207 formatChanged: constant({ unbind: noop }) 23208 }, 23209 editor: { 23210 getContent: empty, 23211 setContent: constant({ content: '', html: '' }), 23212 insertContent: constant(''), 23213 addVisual: noop 23214 }, 23215 selection: { 23216 getContent: empty 23217 }, 23218 autocompleter: { 23219 addDecoration: noop, 23220 removeDecoration: noop 23221 }, 23222 raw: { 23223 getModel: constant(Optional.none()) 23224 } 23225 }; 23226 }; 23227 const isRtc = (editor) => has$2(editor.plugins, 'rtc'); 23228 const getRtcSetup = (editor) => get$
23228a
vendor: 38,074 bytes, lines 23228-24016
23228(editor.plugins, 'rtc').bind((rtcPlugin) => 23229 // This might not exist if the stub plugin is loaded on cloud 23230 Optional.from(rtcPlugin.setup)); 23231 const setup$z = (editor) => { 23232 const editorCast = editor; 23233 return getRtcSetup(editor).fold(() => { 23234 editorCast.rtcInstance = makePlainAdaptor(editor); 23235 return Optional.none(); 23236 }, (setup) => { 23237 // We need to provide a noop adaptor while initializing since any call by the theme or plugins to say undoManager.hasUndo would throw errors 23238 editorCast.rtcInstance = makeNoopAdaptor(); 23239 return Optional.some(() => setup().then((rtcEditor) => { 23240 editorCast.rtcInstance = makeRtcAdaptor(rtcEditor); 23241 return rtcEditor.rtc.isRemote; 23242 })); 23243 }); 23244 }; 23245 const getRtcInstanceWithFallback = (editor) => 23246 // Calls to editor.getContent/editor.setContent should still work even if the rtcInstance is not yet available 23247 editor.rtcInstance ? editor.rtcInstance : makePlainAdaptor(editor); 23248 const getRtcInstanceWithError = (editor) => { 23249 const rtcInstance = editor.rtcInstance; 23250 if (!rtcInstance) { 23251 throw new Error('Failed to get RTC instance not yet initialized.'); 23252 } 23253 else { 23254 return rtcInstance; 23255 } 23256 }; 23257 /** In theory these could all be inlined but having them here makes it clear what is overridden */ 23258 const beforeChange = (editor, locks, beforeBookmark) => { 23259 getRtcInstanceWithError(editor).undoManager.beforeChange(locks, beforeBookmark); 23260 }; 23261 const addUndoLevel = (editor, undoManager, index, locks, beforeBookmark, level, event) => getRtcInstanceWithError(editor).undoManager.add(undoManager, index, locks, beforeBookmark, level, event); 23262 const undo = (editor, undoManager, locks, index) => getRtcInstanceWithError(editor).undoManager.undo(undoManager, locks, index); 23263 const redo = (editor, index, data) => getRtcInstanceWithError(editor).undoManager.redo(index, data); 23264 const clear = (editor, undoManager, index) => { 23265 getRtcInstanceWithError(editor).undoManager.clear(undoManager, index); 23266 }; 23267 const reset = (editor, undoManager) => { 23268 getRtcInstanceWithError(editor).undoManager.reset(undoManager); 23269 }; 23270 const hasUndo = (editor, undoManager, index) => getRtcInstanceWithError(editor).undoManager.hasUndo(undoManager, index); 23271 const hasRedo = (editor, undoManager, index) => getRtcInstanceWithError(editor).undoManager.hasRedo(undoManager, index); 23272 const transact = (editor, undoManager, locks, callback) => getRtcInstanceWithError(editor).undoManager.transact(undoManager, locks, callback); 23273 const ignore = (editor, locks, callback) => { 23274 getRtcInstanceWithError(editor).undoManager.ignore(locks, callback); 23275 }; 23276 const extra = (editor, undoManager, index, callback1, callback2) => { 23277 getRtcInstanceWithError(editor).undoManager.extra(undoManager, index, callback1, callback2); 23278 }; 23279 const matchFormat = (editor, name, vars, node, similar) => getRtcInstanceWithError(editor).formatter.match(name, vars, node, similar); 23280 const matchAllFormats = (editor, names, vars) => getRtcInstanceWithError(editor).formatter.matchAll(names, vars); 23281 const matchNodeFormat = (editor, node, name, vars, similar) => getRtcInstanceWithError(editor).formatter.matchNode(node, name, vars, similar); 23282 const canApplyFormat = (editor, name) => getRtcInstanceWithError(editor).formatter.canApply(name); 23283 const closestFormat = (editor, names) => getRtcInstanceWithError(editor).formatter.closest(names); 23284 const applyFormat = (editor, name, vars, node) => { 23285 getRtcInstanceWithError(editor).formatter.apply(name, vars, node); 23286 }; 23287 const removeFormat = (editor, name, vars, node, similar) => { 23288 getRtcInstanceWithError(editor).formatter.remove(name, vars, node, similar); 23289 }; 23290 const toggleFormat = (editor, name, vars, node) => { 23291 getRtcInstanceWithError(editor).formatter.toggle(name, vars, node); 23292 }; 23293 const formatChanged = (editor, registeredFormatListeners, formats, callback, similar, vars) => getRtcInstanceWithError(editor).formatter.formatChanged(registeredFormatListeners, formats, callback, similar, vars); 23294 const getContent$2 = (editor, args) => getRtcInstanceWithFallback(editor).editor.getContent(args); 23295 const setContent$1 = (editor, content, args) => getRtcInstanceWithFallback(editor).editor.setContent(content, args); 23296 const insertContent$1 = (editor, value, details) => getRtcInstanceWithFallback(editor).editor.insertContent(value, details); 23297 const getSelectedContent = (editor, format, args) => getRtcInstanceWithError(editor).selection.getContent(format, args); 23298 const addVisual$1 = (editor, elm) => getRtcInstanceWithError(editor).editor.addVisual(elm); 23299 const bindEvents = (editor) => getRtcInstanceWithError(editor).init.bindEvents(); 23300 23301 const getContent$1 = (editor, args = {}) => { 23302 const format = args.format ? args.format : 'html'; 23303 return getSelectedContent(editor, format, args); 23304 }; 23305 23306 const deleteFromCallbackMap = (callbackMap, selector, callback) => { 23307 if (has$2(callbackMap, selector)) { 23308 const newCallbacks = filter$5(callbackMap[selector], (cb) => cb !== callback); 23309 if (newCallbacks.length === 0) { 23310 delete callbackMap[selector]; 23311 } 23312 else { 23313 callbackMap[selector] = newCallbacks; 23314 } 23315 } 23316 }; 23317 var SelectorChanged = (dom, editor) => { 23318 let selectorChangedData; 23319 let currentSelectors; 23320 const findMatchingNode = (selector, nodes) => find$2(nodes, (node) => dom.is(node, selector)); 23321 const getParents = (elem) => dom.getParents(elem, undefined, dom.getRoot()); 23322 const setup = () => { 23323 selectorChangedData = {}; 23324 currentSelectors = {}; 23325 editor.on('NodeChange', (e) => { 23326 const node = e.element; 23327 const parents = getParents(node); 23328 const matchedSelectors = {}; 23329 // Check for new matching selectors 23330 each$d(selectorChangedData, (callbacks, selector) => { 23331 findMatchingNode(selector, parents).each((node) => { 23332 if (!currentSelectors[selector]) { 23333 // Execute callbacks 23334 each$e(callbacks, (callback) => { 23335 callback(true, { node, selector, parents }); 23336 }); 23337 currentSelectors[selector] = callbacks; 23338 } 23339 matchedSelectors[selector] = callbacks; 23340 }); 23341 }); 23342 // Check if current selectors still match 23343 each$d(currentSelectors, (callbacks, selector) => { 23344 if (!matchedSelectors[selector]) { 23345 delete currentSelectors[selector]; 23346 each$e(callbacks, (callback) => { 23347 callback(false, { node, selector, parents }); 23348 }); 23349 } 23350 }); 23351 }); 23352 }; 23353 return { 23354 selectorChangedWithUnbind: (selector, callback) => { 23355 if (!selectorChangedData) { 23356 setup(); 23357 } 23358 // Add selector listeners 23359 if (!selectorChangedData[selector]) { 23360 selectorChangedData[selector] = []; 23361 } 23362 selectorChangedData[selector].push(callback); 23363 // Setup the initial state if selected already 23364 findMatchingNode(selector, getParents(editor.selection.getStart())).each(() => { 23365 currentSelectors[selector] = selectorChangedData[selector]; 23366 }); 23367 return { 23368 unbind: () => { 23369 deleteFromCallbackMap(selectorChangedData, selector, callback); 23370 deleteFromCallbackMap(currentSelectors, selector, callback); 23371 } 23372 }; 23373 } 23374 }; 23375 }; 23376 23377 /** 23378 * This class handles text and control selection it's an crossbrowser utility class. 23379 * Consult the TinyMCE API Documentation for more details and examples on how to use this class. 23380 * 23381 * @class tinymce.dom.Selection 23382 * @example 23383 * // Getting the currently selected node for the active editor 23384 * alert(tinymce.activeEditor.selection.getNode().nodeName); 23385 */ 23386 const isAttachedToDom = (node) => { 23387 return !!(node && node.ownerDocument) && contains(SugarElement.fromDom(node.ownerDocument), SugarElement.fromDom(node)); 23388 }; 23389 const isValidRange = (rng) => { 23390 if (!rng) { 23391 return false; 23392 } 23393 else { 23394 return isAttachedToDom(rng.startContainer) && isAttachedToDom(rng.endContainer); 23395 } 23396 }; 23397 /** 23398 * Constructs a new selection instance. 23399 * 23400 * @constructor 23401 * @method Selection 23402 * @param {tinymce.dom.DOMUtils} dom DOMUtils object reference. 23403 * @param {Window} win Window to bind the selection object to. 23404 * @param {tinymce.dom.Serializer} serializer DOM serialization class to use for getContent. 23405 * @param {tinymce.Editor} editor Editor instance of the selection. 23406 */ 23407 const EditorSelection = (dom, win, serializer, editor) => { 23408 let selectedRange; 23409 let explicitRange; 23410 const { selectorChangedWithUnbind } = SelectorChanged(dom, editor); 23411 /** 23412 * Move the selection cursor range to the specified node and offset. 23413 * If there is no node specified it will move it to the first suitable location within the body. 23414 * 23415 * @method setCursorLocation 23416 * @param {Node} node Optional node to put the cursor in. 23417 * @param {Number} offset Optional offset from the start of the node to put the cursor at. 23418 */ 23419 const setCursorLocation = (node, offset) => { 23420 const rng = dom.createRng(); 23421 if (isNonNullable(node) && isNonNullable(offset)) { 23422 rng.setStart(node, offset); 23423 rng.setEnd(node, offset); 23424 setRng(rng); 23425 collapse(false); 23426 } 23427 else { 23428 moveEndPoint(dom, rng, editor.getBody(), true); 23429 setRng(rng); 23430 } 23431 }; 23432 /** 23433 * Returns the selected contents using the DOM serializer passed in to this class. 23434 * 23435 * @method getContent 23436 * @param {Object} args Optional settings class with for example output format text or html. 23437 * @return {String} Selected contents in for example HTML format. 23438 * @example 23439 * // Alerts the currently selected contents 23440 * alert(tinymce.activeEditor.selection.getContent()); 23441 * 23442 * // Alerts the currently selected contents as plain text 23443 * alert(tinymce.activeEditor.selection.getContent({ format: 'text' })); 23444 */ 23445 const getContent = (args) => getContent$1(editor, args); 23446 /** 23447 * This method has been deprecated. Use "editor.insertContent" instead. 23448 * 23449 * Sets the current selection to the specified content. If any contents is selected it will be replaced 23450 * with the contents passed in to this function. If there is no selection the contents will be inserted 23451 * where the caret is placed in the editor/page. 23452 * 23453 * @method setContent 23454 * @param {String} content HTML contents to set could also be other formats depending on settings. 23455 * @param {Object} args Optional settings object with for example data format. 23456 * @example 23457 * // Inserts some HTML contents at the current selection 23458 * tinymce.activeEditor.selection.setContent('<strong>Some contents</strong>'); 23459 */ 23460 const setContent = (content, args) => setContentExternal(editor, content, args); 23461 /** 23462 * Returns the start element of a selection range. If the start is in a text 23463 * node the parent element will be returned. 23464 * 23465 * @method getStart 23466 * @param {Boolean} real Optional state to get the real parent when the selection is collapsed not the closest element. 23467 * @return {Element} Start element of selection range. 23468 */ 23469 const getStart$1 = (real) => getStart(editor.getBody(), getRng$1(), real); 23470 /** 23471 * Returns the end element of a selection range. If the end is in a text 23472 * node the parent element will be returned. 23473 * 23474 * @method getEnd 23475 * @param {Boolean} real Optional state to get the real parent when the selection is collapsed not the closest element. 23476 * @return {Element} End element of selection range. 23477 */ 23478 const getEnd$1 = (real) => getEnd(editor.getBody(), getRng$1(), real); 23479 /** 23480 * Returns a bookmark location for the current selection. This bookmark object 23481 * can then be used to restore the selection after some content modification to the document. 23482 * 23483 * @method getBookmark 23484 * @param {Number} type Optional state if the bookmark should be simple or not. Default is complex. 23485 * @param {Boolean} normalized Optional state that enables you to get a position that it would be after normalization. 23486 * @return {Object} Bookmark object, use moveToBookmark with this object to restore the selection. 23487 * @example 23488 * // Stores a bookmark of the current selection 23489 * const bm = tinymce.activeEditor.selection.getBookmark(); 23490 * 23491 * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content'); 23492 * 23493 * // Restore the selection bookmark 23494 * tinymce.activeEditor.selection.moveToBookmark(bm); 23495 */ 23496 const getBookmark = (type, normalized) => bookmarkManager.getBookmark(type, normalized); 23497 /** 23498 * Restores the selection to the specified bookmark. 23499 * 23500 * @method moveToBookmark 23501 * @param {Object} bookmark Bookmark to restore selection from. 23502 * @example 23503 * // Stores a bookmark of the current selection 23504 * const bm = tinymce.activeEditor.selection.getBookmark(); 23505 * 23506 * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content'); 23507 * 23508 * // Restore the selection bookmark 23509 * tinymce.activeEditor.selection.moveToBookmark(bm); 23510 */ 23511 const moveToBookmark = (bookmark) => bookmarkManager.moveToBookmark(bookmark); 23512 /** 23513 * Selects the specified element. This will place the start and end of the selection range around the element. 23514 * 23515 * @method select 23516 * @param {Element} node HTML DOM element to select. 23517 * @param {Boolean} content Optional bool state if the contents should be selected or not on non IE browser. 23518 * @return {Element} Selected element the same element as the one that got passed in. 23519 * @example 23520 * // Select the first paragraph in the active editor 23521 * tinymce.activeEditor.selection.select(tinymce.activeEditor.dom.select('p')[0]); 23522 */ 23523 const select$1 = (node, content) => { 23524 select(dom, node, content).each(setRng); 23525 return node; 23526 }; 23527 /** 23528 * Returns true/false if the selection range is collapsed or not. Collapsed means if it's a caret or a larger selection. 23529 * 23530 * @method isCollapsed 23531 * @return {Boolean} true/false state if the selection range is collapsed or not. 23532 * Collapsed means if it's a caret or a larger selection. 23533 */ 23534 const isCollapsed = () => { 23535 const rng = getRng$1(), sel = getSel(); 23536 if (!rng || rng.item) { 23537 return false; 23538 } 23539 if (rng.compareEndPoints) { 23540 return rng.compareEndPoints('StartToEnd', rng) === 0; 23541 } 23542 return !sel || rng.collapsed; 23543 }; 23544 /** 23545 * Checks if the current selection’s start and end containers are editable within their parent’s contexts. 23546 * 23547 * @method isEditable 23548 * @return {Boolean} Will be true if the selection is editable and false if it's not editable. 23549 */ 23550 const isEditable = () => { 23551 if (editor.mode.isReadOnly()) { 23552 return false; 23553 } 23554 const rng = getRng$1(); 23555 const fakeSelectedElements = editor.getBody().querySelectorAll('[data-mce-selected="1"]'); 23556 if (fakeSelectedElements.length > 0) { 23557 return forall(fakeSelectedElements, (el) => dom.isEditable(el.parentElement)); 23558 } 23559 else { 23560 return isEditableRange(dom, rng); 23561 } 23562 }; 23563 /** 23564 * Collapse the selection to start or end of range. 23565 * 23566 * @method collapse 23567 * @param {Boolean} toStart Optional boolean state if to collapse to end or not. Defaults to false. 23568 */ 23569 const collapse = (toStart) => { 23570 const rng = getRng$1(); 23571 rng.collapse(!!toStart); 23572 setRng(rng); 23573 }; 23574 /** 23575 * Returns the browsers internal selection object. 23576 * 23577 * @method getSel 23578 * @return {Selection} Internal browser selection object. 23579 */ 23580 const getSel = () => win.getSelection ? win.getSelection() : win.document.selection; 23581 /** 23582 * Returns the browsers internal range object. 23583 * 23584 * @method getRng 23585 * @return {Range} Internal browser range object. 23586 * @see http://www.quirksmode.org/dom/range_intro.html 23587 * @see http://www.dotvoid.com/2001/03/using-the-range-object-in-mozilla/ 23588 */ 23589 const getRng$1 = () => { 23590 let rng; 23591 const tryCompareBoundaryPoints = (how, sourceRange, destinationRange) => { 23592 try { 23593 return sourceRange.compareBoundaryPoints(how, destinationRange); 23594 } 23595 catch { 23596 // Gecko throws wrong document exception if the range points 23597 // to nodes that where removed from the dom #6690 23598 // Browsers should mutate existing DOMRange instances so that they always point 23599 // to something in the document this is not the case in Gecko works fine in IE/WebKit/Blink 23600 // For performance reasons just return -1 23601 return -1; 23602 } 23603 }; 23604 const doc = win.document; 23605 if (isNonNullable(editor.bookmark) && !hasFocus(editor)) { 23606 const bookmark = getRng(editor); 23607 if (bookmark.isSome()) { 23608 return bookmark.map((r) => processRanges(editor, [r])[0]).getOr(doc.createRange()); 23609 } 23610 } 23611 try { 23612 const selection = getSel(); 23613 if (selection && !isRestrictedNode(selection.anchorNode)) { 23614 if (selection.rangeCount > 0) { 23615 rng = selection.getRangeAt(0); 23616 } 23617 else { 23618 rng = doc.createRange(); 23619 } 23620 rng = processRanges(editor, [rng])[0]; 23621 } 23622 } 23623 catch { 23624 // IE throws unspecified error here if TinyMCE is placed in a frame/iframe 23625 } 23626 // No range found then create an empty one 23627 // This can occur when the editor is placed in a hidden container element on Gecko 23628 if (!rng) { 23629 rng = doc.createRange(); 23630 } 23631 // If range is at start of document then move it to start of body 23632 if (isDocument$1(rng.startContainer) && rng.collapsed) { 23633 const elm = dom.getRoot(); 23634 rng.setStart(elm, 0); 23635 rng.setEnd(elm, 0); 23636 } 23637 if (selectedRange && explicitRange) { 23638 if (tryCompareBoundaryPoints(rng.START_TO_START, rng, selectedRange) === 0 && 23639 tryCompareBoundaryPoints(rng.END_TO_END, rng, selectedRange) === 0) { 23640 // Safari, Opera and Chrome only ever select text which causes the range to change. 23641 // This lets us use the originally set range if the selection hasn't been changed by the user. 23642 rng = explicitRange; 23643 } 23644 else { 23645 selectedRange = null; 23646 explicitRange = null; 23647 } 23648 } 23649 return rng; 23650 }; 23651 /** 23652 * Changes the selection to the specified DOM range. 23653 * 23654 * @method setRng 23655 * @param {Range} rng Range to select. 23656 * @param {Boolean} forward Optional boolean if the selection is forwards or backwards. 23657 */ 23658 const setRng = (rng, forward) => { 23659 if (!isValidRange(rng)) { 23660 return; 23661 } 23662 const sel = getSel(); 23663 const evt = editor.dispatch('SetSelectionRange', { range: rng, forward }); 23664 rng = evt.range; 23665 if (sel) { 23666 explicitRange = rng; 23667 try { 23668 sel.removeAllRanges(); 23669 sel.addRange(rng); 23670 } 23671 catch { 23672 // IE might throw errors here if the editor is within a hidden container and selection is changed 23673 } 23674 // Forward is set to false and we have an extend function 23675 if (forward === false && sel.extend) { 23676 sel.collapse(rng.endContainer, rng.endOffset); 23677 sel.extend(rng.startContainer, rng.startOffset); 23678 } 23679 // adding range isn't always successful so we need to check range count otherwise an exception can occur 23680 selectedRange = sel.rangeCount > 0 ? sel.getRangeAt(0) : null; 23681 } 23682 // WebKit edge case selecting images works better using setBaseAndExtent when the image is floated 23683 if (!rng.collapsed && rng.startContainer === rng.endContainer && sel?.setBaseAndExtent) { 23684 if (rng.endOffset - rng.startOffset < 2) { 23685 if (rng.startContainer.hasChildNodes()) { 23686 const node = rng.startContainer.childNodes[rng.startOffset]; 23687 if (node && node.nodeName === 'IMG') { 23688 sel.setBaseAndExtent(rng.startContainer, rng.startOffset, rng.endContainer, rng.endOffset); 23689 // Since the setBaseAndExtent is fixed in more recent Blink versions we 23690 // need to detect if it's doing the wrong thing and falling back to the 23691 // crazy incorrect behavior api call since that seems to be the only way 23692 // to get it to work on Safari WebKit as of 2017-02-23 23693 if (sel.anchorNode !== rng.startContainer || sel.focusNode !== rng.endContainer) { 23694 sel.setBaseAndExtent(node, 0, node, 1); 23695 } 23696 } 23697 } 23698 } 23699 } 23700 editor.dispatch('AfterSetSelectionRange', { range: rng, forward }); 23701 }; 23702 /** 23703 * Sets the current selection to the specified DOM element. 23704 * 23705 * @method setNode 23706 * @param {Element} elm Element to set as the contents of the selection. 23707 * @return {Element} Returns the element that got passed in. 23708 * @example 23709 * // Inserts a DOM node at current selection/caret location 23710 * tinymce.activeEditor.selection.setNode(tinymce.activeEditor.dom.create('img', { src: 'some.gif', title: 'some title' })); 23711 */ 23712 const setNode = (elm) => { 23713 setContentInternal$1(editor, dom.getOuterHTML(elm)); 23714 return elm; 23715 }; 23716 /** 23717 * Returns the currently selected element or the common ancestor element for both start and end of the selection. 23718 * 23719 * @method getNode 23720 * @return {Element} Currently selected element or common ancestor element. 23721 * @example 23722 * // Alerts the currently selected elements node name 23723 * alert(tinymce.activeEditor.selection.getNode().nodeName); 23724 */ 23725 const getNode$1 = () => getNode(editor.getBody(), getRng$1()); 23726 const getSelectedBlocks$1 = (startElm, endElm) => getSelectedBlocks(dom, getRng$1(), startElm, endElm); 23727 const isForward = () => { 23728 const sel = getSel(); 23729 const anchorNode = sel?.anchorNode; 23730 const focusNode = sel?.focusNode; 23731 // No support for selection direction then always return true 23732 if (!sel || !anchorNode || !focusNode || isRestrictedNode(anchorNode) || isRestrictedNode(focusNode)) { 23733 return true; 23734 } 23735 const anchorRange = dom.createRng(); 23736 const focusRange = dom.createRng(); 23737 try { 23738 anchorRange.setStart(anchorNode, sel.anchorOffset); 23739 anchorRange.collapse(true); 23740 focusRange.setStart(focusNode, sel.focusOffset); 23741 focusRange.collapse(true); 23742 } 23743 catch { 23744 // Safari can generate an invalid selection and error. Silently handle it and default to forward. 23745 // See https://bugs.webkit.org/show_bug.cgi?id=230594. 23746 return true; 23747 } 23748 return anchorRange.compareBoundaryPoints(anchorRange.START_TO_START, focusRange) <= 0; 23749 }; 23750 const normalize = () => { 23751 const rng = getRng$1(); 23752 const sel = getSel(); 23753 if (!hasMultipleRanges(sel) && hasAnyRanges(editor)) { 23754 const normRng = normalize$2(dom, rng); 23755 normRng.each((normRng) => { 23756 setRng(normRng, isForward()); 23757 }); 23758 return normRng.getOr(rng); 23759 } 23760 return rng; 23761 }; 23762 /** 23763 * Executes callback when the current selection starts/stops matching the specified selector. The current 23764 * state will be passed to the callback as it's first argument. 23765 * 23766 * @method selectorChanged 23767 * @param {String} selector CSS selector to check for. 23768 * @param {Function} callback Callback with state and args when the selector is matches or not. 23769 */ 23770 // eslint-disable-next-line @typescript-eslint/no-wrapper-object-types 23771 const selectorChanged = (selector, callback) => { 23772 selectorChangedWithUnbind(selector, callback); 23773 return exports; 23774 }; 23775 const getScrollContainer = () => { 23776 let scrollContainer; 23777 let node = dom.getRoot(); 23778 while (node && node.nodeName !== 'BODY') { 23779 if (node.scrollHeight > node.clientHeight) { 23780 scrollContainer = node; 23781 break; 23782 } 23783 node = node.parentNode; 23784 } 23785 return scrollContainer; 23786 }; 23787 const scrollIntoView = (elm, alignToTop) => { 23788 if (isNonNullable(elm)) { 23789 scrollElementIntoView(editor, elm, alignToTop); 23790 } 23791 else { 23792 scrollRangeIntoView(editor, getRng$1(), alignToTop); 23793 } 23794 }; 23795 const placeCaretAt = (clientX, clientY) => setRng(fromPoint(clientX, clientY, editor.getDoc())); 23796 const getBoundingClientRect = () => { 23797 const rng = getRng$1(); 23798 return rng.collapsed ? CaretPosition.fromRangeStart(rng).getClientRects()[0] : rng.getBoundingClientRect(); 23799 }; 23800 const destroy = () => { 23801 win = selectedRange = explicitRange = null; 23802 controlSelection.destroy(); 23803 }; 23804 /** 23805 * Expands the selection range to contain the entire word when the selection is collapsed within the word. 23806 * 23807 * @method expand 23808 * @param {Object} options Optional options provided to the expansion. Defaults to { type: 'word' } 23809 */ 23810 const expand = (options = { type: 'word' }) => setRng(RangeUtils(dom).expand(getRng$1(), options)); 23811 const exports = { 23812 dom, 23813 win, 23814 serializer, 23815 editor, 23816 expand, 23817 collapse, 23818 setCursorLocation, 23819 getContent, 23820 setContent, 23821 getBookmark, 23822 moveToBookmark, 23823 select: select$1, 23824 isCollapsed, 23825 isEditable, 23826 isForward, 23827 setNode, 23828 getNode: getNode$1, 23829 getSel, 23830 setRng, 23831 getRng: getRng$1, 23832 getStart: getStart$1, 23833 getEnd: getEnd$1, 23834 getSelectedBlocks: getSelectedBlocks$1, 23835 normalize, 23836 selectorChanged, 23837 selectorChangedWithUnbind, 23838 getScrollContainer, 23839 scrollIntoView, 23840 placeCaretAt, 23841 getBoundingClientRect, 23842 destroy 23843 }; 23844 const bookmarkManager = BookmarkManager(exports); 23845 const controlSelection = ControlSelection(exports, editor); 23846 exports.bookmarkManager = bookmarkManager; 23847 exports.controlSelection = controlSelection; 23848 return exports; 23849 }; 23850 23851 const addNodeFilter = (settings, htmlParser, schema) => { 23852 htmlParser.addNodeFilter('br', (nodes, _, args) => { 23853 const blockElements = Tools.extend({}, schema.getBlockElements()); 23854 const nonEmptyElements = schema.getNonEmptyElements(); 23855 const whitespaceElements = schema.getWhitespaceElements(); 23856 // Remove brs from body element as well 23857 blockElements.body = 1; 23858 const isBlock = (node) => node.name in blockElements || isTransparentAstBlock(schema, node); 23859 // Must loop forwards since it will otherwise remove all brs in <p>a<br><br><br></p> 23860 for (let i = 0, l = nodes.length; i < l; i++) { 23861 let node = nodes[i]; 23862 let parent = node.parent; 23863 if (parent && isBlock(parent) && node === parent.lastChild) { 23864 // Loop all nodes to the left of the current node and check for other BR elements 23865 // excluding bookmarks since they are invisible 23866 let prev = node.prev; 23867 while (prev) { 23868 const prevName = prev.name; 23869 // Ignore bookmarks 23870 if (prevName !== 'span' || prev.attr('data-mce-type') !== 'bookmark') { 23871 // Found another br it's a <br><br> structure then don't remove anything 23872 if (prevName === 'br') { 23873 node = null; 23874 } 23875 break; 23876 } 23877 prev = prev.prev; 23878 } 23879 if (node) { 23880 node.remove(); 23881 // Is the parent to be considered empty after we removed the BR 23882 if (isEmpty$2(schema, nonEmptyElements, whitespaceElements, parent)) { 23883 const elementRule = schema.getElementRule(parent.name); 23884 // Remove or padd the element depending on schema rule 23885 if (elementRule) { 23886 if (elementRule.removeEmpty) { 23887 parent.remove(); 23888 } 23889 else if (elementRule.paddEmpty) { 23890 paddEmptyNode(settings, args, isBlock, parent); 23891 } 23892 } 23893 } 23894 } 23895 } 23896 else { 23897 // Replaces BR elements inside inline elements like <p><b><i><br></i></b></p> 23898 // so they become <p><b><i> </i></b></p> 23899 let lastParent = node; 23900 while (parent && parent.firstChild === lastParent && parent.lastChild === lastParent) { 23901 lastParent = parent; 23902 if (blockElements[parent.name]) { 23903 break; 23904 } 23905 parent = parent.parent; 23906 } 23907 if (lastParent === parent) { 23908 const textNode = new AstNode('#text', 3); 23909 textNode.value = nbsp; 23910 node.replace(textNode); 23911 } 23912 } 23913 } 23914 }); 23915 }; 23916 23917 const register$3 = (htmlParser, settings, dom) => { 23918 // Convert tabindex back to elements when serializing contents 23919 htmlParser.addAttributeFilter('data-mce-tabindex', (nodes, name) => { 23920 let i = nodes.length; 23921 while (i--) { 23922 const node = nodes[i]; 23923 node.attr('tabindex', node.attr('data-mce-tabindex')); 23924 node.attr(name, null); 23925 } 23926 }); 23927 // Convert move data-mce-src, data-mce-href and data-mce-style into nodes or process them if needed 23928 htmlParser.addAttributeFilter('src,href,style', (nodes, name) => { 23929 const internalName = 'data-mce-' + name; 23930 const urlConverter = settings.url_converter; 23931 const urlConverterScope = settings.url_converter_scope; 23932 let i = nodes.length; 23933 while (i--) { 23934 const node = nodes[i]; 23935 let value = node.attr(internalName); 23936 if (value !== undefined) { 23937 // Set external name to internal value and remove internal 23938 node.attr(name, value.length > 0 ? value : null); 23939 node.attr(internalName, null); 23940 } 23941 else { 23942 // No internal attribute found then convert the value we have in the DOM 23943 value = node.attr(name); 23944 if (name === 'style') { 23945 value = dom.serializeStyle(dom.parseStyle(value), node.name); 23946 } 23947 else if (urlConverter) { 23948 value = urlConverter.call(urlConverterScope, value, name, node.name); 23949 } 23950 node.attr(name, value.length > 0 ? value : null); 23951 } 23952 } 23953 }); 23954 // Remove internal classes mceItem<..> or mceSelected 23955 htmlParser.addAttributeFilter('class', (nodes) => { 23956 let i = nodes.length; 23957 while (i--) { 23958 const node = nodes[i]; 23959 let value = node.attr('class'); 23960 if (value) { 23961 value = value.replace(/(?:^|\s)mce-item-\w+(?!\S)/g, ''); 23962 node.attr('class', value.length > 0 ? value : null); 23963 } 23964 } 23965 }); 23966 // Remove bookmark elements 23967 htmlParser.addAttributeFilter('data-mce-type', (nodes, name, args) => { 23968 let i = nodes.length; 23969 while (i--) { 23970 const node = nodes[i]; 23971 if (node.attr('data-mce-type') === 'bookmark' && !args.cleanup) { 23972 // We maybe dealing with a "filled" bookmark. If so just remove the node, otherwise unwrap it 23973 const hasChildren = Optional.from(node.firstChild).exists((firstChild) => !isZwsp(firstChild.value ?? '')); 23974 if (hasChildren) { 23975 node.unwrap(); 23976 } 23977 else { 23978 node.remove(); 23979 } 23980 } 23981 } 23982 }); 23983 // Force script into CDATA sections and remove the mce- prefix also add comments around styles 23984 htmlParser.addNodeFilter('script,style', (nodes, name) => { 23985 const trim = (value) => { 23986 /* jshint maxlen:255 */ 23987 /* eslint max-len:0 */ 23988 return value.replace(/(<!--\[CDATA\[|\]\]-->)/g, '\n') 23989 .replace(/^[\r\n]*|[\r\n]*$/g, '') 23990 .replace(/^\s*((<!--)?(\s*\/\/)?\s*<!\[CDATA\[|(<!--\s*)?\/\*\s*<!\[CDATA\[\s*\*\/|(\/\/)?\s*<!--|\/\*\s*<!--\s*\*\/)\s*[\r\n]*/gi, '') 23991 .replace(/\s*(\/\*\s*\]\]>\s*\*\/(-->)?|\s*\/\/\s*\]\]>(-->)?|\/\/\s*(-->)?|\]\]>|\/\*\s*-->\s*\*\/|\s*-->\s*)\s*$/g, ''); 23992 }; 23993 let i = nodes.length; 23994 while (i--) { 23995 const node = nodes[i]; 23996 const firstChild = node.firstChild; 23997 const value = firstChild?.value ?? ''; 23998 if (name === 'script') { 23999 // Remove mce- prefix from script elements and remove default type since the user specified 24000 // a script element without type attribute 24001 const type = node.attr('type'); 24002 if (type) { 24003 node.attr('type', type === 'mce-no/type' ? null : type.replace(/^mce\-/, '')); 24004 } 24005 if (settings.element_format === 'xhtml' && firstChild && value.length > 0) { 24006 firstChild.value = '// <![CDATA[\n' + trim(value) + '\n// ]]>'; 24007 } 24008 } 24009 else { 24010 if (settings.element_format === 'xhtml' && firstChild && value.length > 0) { 24011 firstChild.value = '<!--\n' + trim(value) + '\n-->'; 24012 } 24013 } 24014 } 24015 }); 24016 // Convert comments to cdata
24016 and handle protected comments 24017 htmlParser.addNodeFilter('#comment', (nodes) => { 24018 let i = nodes.length; 24019 while (i--) { 24020 const node = nodes[i]; 24021 const value = node.value; 24022 if (settings.preserve_cdata && value?.indexOf('[CDATA[') === 0) { 24023 node.name = '#cdata'; 24024 node.type = 4; 24025 node.value = dom.decode(value.replace(/^\[CDATA\[|\]\]$/g, '')); 24026 } 24027 else if (value?.indexOf('mce:protected ') === 0) { 24028 node.name = '#text'; 24029 node.type = 3; 24030 node.raw = true; 24031 node.value = unescape(value).substr(14
vendor: 18,653 bytes, lines 24031-24479
24031); 24032 } 24033 } 24034 }); 24035 htmlParser.addNodeFilter('xml:namespace,input', (nodes, name) => { 24036 let i = nodes.length; 24037 while (i--) { 24038 const node = nodes[i]; 24039 if (node.type === 7) { 24040 node.remove(); 24041 } 24042 else if (node.type === 1) { 24043 if (name === 'input' && !node.attr('type')) { 24044 node.attr('type', 'text'); 24045 } 24046 } 24047 } 24048 }); 24049 htmlParser.addAttributeFilter('data-mce-type', (nodes) => { 24050 each$e(nodes, (node) => { 24051 if (node.attr('data-mce-type') === 'format-caret') { 24052 if (node.isEmpty(htmlParser.schema.getNonEmptyElements())) { 24053 node.remove(); 24054 } 24055 else { 24056 node.unwrap(); 24057 } 24058 } 24059 }); 24060 }); 24061 // Remove internal data attributes 24062 htmlParser.addAttributeFilter('data-mce-src,data-mce-href,data-mce-style,' + 24063 'data-mce-selected,data-mce-expando,data-mce-block,' + 24064 'data-mce-type,data-mce-resize,data-mce-placeholder', (nodes, name) => { 24065 let i = nodes.length; 24066 while (i--) { 24067 nodes[i].attr(name, null); 24068 } 24069 }); 24070 // Remove <br> at end of block elements Gecko and WebKit injects BR elements to 24071 // make it possible to place the caret inside empty blocks. This logic tries to remove 24072 // these elements and keep br elements that where intended to be there intact 24073 if (settings.remove_trailing_brs) { 24074 addNodeFilter(settings, htmlParser, htmlParser.schema); 24075 } 24076 }; 24077 /** 24078 * IE 11 has a fantastic bug where it will produce two trailing BR elements to iframe bodies when 24079 * the iframe is hidden by display: none on a parent container. The DOM is actually out of sync 24080 * with innerHTML in this case. It's like IE adds shadow DOM BR elements that appears on innerHTML 24081 * but not as the lastChild of the body. So this fix simply removes the last two 24082 * BR elements at the end of the document. 24083 * 24084 * Example of what happens: <body>text</body> becomes <body>text<br><br></body> 24085 */ 24086 const trimTrailingBr = (rootNode) => { 24087 const isBr = (node) => { 24088 return node?.name === 'br'; 24089 }; 24090 const brNode1 = rootNode.lastChild; 24091 if (isBr(brNode1)) { 24092 const brNode2 = brNode1.prev; 24093 if (isBr(brNode2)) { 24094 brNode1.remove(); 24095 brNode2.remove(); 24096 } 24097 } 24098 }; 24099 24100 const preProcess$1 = (editor, node, args) => { 24101 let oldDoc; 24102 const dom = editor.dom; 24103 let clonedNode = node.cloneNode(true); 24104 // Nodes needs to be attached to something in WebKit/Opera 24105 // This fix will make DOM ranges and make Sizzle happy! 24106 const impl = document.implementation; 24107 if (impl.createHTMLDocument) { 24108 // Create an empty HTML document 24109 const doc = impl.createHTMLDocument(''); 24110 // Add the element or it's children if it's a body element to the new document 24111 Tools.each(clonedNode.nodeName === 'BODY' ? clonedNode.childNodes : [clonedNode], (node) => { 24112 doc.body.appendChild(doc.importNode(node, true)); 24113 }); 24114 // Grab first child or body element for serialization 24115 if (clonedNode.nodeName !== 'BODY') { 24116 // We cast to a Element here, as this will be the cloned node imported and appended above 24117 clonedNode = doc.body.firstChild; 24118 } 24119 else { 24120 clonedNode = doc.body; 24121 } 24122 // set the new document in DOMUtils so createElement etc works 24123 oldDoc = dom.doc; 24124 dom.doc = doc; 24125 } 24126 firePreProcess(editor, { ...args, node: clonedNode }); 24127 if (oldDoc) { 24128 dom.doc = oldDoc; 24129 } 24130 return clonedNode; 24131 }; 24132 const shouldFireEvent = (editor, args) => { 24133 return isNonNullable(editor) && editor.hasEventListeners('PreProcess') && !args.no_events; 24134 }; 24135 const process$1 = (editor, node, args) => { 24136 return shouldFireEvent(editor, args) ? preProcess$1(editor, node, args) : node; 24137 }; 24138 24139 const addTempAttr = (htmlParser, tempAttrs, name) => { 24140 if (Tools.inArray(tempAttrs, name) === -1) { 24141 htmlParser.addAttributeFilter(name, (nodes, name) => { 24142 let i = nodes.length; 24143 while (i--) { 24144 nodes[i].attr(name, null); 24145 } 24146 }); 24147 tempAttrs.push(name); 24148 } 24149 }; 24150 const postProcess = (editor, args, content) => { 24151 if (!args.no_events && editor) { 24152 const outArgs = firePostProcess(editor, { ...args, content }); 24153 return outArgs.content; 24154 } 24155 else { 24156 return content; 24157 } 24158 }; 24159 const getHtmlFromNode = (dom, node, args) => { 24160 // TODO: Investigate if using `innerHTML` is correct as DomSerializerPreProcess definitely returns a Node 24161 const html = trim$2(args.getInner ? node.innerHTML : dom.getOuterHTML(node)); 24162 return args.selection || isWsPreserveElement(SugarElement.fromDom(node)) ? html : Tools.trim(html); 24163 }; 24164 const parseHtml = (htmlParser, html, args) => { 24165 const parserArgs = args.selection ? { forced_root_block: false, ...args } : args; 24166 const rootNode = htmlParser.parse(html, parserArgs); 24167 trimTrailingBr(rootNode); 24168 return rootNode; 24169 }; 24170 const serializeNode = (settings, schema, node) => { 24171 const htmlSerializer = HtmlSerializer(settings, schema); 24172 return htmlSerializer.serialize(node); 24173 }; 24174 const toHtml = (editor, settings, schema, rootNode, args) => { 24175 const content = serializeNode(settings, schema, rootNode); 24176 return postProcess(editor, args, content); 24177 }; 24178 const DomSerializerImpl = (settings, editor) => { 24179 const tempAttrs = ['data-mce-selected']; 24180 const defaultedSettings = { 24181 entity_encoding: 'named', 24182 remove_trailing_brs: true, 24183 pad_empty_with_br: false, 24184 ...settings 24185 }; 24186 const dom = editor && editor.dom ? editor.dom : DOMUtils.DOM; 24187 const schema = editor && editor.schema ? editor.schema : Schema(defaultedSettings); 24188 const htmlParser = DomParser(defaultedSettings, schema); 24189 register$3(htmlParser, defaultedSettings, dom); 24190 const serialize = (node, domSerializerArgs = {}) => { 24191 const { indent, entity_encoding, ...rest } = domSerializerArgs; 24192 const parserArgs = { format: 'html', ...rest }; 24193 const targetNode = process$1(editor, node, parserArgs); 24194 const html = getHtmlFromNode(dom, targetNode, parserArgs); 24195 const rootNode = parseHtml(htmlParser, html, parserArgs); 24196 if (parserArgs.format === 'tree') { 24197 return rootNode; 24198 } 24199 const serializerSettings = { 24200 ...defaultedSettings, 24201 ...(isNonNullable(indent) ? { indent } : {}), 24202 ...(isNonNullable(entity_encoding) ? { entity_encoding } : {}), 24203 }; 24204 return toHtml(editor, serializerSettings, schema, rootNode, parserArgs); 24205 }; 24206 return { 24207 schema, 24208 addNodeFilter: htmlParser.addNodeFilter, 24209 addAttributeFilter: htmlParser.addAttributeFilter, 24210 serialize: serialize, 24211 addRules: schema.addValidElements, 24212 setRules: schema.setValidElements, 24213 addTempAttr: curry(addTempAttr, htmlParser, tempAttrs), 24214 getTempAttrs: constant(tempAttrs), 24215 getNodeFilters: htmlParser.getNodeFilters, 24216 getAttributeFilters: htmlParser.getAttributeFilters, 24217 removeNodeFilter: htmlParser.removeNodeFilter, 24218 removeAttributeFilter: htmlParser.removeAttributeFilter 24219 }; 24220 }; 24221 24222 /** 24223 * This class is used to serialize DOM trees into a string. Consult the TinyMCE API Documentation for 24224 * more details and examples on how to use this class. 24225 * 24226 * @class tinymce.dom.Serializer 24227 */ 24228 const DomSerializer = (settings, editor) => { 24229 const domSerializer = DomSerializerImpl(settings, editor); 24230 // Return public methods 24231 return { 24232 /** 24233 * Schema instance that was used to when the Serializer was constructed. 24234 * 24235 * @field {tinymce.html.Schema} schema 24236 */ 24237 schema: domSerializer.schema, 24238 /** 24239 * Adds a node filter function to the parser used by the serializer, the parser will collect the specified nodes by name 24240 * and then execute the callback once it has finished parsing the document. 24241 * 24242 * @method addNodeFilter 24243 * @param {String} name Comma separated list of nodes to collect. 24244 * @param {Function} callback Callback function to execute once it has collected nodes. 24245 * @example 24246 * serializer.addNodeFilter('p,h1', (nodes, name) => { 24247 * for (let i = 0; i < nodes.length; i++) { 24248 * console.log(nodes[i].name); 24249 * } 24250 * }); 24251 */ 24252 addNodeFilter: domSerializer.addNodeFilter, 24253 /** 24254 * Adds an attribute filter function to the parser used by the serializer, the parser will 24255 * collect nodes that has the specified attributes 24256 * and then execute the callback once it has finished parsing the document. 24257 * 24258 * @method addAttributeFilter 24259 * @param {String} name Comma separated list of attributes to collect. 24260 * @param {Function} callback Callback function to execute once it has collected nodes. 24261 * @example 24262 * serializer.addAttributeFilter('src,href', (nodes, name) => { 24263 * for (let i = 0; i < nodes.length; i++) { 24264 * console.log(nodes[i].name); 24265 * } 24266 * }); 24267 */ 24268 addAttributeFilter: domSerializer.addAttributeFilter, 24269 /** 24270 * Serializes the specified browser DOM node into a HTML string. 24271 * 24272 * @method serialize 24273 * @param {DOMNode} node DOM node to serialize. 24274 * @param {Object} args Arguments option that gets passed to event handlers. 24275 */ 24276 serialize: domSerializer.serialize, 24277 /** 24278 * Adds valid elements rules to the serializers schema instance this enables you to specify things 24279 * like what elements should be outputted and what attributes specific elements might have. 24280 * Consult the TinyMCE Documentation for more details on this format. 24281 * 24282 * @method addRules 24283 * @param {String} rules Valid elements rules string to add to schema. 24284 */ 24285 addRules: domSerializer.addRules, 24286 /** 24287 * Sets the valid elements rules to the serializers schema instance this enables you to specify things 24288 * like what elements should be outputted and what attributes specific elements might have. 24289 * Consult the TinyMCE Documentation for more details on this format. 24290 * 24291 * @method setRules 24292 * @param {String} rules Valid elements rules string. 24293 */ 24294 setRules: domSerializer.setRules, 24295 /** 24296 * Adds a temporary internal attribute these attributes will get removed on undo and 24297 * when getting contents out of the editor. 24298 * 24299 * @method addTempAttr 24300 * @param {String} name string 24301 */ 24302 addTempAttr: domSerializer.addTempAttr, 24303 /** 24304 * Returns an array of all added temp attrs names. 24305 * 24306 * @method getTempAttrs 24307 * @return {String[]} Array with attribute names. 24308 */ 24309 getTempAttrs: domSerializer.getTempAttrs, 24310 getNodeFilters: domSerializer.getNodeFilters, 24311 getAttributeFilters: domSerializer.getAttributeFilters, 24312 /** 24313 * Removes a node filter function or removes all filter functions from the parser used by the serializer for the node names provided. 24314 * 24315 * @method removeNodeFilter 24316 * @param {String} name Comma separated list of node names to remove filters for. 24317 * @param {Function} callback Optional callback function to only remove a specific callback. 24318 * @example 24319 * // Remove a single filter 24320 * serializer.removeNodeFilter('p,h1', someCallback); 24321 * 24322 * // Remove all filters 24323 * serializer.removeNodeFilter('p,h1'); 24324 */ 24325 removeNodeFilter: domSerializer.removeNodeFilter, 24326 /** 24327 * Removes an attribute filter function or removes all filter functions from the parser used by the serializer for the attribute names provided. 24328 * 24329 * @method removeAttributeFilter 24330 * @param {String} name Comma separated list of attribute names to remove filters for. 24331 * @param {Function} callback Optional callback function to only remove a specific callback. 24332 * @example 24333 * // Remove a single filter 24334 * serializer.removeAttributeFilter('src,href', someCallback); 24335 * 24336 * // Remove all filters 24337 * serializer.removeAttributeFilter('src,href'); 24338 */ 24339 removeAttributeFilter: domSerializer.removeAttributeFilter 24340 }; 24341 }; 24342 24343 const defaultFormat$1 = 'html'; 24344 const setupArgs$1 = (args, format) => ({ 24345 ...args, 24346 format, 24347 get: true, 24348 getInner: true 24349 }); 24350 const getContent = (editor, args = {}) => { 24351 const format = args.format ? args.format : defaultFormat$1; 24352 const defaultedArgs = setupArgs$1(args, format); 24353 return preProcessGetContent(editor, defaultedArgs).fold(identity, (updatedArgs) => { 24354 const content = getContent$2(editor, updatedArgs); 24355 return postProcessGetContent(editor, content, updatedArgs); 24356 }); 24357 }; 24358 24359 const defaultFormat = 'html'; 24360 const setupArgs = (args, content) => ({ 24361 format: defaultFormat, 24362 ...args, 24363 set: true, 24364 content 24365 }); 24366 const setContent = (editor, content, args = {}) => { 24367 const defaultedArgs = setupArgs(args, content); 24368 preProcessSetContent(editor, defaultedArgs).each((updatedArgs) => { 24369 const result = setContent$1(editor, updatedArgs.content, updatedArgs); 24370 postProcessSetContent(editor, result.html, updatedArgs); 24371 }); 24372 }; 24373 24374 const DOM$a = DOMUtils.DOM; 24375 const restoreOriginalStyles = (editor) => { 24376 DOM$a.setStyle(editor.id, 'display', editor.orgDisplay); 24377 }; 24378 const safeDestroy = (x) => Optional.from(x).each((x) => x.destroy()); 24379 const clearDomReferences = (editor) => { 24380 const ed = editor; 24381 ed.contentAreaContainer = ed.formElement = ed.container = ed.editorContainer = null; 24382 ed.bodyElement = ed.contentDocument = ed.contentWindow = null; 24383 ed.iframeElement = ed.targetElm = null; 24384 const selection = editor.selection; 24385 if (selection) { 24386 const dom = selection.dom; 24387 ed.selection = selection.win = selection.dom = dom.doc = null; 24388 } 24389 }; 24390 const restoreForm = (editor) => { 24391 const form = editor.formElement; 24392 if (form) { 24393 if (form._mceOldSubmit) { 24394 form.submit = form._mceOldSubmit; 24395 delete form._mceOldSubmit; 24396 } 24397 DOM$a.unbind(form, 'submit reset', editor.formEventDelegate); 24398 } 24399 }; 24400 const remove$1 = (editor) => { 24401 if (!editor.removed) { 24402 const { _selectionOverrides, editorUpload } = editor; 24403 const body = editor.getBody(); 24404 const element = editor.getElement(); 24405 if (body) { 24406 editor.save({ is_removing: true }); 24407 } 24408 editor.removed = true; 24409 editor.unbindAllNativeEvents(); 24410 // Remove any hidden input 24411 if (editor.hasHiddenInput && isNonNullable(element?.nextSibling)) { 24412 DOM$a.remove(element.nextSibling); 24413 } 24414 fireRemove(editor); 24415 editor.editorManager.remove(editor); 24416 if (!editor.inline && body) { 24417 restoreOriginalStyles(editor); 24418 } 24419 fireDetach(editor); 24420 DOM$a.remove(editor.getContainer()); 24421 safeDestroy(_selectionOverrides); 24422 safeDestroy(editorUpload); 24423 editor.destroy(); 24424 } 24425 }; 24426 const destroy = (editor, automatic) => { 24427 const { selection, dom } = editor; 24428 if (editor.destroyed) { 24429 return; 24430 } 24431 // If user manually calls destroy and not remove 24432 // Users seems to have logic that calls destroy instead of remove 24433 if (!automatic && !editor.removed) { 24434 editor.remove(); 24435 return; 24436 } 24437 if (!automatic) { 24438 editor.editorManager.off('beforeunload', editor._beforeUnload); 24439 // Manual destroy 24440 if (editor.theme && editor.theme.destroy) { 24441 editor.theme.destroy(); 24442 } 24443 safeDestroy(selection); 24444 safeDestroy(dom); 24445 } 24446 restoreForm(editor); 24447 clearDomReferences(editor); 24448 editor.destroyed = true; 24449 }; 24450 24451 const CreateIconManager = () => { 24452 const lookup = {}; 24453 const add = (id, iconPack) => { 24454 lookup[id] = iconPack; 24455 }; 24456 const get = (id) => { 24457 if (lookup[id]) { 24458 return lookup[id]; 24459 } 24460 else { 24461 return { icons: {} }; 24462 } 24463 }; 24464 const has = (id) => has$2(lookup, id); 24465 return { 24466 add, 24467 get, 24468 has 24469 }; 24470 }; 24471 const IconManager = CreateIconManager(); 24472 24473 const ModelManager = AddOnManager.ModelManager; 24474 24475 const getProp = (propName, elm) => { 24476 const rawElm = elm.dom; 24477 return rawElm[propName]; 24478 }; 24479 const getComputedSizeProp = (propName, elm) => parseInt(get$
244797
vendor: 25,461 bytes, lines 24479-25098
24479(elm, propName), 10); 24480 const getClientWidth = curry(getProp, 'clientWidth'); 24481 const getClientHeight = curry(getProp, 'clientHeight'); 24482 const getMarginTop = curry(getComputedSizeProp, 'margin-top'); 24483 const getMarginLeft = curry(getComputedSizeProp, 'margin-left'); 24484 const getBoundingClientRect = (elm) => elm.dom.getBoundingClientRect(); 24485 const isInsideElementContentArea = (bodyElm, clientX, clientY) => { 24486 const clientWidth = getClientWidth(bodyElm); 24487 const clientHeight = getClientHeight(bodyElm); 24488 return clientX >= 0 && clientY >= 0 && clientX <= clientWidth && clientY <= clientHeight; 24489 }; 24490 const transpose = (inline, elm, clientX, clientY) => { 24491 const clientRect = getBoundingClientRect(elm); 24492 const deltaX = inline ? clientRect.left + elm.dom.clientLeft + getMarginLeft(elm) : 0; 24493 const deltaY = inline ? clientRect.top + elm.dom.clientTop + getMarginTop(elm) : 0; 24494 const x = clientX - deltaX; 24495 const y = clientY - deltaY; 24496 return { x, y }; 24497 }; 24498 // Checks if the specified coordinate is within the visual content area excluding the scrollbars 24499 const isXYInContentArea = (editor, clientX, clientY) => { 24500 const bodyElm = SugarElement.fromDom(editor.getBody()); 24501 const targetElm = editor.inline ? bodyElm : documentElement(bodyElm); 24502 const transposedPoint = transpose(editor.inline, targetElm, clientX, clientY); 24503 return isInsideElementContentArea(targetElm, transposedPoint.x, transposedPoint.y); 24504 }; 24505 const fromDomSafe = (node) => Optional.from(node).map(SugarElement.fromDom); 24506 const isEditorAttachedToDom = (editor) => { 24507 const rawContainer = editor.inline ? editor.getBody() : editor.getContentAreaContainer(); 24508 return fromDomSafe(rawContainer).map(inBody).getOr(false); 24509 }; 24510 24511 var NotificationManagerImpl = () => { 24512 const unimplemented = () => { 24513 throw new Error('Theme did not provide a NotificationManager implementation.'); 24514 }; 24515 return { 24516 open: unimplemented, 24517 close: unimplemented, 24518 getArgs: unimplemented 24519 }; 24520 }; 24521 24522 /** 24523 * This class handles the creation of TinyMCE's notifications. 24524 * 24525 * @class tinymce.NotificationManager 24526 * @example 24527 * // Opens a new notification of type "error" with text "An error occurred." 24528 * tinymce.activeEditor.notificationManager.open({ 24529 * text: 'An error occurred.', 24530 * type: 'error' 24531 * }); 24532 */ 24533 const NotificationManager = (editor) => { 24534 const notifications = []; 24535 const getImplementation = () => { 24536 const theme = editor.theme; 24537 return theme && theme.getNotificationManagerImpl ? theme.getNotificationManagerImpl() : NotificationManagerImpl(); 24538 }; 24539 const getTopNotification = () => { 24540 return Optional.from(notifications[0]); 24541 }; 24542 const isEqual = (a, b) => { 24543 return a.type === b.type && a.text === b.text && !a.progressBar && !a.timeout && !b.progressBar && !b.timeout; 24544 }; 24545 const reposition = () => { 24546 getTopNotification().each((notification) => { 24547 notification.reposition(); 24548 }); 24549 }; 24550 const addNotification = (notification) => { 24551 notifications.push(notification); 24552 }; 24553 const closeNotification = (notification) => { 24554 findIndex$2(notifications, (otherNotification) => { 24555 return otherNotification === notification; 24556 }).each((index) => { 24557 // Mutate here since third party might have stored away the window array 24558 // TODO: Consider breaking this api 24559 notifications.splice(index, 1); 24560 }); 24561 }; 24562 const open = (spec, fireEvent = true) => { 24563 // Never open notification if editor has been removed. 24564 if (editor.removed || !isEditorAttachedToDom(editor)) { 24565 return {}; 24566 } 24567 // fire event to allow notification spec to be mutated before display 24568 if (fireEvent) { 24569 editor.dispatch('BeforeOpenNotification', { notification: spec }); 24570 } 24571 return find$2(notifications, (notification) => { 24572 return isEqual(getImplementation().getArgs(notification), spec); 24573 }).getOrThunk(() => { 24574 editor.editorManager.setActive(editor); 24575 const notification = getImplementation().open(spec, () => { 24576 closeNotification(notification); 24577 }, () => hasEditorOrUiFocus(editor)); 24578 addNotification(notification); 24579 reposition(); 24580 // Ensure notification is not passed by reference to prevent mutation 24581 editor.dispatch('OpenNotification', { notification: { ...notification } }); 24582 return notification; 24583 }); 24584 }; 24585 const close = () => { 24586 getTopNotification().each((notification) => { 24587 getImplementation().close(notification); 24588 closeNotification(notification); 24589 reposition(); 24590 }); 24591 }; 24592 const getNotifications = constant(notifications); 24593 const registerEvents = (editor) => { 24594 editor.on('SkinLoaded', () => { 24595 const serviceMessage = getServiceMessage(editor); 24596 if (serviceMessage) { 24597 // Ensure we pass false for fireEvent so that service message cannot be altered. 24598 open({ 24599 text: serviceMessage, 24600 type: 'warning', 24601 timeout: 0 24602 }, false); 24603 } 24604 // Ensure the notifications are repositioned once the skin has loaded, as otherwise 24605 // any notifications rendered before then may have wrapped and been in the wrong place 24606 reposition(); 24607 }); 24608 // NodeChange is needed for inline mode and autoresize as the positioning is done 24609 // from the bottom up, which changes when the content in the editor changes. 24610 editor.on('show ResizeEditor ResizeWindow NodeChange ToggleView FullscreenStateChanged', () => { 24611 requestAnimationFrame(reposition); 24612 }); 24613 editor.on('remove', () => { 24614 each$e(notifications.slice(), (notification) => { 24615 getImplementation().close(notification); 24616 }); 24617 }); 24618 editor.on('keydown', (e) => { 24619 // TODO: TINY-11429 Remove this once we remove the use of keycodes 24620 const isF12 = e.key?.toLowerCase() === 'f12' || e.keyCode === 123; 24621 if (e.altKey && isF12) { 24622 e.preventDefault(); 24623 getTopNotification() 24624 .map((notificationApi) => SugarElement.fromDom(notificationApi.getEl())) 24625 .each((elm) => focus$1(elm)); 24626 } 24627 }); 24628 }; 24629 registerEvents(editor); 24630 return { 24631 /** 24632 * Opens a new notification. 24633 * 24634 * @method open 24635 * @param {Object} args A <code>name: value</code> collection containing settings such as: <code>timeout</code>, <code>type</code>, and message (<code>text</code>). 24636 * <br /><br /> 24637 * For information on the available settings, see: <a href="https://www.tiny.cloud/docs/tinymce/8/creating-custom-notifications/">Create custom notifications</a>. 24638 */ 24639 open, 24640 /** 24641 * Closes the top most notification. 24642 * 24643 * @method close 24644 */ 24645 close, 24646 /** 24647 * Returns the currently opened notification objects. 24648 * 24649 * @method getNotifications 24650 * @return {Array} Array of the currently opened notifications. 24651 */ 24652 getNotifications 24653 }; 24654 }; 24655 24656 const PluginManager = AddOnManager.PluginManager; 24657 24658 const ThemeManager = AddOnManager.ThemeManager; 24659 24660 var WindowManagerImpl = () => { 24661 const unimplemented = () => { 24662 throw new Error('Theme did not provide a WindowManager implementation.'); 24663 }; 24664 return { 24665 open: unimplemented, 24666 openUrl: unimplemented, 24667 alert: unimplemented, 24668 confirm: unimplemented, 24669 close: unimplemented 24670 }; 24671 }; 24672 24673 const WindowManager = (editor) => { 24674 let dialogs = []; 24675 const getImplementation = () => { 24676 const theme = editor.theme; 24677 return theme && theme.getWindowManagerImpl ? theme.getWindowManagerImpl() : WindowManagerImpl(); 24678 }; 24679 const funcBind = (scope, f) => { 24680 return (...args) => { 24681 return f ? f.apply(scope, args) : undefined; 24682 }; 24683 }; 24684 const fireOpenEvent = (dialog) => { 24685 editor.dispatch('OpenWindow', { 24686 dialog 24687 }); 24688 }; 24689 const fireCloseEvent = (dialog) => { 24690 editor.dispatch('CloseWindow', { 24691 dialog 24692 }); 24693 }; 24694 const addDialog = (dialog, triggerElement) => { 24695 dialogs.push({ instanceApi: dialog, triggerElement }); 24696 fireOpenEvent(dialog); 24697 }; 24698 const closeDialog = (dialog) => { 24699 fireCloseEvent(dialog); 24700 const dialogTriggerElement = findMap(dialogs, ({ instanceApi, triggerElement }) => instanceApi === dialog ? triggerElement : Optional.none()); 24701 dialogs = filter$5(dialogs, ({ instanceApi }) => instanceApi !== dialog); 24702 // Move focus back to editor when the last window is closed 24703 if (dialogs.length === 0) { 24704 editor.focus(); 24705 } 24706 else { 24707 // Move focus to the element that was active before the dialog was opened 24708 dialogTriggerElement.filter(inBody).each(focus$1); 24709 } 24710 }; 24711 const getTopDialog = () => { 24712 return Optional.from(dialogs[dialogs.length - 1]); 24713 }; 24714 const storeSelectionAndOpenDialog = (openDialog) => { 24715 editor.editorManager.setActive(editor); 24716 store(editor); 24717 const activeEl = active(); 24718 editor.ui.show(); 24719 const dialog = openDialog(); 24720 addDialog(dialog, activeEl); 24721 return dialog; 24722 }; 24723 const open = (args, params) => { 24724 return storeSelectionAndOpenDialog(() => getImplementation().open(args, params, closeDialog)); 24725 }; 24726 const openUrl = (args) => { 24727 return storeSelectionAndOpenDialog(() => getImplementation().openUrl(args, closeDialog)); 24728 }; 24729 const restoreFocus = (activeEl) => { 24730 if (dialogs.length !== 0) { 24731 // If there are some dialogs, the confirm/alert was probably triggered from the dialog 24732 // Move focus to the element that was active before the confirm/alert was opened 24733 activeEl.each((el) => focus$1(el)); 24734 } 24735 }; 24736 const alert = (message, callback, scope) => { 24737 const activeEl = active(); 24738 const windowManagerImpl = getImplementation(); 24739 windowManagerImpl.alert(message, funcBind(scope ? scope : windowManagerImpl, () => { 24740 restoreFocus(activeEl); 24741 callback?.(); 24742 })); 24743 }; 24744 const confirm = (message, callback, scope) => { 24745 const activeEl = active(); 24746 const windowManagerImpl = getImplementation(); 24747 windowManagerImpl.confirm(message, funcBind(scope ? scope : windowManagerImpl, (state) => { 24748 restoreFocus(activeEl); 24749 callback?.(state); 24750 })); 24751 }; 24752 const close = () => { 24753 getTopDialog().each(({ instanceApi: dialog }) => { 24754 getImplementation().close(dialog); 24755 closeDialog(dialog); 24756 }); 24757 }; 24758 editor.on('remove', () => { 24759 each$e(dialogs, ({ instanceApi: dialog }) => { 24760 getImplementation().close(dialog); 24761 }); 24762 }); 24763 return { 24764 /** 24765 * Opens a new window. 24766 * 24767 * @method open 24768 * @param {Object} config For information on the available options, see: <a href="https://www.tiny.cloud/docs/tinymce/8/dialog-configuration/#options">Dialog - Configuration options</a>. 24769 * @param {Object} params (Optional) For information on the available options, see: <a href="https://www.tiny.cloud/docs/tinymce/8/dialog-configuration/#configuration-parameters">Dialog - Configuration parameters</a>. 24770 * @returns {WindowManager.DialogInstanceApi} A new dialog instance. 24771 */ 24772 open, 24773 /** 24774 * Opens a new window for the specified url. 24775 * 24776 * @method openUrl 24777 * @param {Object} config For information on the available options, see: <a href="https://www.tiny.cloud/docs/tinymce/8/urldialog/#configuration">URL dialog - Configuration</a>. 24778 * @returns {WindowManager.UrlDialogInstanceApi} A new URL dialog instance. 24779 */ 24780 openUrl, 24781 /** 24782 * Creates an alert dialog. Do not use the blocking behavior of this 24783 * native version. Use the callback method instead; then it can be extended. 24784 * 24785 * @method alert 24786 * @param {String} message Text to display in the new alert dialog. 24787 * @param {Function} callback (Optional) Callback function to be executed after the user has selected ok. 24788 * @param {Object} scope (Optional) Scope to execute the callback in. 24789 * @example 24790 * // Displays an alert box using the active editors window manager instance 24791 * tinymce.activeEditor.windowManager.alert('Hello world!'); 24792 */ 24793 alert, 24794 /** 24795 * Creates an alert dialog. Do not use the blocking behavior of this 24796 * native version. Use the callback method instead; then it can be extended. 24797 * 24798 * @method confirm 24799 * @param {String} message Text to display in the new confirm dialog. 24800 * @param {Function} callback (Optional) Callback function to be executed after the user has selected ok or cancel. 24801 * @param {Object} scope (Optional) Scope to execute the callback in. 24802 * @example 24803 * // Displays a confirm box and an alert message will be displayed depending on what you choose in the confirm 24804 * tinymce.activeEditor.windowManager.confirm('Do you want to do something?', (state) => { 24805 * const message = state ? 'Ok' : 'Cancel'; 24806 * tinymce.activeEditor.windowManager.alert(message); 24807 * }); 24808 */ 24809 confirm, 24810 /** 24811 * Closes the top most window. 24812 * 24813 * @method close 24814 */ 24815 close 24816 }; 24817 }; 24818 24819 const displayNotification$1 = (editor, message) => { 24820 editor.notificationManager.open({ 24821 type: 'error', 24822 text: message 24823 }); 24824 }; 24825 const displayError = (editor, message) => { 24826 if (editor._skinLoaded) { 24827 displayNotification$1(editor, message); 24828 } 24829 else { 24830 editor.on('SkinLoaded', () => { 24831 displayNotification$1(editor, message); 24832 }); 24833 } 24834 }; 24835 const uploadError = (editor, message) => { 24836 displayError(editor, I18n.translate(['Failed to upload image: {0}', message])); 24837 }; 24838 const logError = (editor, errorType, msg) => { 24839 fireError(editor, errorType, { message: msg }); 24840 // eslint-disable-next-line no-console 24841 console.error(msg); 24842 }; 24843 const createLoadError = (type, url, name) => name ? 24844 `Failed to load ${type}: ${name} from url ${url}` : 24845 `Failed to load ${type} url: ${url}`; 24846 const pluginLoadError = (editor, url, name) => { 24847 logError(editor, 'PluginLoadError', createLoadError('plugin', url, name)); 24848 }; 24849 const iconsLoadError = (editor, url, name) => { 24850 logError(editor, 'IconsLoadError', createLoadError('icons', url, name)); 24851 }; 24852 const languageLoadError = (editor, url, name) => { 24853 logError(editor, 'LanguageLoadError', createLoadError('language', url, name)); 24854 }; 24855 const themeLoadError = (editor, url, name) => { 24856 logError(editor, 'ThemeLoadError', createLoadError('theme', url, name)); 24857 }; 24858 const modelLoadError = (editor, url, name) => { 24859 logError(editor, 'ModelLoadError', createLoadError('model', url, name)); 24860 }; 24861 const licenseKeyManagerLoadError = (editor, url) => { 24862 logError(editor, 'LicenseKeyManagerLoadError', createLoadError('license key manager', url)); 24863 }; 24864 const componentLoadError = (editor, url) => { 24865 logError(editor, 'ComponentLoadError', createLoadError('component', url)); 24866 }; 24867 const pluginInitError = (editor, name, err) => { 24868 const message = I18n.translate(['Failed to initialize plugin: {0}', name]); 24869 fireError(editor, 'PluginLoadError', { message }); 24870 initError(message, err); 24871 displayError(editor, message); 24872 }; 24873 const initError = (message, ...x) => { 24874 const console = window.console; 24875 if (console) { // Skip test env 24876 if (console.error) { 24877 console.error(message, ...x); 24878 } 24879 else { 24880 console.log(message, ...x); 24881 } 24882 } 24883 }; 24884 24885 // Map to track which editors have already been processed and disabled 24886 const processedEditors = new WeakMap(); 24887 const forceDisable = (editor) => { 24888 // Check if we've already disabled the editor 24889 if (processedEditors.has(editor)) { 24890 return; 24891 } 24892 // Mark this editor as processed 24893 processedEditors.set(editor, true); 24894 const switchModeListener = () => { 24895 editor.on('SwitchMode', (e) => { 24896 const { mode } = e; 24897 if (mode !== 'readonly') { 24898 editor.mode.set('readonly'); 24899 } 24900 }); 24901 }; 24902 const disabledStateChangeListener = () => { 24903 editor.on('DisabledStateChange', (e) => { 24904 const { state } = e; 24905 if (!state) { 24906 e.preventDefault(); 24907 } 24908 }, true); 24909 }; 24910 if (editor.initialized) { 24911 // Set readonly before setting disabled as disabling editor prevents mode from being changed 24912 if (!editor.removed) { 24913 editor.mode.set('readonly'); 24914 } 24915 editor.options.set('disabled', true); 24916 } 24917 else { 24918 editor.on('init', () => { 24919 // Set readonly before setting disabled as disabling editor prevents mode from being changed 24920 if (!editor.removed) { 24921 editor.mode.set('readonly'); 24922 } 24923 editor.options.set('disabled', true); 24924 }); 24925 } 24926 disabledStateChangeListener(); 24927 switchModeListener(); 24928 }; 24929 24930 /* eslint-disable no-console */ 24931 const displayNotification = (editor, messageData) => { 24932 const { type, message } = messageData; 24933 editor.notificationManager.open({ 24934 type, 24935 text: message 24936 }); 24937 }; 24938 const getConsoleFn = (type) => { 24939 switch (type) { 24940 case 'error': 24941 return console.error; 24942 case 'info': 24943 return console.info; 24944 case 'warn': 24945 return console.warn; 24946 case 'log': 24947 default: 24948 return console.log; 24949 } 24950 }; 24951 const displayConsoleMessage = (messageData) => { 24952 const consoleFn = getConsoleFn(messageData.type); 24953 consoleFn(messageData.message); 24954 }; 24955 const reportMessage = (editor, message) => { 24956 const { console, editor: editorUi } = message; 24957 if (isNonNullable(editorUi)) { 24958 if (editor._skinLoaded) { 24959 displayNotification(editor, editorUi); 24960 } 24961 else { 24962 editor.on('SkinLoaded', () => { 24963 displayNotification(editor, editorUi); 24964 }); 24965 } 24966 } 24967 if (isNonNullable(console)) { 24968 displayConsoleMessage(console); 24969 } 24970 }; 24971 24972 const DOCS_URL = 'https://www.tiny.cloud/docs/tinymce/latest/license-key/'; 24973 const DOCS_URL_MESSAGE = `Read more: ${DOCS_URL}`; 24974 const PROVIDE_LICENSE_KEY_MESSAGE = `Make sure to provide a valid license key or add license_key: 'gpl' to the init config to agree to the open source license terms.`; 24975 const reportNoKeyError = (editor) => { 24976 const baseMessage = 'The editor is disabled because a TinyMCE license key has not been provided.'; 24977 reportMessage(editor, { 24978 console: { 24979 type: 'error', 24980 message: [ 24981 `${baseMessage}`, 24982 PROVIDE_LICENSE_KEY_MESSAGE, 24983 DOCS_URL_MESSAGE 24984 ].join(' ') 24985 }, 24986 editor: { 24987 type: 'warning', 24988 message: `${baseMessage}` 24989 } 24990 }); 24991 }; 24992 const reportLoadError = (editor, onlineStatus) => { 24993 const key = `${onlineStatus === 'online' ? 'API' : 'license'} key`; 24994 const baseMessage = `The editor is disabled because the TinyMCE ${key} could not be validated.`; 24995 reportMessage(editor, { 24996 console: { 24997 type: 'error', 24998 message: [ 24999 `${baseMessage}`, 25000 `The TinyMCE Commercial License Key Manager plugin is required for the provided ${key} to be validated but could not be loaded.`, 25001 DOCS_URL_MESSAGE 25002 ].join(' ') 25003 }, 25004 editor: { 25005 type: 'warning', 25006 message: `${baseMessage}` 25007 } 25008 }); 25009 }; 25010 const reportInvalidPlugin = (editor, pluginCode, hasShownPluginNotification) => { 25011 const baseMessage = `The "${pluginCode}" plugin requires a valid TinyMCE license key.`; 25012 reportMessage(editor, { 25013 console: { 25014 type: 'error', 25015 message: [ 25016 `${baseMessage}`, 25017 DOCS_URL_MESSAGE 25018 ].join(' ') 25019 }, 25020 ...hasShownPluginNotification ? {} : { 25021 editor: { 25022 type: 'warning', 25023 message: `One or more premium plugins are disabled due to license key restrictions.` 25024 } 25025 } 25026 }); 25027 }; 25028 25029 const PLUGIN_CODE$1 = 'licensekeymanager'; 25030 const getOnlineStatus = (editor) => { 25031 const hasApiKey = isString(getApiKey(editor)); 25032 return hasApiKey ? 'online' : 'offline'; 25033 }; 25034 const getLicenseKeyType = (editor) => { 25035 const licenseKey = getLicenseKey(editor)?.toLowerCase(); 25036 if (licenseKey === 'gpl') { 25037 return 'gpl'; 25038 } 25039 else if (isNullable(licenseKey)) { 25040 return 'no_key'; 25041 } 25042 else { 25043 return 'non_gpl'; 25044 } 25045 }; 25046 const determineStrategy = (editor) => { 25047 const onlineStatus = getOnlineStatus(editor); 25048 const licenseKeyType = getLicenseKeyType(editor); 25049 const forcePlugin = new Set([ 25050 ...getPlugins(editor), 25051 ...keys(getExternalPlugins$1(editor)), 25052 ]).has(PLUGIN_CODE$1); 25053 if (licenseKeyType !== 'gpl' || onlineStatus === 'online' || forcePlugin) { 25054 return { 25055 type: 'use_plugin', 25056 onlineStatus, 25057 licenseKeyType, 25058 forcePlugin 25059 }; 25060 } 25061 else { 25062 return { 25063 type: 'use_gpl', 25064 onlineStatus, 25065 licenseKeyType, 25066 forcePlugin 25067 }; 25068 } 25069 }; 25070 25071 const createFallbackLicenseKeyManager = (canValidate) => (editor) => { 25072 let hasShownPluginNotification = false; 25073 return { 25074 validate: (data) => { 25075 const { plugin } = data; 25076 const hasPlugin = isString(plugin); 25077 // Premium plugins are not allowed 25078 if (hasPlugin) { 25079 reportInvalidPlugin(editor, plugin, hasShownPluginNotification); 25080 hasShownPluginNotification = true; 25081 } 25082 return Promise.resolve(canValidate && !hasPlugin); 25083 }, 25084 }; 25085 }; 25086 const NoLicenseKeyManager = createFallbackLicenseKeyManager(false); 25087 const GplLicenseKeyManager = createFallbackLicenseKeyManager(true); 25088 const ADDON_KEY = 'manager'; 25089 const PLUGIN_CODE = PLUGIN_CODE$1; 25090 const setup$y = () => { 25091 const addOnManager = AddOnManager(); 25092 const add = (addOn) => { 25093 addOnManager.add(ADDON_KEY, addOn); 25094 }; 25095 const load = (editor, suffix) => { 25096 const strategy = determineStrategy(editor); 25097 if (strategy.type === 'use_plugin') { 25098 const externalUrl = get$
vendor: 7,945 bytes, lines 25098-25290
25098a(getExternalPlugins$1(editor), PLUGIN_CODE).map(trim$4).filter(isNotEmpty); 25099 const url = externalUrl.getOr(`plugins/${PLUGIN_CODE}/plugin${suffix}.js`); 25100 addOnManager.load(ADDON_KEY, url).catch(() => { 25101 licenseKeyManagerLoadError(editor, url); 25102 }); 25103 } 25104 }; 25105 const init = (editor) => { 25106 const setLicenseKeyManager = (licenseKeyManager) => { 25107 Object.defineProperty(editor, 'licenseKeyManager', { 25108 value: licenseKeyManager, 25109 writable: false, 25110 configurable: false, 25111 enumerable: true, 25112 }); 25113 }; 25114 const strategy = determineStrategy(editor); 25115 const LicenseKeyManager = addOnManager.get(ADDON_KEY); 25116 // Use plugin if it is already loaded as it can handle all license key types 25117 if (isNonNullable(LicenseKeyManager)) { 25118 const licenseKeyManagerApi = LicenseKeyManager(editor, addOnManager.urls[ADDON_KEY]); 25119 setLicenseKeyManager(licenseKeyManagerApi); 25120 } 25121 else { 25122 switch (strategy.type) { 25123 case 'use_gpl': { 25124 setLicenseKeyManager(GplLicenseKeyManager(editor)); 25125 break; 25126 } 25127 case 'use_plugin': { 25128 // We know the plugin hasn't loaded and it is required 25129 forceDisable(editor); 25130 setLicenseKeyManager(NoLicenseKeyManager(editor)); 25131 if (strategy.onlineStatus === 'offline' && strategy.licenseKeyType === 'no_key') { 25132 reportNoKeyError(editor); 25133 } 25134 else { 25135 reportLoadError(editor, strategy.onlineStatus); 25136 } 25137 break; 25138 } 25139 } 25140 } 25141 // Validation of the license key is done asynchronously and does 25142 // not block initialization of the editor 25143 // The validate function is expected to set the editor to the correct 25144 // state depending on if the license key is valid or not 25145 // eslint-disable-next-line @typescript-eslint/no-floating-promises 25146 editor.licenseKeyManager.validate({}); 25147 }; 25148 return { 25149 load, 25150 add, 25151 init 25152 }; 25153 }; 25154 const LicenseKeyManagerLoader = setup$y(); 25155 25156 const removeFakeSelection = (editor) => { 25157 Optional.from(editor.selection.getNode()).each((elm) => { 25158 elm.removeAttribute('data-mce-selected'); 25159 }); 25160 }; 25161 const setEditorCommandState = (editor, cmd, state) => { 25162 try { 25163 // execCommand needs a string for the value, so convert the boolean to a string 25164 // See: https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand#Parameters 25165 editor.getDoc().execCommand(cmd, false, String(state)); 25166 } 25167 catch { 25168 // Ignore 25169 } 25170 }; 25171 const setCommonEditorCommands = (editor, state) => { 25172 setEditorCommandState(editor, 'StyleWithCSS', state); 25173 setEditorCommandState(editor, 'enableInlineTableEditing', state); 25174 setEditorCommandState(editor, 'enableObjectResizing', state); 25175 }; 25176 const restoreFakeSelection = (editor) => { 25177 editor.selection.setRng(editor.selection.getRng()); 25178 }; 25179 // Not quite sugar Class.toggle, it's more of a Class.set 25180 const toggleClass = (elm, cls, state) => { 25181 if (has(elm, cls) && !state) { 25182 remove$4(elm, cls); 25183 } 25184 else if (state) { 25185 add$2(elm, cls); 25186 } 25187 }; 25188 const disableEditor = (editor) => { 25189 const body = SugarElement.fromDom(editor.getBody()); 25190 toggleClass(body, 'mce-content-readonly', true); 25191 editor.selection.controlSelection.hideResizeRect(); 25192 editor._selectionOverrides.hideFakeCaret(); 25193 removeFakeSelection(editor); 25194 }; 25195 const enableEditor = (editor) => { 25196 const body = SugarElement.fromDom(editor.getBody()); 25197 toggleClass(body, 'mce-content-readonly', false); 25198 if (editor.hasEditableRoot()) { 25199 set(body, true); 25200 } 25201 setCommonEditorCommands(editor, false); 25202 if (hasEditorOrUiFocus(editor)) { 25203 editor.focus(); 25204 } 25205 restoreFakeSelection(editor); 25206 editor.nodeChanged(); 25207 }; 25208 25209 const isDisabled = (editor) => isDisabled$1(editor); 25210 const internalContentEditableAttr = 'data-mce-contenteditable'; 25211 const switchOffContentEditableTrue = (elm) => { 25212 each$e(descendants(elm, '*[contenteditable="true"]'), (elm) => { 25213 set$4(elm, internalContentEditableAttr, 'true'); 25214 set(elm, false); 25215 }); 25216 }; 25217 const switchOnContentEditableTrue = (elm) => { 25218 each$e(descendants(elm, `*[${internalContentEditableAttr}="true"]`), (elm) => { 25219 remove$9(elm, internalContentEditableAttr); 25220 set(elm, true); 25221 }); 25222 }; 25223 const toggleDisabled = (editor, state) => { 25224 const body = SugarElement.fromDom(editor.getBody()); 25225 if (state) { 25226 disableEditor(editor); 25227 set(body, false); 25228 switchOffContentEditableTrue(body); 25229 } 25230 else { 25231 switchOnContentEditableTrue(body); 25232 enableEditor(editor); 25233 } 25234 }; 25235 const registerDisabledContentFilters = (editor) => { 25236 if (editor.serializer) { 25237 registerFilters(editor); 25238 } 25239 else { 25240 editor.on('PreInit', () => { 25241 registerFilters(editor); 25242 }); 25243 } 25244 }; 25245 const registerFilters = (editor) => { 25246 editor.parser.addAttributeFilter('contenteditable', (nodes) => { 25247 if (isDisabled(editor)) { 25248 each$e(nodes, (node) => { 25249 node.attr(internalContentEditableAttr, node.attr('contenteditable')); 25250 node.attr('contenteditable', 'false'); 25251 }); 25252 } 25253 }); 25254 editor.serializer.addAttributeFilter(internalContentEditableAttr, (nodes) => { 25255 if (isDisabled(editor)) { 25256 each$e(nodes, (node) => { 25257 node.attr('contenteditable', node.attr(internalContentEditableAttr)); 25258 }); 25259 } 25260 }); 25261 editor.serializer.addTempAttr(internalContentEditableAttr); 25262 }; 25263 const isClickEvent = (e) => e.type === 'click'; 25264 const allowedEvents = ['copy']; 25265 const isAllowedEventInDisabledMode = (e) => contains$2(allowedEvents, e.type); 25266 const getAnchorHrefOpt = (editor, elm) => { 25267 const isRoot = (elm) => eq(elm, SugarElement.fromDom(editor.getBody())); 25268 return closest$4(elm, 'a', isRoot).bind((a) => getOpt(a, 'href')); 25269 }; 25270 const hasAccordion = (editor, elm) => { 25271 const isRoot = (elm) => eq(elm, SugarElement.fromDom(editor.getBody())); 25272 return closest$2(elm, 'details', isRoot); 25273 }; 25274 const processDisabledEvents = (editor, e) => { 25275 /* 25276 If an event is a click event on or within an anchor, and the CMD/CTRL key is 25277 not held, then we want to prevent default behaviour and either: 25278 a) scroll to the relevant bookmark 25279 b) open the link using default browser behaviour 25280 */ 25281 if (isClickEvent(e) && !VK.metaKeyPressed(e)) { 25282 const elm = SugarElement.fromDom(e.target); 25283 getAnchorHrefOpt(editor, elm).fold(() => { 25284 if (hasAccordion(editor, elm)) { 25285 e.preventDefault(); 25286 } 25287 }, (href) => { 25288 e.preventDefault(); 25289 if (/^#/.test(href)) { 25290 const targetEl = editor.dom.select(`${href},[name="${removeLeading(href, '#')
vendor: 29,264 bytes, lines 25290-26000
25290}"]`); 25291 if (targetEl.length) { 25292 editor.selection.scrollIntoView(targetEl[0], true); 25293 } 25294 } 25295 else { 25296 window.open(href, '_blank', 'rel=noopener noreferrer,menubar=yes,toolbar=yes,location=yes,status=yes,resizable=yes,scrollbars=yes'); 25297 } 25298 }); 25299 } 25300 else if (isAllowedEventInDisabledMode(e)) { 25301 editor.dispatch(e.type, e); 25302 } 25303 }; 25304 const registerDisabledModeEventHandlers = (editor) => { 25305 editor.on('ShowCaret ObjectSelected', (e) => { 25306 if (isDisabled(editor)) { 25307 e.preventDefault(); 25308 } 25309 }); 25310 // Preprend to the handlers as this should be the first to fire 25311 editor.on('DisabledStateChange', (e) => { 25312 if (!e.isDefaultPrevented()) { 25313 toggleDisabled(editor, e.state); 25314 } 25315 }); 25316 }; 25317 const registerEventsAndFilters$1 = (editor) => { 25318 registerDisabledContentFilters(editor); 25319 registerDisabledModeEventHandlers(editor); 25320 }; 25321 25322 const isContentCssSkinName = (url) => /^[a-z0-9\-]+$/i.test(url); 25323 const toContentSkinResourceName = (name) => 'content/' + name + '/content.css'; 25324 const isBundledCssSkinName = (name) => tinymce.Resource.has(toContentSkinResourceName(name)); 25325 const getContentCssUrls = (editor) => { 25326 return transformToUrls(editor, getContentCss(editor)); 25327 }; 25328 const getFontCssUrls = (editor) => { 25329 return transformToUrls(editor, getFontCss(editor)); 25330 }; 25331 const transformToUrls = (editor, cssLinks) => { 25332 const skinUrl = editor.editorManager.baseURL + '/skins/content'; 25333 const suffix = editor.editorManager.suffix; 25334 const contentCssFile = `content${suffix}.css`; 25335 return map$3(cssLinks, (url) => { 25336 if (isBundledCssSkinName(url)) { 25337 return toContentSkinResourceName(url); 25338 } 25339 else if (isContentCssSkinName(url) && !editor.inline) { 25340 return `${skinUrl}/${url}/${contentCssFile}`; 25341 } 25342 else { 25343 return editor.documentBaseURI.toAbsolute(url); 25344 } 25345 }); 25346 }; 25347 const appendContentCssFromSettings = (editor) => { 25348 editor.contentCSS = editor.contentCSS.concat(getContentCssUrls(editor), getFontCssUrls(editor)); 25349 }; 25350 25351 /** 25352 * Finds images with data uris or blob uris. If data uris are found it will convert them into blob uris. 25353 * 25354 * @private 25355 * @class tinymce.file.ImageScanner 25356 */ 25357 const getAllImages = (elm) => { 25358 return elm ? from(elm.getElementsByTagName('img')) : []; 25359 }; 25360 const ImageScanner = (uploadStatus, blobCache) => { 25361 const cachedPromises = {}; 25362 const findAll = (elm, predicate = always) => { 25363 const images = filter$5(getAllImages(elm), (img) => { 25364 const src = img.src; 25365 if (img.hasAttribute('data-mce-bogus')) { 25366 return false; 25367 } 25368 if (img.hasAttribute('data-mce-placeholder')) { 25369 return false; 25370 } 25371 if (!src || src === Env.transparentSrc) { 25372 return false; 25373 } 25374 if (startsWith(src, 'blob:')) { 25375 return !uploadStatus.isUploaded(src) && predicate(img); 25376 } 25377 if (startsWith(src, 'data:')) { 25378 return predicate(img); 25379 } 25380 return false; 25381 }); 25382 const promises = map$3(images, (img) => { 25383 const imageSrc = img.src; 25384 if (has$2(cachedPromises, imageSrc)) { 25385 // Since the cached promise will return the cached image 25386 // We need to wrap it and resolve with the actual image 25387 return cachedPromises[imageSrc].then((imageInfo) => { 25388 if (isString(imageInfo)) { // error apparently 25389 return imageInfo; 25390 } 25391 else { 25392 return { 25393 image: img, 25394 blobInfo: imageInfo.blobInfo 25395 }; 25396 } 25397 }); 25398 } 25399 else { 25400 const newPromise = imageToBlobInfo(blobCache, imageSrc) 25401 .then((blobInfo) => { 25402 delete cachedPromises[imageSrc]; 25403 return { image: img, blobInfo }; 25404 }).catch((error) => { 25405 delete cachedPromises[imageSrc]; 25406 return error; 25407 }); 25408 cachedPromises[imageSrc] = newPromise; 25409 return newPromise; 25410 } 25411 }); 25412 return Promise.all(promises); 25413 }; 25414 return { 25415 findAll 25416 }; 25417 }; 25418 25419 /** 25420 * Holds the current status of a blob uri, if it's pending or uploaded and what the result urls was. 25421 * 25422 * @private 25423 * @class tinymce.file.UploadStatus 25424 */ 25425 const UploadStatus = () => { 25426 const PENDING = 1, UPLOADED = 2; 25427 let blobUriStatuses = {}; 25428 const createStatus = (status, resultUri) => { 25429 return { 25430 status, 25431 resultUri 25432 }; 25433 }; 25434 const hasBlobUri = (blobUri) => { 25435 return blobUri in blobUriStatuses; 25436 }; 25437 const getResultUri = (blobUri) => { 25438 const result = blobUriStatuses[blobUri]; 25439 return result ? result.resultUri : null; 25440 }; 25441 const isPending = (blobUri) => { 25442 return hasBlobUri(blobUri) ? blobUriStatuses[blobUri].status === PENDING : false; 25443 }; 25444 const isUploaded = (blobUri) => { 25445 return hasBlobUri(blobUri) ? blobUriStatuses[blobUri].status === UPLOADED : false; 25446 }; 25447 const markPending = (blobUri) => { 25448 blobUriStatuses[blobUri] = createStatus(PENDING, null); 25449 }; 25450 const markUploaded = (blobUri, resultUri) => { 25451 blobUriStatuses[blobUri] = createStatus(UPLOADED, resultUri); 25452 }; 25453 const removeFailed = (blobUri) => { 25454 delete blobUriStatuses[blobUri]; 25455 }; 25456 const destroy = () => { 25457 blobUriStatuses = {}; 25458 }; 25459 return { 25460 hasBlobUri, 25461 getResultUri, 25462 isPending, 25463 isUploaded, 25464 markPending, 25465 markUploaded, 25466 removeFailed, 25467 destroy 25468 }; 25469 }; 25470 25471 /** 25472 * Generates unique ids. 25473 * 25474 * @class tinymce.util.Uuid 25475 * @private 25476 */ 25477 let count = 0; 25478 const seed = () => { 25479 const rnd = () => { 25480 return Math.round(random() * 0xFFFFFFFF).toString(36); 25481 }; 25482 const now = new Date().getTime(); 25483 return 's' + now.toString(36) + rnd() + rnd() + rnd(); 25484 }; 25485 const uuid = (prefix) => { 25486 return prefix + (count++) + seed(); 25487 }; 25488 25489 const BlobCache = () => { 25490 let cache = []; 25491 const mimeToExt = (mime) => { 25492 const mimes = { 25493 'image/jpeg': 'jpg', 25494 'image/jpg': 'jpg', 25495 'image/gif': 'gif', 25496 'image/png': 'png', 25497 'image/apng': 'apng', 25498 'image/avif': 'avif', 25499 'image/svg+xml': 'svg', 25500 'image/webp': 'webp', 25501 'image/bmp': 'bmp', 25502 'image/tiff': 'tiff' 25503 }; 25504 return mimes[mime.toLowerCase()] || 'dat'; 25505 }; 25506 const create = (o, blob, base64, name, filename) => { 25507 if (isString(o)) { 25508 const id = o; 25509 return toBlobInfo({ 25510 id, 25511 name, 25512 filename, 25513 blob: blob, 25514 base64: base64 25515 }); 25516 } 25517 else if (isObject(o)) { 25518 return toBlobInfo(o); 25519 } 25520 else { 25521 throw new Error('Unknown input type'); 25522 } 25523 }; 25524 const toBlobInfo = (o) => { 25525 if (isNullable(o.blob) || isNullable(o.base64) || (o.base64 === '' && !o.allowEmptyFile)) { 25526 throw new Error('blob and base64 representations of the image are required for BlobInfo to be created'); 25527 } 25528 const id = o.id || uuid('blobid'); 25529 const name = o.name || id; 25530 const blob = o.blob; 25531 return { 25532 id: constant(id), 25533 name: constant(name), 25534 filename: constant(o.filename || name + '.' + mimeToExt(blob.type)), 25535 blob: constant(blob), 25536 base64: constant(o.base64), 25537 blobUri: constant(o.blobUri || URL.createObjectURL(blob)), 25538 uri: constant(o.uri) 25539 }; 25540 }; 25541 const add = (blobInfo) => { 25542 if (!get(blobInfo.id())) { 25543 cache.push(blobInfo); 25544 } 25545 }; 25546 const findFirst = (predicate) => find$2(cache, predicate).getOrUndefined(); 25547 const get = (id) => findFirst((cachedBlobInfo) => cachedBlobInfo.id() === id); 25548 const getByUri = (blobUri) => findFirst((blobInfo) => blobInfo.blobUri() === blobUri); 25549 const getByData = (base64, type) => findFirst((blobInfo) => blobInfo.base64() === base64 && blobInfo.blob().type === type); 25550 const removeByUri = (blobUri) => { 25551 cache = filter$5(cache, (blobInfo) => { 25552 if (blobInfo.blobUri() === blobUri) { 25553 URL.revokeObjectURL(blobInfo.blobUri()); 25554 return false; 25555 } 25556 return true; 25557 }); 25558 }; 25559 const destroy = () => { 25560 each$e(cache, (cachedBlobInfo) => { 25561 URL.revokeObjectURL(cachedBlobInfo.blobUri()); 25562 }); 25563 cache = []; 25564 }; 25565 return { 25566 create, 25567 add, 25568 get, 25569 getByUri, 25570 getByData, 25571 findFirst, 25572 removeByUri, 25573 destroy 25574 }; 25575 }; 25576 25577 const Uploader = (uploadStatus, settings) => { 25578 const pendingPromises = {}; 25579 const pathJoin = (path1, path2) => { 25580 if (path1) { 25581 return path1.replace(/\/$/, '') + '/' + path2.replace(/^\//, ''); 25582 } 25583 return path2; 25584 }; 25585 const defaultHandler = (blobInfo, progress) => new Promise((success, failure) => { 25586 const xhr = new XMLHttpRequest(); 25587 xhr.open('POST', settings.url); 25588 xhr.withCredentials = settings.credentials; 25589 xhr.upload.onprogress = (e) => { 25590 progress(e.loaded / e.total * 100); 25591 }; 25592 xhr.onerror = () => { 25593 failure('Image upload failed due to a XHR Transport error. Code: ' + xhr.status); 25594 }; 25595 xhr.onload = () => { 25596 if (xhr.status < 200 || xhr.status >= 300) { 25597 failure('HTTP Error: ' + xhr.status); 25598 return; 25599 } 25600 const json = JSON.parse(xhr.responseText); 25601 if (!json || !isString(json.location)) { 25602 failure('Invalid JSON: ' + xhr.responseText); 25603 return; 25604 } 25605 success(pathJoin(settings.basePath, json.location)); 25606 }; 25607 const formData = new FormData(); 25608 formData.append('file', blobInfo.blob(), blobInfo.filename()); 25609 xhr.send(formData); 25610 }); 25611 const uploadHandler = isFunction(settings.handler) ? settings.handler : defaultHandler; 25612 const noUpload = () => new Promise((resolve) => { 25613 resolve([]); 25614 }); 25615 const handlerSuccess = (blobInfo, url) => ({ 25616 url, 25617 blobInfo, 25618 status: true 25619 }); 25620 const handlerFailure = (blobInfo, error) => ({ 25621 url: '', 25622 blobInfo, 25623 status: false, 25624 error 25625 }); 25626 const resolvePending = (blobUri, result) => { 25627 Tools.each(pendingPromises[blobUri], (resolve) => { 25628 resolve(result); 25629 }); 25630 delete pendingPromises[blobUri]; 25631 }; 25632 const uploadBlobInfo = (blobInfo, handler, openNotification) => { 25633 uploadStatus.markPending(blobInfo.blobUri()); 25634 return new Promise((resolve) => { 25635 let notification; 25636 let progress; 25637 try { 25638 const closeNotification = () => { 25639 if (notification) { 25640 notification.close(); 25641 progress = noop; // Once it's closed it's closed 25642 } 25643 }; 25644 const success = (data) => { 25645 closeNotification(); 25646 const url = isString(data) ? data : data.url; 25647 uploadStatus.markUploaded(blobInfo.blobUri(), url); 25648 resolvePending(blobInfo.blobUri(), handlerSuccess(blobInfo, url)); 25649 resolve(handlerSuccess(blobInfo, url)); 25650 }; 25651 const failure = (error) => { 25652 closeNotification(); 25653 uploadStatus.removeFailed(blobInfo.blobUri()); 25654 resolvePending(blobInfo.blobUri(), handlerFailure(blobInfo, error)); 25655 resolve(handlerFailure(blobInfo, error)); 25656 }; 25657 progress = (percent) => { 25658 if (percent < 0 || percent > 100) { 25659 return; 25660 } 25661 Optional.from(notification) 25662 .orThunk(() => Optional.from(openNotification).map(apply$1)) 25663 .each((n) => { 25664 notification = n; 25665 n.progressBar.value(percent); 25666 }); 25667 }; 25668 handler(blobInfo, progress).then(success, (err) => { 25669 failure(isString(err) ? { message: err } : err); 25670 }); 25671 } 25672 catch (ex) { 25673 resolve(handlerFailure(blobInfo, ex)); 25674 } 25675 }); 25676 }; 25677 const isDefaultHandler = (handler) => handler === defaultHandler; 25678 const pendingUploadBlobInfo = (blobInfo) => { 25679 const blobUri = blobInfo.blobUri(); 25680 return new Promise((resolve) => { 25681 pendingPromises[blobUri] = pendingPromises[blobUri] || []; 25682 pendingPromises[blobUri].push(resolve); 25683 }); 25684 }; 25685 const uploadBlobs = (blobInfos, openNotification) => { 25686 blobInfos = Tools.grep(blobInfos, (blobInfo) => !uploadStatus.isUploaded(blobInfo.blobUri())); 25687 return Promise.all(Tools.map(blobInfos, (blobInfo) => uploadStatus.isPending(blobInfo.blobUri()) ? 25688 pendingUploadBlobInfo(blobInfo) : uploadBlobInfo(blobInfo, uploadHandler, openNotification))); 25689 }; 25690 const upload = (blobInfos, openNotification) => (!settings.url && isDefaultHandler(uploadHandler)) ? noUpload() : uploadBlobs(blobInfos, openNotification); 25691 return { 25692 upload 25693 }; 25694 }; 25695 25696 const openNotification = (editor) => () => editor.notificationManager.open({ 25697 text: editor.translate('Image uploading...'), 25698 type: 'info', 25699 timeout: -1, 25700 progressBar: true 25701 }); 25702 const createUploader = (editor, uploadStatus) => Uploader(uploadStatus, { 25703 url: getImageUploadUrl(editor), 25704 basePath: getImageUploadBasePath(editor), 25705 credentials: getImagesUploadCredentials(editor), 25706 handler: getImagesUploadHandler(editor) 25707 }); 25708 /** 25709 * This class handles uploading images to a back-end server. 25710 * 25711 * @class tinymce.util.ImageUploader 25712 */ 25713 const ImageUploader = (editor) => { 25714 const uploadStatus = UploadStatus(); 25715 const uploader = createUploader(editor, uploadStatus); 25716 return { 25717 /** 25718 * Uploads images to the configured image upload URL (`images_upload_url`) or passes the images to the defined image upload handler function (`images_upload_handler`). 25719 * 25720 * @method upload 25721 * @param {Array} blobInfos A BlobInfo array containing the image data to upload. A BlobInfo can be created by calling `editor.editorUpload.blobCache.create()`. 25722 * @param {Boolean} showNotification (Optional) When set to true, a notification with a progress bar will be shown during image uploads. 25723 */ 25724 upload: (blobInfos, showNotification = true) => uploader.upload(blobInfos, showNotification ? openNotification(editor) : undefined) 25725 }; 25726 }; 25727 25728 const isEmptyForPadding = (editor, element) => editor.dom.isEmpty(element.dom) && isNonNullable(editor.schema.getTextBlockElements()[name(element)]); 25729 const addPaddingToEmpty = (editor) => (element) => { 25730 if (isEmptyForPadding(editor, element)) { 25731 append$1(element, SugarElement.fromHtml('<br data-mce-bogus="1" />')); 25732 } 25733 }; 25734 const EditorUpload = (editor) => { 25735 const blobCache = BlobCache(); 25736 let uploader, imageScanner; 25737 const uploadStatus = UploadStatus(); 25738 const urlFilters = []; 25739 const aliveGuard = (callback) => { 25740 return (result) => { 25741 if (editor.selection) { 25742 return callback(result); 25743 } 25744 return []; 25745 }; 25746 }; 25747 const cacheInvalidator = (url) => url + (url.indexOf('?') === -1 ? '?' : '&') + (new Date()).getTime(); 25748 // Replaces strings without regexps to avoid FF regexp to big issue 25749 const replaceString = (content, search, replace) => { 25750 let index = 0; 25751 do { 25752 index = content.indexOf(search, index); 25753 if (index !== -1) { 25754 content = content.substring(0, index) + replace + content.substr(index + search.length); 25755 index += replace.length - search.length + 1; 25756 } 25757 } while (index !== -1); 25758 return content; 25759 }; 25760 const replaceImageUrl = (content, targetUrl, replacementUrl) => { 25761 const replacementString = `src="${replacementUrl}"${replacementUrl === Env.transparentSrc ? ' data-mce-placeholder="1"' : ''}`; 25762 content = replaceString(content, `src="${targetUrl}"`, replacementString); 25763 content = replaceString(content, 'data-mce-src="' + targetUrl + '"', 'data-mce-src="' + replacementUrl + '"'); 25764 return content; 25765 }; 25766 const replaceUrlInUndoStack = (targetUrl, replacementUrl) => { 25767 each$e(editor.undoManager.data, (level) => { 25768 if (level.type === 'fragmented') { 25769 level.fragments = map$3(level.fragments, (fragment) => replaceImageUrl(fragment, targetUrl, replacementUrl)); 25770 } 25771 else { 25772 level.content = replaceImageUrl(level.content, targetUrl, replacementUrl); 25773 } 25774 }); 25775 }; 25776 const replaceImageUriInView = (image, resultUri) => { 25777 const src = editor.convertURL(resultUri, 'src'); 25778 replaceUrlInUndoStack(image.src, resultUri); 25779 setAll$1(SugarElement.fromDom(image), { 25780 'src': shouldReuseFileName(editor) ? cacheInvalidator(resultUri) : resultUri, 25781 'data-mce-src': src 25782 }); 25783 }; 25784 const uploadImages = () => { 25785 if (!uploader) { 25786 uploader = createUploader(editor, uploadStatus); 25787 } 25788 return scanForImages().then(aliveGuard((imageInfos) => { 25789 const blobInfos = map$3(imageInfos, (imageInfo) => imageInfo.blobInfo); 25790 return uploader.upload(blobInfos, openNotification(editor)).then(aliveGuard((result) => { 25791 const imagesToRemove = []; 25792 let shouldDispatchChange = false; 25793 const filteredResult = map$3(result, (uploadInfo, index) => { 25794 const { blobInfo, image } = imageInfos[index]; 25795 let removed = false; 25796 if (uploadInfo.status && shouldReplaceBlobUris(editor)) { 25797 if (uploadInfo.url && !contains$1(image.src, uploadInfo.url)) { 25798 shouldDispatchChange = true; 25799 } 25800 blobCache.removeByUri(image.src); 25801 if (isRtc(editor)) ; 25802 else { 25803 replaceImageUriInView(image, uploadInfo.url); 25804 } 25805 } 25806 else if (uploadInfo.error) { 25807 if (uploadInfo.error.remove) { 25808 replaceUrlInUndoStack(image.src, Env.transparentSrc); 25809 imagesToRemove.push(image); 25810 removed = true; 25811 } 25812 uploadError(editor, uploadInfo.error.message); 25813 } 25814 return { 25815 element: image, 25816 status: uploadInfo.status, 25817 uploadUri: uploadInfo.url, 25818 blobInfo, 25819 removed 25820 }; 25821 }); 25822 if (imagesToRemove.length > 0 && !isRtc(editor)) { 25823 editor.undoManager.transact(() => { 25824 each$e(fromDom$1(imagesToRemove), (sugarElement) => { 25825 const parentOpt = parent(sugarElement); 25826 remove$8(sugarElement); 25827 // This needs a more editor-wide fix, see issue TINY-9802. Short version: Removing the image resulted in empty <p> elements, which confused the editor. 25828 parentOpt.each(addPaddingToEmpty(editor)); 25829 blobCache.removeByUri(sugarElement.dom.src); 25830 }); 25831 }); 25832 } 25833 else if (shouldDispatchChange) { 25834 editor.undoManager.dispatchChange(); 25835 } 25836 return filteredResult; 25837 })); 25838 })); 25839 }; 25840 const uploadImagesAuto = () => isAutomaticUploadsEnabled(editor) ? uploadImages() : Promise.resolve([]); 25841 const isValidDataUriImage = (imgElm) => forall(urlFilters, (filter) => filter(imgElm)); 25842 const addFilter = (filter) => { 25843 urlFilters.push(filter); 25844 }; 25845 const scanForImages = () => { 25846 if (!imageScanner) { 25847 imageScanner = ImageScanner(uploadStatus, blobCache); 25848 } 25849 return imageScanner.findAll(editor.getBody(), isValidDataUriImage).then(aliveGuard((result) => { 25850 const filteredResult = filter$5(result, (resultItem) => { 25851 // ImageScanner internally converts images that it finds, but it may fail to do so if image source is inaccessible. 25852 // In such case resultItem will contain appropriate text error message, instead of image data. 25853 if (isString(resultItem)) { 25854 displayError(editor, resultItem); 25855 return false; 25856 } 25857 else if (resultItem.uriType === 'blob') { 25858 return false; 25859 } 25860 else { 25861 return true; 25862 } 25863 }); 25864 if (isRtc(editor)) ; 25865 else { 25866 each$e(filteredResult, (resultItem) => { 25867 replaceUrlInUndoStack(resultItem.image.src, resultItem.blobInfo.blobUri()); 25868 resultItem.image.src = resultItem.blobInfo.blobUri(); 25869 resultItem.image.removeAttribute('data-mce-src'); 25870 }); 25871 } 25872 return filteredResult; 25873 })); 25874 }; 25875 const destroy = () => { 25876 blobCache.destroy(); 25877 uploadStatus.destroy(); 25878 imageScanner = uploader = null; 25879 }; 25880 const replaceBlobUris = (content) => { 25881 return content.replace(/src="(blob:[^"]+)"/g, (match, blobUri) => { 25882 const resultUri = uploadStatus.getResultUri(blobUri); 25883 if (resultUri) { 25884 return 'src="' + resultUri + '"'; 25885 } 25886 let blobInfo = blobCache.getByUri(blobUri); 25887 if (!blobInfo) { 25888 blobInfo = foldl(editor.editorManager.get(), (result, editor) => { 25889 return result || editor.editorUpload && editor.editorUpload.blobCache.getByUri(blobUri); 25890 }, undefined); 25891 } 25892 if (blobInfo) { 25893 const blob = blobInfo.blob(); 25894 return 'src="data:' + blob.type + ';base64,' + blobInfo.base64() + '"'; 25895 } 25896 return match; 25897 }); 25898 }; 25899 editor.on('SetContent', () => { 25900 if (isAutomaticUploadsEnabled(editor)) { 25901 // eslint-disable-next-line @typescript-eslint/no-floating-promises 25902 uploadImagesAuto(); 25903 } 25904 else { 25905 // eslint-disable-next-line @typescript-eslint/no-floating-promises 25906 scanForImages(); 25907 } 25908 }); 25909 editor.on('RawSaveContent', (e) => { 25910 e.content = replaceBlobUris(e.content); 25911 }); 25912 editor.on('GetContent', (e) => { 25913 if (e.source_view || e.format === 'raw' || e.format === 'tree') { 25914 return; 25915 } 25916 e.content = replaceBlobUris(e.content); 25917 }); 25918 editor.on('PostRender', () => { 25919 editor.parser.addNodeFilter('img', (images) => { 25920 each$e(images, (img) => { 25921 const src = img.attr('src'); 25922 if (!src || blobCache.getByUri(src)) { 25923 return; 25924 } 25925 const resultUri = uploadStatus.getResultUri(src); 25926 if (resultUri) { 25927 img.attr('src', resultUri); 25928 } 25929 }); 25930 }); 25931 }); 25932 return { 25933 blobCache, 25934 addFilter, 25935 uploadImages, 25936 uploadImagesAuto, 25937 scanForImages, 25938 destroy 25939 }; 25940 }; 25941 25942 const get$1 = (editor) => { 25943 const dom = editor.dom; 25944 const schemaType = editor.schema.type; 25945 const formats = { 25946 valigntop: [ 25947 { selector: 'td,th', styles: { verticalAlign: 'top' } } 25948 ], 25949 valignmiddle: [ 25950 { selector: 'td,th', styles: { verticalAlign: 'middle' } } 25951 ], 25952 valignbottom: [ 25953 { selector: 'td,th', styles: { verticalAlign: 'bottom' } } 25954 ], 25955 alignleft: [ 25956 { 25957 selector: 'figure.image', 25958 collapsed: false, 25959 classes: 'align-left', 25960 ceFalseOverride: true, 25961 preview: 'font-family font-size' 25962 }, 25963 { 25964 selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li,pre', 25965 styles: { 25966 textAlign: 'left' 25967 }, 25968 inherit: false, 25969 preview: false 25970 }, 25971 { 25972 selector: 'img,audio,video', 25973 collapsed: false, 25974 styles: { 25975 float: 'left', 25976 }, 25977 preview: 'font-family font-size' 25978 }, 25979 { 25980 selector: '.mce-placeholder', 25981 styles: { 25982 float: 'left', 25983 }, 25984 ceFalseOverride: true 25985 }, 25986 { 25987 selector: 'table', 25988 collapsed: false, 25989 styles: { 25990 marginLeft: '0px', 25991 marginRight: 'auto', 25992 }, 25993 onformat: (table) => { 25994 // Remove conflicting float style 25995 dom.setStyle(table, 'float', null); 25996 }, 25997 preview: 'font-family font-size' 25998 }, 25999 { 26000 selector: '.mce-preview-object,[data-ephox-embed-iri]
26000', 26001 ceFalseOverride: true, 26002 styles: { 26003 float: 'left' 26004 } 26005 } 26006 ], 26007 aligncenter: [ 26008 { 26009 selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li,pre', 26010 styles: { 26011 textAlign: 'center' 26012 }, 26013 inherit: false, 26014 preview: 'font-family font-size' 26015 }, 26016 { 26017 selector: 'figure.image', 26018 collapsed: false, 26019 classes: 'align-center', 26020 ceFalseOverride: true, 26021 preview: 'font-family font-size' 26022 }, 26023 { 26024 selector: 'img,audio,video', 26025 collapsed: false, 26026 styles: { 26027 display: 'block', 26028 marginLeft: 'auto', 26029 marginRight: 'auto' 26030 }, 26031 preview: false 26032 }, 26033 { 26034 selector: '.mce-placeholder', 26035 styles: { 26036 display: 'block', 26037 marginLeft: 'auto', 26038 marginRight: 'auto', 26039 }, 26040 ceFalseOverride: true 26041 }, 26042 { 26043 selector: 'table',
vendor: 2,652 bytes, lines 26044-26116
26044 collapsed: false, 26045 styles: { 26046 marginLeft: 'auto', 26047 marginRight: 'auto' 26048 }, 26049 preview: 'font-family font-size' 26050 }, 26051 { 26052 selector: '.mce-preview-object', 26053 ceFalseOverride: true, 26054 styles: { 26055 display: 'table', // Needs to be `table` to properly render while editing 26056 marginLeft: 'auto', 26057 marginRight: 'auto' 26058 }, 26059 preview: false 26060 }, 26061 { 26062 selector: '[data-ephox-embed-iri]', 26063 ceFalseOverride: true, 26064 styles: { 26065 marginLeft: 'auto', 26066 marginRight: 'auto' 26067 }, 26068 preview: false 26069 } 26070 ], 26071 alignright: [ 26072 { 26073 selector: 'figure.image', 26074 collapsed: false, 26075 classes: 'align-right', 26076 ceFalseOverride: true, 26077 preview: 'font-family font-size' 26078 }, 26079 { 26080 selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li,pre', 26081 styles: { 26082 textAlign: 'right' 26083 }, 26084 inherit: false, 26085 preview: 'font-family font-size' 26086 }, 26087 { 26088 selector: 'img,audio,video', 26089 collapsed: false, 26090 styles: { 26091 float: 'right' 26092 }, 26093 preview: 'font-family font-size' 26094 }, 26095 { 26096 selector: '.mce-placeholder', 26097 styles: { 26098 float: 'right' 26099 }, 26100 ceFalseOverride: true 26101 }, 26102 { 26103 selector: 'table', 26104 collapsed: false, 26105 styles: { 26106 marginRight: '0px', 26107 marginLeft: 'auto', 26108 }, 26109 onformat: (table) => { 26110 // Remove conflicting float style 26111 dom.setStyle(table, 'float', null); 26112 }, 26113 preview: 'font-family font-size' 26114 }, 26115 { 26116 selector: '.mce-preview-object,[data-ephox-embed-iri]
vendor: 29,872 bytes, lines 26116-26803
26116', 26117 ceFalseOverride: true, 26118 styles: { 26119 float: 'right' 26120 }, 26121 preview: false 26122 } 26123 ], 26124 alignjustify: [ 26125 { 26126 selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li,pre', 26127 styles: { 26128 textAlign: 'justify' 26129 }, 26130 inherit: false, 26131 preview: 'font-family font-size' 26132 } 26133 ], 26134 bold: [ 26135 { inline: 'strong', remove: 'all', preserve_attributes: ['class', 'style'] }, 26136 { inline: 'span', styles: { fontWeight: 'bold' } }, 26137 { inline: 'b', remove: 'all', preserve_attributes: ['class', 'style'] } 26138 ], 26139 italic: [ 26140 { inline: 'em', remove: 'all', preserve_attributes: ['class', 'style'] }, 26141 { inline: 'span', styles: { fontStyle: 'italic' } }, 26142 { inline: 'i', remove: 'all', preserve_attributes: ['class', 'style'] } 26143 ], 26144 underline: [ 26145 { inline: 'span', styles: { textDecoration: 'underline' }, exact: true }, 26146 { inline: 'u', remove: 'all', preserve_attributes: ['class', 'style'] } 26147 ], 26148 strikethrough: (() => { 26149 const span = { inline: 'span', styles: { textDecoration: 'line-through' }, exact: true }; 26150 const strike = { inline: 'strike', remove: 'all', preserve_attributes: ['class', 'style'] }; 26151 const s = { inline: 's', remove: 'all', preserve_attributes: ['class', 'style'] }; 26152 return schemaType !== 'html4' ? [s, span, strike] : [span, s, strike]; 26153 })(), 26154 forecolor: { inline: 'span', styles: { color: '%value' }, links: true, remove_similar: true, clear_child_styles: true }, 26155 hilitecolor: { inline: 'span', styles: { backgroundColor: '%value' }, links: true, remove_similar: true, clear_child_styles: true }, 26156 fontname: { inline: 'span', toggle: false, styles: { fontFamily: '%value' }, clear_child_styles: true }, 26157 fontsize: { inline: 'span', toggle: false, styles: { fontSize: '%value' }, clear_child_styles: true }, 26158 lineheight: { selector: 'h1,h2,h3,h4,h5,h6,p,li,td,th,div', styles: { lineHeight: '%value' }, remove_similar: true }, 26159 fontsize_class: { inline: 'span', attributes: { class: '%value' } }, 26160 blockquote: { block: 'blockquote', wrapper: true, remove: 'all' }, 26161 subscript: { inline: 'sub' }, 26162 superscript: { inline: 'sup' }, 26163 code: { inline: 'code' }, 26164 samp: { inline: 'samp' }, 26165 link: { 26166 inline: 'a', selector: 'a', remove: 'all', split: true, deep: true, 26167 onmatch: (node, _fmt, _itemName) => { 26168 return isElement$7(node) && node.hasAttribute('href'); 26169 }, 26170 onformat: (elm, _fmt, vars) => { 26171 Tools.each(vars, (value, key) => { 26172 dom.setAttrib(elm, key, value); 26173 }); 26174 } 26175 }, 26176 lang: { 26177 inline: 'span', 26178 clear_child_styles: true, 26179 remove_similar: true, 26180 attributes: { 26181 'lang': '%value', 26182 'data-mce-lang': (vars) => vars?.customValue ?? null 26183 } 26184 }, 26185 removeformat: [ 26186 { 26187 selector: 'b,strong,em,i,font,u,strike,s,sub,sup,dfn,code,samp,kbd,var,cite,mark,q,del,ins,small', 26188 remove: 'all', 26189 split: true, 26190 expand: false, 26191 block_expand: true, 26192 deep: true 26193 }, 26194 { selector: 'span', attributes: ['style', 'class'], remove: 'empty', split: true, expand: false, deep: true }, 26195 { selector: '*', attributes: ['style', 'class'], split: false, expand: false, deep: true } 26196 ] 26197 }; 26198 Tools.each('p h1 h2 h3 h4 h5 h6 div address pre dt dd'.split(/\s/), (name) => { 26199 formats[name] = { block: name, remove: 'all' }; 26200 }); 26201 return formats; 26202 }; 26203 26204 const genericBase = { 26205 remove_similar: true, 26206 inherit: false 26207 }; 26208 const cellBase = { 26209 selector: 'td,th', 26210 ...genericBase 26211 }; 26212 const cellFormats = { 26213 tablecellbackgroundcolor: { 26214 styles: { backgroundColor: '%value' }, 26215 ...cellBase 26216 }, 26217 tablecellverticalalign: { 26218 styles: { 26219 'vertical-align': '%value' 26220 }, 26221 ...cellBase 26222 }, 26223 tablecellbordercolor: { 26224 styles: { borderColor: '%value' }, 26225 ...cellBase 26226 }, 26227 tablecellclass: { 26228 classes: ['%value'], 26229 ...cellBase 26230 }, 26231 tableclass: { 26232 selector: 'table', 26233 classes: ['%value'], 26234 ...genericBase 26235 }, 26236 tablecellborderstyle: { 26237 styles: { borderStyle: '%value' }, 26238 ...cellBase 26239 }, 26240 tablecellborderwidth: { 26241 styles: { borderWidth: '%value' }, 26242 ...cellBase 26243 } 26244 }; 26245 const get = constant(cellFormats); 26246 26247 const FormatRegistry = (editor) => { 26248 const formats = {}; 26249 const get$2 = (name) => isNonNullable(name) ? formats[name] : formats; 26250 const has = (name) => has$2(formats, name); 26251 const register = (name, format) => { 26252 if (name) { 26253 if (!isString(name)) { 26254 each$d(name, (format, name) => { 26255 register(name, format); 26256 }); 26257 } 26258 else { 26259 // Force format into array and add it to internal collection 26260 if (!isArray$1(format)) { 26261 format = [format]; 26262 } 26263 each$e(format, (format) => { 26264 // Set deep to false by default on selector formats this to avoid removing 26265 // alignment on images inside paragraphs when alignment is changed on paragraphs 26266 if (isUndefined(format.deep)) { 26267 format.deep = !isSelectorFormat(format); 26268 } 26269 // Default to true 26270 if (isUndefined(format.split)) { 26271 format.split = !isSelectorFormat(format) || isInlineFormat(format); 26272 } 26273 // Default to true 26274 if (isUndefined(format.remove) && isSelectorFormat(format) && !isInlineFormat(format)) { 26275 format.remove = 'none'; 26276 } 26277 // Mark format as a mixed format inline + block level 26278 if (isSelectorFormat(format) && isInlineFormat(format)) { 26279 format.mixed = true; 26280 format.block_expand = true; 26281 } 26282 // Split classes if needed 26283 if (isString(format.classes)) { 26284 format.classes = format.classes.split(/\s+/); 26285 } 26286 }); 26287 formats[name] = format; 26288 } 26289 } 26290 }; 26291 const unregister = (name) => { 26292 if (name && formats[name]) { 26293 delete formats[name]; 26294 } 26295 return formats; 26296 }; 26297 register(get$1(editor)); 26298 register(get()); 26299 register(getFormats(editor)); 26300 return { 26301 get: get$2, 26302 has, 26303 register, 26304 unregister 26305 }; 26306 }; 26307 26308 const each$3 = Tools.each; 26309 const dom = DOMUtils.DOM; 26310 const isPreviewItem = (item) => isNonNullable(item) && isObject(item); 26311 const parsedSelectorToHtml = (ancestry, editor) => { 26312 const schema = editor && editor.schema || Schema({}); 26313 const decorate = (elm, item) => { 26314 if (item.classes.length > 0) { 26315 dom.addClass(elm, item.classes.join(' ')); 26316 } 26317 dom.setAttribs(elm, item.attrs); 26318 }; 26319 const createElement = (sItem) => { 26320 const item = isString(sItem) ? { 26321 name: sItem, 26322 classes: [], 26323 attrs: {} 26324 } : sItem; 26325 const elm = dom.create(item.name); 26326 decorate(elm, item); 26327 return elm; 26328 }; 26329 const getRequiredParent = (elm, candidate) => { 26330 const elmRule = schema.getElementRule(elm.nodeName.toLowerCase()); 26331 const parentsRequired = elmRule?.parentsRequired; 26332 if (parentsRequired && parentsRequired.length) { 26333 return candidate && contains$2(parentsRequired, candidate) ? candidate : parentsRequired[0]; 26334 } 26335 else { 26336 return false; 26337 } 26338 }; 26339 const wrapInHtml = (elm, ancestors, siblings) => { 26340 let parentCandidate; 26341 const ancestor = ancestors[0]; 26342 const ancestorName = isPreviewItem(ancestor) ? ancestor.name : undefined; 26343 const parentRequired = getRequiredParent(elm, ancestorName); 26344 if (parentRequired) { 26345 if (ancestorName === parentRequired) { 26346 parentCandidate = ancestor; 26347 ancestors = ancestors.slice(1); 26348 } 26349 else { 26350 parentCandidate = parentRequired; 26351 } 26352 } 26353 else if (ancestor) { 26354 parentCandidate = ancestor; 26355 ancestors = ancestors.slice(1); 26356 } 26357 else if (!siblings) { 26358 return elm; 26359 } 26360 // if no more ancestry, wrap in generic div 26361 const parent = parentCandidate ? createElement(parentCandidate) : dom.create('div'); 26362 parent.appendChild(elm); 26363 if (siblings) { 26364 Tools.each(siblings, (sibling) => { 26365 const siblingElm = createElement(sibling); 26366 parent.insertBefore(siblingElm, elm); 26367 }); 26368 } 26369 const parentSiblings = isPreviewItem(parentCandidate) ? parentCandidate.siblings : undefined; 26370 return wrapInHtml(parent, ancestors, parentSiblings); 26371 }; 26372 const fragment = dom.create('div'); 26373 if (ancestry.length > 0) { 26374 const item = ancestry[0]; 26375 const elm = createElement(item); 26376 const siblings = isPreviewItem(item) ? item.siblings : undefined; 26377 fragment.appendChild(wrapInHtml(elm, ancestry.slice(1), siblings)); 26378 } 26379 return fragment; 26380 }; 26381 const parseSelectorItem = (item) => { 26382 item = Tools.trim(item); 26383 let tagName = 'div'; 26384 const obj = { 26385 name: tagName, 26386 classes: [], 26387 attrs: {}, 26388 selector: item 26389 }; 26390 if (item !== '*') { 26391 // matching IDs, CLASSes, ATTRIBUTES and PSEUDOs 26392 tagName = item.replace(/(?:([#\.]|::?)([\w\-]+)|(\[)([^\]]+)\]?)/g, ($0, $1, $2, $3, $4) => { 26393 switch ($1) { 26394 case '#': 26395 obj.attrs.id = $2; 26396 break; 26397 case '.': 26398 obj.classes.push($2); 26399 break; 26400 case ':': 26401 if (Tools.inArray('checked disabled enabled read-only required'.split(' '), $2) !== -1) { 26402 obj.attrs[$2] = $2; 26403 } 26404 break; 26405 } 26406 // attribute matched 26407 if ($3 === '[') { 26408 const m = $4.match(/([\w\-]+)(?:\=\"([^\"]+))?/); 26409 if (m) { 26410 obj.attrs[m[1]] = m[2]; 26411 } 26412 } 26413 return ''; 26414 }); 26415 } 26416 obj.name = tagName || 'div'; 26417 return obj; 26418 }; 26419 const parseSelector = (selector) => { 26420 if (!isString(selector)) { 26421 return []; 26422 } 26423 // take into account only first one 26424 selector = selector.split(/\s*,\s*/)[0]; 26425 // tighten 26426 selector = selector.replace(/\s*(~\+|~|\+|>)\s*/g, '$1'); 26427 // split either on > or on space, but not the one inside brackets 26428 return Tools.map(selector.split(/(?:>|\s+(?![^\[\]]+\]))/), (item) => { 26429 // process each sibling selector separately 26430 const siblings = Tools.map(item.split(/(?:~\+|~|\+)/), parseSelectorItem); 26431 const obj = siblings.pop(); // the last one is our real target 26432 if (siblings.length) { 26433 obj.siblings = siblings; 26434 } 26435 return obj; 26436 }).reverse(); 26437 }; 26438 const getCssText = (editor, format) => { 26439 let previewCss = ''; 26440 let previewStyles = getPreviewStyles(editor); 26441 // No preview forced 26442 if (previewStyles === '') { 26443 return ''; 26444 } 26445 // Removes any variables since these can't be previewed 26446 const removeVars = (val) => { 26447 return isString(val) ? val.replace(/%(\w+)/g, '') : ''; 26448 }; 26449 const getComputedStyle = (name, elm) => { 26450 return dom.getStyle(elm ?? editor.getBody(), name, true); 26451 }; 26452 // Create block/inline element to use for preview 26453 if (isString(format)) { 26454 const formats = editor.formatter.get(format); 26455 if (!formats) { 26456 return ''; 26457 } 26458 format = formats[0]; 26459 } 26460 // Format specific preview override 26461 // TODO: This should probably be further reduced by the previewStyles option 26462 if ('preview' in format) { 26463 const preview = format.preview; 26464 if (preview === false) { 26465 return ''; 26466 } 26467 else { 26468 previewStyles = preview || previewStyles; 26469 } 26470 } 26471 let name = format.block || format.inline || 'span'; 26472 let previewFrag; 26473 const items = parseSelector(format.selector); 26474 if (items.length > 0) { 26475 if (!items[0].name) { // e.g. something like ul > .someClass was provided 26476 items[0].name = name; 26477 } 26478 name = format.selector; 26479 previewFrag = parsedSelectorToHtml(items, editor); 26480 } 26481 else { 26482 previewFrag = parsedSelectorToHtml([name], editor); 26483 } 26484 const previewElm = dom.select(name, previewFrag)[0] || previewFrag.firstChild; 26485 // Add format styles to preview element 26486 each$3(format.styles, (value, name) => { 26487 const newValue = removeVars(value); 26488 if (newValue) { 26489 dom.setStyle(previewElm, name, newValue); 26490 } 26491 }); 26492 // Add attributes to preview element 26493 each$3(format.attributes, (value, name) => { 26494 const newValue = removeVars(value); 26495 if (newValue) { 26496 dom.setAttrib(previewElm, name, newValue); 26497 } 26498 }); 26499 // Add classes to preview element 26500 each$3(format.classes, (value) => { 26501 const newValue = removeVars(value); 26502 if (!dom.hasClass(previewElm, newValue)) { 26503 dom.addClass(previewElm, newValue); 26504 } 26505 }); 26506 editor.dispatch('PreviewFormats'); 26507 // Add the previewElm outside the visual area 26508 dom.setStyles(previewFrag, { position: 'absolute', left: -0xFFFF }); 26509 editor.getBody().appendChild(previewFrag); 26510 // Get parent container font size so we can compute px values out of em/% for older IE:s 26511 const rawParentFontSize = getComputedStyle('fontSize'); 26512 const parentFontSize = /px$/.test(rawParentFontSize) ? parseInt(rawParentFontSize, 10) : 0; 26513 each$3(previewStyles.split(' '), (name) => { 26514 let value = getComputedStyle(name, previewElm); 26515 // If background is transparent then check if the body has a background color we can use 26516 if (name === 'background-color' && /transparent|rgba\s*\([^)]+,\s*0\)/.test(value)) { 26517 value = getComputedStyle(name); 26518 // Ignore white since it's the default color, not the nicest fix 26519 // TODO: Fix this by detecting runtime style 26520 if (rgbaToHexString(value).toLowerCase() === '#ffffff') { 26521 return; 26522 } 26523 } 26524 if (name === 'color') { 26525 // Ignore black since it's the default color, not the nicest fix 26526 // TODO: Fix this by detecting runtime style 26527 if (rgbaToHexString(value).toLowerCase() === '#000000') { 26528 return; 26529 } 26530 } 26531 // Old IE won't calculate the font size so we need to do that manually 26532 if (name === 'font-size') { 26533 if (/em|%$/.test(value)) { 26534 if (parentFontSize === 0) { 26535 return; 26536 } 26537 // Convert font size from em/% to px 26538 const numValue = parseFloat(value) / (/%$/.test(value) ? 100 : 1); 26539 value = (numValue * parentFontSize) + 'px'; 26540 } 26541 } 26542 if (name === 'border' && value) { 26543 previewCss += 'padding:0 2px;'; 26544 } 26545 previewCss += name + ':' + value + ';'; 26546 }); 26547 editor.dispatch('AfterPreviewFormats'); 26548 // previewCss += 'line-height:normal'; 26549 dom.remove(previewFrag); 26550 return previewCss; 26551 }; 26552 26553 const setup$x = (editor) => { 26554 // Add some inline shortcuts 26555 editor.addShortcut('meta+b', '', 'Bold'); 26556 editor.addShortcut('meta+i', '', 'Italic'); 26557 editor.addShortcut('meta+u', '', 'Underline'); 26558 // BlockFormat shortcuts keys 26559 for (let i = 1; i <= 6; i++) { 26560 editor.addShortcut('access+' + i, '', ['FormatBlock', false, 'h' + i]); 26561 } 26562 editor.addShortcut('access+7', '', ['FormatBlock', false, 'p']); 26563 editor.addShortcut('access+8', '', ['FormatBlock', false, 'div']); 26564 editor.addShortcut('access+9', '', ['FormatBlock', false, 'address']); 26565 }; 26566 26567 const Formatter = (editor) => { 26568 const formats = FormatRegistry(editor); 26569 const formatChangeState = Cell({}); 26570 setup$x(editor); 26571 setup$B(editor); 26572 if (!isRtc(editor)) { 26573 setup$A(formatChangeState, editor); 26574 } 26575 return { 26576 /** 26577 * Returns the format by name or all formats if no name is specified. 26578 * 26579 * @method get 26580 * @param {String} name Optional name to retrieve by. 26581 * @return {Array/Object} Array/Object with all registered formats or a specific format. 26582 */ 26583 get: formats.get, 26584 /** 26585 * Returns true or false if a format is registered for the specified name. 26586 * 26587 * @method has 26588 * @param {String} name Format name to check if a format exists. 26589 * @return {Boolean} True/False if a format for the specified name exists. 26590 */ 26591 has: formats.has, 26592 /** 26593 * Registers a specific format by name. 26594 * 26595 * @method register 26596 * @param {Object/String} name Name of the format for example "bold". 26597 * @param {Object/Array} format Optional format object or array of format variants 26598 * can only be omitted if the first arg is an object. 26599 */ 26600 register: formats.register, 26601 /** 26602 * Unregister a specific format by name. 26603 * 26604 * @method unregister 26605 * @param {String} name Name of the format for example "bold". 26606 */ 26607 unregister: formats.unregister, 26608 /** 26609 * Applies the specified format to the current selection or specified node. 26610 * 26611 * @method apply 26612 * @param {String} name Name of format to apply. 26613 * @param {Object} vars Optional list of variables to replace within format before applying it. 26614 * @param {Node} node Optional node to apply the format to defaults to current selection. 26615 */ 26616 apply: (name, vars, node) => { 26617 applyFormat(editor, name, vars, node); 26618 }, 26619 /** 26620 * Removes the specified format from the current selection or specified node. 26621 * 26622 * @method remove 26623 * @param {String} name Name of format to remove. 26624 * @param {Object} vars Optional list of variables to replace within format before removing it. 26625 * @param {Node/Range} node Optional node or DOM range to remove the format from defaults to current selection. 26626 */ 26627 remove: (name, vars, node, similar) => { 26628 removeFormat(editor, name, vars, node, similar); 26629 }, 26630 /** 26631 * Toggles the specified format on/off. 26632 * 26633 * @method toggle 26634 * @param {String} name Name of format to apply/remove. 26635 * @param {Object} vars Optional list of variables to replace within format before applying/removing it. 26636 * @param {Node} node Optional node to apply the format to or remove from. Defaults to current selection. 26637 */ 26638 toggle: (name, vars, node) => { 26639 toggleFormat(editor, name, vars, node); 26640 }, 26641 /** 26642 * Matches the current selection or specified node against the specified format name. 26643 * 26644 * @method match 26645 * @param {String} name Name of format to match. 26646 * @param {Object} vars Optional list of variables to replace before checking it. 26647 * @param {Node} node Optional node to check. 26648 * @param {Boolean} similar Optional argument to specify that similar formats should be checked instead of only exact formats. 26649 * @return {Boolean} true/false if the specified selection/node matches the format. 26650 */ 26651 match: (name, vars, node, similar) => matchFormat(editor, name, vars, node, similar), 26652 /** 26653 * Finds the closest matching format from a set of formats for the current selection. 26654 * 26655 * @method closest 26656 * @param {Array} names Format names to check for. 26657 * @return {String} The closest matching format name or null. 26658 */ 26659 closest: (names) => closestFormat(editor, names), 26660 /** 26661 * Matches the current selection against the array of formats and returns a new array with matching formats. 26662 * 26663 * @method matchAll 26664 * @param {Array} names Name of format to match. 26665 * @param {Object} vars Optional list of variables to replace before checking it. 26666 * @return {Array} Array with matched formats. 26667 */ 26668 matchAll: (names, vars) => matchAllFormats(editor, names, vars), 26669 /** 26670 * Return true/false if the specified node has the specified format. 26671 * 26672 * @method matchNode 26673 * @param {Node} node Node to check the format on. 26674 * @param {String} name Format name to check. 26675 * @param {Object} vars Optional list of variables to replace before checking it. 26676 * @param {Boolean} similar Match format that has similar properties. 26677 * @return {Object} Returns the format object it matches or undefined if it doesn't match. 26678 */ 26679 matchNode: (node, name, vars, similar) => matchNodeFormat(editor, node, name, vars, similar), 26680 /** 26681 * Returns true/false if the specified format can be applied to the current selection or not. It 26682 * will currently only check the state for selector formats, it returns true on all other format types. 26683 * 26684 * @method canApply 26685 * @param {String} name Name of format to check. 26686 * @return {Boolean} true/false if the specified format can be applied to the current selection/node. 26687 */ 26688 canApply: (name) => canApplyFormat(editor, name), 26689 /** 26690 * Executes the specified callback when the current selection matches the formats or not. 26691 * 26692 * @method formatChanged 26693 * @param {String} formats Comma separated list of formats to check for. 26694 * @param {Function} callback Callback with state and args when the format is changed/toggled on/off. 26695 * @param {Boolean} similar True/false state if the match should handle similar or exact formats. 26696 * @param {Object} vars Restrict the format being watched to only match if the variables applied are equal to vars. 26697 */ 26698 formatChanged: (formats, callback, similar, vars) => formatChanged(editor, formatChangeState, formats, callback, similar, vars), 26699 /** 26700 * Returns a preview css text for the specified format. 26701 * 26702 * @method getCssText 26703 * @param {String/Object} format Format to generate preview css text for. 26704 * @return {String} Css text for the specified format. 26705 * @example 26706 * const cssText1 = editor.formatter.getCssText('bold'); 26707 * const cssText2 = editor.formatter.getCssText({ inline: 'b' }); 26708 */ 26709 getCssText: curry(getCssText, editor) 26710 }; 26711 }; 26712 26713 // Avoid adding non-typing undo levels for commands that could cause duplicate undo levels to be created 26714 // or do not alter the editor content or selection in any way 26715 const shouldIgnoreCommand = (cmd) => { 26716 switch (cmd.toLowerCase()) { 26717 case 'undo': 26718 case 'redo': 26719 case 'mcefocus': 26720 return true; 26721 default: 26722 return false; 26723 } 26724 }; 26725 const registerEvents = (editor, undoManager, locks) => { 26726 const isFirstTypedCharacter = Cell(false); 26727 const addNonTypingUndoLevel = (e) => { 26728 setTyping(undoManager, false, locks); 26729 undoManager.add({}, e); 26730 }; 26731 // Add initial undo level when the editor is initialized 26732 editor.on('init', () => { 26733 undoManager.add(); 26734 }); 26735 // Get position before an execCommand is processed 26736 editor.on('BeforeExecCommand', (e) => { 26737 const cmd = e.command; 26738 if (!shouldIgnoreCommand(cmd)) { 26739 endTyping(undoManager, locks); 26740 undoManager.beforeChange(); 26741 } 26742 }); 26743 // Add undo level after an execCommand call was made 26744 editor.on('ExecCommand', (e) => { 26745 const cmd = e.command; 26746 if (!shouldIgnoreCommand(cmd)) { 26747 addNonTypingUndoLevel(e); 26748 } 26749 }); 26750 editor.on('ObjectResizeStart cut', () => { 26751 undoManager.beforeChange(); 26752 }); 26753 editor.on('SaveContent ObjectResized blur', addNonTypingUndoLevel); 26754 editor.on('dragend', addNonTypingUndoLevel); 26755 editor.on('keyup', (e) => { 26756 const keyCode = e.keyCode; 26757 // If key is prevented then don't add undo level 26758 // This would happen on keyboard shortcuts for example 26759 if (e.isDefaultPrevented()) { 26760 return; 26761 } 26762 const isMeta = Env.os.isMacOS() && e.key === 'Meta'; 26763 if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode === 45 || e.ctrlKey || isMeta) { 26764 addNonTypingUndoLevel(); 26765 editor.nodeChanged(); 26766 } 26767 if (keyCode === 46 || keyCode === 8) { 26768 editor.nodeChanged(); 26769 } 26770 // Fire a TypingUndo event on the first character entered 26771 if (isFirstTypedCharacter.get() && undoManager.typing && !isEq$1(editor.readonly, createFromEditor(editor), undoManager.data[0])) { 26772 if (!editor.isDirty()) { 26773 editor.setDirty(true); 26774 } 26775 editor.dispatch('TypingUndo'); 26776 isFirstTypedCharacter.set(false); 26777 editor.nodeChanged(); 26778 } 26779 }); 26780 editor.on('keydown', (e) => { 26781 const keyCode = e.keyCode; 26782 // If key is prevented then don't add undo level 26783 // This would happen on keyboard shortcuts for example 26784 if (e.isDefaultPrevented()) { 26785 return; 26786 } 26787 // Is character position keys left,right,up,down,home,end,pgdown,pgup,enter 26788 if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode === 45) { 26789 if (undoManager.typing) { 26790 addNonTypingUndoLevel(e); 26791 } 26792 return; 26793 } 26794 // If key isn't Ctrl+Alt/AltGr 26795 const modKey = (e.ctrlKey && !e.altKey) || e.metaKey; 26796 if ((keyCode < 16 || keyCode > 20) && keyCode !== 224 && keyCode !== 91 && !undoManager.typing && !modKey) { 26797 undoManager.beforeChange(); 26798 setTyping(undoManager, true, locks); 26799 undoManager.add({}, e); 26800 isFirstTypedCharacter.set(true); 26801 return; 26802 } 26803 const has
vendor: 2,744 bytes, lines 26803-26863
26803OnlyMetaOrCtrlModifier = Env.os.isMacOS() ? e.metaKey : e.ctrlKey && !e.altKey; 26804 if (hasOnlyMetaOrCtrlModifier) { 26805 undoManager.beforeChange(); 26806 } 26807 }); 26808 editor.on('mousedown', (e) => { 26809 if (undoManager.typing) { 26810 addNonTypingUndoLevel(e); 26811 } 26812 }); 26813 // Special inputType, currently only Chrome implements this: https://www.w3.org/TR/input-events-2/#x5.1.2-attributes 26814 const isInsertReplacementText = (event) => event.inputType === 'insertReplacementText'; 26815 // Safari just shows inputType `insertText` but with data set to null so we can use that 26816 const isInsertTextDataNull = (event) => event.inputType === 'insertText' && event.data === null; 26817 const isInsertFromPasteOrDrop = (event) => event.inputType === 'insertFromPaste' || event.inputType === 'insertFromDrop'; 26818 // For detecting when user has replaced text using the browser built-in spell checker or paste/drop events 26819 editor.on('input', (e) => { 26820 if (e.inputType && (isInsertReplacementText(e) || isInsertTextDataNull(e) || isInsertFromPasteOrDrop(e))) { 26821 addNonTypingUndoLevel(e); 26822 } 26823 }); 26824 editor.on('AddUndo Undo Redo ClearUndos', (e) => { 26825 if (!e.isDefaultPrevented()) { 26826 editor.nodeChanged(); 26827 } 26828 }); 26829 }; 26830 const addKeyboardShortcuts = (editor) => { 26831 editor.addShortcut('meta+z', '', 'Undo'); 26832 editor.addShortcut('meta+y,meta+shift+z', '', 'Redo'); 26833 }; 26834 26835 /** 26836 * This class handles the undo/redo history levels for the editor. Since the built-in undo/redo has major drawbacks a custom one was needed. 26837 * 26838 * @class tinymce.UndoManager 26839 */ 26840 const UndoManager = (editor) => { 26841 const beforeBookmark = value$1(); 26842 const locks = Cell(0); 26843 const index = Cell(0); 26844 /* eslint consistent-this:0 */ 26845 const undoManager = { 26846 data: [], // Gets mutated both internally and externally by plugins like remark, not documented 26847 /** 26848 * State if the user is currently typing or not. This will add a typing operation into one undo 26849 * level instead of one new level for each keystroke. 26850 * 26851 * @field {Boolean} typing 26852 */ 26853 typing: false, 26854 /** 26855 * Stores away a bookmark to be used when performing an undo action so that the selection is before 26856 * the change has been made. 26857 * 26858 * @method beforeChange 26859 */ 26860 beforeChange: () => { 26861 beforeChange(editor, locks, beforeBookmark); 26862 }, 26863 /**
26864 * Adds a new undo level/snapshot to the undo list. 26865 * 26866 * @method add 26867 * @param {Object} level Optional undo level object to add. 26868 * @param {EditorEvent} event Optional event responsible for the creation of the undo level. 26869 * @return {Object} Undo level that got added or null if a level wasn't needed. 26870 */ 26871 add: (level, event) => { 26872 return addUndoLevel(editor, undoManager, index, locks, beforeBookmark, level, event); 26873 }, 26874 /** 26875 * Dispatch a change event with current editor status as level and current undoManager layer as lastLevel 26876 * 26877 * @method dispatchChange 26878 */ 26879 dispatchChange: () => { 26880 editor.setDirty(true); 26881 const level = createFromEditor(editor); 26882 level.bookmark = getUndoBookmark(editor.selection); 26883 editor.dispatch('change', { 26884 level, 26885 lastLevel: get$b
vendor: 10,509 bytes, lines 26885-27127
26885(undoManager.data, index.get()).getOrUndefined() 26886 }); 26887 }, 26888 /** 26889 * Undoes the last action. 26890 * 26891 * @method undo 26892 * @return {Object} Undo level or null if no undo was performed. 26893 */ 26894 undo: () => { 26895 return undo(editor, undoManager, locks, index); 26896 }, 26897 /** 26898 * Redoes the last action. 26899 * 26900 * @method redo 26901 * @return {Object} Redo level or null if no redo was performed. 26902 */ 26903 redo: () => { 26904 return redo(editor, index, undoManager.data); 26905 }, 26906 /** 26907 * Removes all undo levels. 26908 * 26909 * @method clear 26910 */ 26911 clear: () => { 26912 clear(editor, undoManager, index); 26913 }, 26914 /** 26915 * Resets the undo manager levels by clearing all levels and then adding an initial level. 26916 * 26917 * @method reset 26918 */ 26919 reset: () => { 26920 reset(editor, undoManager); 26921 }, 26922 /** 26923 * Returns true/false if the undo manager has any undo levels. 26924 * 26925 * @method hasUndo 26926 * @return {Boolean} true/false if the undo manager has any undo levels. 26927 */ 26928 hasUndo: () => { 26929 return hasUndo(editor, undoManager, index); 26930 }, 26931 /** 26932 * Returns true/false if the undo manager has any redo levels. 26933 * 26934 * @method hasRedo 26935 * @return {Boolean} true/false if the undo manager has any redo levels. 26936 */ 26937 hasRedo: () => { 26938 return hasRedo(editor, undoManager, index); 26939 }, 26940 /** 26941 * Executes the specified mutator function as an undo transaction. The selection 26942 * before the modification will be stored to the undo stack and if the DOM changes 26943 * it will add a new undo level. Any logic within the translation that adds undo levels will 26944 * be ignored. So a translation can include calls to execCommand or editor.insertContent. 26945 * 26946 * @method transact 26947 * @param {Function} callback Function that gets executed and has dom manipulation logic in it. 26948 * @return {Object} Undo level that got added or null it a level wasn't needed. 26949 */ 26950 transact: (callback) => { 26951 return transact(editor, undoManager, locks, callback); 26952 }, 26953 /** 26954 * Executes the specified mutator function as an undo transaction. But without adding an undo level. 26955 * Any logic within the translation that adds undo levels will be ignored. So a translation can 26956 * include calls to execCommand or editor.insertContent. 26957 * 26958 * @method ignore 26959 * @param {Function} callback Function that gets executed and has dom manipulation logic in it. 26960 */ 26961 ignore: (callback) => { 26962 ignore(editor, locks, callback); 26963 }, 26964 /** 26965 * Adds an extra "hidden" undo level by first applying the first mutation and store that to the undo stack 26966 * then roll back that change and do the second mutation on top of the stack. This will produce an extra 26967 * undo level that the user doesn't see until they undo. 26968 * 26969 * @method extra 26970 * @param {Function} callback1 Function that does mutation but gets stored as a "hidden" extra undo level. 26971 * @param {Function} callback2 Function that does mutation but gets displayed to the user. 26972 */ 26973 extra: (callback1, callback2) => { 26974 extra(editor, undoManager, index, callback1, callback2); 26975 } 26976 }; 26977 if (!isRtc(editor)) { 26978 registerEvents(editor, undoManager, locks); 26979 } 26980 addKeyboardShortcuts(editor); 26981 return undoManager; 26982 }; 26983 26984 const nonTypingKeycodes = [ 26985 // tab, esc, home, end 26986 9, 27, VK.HOME, VK.END, 26987 // pause, capslock, print screen, numlock, scroll lock 26988 19, 20, 44, 144, 145, 26989 // page up/down, insert 26990 33, 34, 45, 26991 // alt, shift, ctrl 26992 16, 17, 18, 26993 // meta/windows key 26994 91, 92, 93, 26995 // direction 26996 VK.DOWN, VK.UP, VK.LEFT, VK.RIGHT 26997 ].concat( 26998 // Meta key on firefox is different 26999 Env.browser.isFirefox() ? [224] : []); 27000 const placeholderAttr = 'data-mce-placeholder'; 27001 const isKeyboardEvent = (e) => e.type === 'keydown' || e.type === 'keyup'; 27002 const isDeleteEvent = (e) => { 27003 const keyCode = e.keyCode; 27004 return keyCode === VK.BACKSPACE || keyCode === VK.DELETE; 27005 }; 27006 const isNonTypingKeyboardEvent = (e) => { 27007 if (isKeyboardEvent(e)) { 27008 const keyCode = e.keyCode; 27009 // Ctrl/Meta/Alt key pressed, F1-12 or non typing keycode 27010 return !isDeleteEvent(e) && (VK.metaKeyPressed(e) || e.altKey || keyCode >= 112 && keyCode <= 123 || contains$2(nonTypingKeycodes, keyCode)); 27011 } 27012 else { 27013 return false; 27014 } 27015 }; 27016 const isTypingKeyboardEvent = (e) => 27017 // 229 === Unidentified, so since we don't know what it is treat it as a non typing event on keyup but as a typing event on keydown 27018 // Android will generally always send a 229 keycode since it uses an IME to input text 27019 isKeyboardEvent(e) && !(isDeleteEvent(e) || e.type === 'keyup' && e.keyCode === 229); 27020 const isVisuallyEmpty = (dom, rootElm, forcedRootBlock) => { 27021 if (dom.isEmpty(rootElm, undefined, { skipBogus: false, includeZwsp: true })) { 27022 // Ensure the node matches the forced_root_block setting, as the content could be an empty list, etc... 27023 // and also check that the content isn't indented 27024 const firstElement = rootElm.firstElementChild; 27025 if (!firstElement) { 27026 return true; 27027 } 27028 else if (dom.getStyle(rootElm.firstElementChild, 'padding-left') || dom.getStyle(rootElm.firstElementChild, 'padding-right')) { 27029 return false; 27030 } 27031 else { 27032 return forcedRootBlock === firstElement.nodeName.toLowerCase(); 27033 } 27034 } 27035 else { 27036 return false; 27037 } 27038 }; 27039 const setup$w = (editor) => { 27040 const dom = editor.dom; 27041 const rootBlock = getForcedRootBlock(editor); 27042 const placeholder = getPlaceholder(editor) ?? ''; 27043 const updatePlaceholder = (e, initial) => { 27044 if (isNonTypingKeyboardEvent(e)) { 27045 return; 27046 } 27047 // Check to see if we should show the placeholder 27048 const body = editor.getBody(); 27049 const showPlaceholder = isTypingKeyboardEvent(e) ? false : isVisuallyEmpty(dom, body, rootBlock); 27050 // Update the attribute as required 27051 const isPlaceholderShown = dom.getAttrib(body, placeholderAttr) !== ''; 27052 if (isPlaceholderShown !== showPlaceholder || initial) { 27053 dom.setAttrib(body, placeholderAttr, showPlaceholder ? placeholder : null); 27054 firePlaceholderToggle(editor, showPlaceholder); 27055 // Swap the key listener state 27056 editor.on(showPlaceholder ? 'keydown' : 'keyup', updatePlaceholder); 27057 editor.off(showPlaceholder ? 'keyup' : 'keydown', updatePlaceholder); 27058 } 27059 }; 27060 if (isNotEmpty(placeholder)) { 27061 editor.on('init', (e) => { 27062 // Setup the initial state 27063 updatePlaceholder(e, true); 27064 editor.on('change SetContent ExecCommand', updatePlaceholder); 27065 // TINY-4828: Update the placeholder after pasting content. This needs to use a timeout as 27066 // the browser doesn't update the dom until after the paste event has fired 27067 editor.on('paste', (e) => Delay.setEditorTimeout(editor, () => updatePlaceholder(e))); 27068 }); 27069 } 27070 }; 27071 27072 const createAndFireInputEvent = (eventType) => (editor, inputType, specifics = {}) => { 27073 const target = editor.getBody(); 27074 const overrides = { 27075 bubbles: true, 27076 composed: true, 27077 data: null, 27078 isComposing: false, 27079 detail: 0, 27080 view: null, 27081 target, 27082 currentTarget: target, 27083 eventPhase: Event.AT_TARGET, 27084 originalTarget: target, 27085 explicitOriginalTarget: target, 27086 isTrusted: false, 27087 srcElement: target, 27088 cancelable: false, 27089 preventDefault: noop, 27090 inputType 27091 }; 27092 const input = clone$2(new InputEvent(eventType)); 27093 return editor.dispatch(eventType, { ...input, ...overrides, ...specifics }); 27094 }; 27095 const fireInputEvent = createAndFireInputEvent('input'); 27096 const fireBeforeInputEvent = createAndFireInputEvent('beforeinput'); 27097 27098 const symulateDelete = (editor, isForward, deleteFun) => { 27099 // Some delete actions may prevent the input event from being fired. If we do not detect it, we fire it ourselves. 27100 let shouldFireInput = true; 27101 const inputHandler = () => shouldFireInput = false; 27102 const beforeInputEvent = fireBeforeInputEvent(editor, isForward ? 'deleteContentForward' : 'deleteContentBackward'); 27103 if (beforeInputEvent.isDefaultPrevented()) { 27104 return false; 27105 } 27106 editor.on('input', inputHandler); 27107 try { 27108 deleteFun(); 27109 } 27110 finally { 27111 editor.off('input', inputHandler); 27112 } 27113 if (shouldFireInput) { 27114 editor.dispatch('input'); 27115 } 27116 return true; 27117 }; 27118 27119 const matchNodeName = (name) => (node) => isNonNullable(node) && node.nodeName.toLowerCase() === name; 27120 const matchNodeNames = (regex) => (node) => isNonNullable(node) && regex.test(node.nodeName); 27121 const isTextNode$1 = (node) => isNonNullable(node) && node.nodeType === 3; 27122 const isElement$1 = (node) => isNonNullable(node) && node.nodeType === 1; 27123 const isListNode = matchNodeNames(/^(OL|UL|DL)$/); 27124 const isOlUlNode = matchNodeNames(/^(OL|UL)$/); 27125 const isListItemNode = matchNodeNames(/^(LI|DT|DD)$/); 27126 const isDlItemNode = matchNodeNames(/^(DT|DD)$/); 27127 const is
vendor: 19,327 bytes, lines 27127-27570
27127Br$1 = matchNodeName('br'); 27128 const isFirstChild$1 = (node) => node.parentNode?.firstChild === node; 27129 const isTextBlock = (editor, node) => isNonNullable(node) && node.nodeName in editor.schema.getTextBlockElements(); 27130 const isBlock = (node, blockElements) => isNonNullable(node) && node.nodeName in blockElements; 27131 const isVoid = (editor, node) => isNonNullable(node) && node.nodeName in editor.schema.getVoidElements(); 27132 const isBogusBr = (dom, node) => { 27133 if (!isBr$1(node)) { 27134 return false; 27135 } 27136 return dom.isBlock(node.nextSibling) && !isBr$1(node.previousSibling); 27137 }; 27138 const isEmpty$1 = (dom, elm, keepBookmarks) => { 27139 const empty = dom.isEmpty(elm); 27140 if (keepBookmarks && dom.select('span[data-mce-type=bookmark]', elm).length > 0) { 27141 return false; 27142 } 27143 return empty; 27144 }; 27145 const isChildOfBody = (dom, elm) => dom.isChildOf(elm, dom.getRoot()); 27146 27147 const DOM$9 = DOMUtils.DOM; 27148 const normalizeList = (dom, list) => { 27149 const parentNode = list.parentElement; 27150 // Move UL/OL to previous LI if it's the only child of a LI 27151 if (parentNode && parentNode.nodeName === 'LI' && parentNode.firstChild === list) { 27152 const sibling = parentNode.previousSibling; 27153 if (sibling && sibling.nodeName === 'LI') { 27154 sibling.appendChild(list); 27155 if (isEmpty$1(dom, parentNode)) { 27156 DOM$9.remove(parentNode); 27157 } 27158 } 27159 else { 27160 DOM$9.setStyle(parentNode, 'listStyleType', 'none'); 27161 } 27162 } 27163 // Append OL/UL to previous LI if it's in a parent OL/UL i.e. old HTML4 27164 if (isListNode(parentNode)) { 27165 const sibling = parentNode.previousSibling; 27166 if (sibling && sibling.nodeName === 'LI') { 27167 sibling.appendChild(list); 27168 } 27169 } 27170 }; 27171 const normalizeLists = (dom, element) => { 27172 const lists = Tools.grep(dom.select('ol,ul', element)); 27173 Tools.each(lists, (list) => { 27174 normalizeList(dom, list); 27175 }); 27176 }; 27177 27178 const getNormalizedPoint = (container, offset) => { 27179 if (isTextNode$1(container)) { 27180 return { container, offset }; 27181 } 27182 const node = RangeUtils.getNode(container, offset); 27183 if (isTextNode$1(node)) { 27184 return { 27185 container: node, 27186 offset: offset >= container.childNodes.length ? node.data.length : 0 27187 }; 27188 } 27189 else if (node.previousSibling && isTextNode$1(node.previousSibling)) { 27190 return { 27191 container: node.previousSibling, 27192 offset: node.previousSibling.data.length 27193 }; 27194 } 27195 else if (node.nextSibling && isTextNode$1(node.nextSibling)) { 27196 return { 27197 container: node.nextSibling, 27198 offset: 0 27199 }; 27200 } 27201 return { container, offset }; 27202 }; 27203 const normalizeRange = (rng) => { 27204 const outRng = rng.cloneRange(); 27205 const rangeStart = getNormalizedPoint(rng.startContainer, rng.startOffset); 27206 outRng.setStart(rangeStart.container, rangeStart.offset); 27207 const rangeEnd = getNormalizedPoint(rng.endContainer, rng.endOffset); 27208 outRng.setEnd(rangeEnd.container, rangeEnd.offset); 27209 return outRng; 27210 }; 27211 27212 const listNames = ['OL', 'UL', 'DL']; 27213 const listSelector = listNames.join(','); 27214 const getParentList = (editor, node) => { 27215 const selectionStart = node || editor.selection.getStart(true); 27216 return editor.dom.getParent(selectionStart, listSelector, getClosestListHost(editor, selectionStart, editor.selection.isCollapsed())); 27217 }; 27218 const isParentListSelected = (parentList, selectedBlocks) => isNonNullable(parentList) && selectedBlocks.length === 1 && selectedBlocks[0] === parentList; 27219 const findSubLists = (parentList) => filter$5(parentList.querySelectorAll(listSelector), isListNode); 27220 const getSelectedSubLists = (editor) => { 27221 const parentList = getParentList(editor); 27222 const selectedBlocks = editor.selection.getSelectedBlocks(); 27223 if (isParentListSelected(parentList, selectedBlocks)) { 27224 return findSubLists(parentList); 27225 } 27226 else { 27227 return filter$5(selectedBlocks, (elm) => { 27228 return isListNode(elm) && parentList !== elm; 27229 }); 27230 } 27231 }; 27232 const findParentListItemsNodes = (editor, elms, isCollapsed) => { 27233 const listItemsElms = Tools.map(elms, (elm) => { 27234 const parentLi = editor.dom.getParent(elm, 'li,dd,dt', getClosestListHost(editor, elm, isCollapsed)); 27235 return parentLi ? parentLi : elm; 27236 }); 27237 return unique$1(listItemsElms); 27238 }; 27239 const getSelectedListItems = (editor) => { 27240 const selectedBlocks = editor.selection.getSelectedBlocks(); 27241 return filter$5(findParentListItemsNodes(editor, selectedBlocks, editor.selection.isCollapsed()), isListItemNode); 27242 }; 27243 const getSelectedDlItems = (editor) => filter$5(getSelectedListItems(editor), isDlItemNode); 27244 const getClosestEditingHost = (editor, elm) => { 27245 const parentTableCell = editor.dom.getParents(elm, 'TD,TH'); 27246 return parentTableCell.length > 0 ? parentTableCell[0] : editor.getBody(); 27247 }; 27248 const isListHost = (schema, node) => !isListNode(node) && !isListItemNode(node) && exists(listNames, (listName) => schema.isValidChild(node.nodeName, listName)); 27249 const getClosestListHost = (editor, elm, isCollapsed) => { 27250 const parentBlocks = editor.dom.getParents(elm, editor.dom.isBlock); 27251 const isNotForcedRootBlock = (elm) => elm.nodeName.toLowerCase() !== getForcedRootBlock(editor); 27252 const parentBlock = find$2(parentBlocks, (elm) => (!isCollapsed || isNotForcedRootBlock(elm)) && isListHost(editor.schema, elm)); 27253 return parentBlock.getOr(editor.getBody()); 27254 }; 27255 const isListInsideAnLiWithFirstAndLastNotListElement = (list) => parent(list).exists((parent) => isListItemNode(parent.dom) 27256 && firstChild(parent).exists((firstChild) => !isListNode(firstChild.dom)) 27257 && lastChild(parent).exists((lastChild) => !isListNode(lastChild.dom))); 27258 const findLastParentListNode = (editor, elm) => { 27259 const parentLists = editor.dom.getParents(elm, 'ol,ul', getClosestListHost(editor, elm, true)); 27260 return last$2(parentLists); 27261 }; 27262 const getSelectedLists = (editor) => { 27263 const firstList = findLastParentListNode(editor, editor.selection.getStart()); 27264 const subsequentLists = filter$5(editor.selection.getSelectedBlocks(), isOlUlNode); 27265 return firstList.toArray().concat(subsequentLists); 27266 }; 27267 const getParentLists = (editor) => { 27268 const elm = editor.selection.getStart(); 27269 return editor.dom.getParents(elm, 'ol,ul', getClosestListHost(editor, elm, editor.selection.isCollapsed())); 27270 }; 27271 const getSelectedListRoots = (editor) => { 27272 const selectedLists = getSelectedLists(editor); 27273 const parentLists = getParentLists(editor); 27274 return find$2(parentLists, (p) => isListInsideAnLiWithFirstAndLastNotListElement(SugarElement.fromDom(p))).fold(() => getUniqueListRoots(editor, selectedLists), (l) => [l]); 27275 }; 27276 const getUniqueListRoots = (editor, lists) => { 27277 const listRoots = map$3(lists, (list) => findLastParentListNode(editor, list).getOr(list)); 27278 return unique$1(listRoots); 27279 }; 27280 27281 const isCustomList = (list) => /\btox\-/.test(list.className); 27282 // Advlist/core/ListUtils.ts - Duplicated in Advlist plugin 27283 const isWithinNonEditable = (editor, element) => element !== null && !editor.dom.isEditable(element); 27284 const selectionIsWithinNonEditableList = (editor) => { 27285 const parentList = getParentList(editor); 27286 return isWithinNonEditable(editor, parentList) || !editor.selection.isEditable(); 27287 }; 27288 const isWithinNonEditableList$1 = (editor, element) => { 27289 const parentList = editor.dom.getParent(element, 'ol,ul,dl'); 27290 return isWithinNonEditable(editor, parentList) || !editor.selection.isEditable(); 27291 }; 27292 27293 const fireListEvent = (editor, action, element) => editor.dispatch('ListMutation', { action, element }); 27294 27295 const createTextBlock$1 = (editor, contentNode, attrs = {}) => { 27296 const dom = editor.dom; 27297 const blockElements = editor.schema.getBlockElements(); 27298 const fragment = dom.createFragment(); 27299 const blockName = getForcedRootBlock(editor); 27300 const blockAttrs = getForcedRootBlockAttrs(editor); 27301 let node; 27302 let textBlock; 27303 let hasContentNode = false; 27304 textBlock = dom.create(blockName, { 27305 ...blockAttrs, 27306 ...(attrs.style ? { style: attrs.style } : {}) 27307 }); 27308 if (!isBlock(contentNode.firstChild, blockElements)) { 27309 fragment.appendChild(textBlock); 27310 } 27311 while ((node = contentNode.firstChild)) { 27312 const nodeName = node.nodeName; 27313 if (!hasContentNode && (nodeName !== 'SPAN' || node.getAttribute('data-mce-type') !== 'bookmark')) { 27314 hasContentNode = true; 27315 } 27316 if (isBlock(node, blockElements)) { 27317 fragment.appendChild(node); 27318 textBlock = null; 27319 } 27320 else { 27321 if (!textBlock) { 27322 textBlock = dom.create(blockName, blockAttrs); 27323 fragment.appendChild(textBlock); 27324 } 27325 textBlock.appendChild(node); 27326 } 27327 } 27328 // BR is needed in empty blocks 27329 if (!hasContentNode && textBlock) { 27330 textBlock.appendChild(dom.create('br', { 'data-mce-bogus': '1' })); 27331 } 27332 return fragment; 27333 }; 27334 27335 const isList = (el) => is$1(el, 'OL,UL'); 27336 const isListItem$1 = (el) => is$1(el, 'LI'); 27337 const hasFirstChildList = (el) => firstChild(el).exists(isList); 27338 const hasLastChildList = (el) => lastChild(el).exists(isList); 27339 const canIncreaseDepthOfList = (editor, amount) => { 27340 return getListMaxDepth(editor).map((max) => max >= amount).getOr(true); 27341 }; 27342 27343 const isEntryList = (entry) => 'listAttributes' in entry; 27344 const isEntryComment = (entry) => 'isComment' in entry; 27345 const isEntryFragment = (entry) => 'isFragment' in entry; 27346 const isIndented = (entry) => entry.depth > 0; 27347 const isSelected = (entry) => entry.isSelected; 27348 const cloneItemContent = (li) => { 27349 const children = children$1(li); 27350 const content = hasLastChildList(li) ? children.slice(0, -1) : children; 27351 return map$3(content, deep); 27352 }; 27353 const createEntry = (li, depth, isSelected) => parent(li).filter(isElement$8).map((list) => ({ 27354 depth, 27355 dirty: false, 27356 isSelected, 27357 content: cloneItemContent(li), 27358 itemAttributes: clone$4(li), 27359 listAttributes: clone$4(list), 27360 listType: name(list), 27361 isInPreviousLi: false 27362 })); 27363 27364 const joinSegment = (parent, child) => { 27365 append$1(parent.item, child.list); 27366 }; 27367 const joinSegments = (segments) => { 27368 for (let i = 1; i < segments.length; i++) { 27369 joinSegment(segments[i - 1], segments[i]); 27370 } 27371 }; 27372 const appendSegments = (head$1, tail) => { 27373 lift2(last$2(head$1), head(tail), joinSegment); 27374 }; 27375 const createSegment = (scope, listType) => { 27376 const segment = { 27377 list: SugarElement.fromTag(listType, scope), 27378 item: SugarElement.fromTag('li', scope) 27379 }; 27380 append$1(segment.list, segment.item); 27381 return segment; 27382 }; 27383 const createSegments = (scope, entry, size) => { 27384 const segments = []; 27385 for (let i = 0; i < size; i++) { 27386 segments.push(createSegment(scope, isEntryList(entry) ? entry.listType : entry.parentListType)); 27387 } 27388 return segments; 27389 }; 27390 const populateSegments = (segments, entry) => { 27391 for (let i = 0; i < segments.length - 1; i++) { 27392 set$2(segments[i].item, 'list-style-type', 'none'); 27393 } 27394 last$2(segments).each((segment) => { 27395 if (isEntryList(entry)) { 27396 setAll$1(segment.list, entry.listAttributes); 27397 setAll$1(segment.item, entry.itemAttributes); 27398 } 27399 append(segment.item, entry.content); 27400 }); 27401 }; 27402 const normalizeSegment = (segment, entry) => { 27403 if (name(segment.list) !== entry.listType) { 27404 segment.list = mutate(segment.list, entry.listType); 27405 } 27406 setAll$1(segment.list, entry.listAttributes); 27407 }; 27408 const createItem = (scope, attr, content) => { 27409 const item = SugarElement.fromTag('li', scope); 27410 setAll$1(item, attr); 27411 append(item, content); 27412 return item; 27413 }; 27414 const appendItem = (segment, item) => { 27415 append$1(segment.list, item); 27416 segment.item = item; 27417 }; 27418 const writeShallow = (scope, cast, entry) => { 27419 const newCast = cast.slice(0, entry.depth); 27420 last$2(newCast).each((segment) => { 27421 if (isEntryList(entry)) { 27422 const item = createItem(scope, entry.itemAttributes, entry.content); 27423 appendItem(segment, item); 27424 normalizeSegment(segment, entry); 27425 } 27426 else if (isEntryFragment(entry)) { 27427 append(segment.item, entry.content); 27428 } 27429 else { 27430 const item = SugarElement.fromHtml(`<!--${entry.content}-->`); 27431 append$1(segment.list, item); 27432 } 27433 }); 27434 return newCast; 27435 }; 27436 const writeDeep = (scope, cast, entry) => { 27437 const segments = createSegments(scope, entry, entry.depth - cast.length); 27438 joinSegments(segments); 27439 populateSegments(segments, entry); 27440 appendSegments(cast, segments); 27441 return cast.concat(segments); 27442 }; 27443 const composeList = (scope, entries) => { 27444 let firstCommentEntryOpt = Optional.none(); 27445 const cast = foldl(entries, (cast, entry, i) => { 27446 if (!isEntryComment(entry)) { 27447 return entry.depth > cast.length ? writeDeep(scope, cast, entry) : writeShallow(scope, cast, entry); 27448 } 27449 else { 27450 // this is needed becuase if the first element of the list is a comment we would not have the data to create the new list 27451 if (i === 0) { 27452 firstCommentEntryOpt = Optional.some(entry); 27453 return cast; 27454 } 27455 return writeShallow(scope, cast, entry); 27456 } 27457 }, []); 27458 firstCommentEntryOpt.each((firstCommentEntry) => { 27459 const item = SugarElement.fromHtml(`<!--${firstCommentEntry.content}-->`); 27460 head(cast).each((fistCast) => { 27461 prepend(fistCast.list, item); 27462 }); 27463 }); 27464 return head(cast).map((segment) => segment.list); 27465 }; 27466 27467 const indentEntry = (editor, indentation, entry) => { 27468 switch (indentation) { 27469 case "Indent" /* Indentation.Indent */: 27470 if (canIncreaseDepthOfList(editor, entry.depth)) { 27471 entry.depth++; 27472 } 27473 else { 27474 return; 27475 } 27476 break; 27477 case "Outdent" /* Indentation.Outdent */: 27478 entry.depth--; 27479 break; 27480 case "Flatten" /* Indentation.Flatten */: 27481 entry.depth = 0; 27482 } 27483 entry.dirty = true; 27484 }; 27485 27486 const cloneListProperties = (target, source) => { 27487 if (isEntryList(target) && isEntryList(source)) { 27488 target.listType = source.listType; 27489 target.listAttributes = { ...source.listAttributes }; 27490 } 27491 }; 27492 const cleanListProperties = (entry) => { 27493 // Remove the start attribute if generating a new list 27494 entry.listAttributes = filter$4(entry.listAttributes, (_value, key) => key !== 'start'); 27495 }; 27496 // Closest entry above/below in the same list 27497 const closestSiblingEntry = (entries, start) => { 27498 const depth = entries[start].depth; 27499 // Ignore dirty items as they've been moved and won't have the right list data yet 27500 const matches = (entry) => entry.depth === depth && !entry.dirty; 27501 const until = (entry) => entry.depth < depth; 27502 // Check in reverse to see if there's an entry as the same depth before the current entry 27503 // but if not, then try to walk forwards as well 27504 return findUntil$1(reverse(entries.slice(0, start)), matches, until) 27505 .orThunk(() => findUntil$1(entries.slice(start + 1), matches, until)); 27506 }; 27507 const normalizeEntries = (entries) => { 27508 each$e(entries, (entry, i) => { 27509 closestSiblingEntry(entries, i).fold(() => { 27510 if (entry.dirty && isEntryList(entry)) { 27511 cleanListProperties(entry); 27512 } 27513 }, (matchingEntry) => cloneListProperties(entry, matchingEntry)); 27514 }); 27515 return entries; 27516 }; 27517 27518 const parseSingleItem = (depth, itemSelection, selectionState, item) => { 27519 if (isComment$1(item)) { 27520 return [{ 27521 depth: depth + 1, 27522 content: item.dom.nodeValue ?? '', 27523 dirty: false, 27524 isSelected: false, 27525 isComment: true 27526 }]; 27527 } 27528 itemSelection.each((selection) => { 27529 if (eq(selection.start, item)) { 27530 selectionState.set(true); 27531 } 27532 }); 27533 const currentItemEntry = createEntry(item, depth, selectionState.get()); 27534 // Update selectionState (end) 27535 itemSelection.each((selection) => { 27536 if (eq(selection.end, item)) { 27537 selectionState.set(false); 27538 } 27539 }); 27540 const childListEntries = lastChild(item) 27541 .filter(isList) 27542 .map((list) => parseList(depth, itemSelection, selectionState, list)) 27543 .getOr([]); 27544 return currentItemEntry.toArray().concat(childListEntries); 27545 }; 27546 const parseItem = (depth, itemSelection, selectionState, item) => firstChild(item).filter(isList).fold(() => parseSingleItem(depth, itemSelection, selectionState, item), (list) => { 27547 const parsedSiblings = foldl(children$1(item), (acc, liChild, i) => { 27548 if (i === 0) { 27549 return acc; 27550 } 27551 else { 27552 if (isListItem$1(liChild)) { 27553 return acc.concat(parseSingleItem(depth, itemSelection, selectionState, liChild)); 27554 } 27555 else { 27556 const fragment = { 27557 isFragment: true, 27558 depth, 27559 content: [liChild], 27560 isSelected: false, 27561 dirty: false, 27562 parentListType: name(list) 27563 }; 27564 return acc.concat(fragment); 27565 } 27566 } 27567 }, []); 27568 return parseList(depth, itemSelection, selectionState, list).concat(parsedSiblings); 27569 }); 27570 const parseList = (depth, itemSelection, selectionState, list) => bind$3(children$1
vendor: 29,534 bytes, lines 27570-28233
27570(list), (element) => { 27571 const parser = isList(element) ? parseList : parseItem; 27572 const newDepth = depth + 1; 27573 return parser(newDepth, itemSelection, selectionState, element); 27574 }); 27575 const parseLists = (lists, itemSelection) => { 27576 const selectionState = Cell(false); 27577 const initialDepth = 0; 27578 return map$3(lists, (list) => ({ 27579 sourceList: list, 27580 entries: parseList(initialDepth, itemSelection, selectionState, list) 27581 })); 27582 }; 27583 27584 const outdentedComposer = (editor, entries) => { 27585 const normalizedEntries = normalizeEntries(entries); 27586 return map$3(normalizedEntries, (entry) => { 27587 const content = !isEntryComment(entry) 27588 ? fromElements(entry.content) 27589 : fromElements([SugarElement.fromHtml(`<!--${entry.content}-->`)]); 27590 const listItemAttrs = isEntryList(entry) ? entry.itemAttributes : {}; 27591 return SugarElement.fromDom(createTextBlock$1(editor, content.dom, listItemAttrs)); 27592 }); 27593 }; 27594 const indentedComposer = (editor, entries) => { 27595 const normalizedEntries = normalizeEntries(entries); 27596 return composeList(editor.contentDocument, normalizedEntries).toArray(); 27597 }; 27598 const composeEntries = (editor, entries) => bind$3(groupBy(entries, isIndented), (entries) => { 27599 const groupIsIndented = head(entries).exists(isIndented); 27600 return groupIsIndented ? indentedComposer(editor, entries) : outdentedComposer(editor, entries); 27601 }); 27602 const indentSelectedEntries = (editor, entries, indentation) => { 27603 each$e(filter$5(entries, isSelected), (entry) => indentEntry(editor, indentation, entry)); 27604 }; 27605 const getItemSelection = (editor) => { 27606 const selectedListItems = map$3(getSelectedListItems(editor), SugarElement.fromDom); 27607 return lift2(find$2(selectedListItems, not(hasFirstChildList)), find$2(reverse(selectedListItems), not(hasFirstChildList)), (start, end) => ({ start, end })); 27608 }; 27609 const listIndentation = (editor, lists, indentation) => { 27610 const entrySets = parseLists(lists, getItemSelection(editor)); 27611 each$e(entrySets, (entrySet) => { 27612 indentSelectedEntries(editor, entrySet.entries, indentation); 27613 const composedLists = composeEntries(editor, entrySet.entries); 27614 each$e(composedLists, (composedList) => { 27615 fireListEvent(editor, indentation === "Indent" /* Indentation.Indent */ ? "IndentList" /* ListAction.IndentList */ : "OutdentList" /* ListAction.OutdentList */, composedList.dom); 27616 }); 27617 before$3(entrySet.sourceList, composedLists); 27618 remove$8(entrySet.sourceList); 27619 }); 27620 }; 27621 const canIndent$1 = (editor) => getListMaxDepth(editor).forall((max) => { 27622 const blocks = editor.selection.getSelectedBlocks(); 27623 return exists(blocks, (element) => { 27624 return closest$4(SugarElement.fromDom(element), 'li').forall((sugarElement) => ancestors(sugarElement, 'ol,ul').length <= max); 27625 }); 27626 }); 27627 27628 const DOM$8 = DOMUtils.DOM; 27629 const splitList = (editor, list, li) => { 27630 const removeAndKeepBookmarks = (targetNode) => { 27631 const parent = targetNode.parentNode; 27632 if (parent) { 27633 Tools.each(bookmarks, (node) => { 27634 parent.insertBefore(node, li.parentNode); 27635 }); 27636 } 27637 DOM$8.remove(targetNode); 27638 }; 27639 const bookmarks = DOM$8.select('span[data-mce-type="bookmark"]', list); 27640 const newBlock = createTextBlock$1(editor, li); 27641 const tmpRng = DOM$8.createRng(); 27642 tmpRng.setStartAfter(li); 27643 tmpRng.setEndAfter(list); 27644 const fragment = tmpRng.extractContents(); 27645 for (let node = fragment.firstChild; node; node = node.firstChild) { 27646 if (node.nodeName === 'LI' && editor.dom.isEmpty(node)) { 27647 DOM$8.remove(node); 27648 break; 27649 } 27650 } 27651 if (!editor.dom.isEmpty(fragment)) { 27652 DOM$8.insertAfter(fragment, list); 27653 } 27654 DOM$8.insertAfter(newBlock, list); 27655 const parent = li.parentElement; 27656 if (parent && isEmpty$1(editor.dom, parent)) { 27657 removeAndKeepBookmarks(parent); 27658 } 27659 DOM$8.remove(li); 27660 if (isEmpty$1(editor.dom, list)) { 27661 DOM$8.remove(list); 27662 } 27663 }; 27664 27665 const isDescriptionDetail = isTag('dd'); 27666 const isDescriptionTerm = isTag('dt'); 27667 const outdentDlItem = (editor, item) => { 27668 if (isDescriptionDetail(item)) { 27669 mutate(item, 'dt'); 27670 } 27671 else if (isDescriptionTerm(item)) { 27672 parentElement(item).each((dl) => splitList(editor, dl.dom, item.dom)); 27673 } 27674 }; 27675 const indentDlItem = (item) => { 27676 if (isDescriptionTerm(item)) { 27677 mutate(item, 'dd'); 27678 } 27679 }; 27680 const dlIndentation = (editor, indentation, dlItems) => { 27681 if (indentation === "Indent" /* Indentation.Indent */) { 27682 each$e(dlItems, indentDlItem); 27683 } 27684 else { 27685 each$e(dlItems, (item) => outdentDlItem(editor, item)); 27686 } 27687 }; 27688 27689 const selectionIndentation = (editor, indentation) => { 27690 const lists = fromDom$1(getSelectedListRoots(editor)); 27691 const dlItems = fromDom$1(getSelectedDlItems(editor)); 27692 let isHandled = false; 27693 if (lists.length || dlItems.length) { 27694 const bookmark = editor.selection.getBookmark(); 27695 listIndentation(editor, lists, indentation); 27696 dlIndentation(editor, indentation, dlItems); 27697 editor.selection.moveToBookmark(bookmark); 27698 editor.selection.setRng(normalizeRange(editor.selection.getRng())); 27699 editor.nodeChanged(); 27700 isHandled = true; 27701 } 27702 return isHandled; 27703 }; 27704 const handleIndentation = (editor, indentation) => !selectionIsWithinNonEditableList(editor) && selectionIndentation(editor, indentation); 27705 const indentListSelection = (editor) => handleIndentation(editor, "Indent" /* Indentation.Indent */); 27706 const outdentListSelection = (editor) => handleIndentation(editor, "Outdent" /* Indentation.Outdent */); 27707 const flattenListSelection = (editor) => handleIndentation(editor, "Flatten" /* Indentation.Flatten */); 27708 27709 const listToggleActionFromListName = (listName) => { 27710 switch (listName) { 27711 case 'UL': return "ToggleUlList" /* ListAction.ToggleUlList */; 27712 case 'OL': return "ToggleOlList" /* ListAction.ToggleOlList */; 27713 case 'DL': return "ToggleDLList" /* ListAction.ToggleDLList */; 27714 } 27715 }; 27716 27717 const updateListStyle = (dom, el, detail) => { 27718 const type = detail['list-style-type'] ? detail['list-style-type'] : null; 27719 dom.setStyle(el, 'list-style-type', type); 27720 }; 27721 const setAttribs = (elm, attrs) => { 27722 Tools.each(attrs, (value, key) => { 27723 elm.setAttribute(key, value); 27724 }); 27725 }; 27726 const updateListAttrs = (dom, el, detail) => { 27727 setAttribs(el, detail['list-attributes']); 27728 Tools.each(dom.select('li', el), (li) => { 27729 setAttribs(li, detail['list-item-attributes']); 27730 }); 27731 }; 27732 const updateListWithDetails = (dom, el, detail) => { 27733 updateListStyle(dom, el, detail); 27734 updateListAttrs(dom, el, detail); 27735 }; 27736 const removeStyles = (dom, element, styles) => { 27737 Tools.each(styles, (style) => dom.setStyle(element, style, '')); 27738 }; 27739 const isInline$1 = (editor, node) => isNonNullable(node) && !isBlock(node, editor.schema.getBlockElements()); 27740 const getEndPointNode = (editor, rng, start, root) => { 27741 let container = rng[start ? 'startContainer' : 'endContainer']; 27742 const offset = rng[start ? 'startOffset' : 'endOffset']; 27743 // Resolve node index 27744 if (isElement$1(container)) { 27745 container = container.childNodes[Math.min(offset, container.childNodes.length - 1)] || container; 27746 } 27747 if (!start && isBr$1(container.nextSibling)) { 27748 container = container.nextSibling; 27749 } 27750 const findBlockAncestor = (node) => { 27751 while (!editor.dom.isBlock(node) && node.parentNode && root !== node) { 27752 node = node.parentNode; 27753 } 27754 return node; 27755 }; 27756 // The reason why the next two if statements exist is because when the root node is a table cell (possibly some other node types) 27757 // then the highest we can go up the dom hierarchy is one level below the table cell. 27758 // So what happens when we have a bunch of inline nodes and text nodes in the table cell 27759 // and when the selection is collapsed inside one of the inline nodes then only that inline node (or text node) will be included 27760 // in the created list because that would be one level below td node and the other inline nodes won't be included. 27761 // So the fix proposed is to traverse left when looking for start node (and traverse right when looking for end node) 27762 // and keep traversing as long as we have an inline or text node (same for traversing right). 27763 // This way we end up including all the inline elements in the created list. 27764 // For more info look at #TINY-6853 27765 const findBetterContainer = (container, forward) => { 27766 const walker = new DomTreeWalker(container, findBlockAncestor(container)); 27767 const dir = forward ? 'next' : 'prev'; 27768 let node; 27769 while ((node = walker[dir]())) { 27770 if (!(isVoid(editor, node) || isZwsp$2(node.textContent) || node.textContent?.length === 0)) { 27771 return Optional.some(node); 27772 } 27773 } 27774 return Optional.none(); 27775 }; 27776 // Traverse left to include inline/text nodes 27777 if (start && isTextNode$1(container)) { 27778 if (isZwsp$2(container.textContent)) { 27779 container = findBetterContainer(container, false).getOr(container); 27780 } 27781 else { 27782 if (container.parentNode !== null && isInline$1(editor, container.parentNode)) { 27783 container = container.parentNode; 27784 } 27785 while (container.previousSibling !== null && (isInline$1(editor, container.previousSibling) || isTextNode$1(container.previousSibling))) { 27786 container = container.previousSibling; 27787 } 27788 } 27789 } 27790 // Traverse right to include inline/text nodes 27791 if (!start && isTextNode$1(container)) { 27792 if (isZwsp$2(container.textContent)) { 27793 container = findBetterContainer(container, true).getOr(container); 27794 } 27795 else { 27796 if (container.parentNode !== null && isInline$1(editor, container.parentNode)) { 27797 container = container.parentNode; 27798 } 27799 while (container.nextSibling !== null && (isInline$1(editor, container.nextSibling) || isTextNode$1(container.nextSibling))) { 27800 container = container.nextSibling; 27801 } 27802 } 27803 } 27804 while (container.parentNode !== root) { 27805 const parent = container.parentNode; 27806 if (isTextBlock(editor, container)) { 27807 return container; 27808 } 27809 if (/^(TD|TH)$/.test(parent.nodeName)) { 27810 return container; 27811 } 27812 container = parent; 27813 } 27814 return container; 27815 }; 27816 const getSelectedTextBlocks = (editor, rng, root) => { 27817 const textBlocks = []; 27818 const dom = editor.dom; 27819 const startNode = getEndPointNode(editor, rng, true, root); 27820 const endNode = getEndPointNode(editor, rng, false, root); 27821 let block; 27822 const siblings = []; 27823 for (let node = startNode; node; node = node.nextSibling) { 27824 siblings.push(node); 27825 if (node === endNode) { 27826 break; 27827 } 27828 } 27829 Tools.each(siblings, (node) => { 27830 if (isTextBlock(editor, node)) { 27831 textBlocks.push(node); 27832 block = null; 27833 return; 27834 } 27835 if (dom.isBlock(node) || isBr$1(node)) { 27836 if (isBr$1(node)) { 27837 dom.remove(node); 27838 } 27839 block = null; 27840 return; 27841 } 27842 const nextSibling = node.nextSibling; 27843 if (BookmarkManager.isBookmarkNode(node)) { 27844 if (isListNode(nextSibling) || isTextBlock(editor, nextSibling) || (!nextSibling && node.parentNode === root)) { 27845 block = null; 27846 return; 27847 } 27848 } 27849 if (!block) { 27850 block = dom.create('p'); 27851 node.parentNode?.insertBefore(block, node); 27852 textBlocks.push(block); 27853 } 27854 block.appendChild(node); 27855 }); 27856 return textBlocks; 27857 }; 27858 const hasCompatibleStyle = (dom, sib, detail) => { 27859 const sibStyle = dom.getStyle(sib, 'list-style-type'); 27860 let detailStyle = detail ? detail['list-style-type'] : ''; 27861 detailStyle = detailStyle === null ? '' : detailStyle; 27862 return sibStyle === detailStyle; 27863 }; 27864 /* 27865 Find the first element we would transform into a li-element if given no constraints. 27866 If the common ancestor is higher up than that provide it as the starting-point for the search for the root instead of the first selected element. 27867 This helps avoid issues with divs that should become li-elements are detected as the root when they should not be. 27868 */ 27869 const getRootSearchStart = (editor, range) => { 27870 const start = editor.selection.getStart(true); 27871 const startPoint = getEndPointNode(editor, range, true, editor.getBody()); 27872 if (ancestor$2(SugarElement.fromDom(startPoint), SugarElement.fromDom(range.commonAncestorContainer))) { 27873 return range.commonAncestorContainer; 27874 } 27875 else { 27876 return start; 27877 } 27878 }; 27879 const applyList = (editor, listName, detail) => { 27880 const rng = editor.selection.getRng(); 27881 let listItemName = 'LI'; 27882 const root = getClosestListHost(editor, getRootSearchStart(editor, rng), rng.collapsed); 27883 const dom = editor.dom; 27884 if (dom.getContentEditable(editor.selection.getNode()) === 'false') { 27885 return; 27886 } 27887 listName = listName.toUpperCase(); 27888 if (listName === 'DL') { 27889 listItemName = 'DT'; 27890 } 27891 const bookmark = createBookmark(rng); 27892 const selectedTextBlocks = filter$5(getSelectedTextBlocks(editor, rng, root), editor.dom.isEditable); 27893 Tools.each(selectedTextBlocks, (block) => { 27894 let listBlock; 27895 const sibling = block.previousSibling; 27896 const parent = block.parentNode; 27897 if (!isListItemNode(parent)) { 27898 if (sibling && isListNode(sibling) && sibling.nodeName === listName && hasCompatibleStyle(dom, sibling, detail)) { 27899 listBlock = sibling; 27900 block = dom.rename(block, listItemName); 27901 sibling.appendChild(block); 27902 } 27903 else { 27904 listBlock = dom.create(listName); 27905 parent.insertBefore(listBlock, block); 27906 listBlock.appendChild(block); 27907 block = dom.rename(block, listItemName); 27908 } 27909 removeStyles(dom, block, [ 27910 'margin', 'margin-right', 'margin-bottom', 'margin-left', 'margin-top', 27911 'padding', 'padding-right', 'padding-bottom', 'padding-left', 'padding-top' 27912 ]); 27913 updateListWithDetails(dom, listBlock, detail); 27914 mergeWithAdjacentLists(editor.dom, listBlock); 27915 } 27916 }); 27917 editor.selection.setRng(resolveBookmark(bookmark)); 27918 }; 27919 const isValidLists = (list1, list2) => { 27920 return isListNode(list1) && list1.nodeName === list2?.nodeName; 27921 }; 27922 const hasSameListStyle = (dom, list1, list2) => { 27923 const targetStyle = dom.getStyle(list1, 'list-style-type', true); 27924 const style = dom.getStyle(list2, 'list-style-type', true); 27925 return targetStyle === style; 27926 }; 27927 const hasSameClasses = (elm1, elm2) => { 27928 return elm1.className === elm2.className; 27929 }; 27930 const shouldMerge = (dom, list1, list2) => { 27931 return isValidLists(list1, list2) && 27932 // Note: isValidLists will ensure list1 and list2 are a HTMLElement. Unfortunately TypeScript doesn't 27933 // support type guards on multiple variables. See https://github.com/microsoft/TypeScript/issues/26916 27934 hasSameListStyle(dom, list1, list2) && 27935 hasSameClasses(list1, list2); 27936 }; 27937 const mergeWithAdjacentLists = (dom, listBlock) => { 27938 let node; 27939 let sibling = listBlock.nextSibling; 27940 if (shouldMerge(dom, listBlock, sibling)) { 27941 const liSibling = sibling; 27942 while ((node = liSibling.firstChild)) { 27943 listBlock.appendChild(node); 27944 } 27945 dom.remove(liSibling); 27946 } 27947 sibling = listBlock.previousSibling; 27948 if (shouldMerge(dom, listBlock, sibling)) { 27949 const liSibling = sibling; 27950 while ((node = liSibling.lastChild)) { 27951 listBlock.insertBefore(node, listBlock.firstChild); 27952 } 27953 dom.remove(liSibling); 27954 } 27955 }; 27956 const updateList$1 = (editor, list, listName, detail) => { 27957 if (list.nodeName !== listName) { 27958 const newList = editor.dom.rename(list, listName); 27959 updateListWithDetails(editor.dom, newList, detail); 27960 fireListEvent(editor, listToggleActionFromListName(listName), newList); 27961 } 27962 else { 27963 updateListWithDetails(editor.dom, list, detail); 27964 fireListEvent(editor, listToggleActionFromListName(listName), list); 27965 } 27966 }; 27967 const updateCustomList = (editor, list, listName, detail) => { 27968 list.classList.forEach((cls, _, classList) => { 27969 if (cls.startsWith('tox-')) { 27970 classList.remove(cls); 27971 if (classList.length === 0) { 27972 list.removeAttribute('class'); 27973 } 27974 } 27975 }); 27976 if (list.nodeName !== listName) { 27977 const newList = editor.dom.rename(list, listName); 27978 updateListWithDetails(editor.dom, newList, detail); 27979 fireListEvent(editor, listToggleActionFromListName(listName), newList); 27980 } 27981 else { 27982 updateListWithDetails(editor.dom, list, detail); 27983 fireListEvent(editor, listToggleActionFromListName(listName), list); 27984 } 27985 }; 27986 const toggleMultipleLists = (editor, parentList, lists, listName, detail) => { 27987 const parentIsList = isListNode(parentList); 27988 if (parentIsList && parentList.nodeName === listName && !hasListStyleDetail(detail) && !isCustomList(parentList)) { 27989 flattenListSelection(editor); 27990 } 27991 else { 27992 applyList(editor, listName, detail); 27993 const bookmark = createBookmark(editor.selection.getRng()); 27994 const allLists = parentIsList ? [parentList, ...lists] : lists; 27995 const updateFunction = (parentIsList && isCustomList(parentList)) ? updateCustomList : updateList$1; 27996 Tools.each(allLists, (elm) => { 27997 updateFunction(editor, elm, listName, detail); 27998 }); 27999 editor.selection.setRng(resolveBookmark(bookmark)); 28000 } 28001 }; 28002 const hasListStyleDetail = (detail) => { 28003 return 'list-style-type' in detail; 28004 }; 28005 const toggleSingleList = (editor, parentList, listName, detail) => { 28006 if (parentList === editor.getBody()) { 28007 return; 28008 } 28009 if (parentList) { 28010 if (parentList.nodeName === listName && !hasListStyleDetail(detail) && !isCustomList(parentList)) { 28011 flattenListSelection(editor); 28012 } 28013 else { 28014 const bookmark = createBookmark(editor.selection.getRng()); 28015 if (isCustomList(parentList)) { 28016 parentList.classList.forEach((cls, _, classList) => { 28017 if (cls.startsWith('tox-')) { 28018 classList.remove(cls); 28019 if (classList.length === 0) { 28020 parentList.removeAttribute('class'); 28021 } 28022 } 28023 }); 28024 } 28025 updateListWithDetails(editor.dom, parentList, detail); 28026 const newList = editor.dom.rename(parentList, listName); 28027 mergeWithAdjacentLists(editor.dom, newList); 28028 editor.selection.setRng(resolveBookmark(bookmark)); 28029 applyList(editor, listName, detail); 28030 fireListEvent(editor, listToggleActionFromListName(listName), newList); 28031 } 28032 } 28033 else { 28034 applyList(editor, listName, detail); 28035 fireListEvent(editor, listToggleActionFromListName(listName), parentList); 28036 } 28037 }; 28038 const toggleList = (editor, listName, _detail) => { 28039 const parentList = getParentList(editor); 28040 if (isWithinNonEditableList$1(editor, parentList)) { 28041 return; 28042 } 28043 const selectedSubLists = getSelectedSubLists(editor); 28044 const detail = isObject(_detail) ? _detail : {}; 28045 if (selectedSubLists.length > 0) { 28046 toggleMultipleLists(editor, parentList, selectedSubLists, listName, detail); 28047 } 28048 else { 28049 toggleSingleList(editor, parentList, listName, detail); 28050 } 28051 }; 28052 28053 const findNextCaretContainer = (editor, rng, isForward, root) => { 28054 let node = rng.startContainer; 28055 const offset = rng.startOffset; 28056 if (isTextNode$1(node) && (isForward ? offset < node.data.length : offset > 0)) { 28057 return node; 28058 } 28059 const nonEmptyBlocks = editor.schema.getNonEmptyElements(); 28060 if (isElement$1(node)) { 28061 node = RangeUtils.getNode(node, offset); 28062 } 28063 const walker = new DomTreeWalker(node, root); 28064 // Delete at <li>|<br></li> then jump over the bogus br 28065 if (isForward) { 28066 if (isBogusBr(editor.dom, node)) { 28067 walker.next(); 28068 } 28069 } 28070 const walkFn = isForward ? walker.next.bind(walker) : walker.prev2.bind(walker); 28071 while ((node = walkFn())) { 28072 if (node.nodeName === 'LI' && !node.hasChildNodes()) { 28073 return node; 28074 } 28075 if (nonEmptyBlocks[node.nodeName]) { 28076 return node; 28077 } 28078 if (isTextNode$1(node) && node.data.length > 0) { 28079 return node; 28080 } 28081 } 28082 return null; 28083 }; 28084 const hasOnlyOneBlockChild = (dom, elm) => { 28085 const childNodes = elm.childNodes; 28086 return childNodes.length === 1 && !isListNode(childNodes[0]) && dom.isBlock(childNodes[0]); 28087 }; 28088 const isUnwrappable = (node) => Optional.from(node) 28089 .map(SugarElement.fromDom) 28090 .filter(isHTMLElement$1) 28091 .exists((el) => isEditable$2(el) && !contains$2(['details'], name(el))); 28092 const unwrapSingleBlockChild = (dom, elm) => { 28093 if (hasOnlyOneBlockChild(dom, elm) && isUnwrappable(elm.firstChild)) { 28094 dom.remove(elm.firstChild, true); 28095 } 28096 }; 28097 const moveChildren = (dom, fromElm, toElm) => { 28098 let node; 28099 const targetElm = hasOnlyOneBlockChild(dom, toElm) ? toElm.firstChild : toElm; 28100 unwrapSingleBlockChild(dom, fromElm); 28101 if (!isEmpty$1(dom, fromElm, true)) { 28102 while ((node = fromElm.firstChild)) { 28103 targetElm.appendChild(node); 28104 } 28105 } 28106 }; 28107 const mergeLiElements = (dom, fromElm, toElm) => { 28108 let listNode; 28109 const ul = fromElm.parentNode; 28110 if (!isChildOfBody(dom, fromElm) || !isChildOfBody(dom, toElm)) { 28111 return; 28112 } 28113 if (isListNode(toElm.lastChild)) { 28114 listNode = toElm.lastChild; 28115 } 28116 if (ul === toElm.lastChild) { 28117 if (isBr$1(ul.previousSibling)) { 28118 dom.remove(ul.previousSibling); 28119 } 28120 } 28121 const node = toElm.lastChild; 28122 if (node && isBr$1(node) && fromElm.hasChildNodes()) { 28123 dom.remove(node); 28124 } 28125 if (isEmpty$1(dom, toElm, true)) { 28126 empty(SugarElement.fromDom(toElm)); 28127 } 28128 moveChildren(dom, fromElm, toElm); 28129 if (listNode) { 28130 toElm.appendChild(listNode); 28131 } 28132 const contains$1 = contains(SugarElement.fromDom(toElm), SugarElement.fromDom(fromElm)); 28133 const nestedLists = contains$1 ? dom.getParents(fromElm, isListNode, toElm) : []; 28134 dom.remove(fromElm); 28135 each$e(nestedLists, (list) => { 28136 if (isEmpty$1(dom, list) && list !== dom.getRoot()) { 28137 dom.remove(list); 28138 } 28139 }); 28140 }; 28141 const mergeIntoEmptyLi = (editor, fromLi, toLi) => { 28142 empty(SugarElement.fromDom(toLi)); 28143 mergeLiElements(editor.dom, fromLi, toLi); 28144 editor.selection.setCursorLocation(toLi, 0); 28145 }; 28146 const mergeForward = (editor, rng, fromLi, toLi) => { 28147 const dom = editor.dom; 28148 if (dom.isEmpty(toLi)) { 28149 mergeIntoEmptyLi(editor, fromLi, toLi); 28150 } 28151 else { 28152 const bookmark = createBookmark(rng); 28153 mergeLiElements(dom, fromLi, toLi); 28154 editor.selection.setRng(resolveBookmark(bookmark)); 28155 } 28156 }; 28157 const mergeBackward = (editor, rng, fromLi, toLi) => { 28158 const bookmark = createBookmark(rng); 28159 mergeLiElements(editor.dom, fromLi, toLi); 28160 const resolvedBookmark = resolveBookmark(bookmark); 28161 editor.selection.setRng(resolvedBookmark); 28162 }; 28163 const backspaceDeleteFromListToListCaret = (editor, isForward) => { 28164 const dom = editor.dom, selection = editor.selection; 28165 const selectionStartElm = selection.getStart(); 28166 const root = getClosestEditingHost(editor, selectionStartElm); 28167 const li = dom.getParent(selection.getStart(), 'LI', root); 28168 if (li) { 28169 const ul = li.parentElement; 28170 if (ul === editor.getBody() && isEmpty$1(dom, ul)) { 28171 return true; 28172 } 28173 const rng = normalizeRange(selection.getRng()); 28174 const otherLi = dom.getParent(findNextCaretContainer(editor, rng, isForward, root), 'LI', root); 28175 const willMergeParentIntoChild = otherLi && (isForward ? dom.isChildOf(li, otherLi) : dom.isChildOf(otherLi, li)); 28176 if (otherLi && otherLi !== li && !willMergeParentIntoChild) { 28177 editor.undoManager.transact(() => { 28178 if (isForward) { 28179 mergeForward(editor, rng, otherLi, li); 28180 } 28181 else { 28182 if (isFirstChild$1(li)) { 28183 outdentListSelection(editor); 28184 } 28185 else { 28186 mergeBackward(editor, rng, li, otherLi); 28187 } 28188 } 28189 }); 28190 return true; 28191 } 28192 else if (willMergeParentIntoChild && !isForward && otherLi !== li) { 28193 const commonAncestorParent = rng.commonAncestorContainer.parentElement; 28194 if (!commonAncestorParent || dom.isChildOf(otherLi, commonAncestorParent)) { 28195 return false; 28196 } 28197 editor.undoManager.transact(() => { 28198 const bookmark = createBookmark(rng); 28199 moveChildren(dom, commonAncestorParent, otherLi); 28200 commonAncestorParent.remove(); 28201 const resolvedBookmark = resolveBookmark(bookmark); 28202 editor.selection.setRng(resolvedBookmark); 28203 }); 28204 return true; 28205 } 28206 else if (!otherLi) { 28207 if (!isForward && rng.startOffset === 0 && rng.endOffset === 0) { 28208 editor.undoManager.transact(() => { 28209 flattenListSelection(editor); 28210 }); 28211 return true; 28212 } 28213 } 28214 } 28215 return false; 28216 }; 28217 const removeBlock = (dom, block, root) => { 28218 const parentBlock = dom.getParent(block.parentNode, dom.isBlock, root); 28219 dom.remove(block); 28220 if (parentBlock && dom.isEmpty(parentBlock)) { 28221 dom.remove(parentBlock); 28222 } 28223 }; 28224 const backspaceDeleteIntoListCaret = (editor, isForward) => { 28225 const dom = editor.dom; 28226 const selectionStartElm = editor.selection.getStart(); 28227 const root = getClosestEditingHost(editor, selectionStartElm); 28228 const block = dom.getParent(selectionStartElm, dom.isBlock, root); 28229 if (block && dom.isEmpty(block, undefined, { checkRootAsContent: true })) { 28230 const rng = normalizeRange(editor.selection.getRng()); 28231 const nextCaretContainer = findNextCaretContainer(editor, rng, isForward, root); 28232 const otherLi = dom.getParent(nextCaretContainer, 'LI', root); 28233 if (nextCaretContainer && otherLi
vendor: 5,464 bytes, lines 28233-28331
28233) { 28234 const findValidElement = (element) => contains$2(['td', 'th', 'caption'], name(element)); 28235 const findRoot = (node) => node.dom === root; 28236 const otherLiCell = closest$5(SugarElement.fromDom(otherLi), findValidElement, findRoot); 28237 const caretCell = closest$5(SugarElement.fromDom(rng.startContainer), findValidElement, findRoot); 28238 if (!equals(otherLiCell, caretCell, eq)) { 28239 return false; 28240 } 28241 editor.undoManager.transact(() => { 28242 const parentNode = otherLi.parentNode; 28243 removeBlock(dom, block, root); 28244 mergeWithAdjacentLists(dom, parentNode); 28245 editor.selection.select(nextCaretContainer, true); 28246 editor.selection.collapse(isForward); 28247 }); 28248 return true; 28249 } 28250 } 28251 return false; 28252 }; 28253 const backspaceDeleteCaret$1 = (editor, isForward) => { 28254 return backspaceDeleteFromListToListCaret(editor, isForward) || backspaceDeleteIntoListCaret(editor, isForward); 28255 }; 28256 const hasListSelection = (editor) => { 28257 const selectionStartElm = editor.selection.getStart(); 28258 const root = getClosestEditingHost(editor, selectionStartElm); 28259 const startListParent = editor.dom.getParent(selectionStartElm, 'LI,DT,DD', root); 28260 return isNonNullable(startListParent) || getSelectedListItems(editor).length > 0; 28261 }; 28262 const backspaceDeleteRange$1 = (editor, isForward) => { 28263 if (hasListSelection(editor)) { 28264 editor.undoManager.transact(() => { 28265 if (symulateDelete(editor, isForward, () => editor.execCommand('Delete'))) { 28266 normalizeLists(editor.dom, editor.getBody()); 28267 } 28268 }); 28269 return true; 28270 } 28271 return false; 28272 }; 28273 const backspaceDelete$c = (editor, isForward) => { 28274 const selection = editor.selection; 28275 return !isWithinNonEditableList$1(editor, selection.getNode()) && (selection.isCollapsed() ? 28276 backspaceDeleteCaret$1(editor, isForward) : backspaceDeleteRange$1(editor, isForward)); 28277 }; 28278 28279 const blockPosition = (block, position) => ({ 28280 block, 28281 position 28282 }); 28283 const blockBoundary = (from, to) => ({ 28284 from, 28285 to 28286 }); 28287 const getBlockPosition = (rootNode, pos) => { 28288 const rootElm = SugarElement.fromDom(rootNode); 28289 const containerElm = SugarElement.fromDom(pos.container()); 28290 return getParentBlock$2(rootElm, containerElm).map((block) => blockPosition(block, pos)); 28291 }; 28292 const isNotAncestorial = (blockBoundary) => !(contains(blockBoundary.to.block, blockBoundary.from.block) || contains(blockBoundary.from.block, blockBoundary.to.block)); 28293 const isDifferentBlocks = (blockBoundary) => !eq(blockBoundary.from.block, blockBoundary.to.block); 28294 const getClosestHost = (root, scope) => { 28295 const isRoot = (node) => eq(node, root); 28296 const isHost = (node) => isTableCell$2(node) || isContentEditableTrue$3(node.dom); 28297 return closest$5(scope, isHost, isRoot).filter(isElement$8).getOr(root); 28298 }; 28299 const hasSameHost = (rootNode, blockBoundary) => { 28300 const root = SugarElement.fromDom(rootNode); 28301 return eq(getClosestHost(root, blockBoundary.from.block), getClosestHost(root, blockBoundary.to.block)); 28302 }; 28303 const isEditable$1 = (blockBoundary) => isContentEditableFalse$a(blockBoundary.from.block.dom) === false && isContentEditableFalse$a(blockBoundary.to.block.dom) === false; 28304 const hasValidBlocks = (blockBoundary) => { 28305 const isValidBlock = (block) => isTextBlock$3(block) || hasBlockAttr(block.dom) || isListItem$2(block); 28306 return isValidBlock(blockBoundary.from.block) && isValidBlock(blockBoundary.to.block); 28307 }; 28308 const skipLastBr = (schema, rootNode, forward, blockPosition) => { 28309 if (isBr$7(blockPosition.position.getNode()) && !isEmpty$4(schema, blockPosition.block)) { 28310 return positionIn(false, blockPosition.block.dom).bind((lastPositionInBlock) => { 28311 if (lastPositionInBlock.isEqual(blockPosition.position)) { 28312 return fromPosition(forward, rootNode, lastPositionInBlock).bind((to) => getBlockPosition(rootNode, to)); 28313 } 28314 else { 28315 return Optional.some(blockPosition); 28316 } 28317 }).getOr(blockPosition); 28318 } 28319 else { 28320 return blockPosition; 28321 } 28322 }; 28323 const readFromRange = (schema, rootNode, forward, rng) => { 28324 const fromBlockPos = getBlockPosition(rootNode, CaretPosition.fromRangeStart(rng)); 28325 const toBlockPos = fromBlockPos.bind((blockPos) => fromPosition(forward, rootNode, blockPos.position).bind((to) => getBlockPosition(rootNode, to).map((blockPos) => skipLastBr(schema, rootNode, forward, blockPos)))); 28326 return lift2(fromBlockPos, toBlockPos, blockBoundary).filter((blockBoundary) => isDifferentBlocks(blockBoundary) && hasSameHost(rootNode, blockBoundary) && isEditable$1(blockBoundary) && hasValidBlocks(blockBoundary) && isNotAncestorial(blockBoundary)); 28327 }; 28328 const read$1 = (schema, rootNode, forward, rng) => rng.collapsed ? readFromRange(schema, rootNode, forward, rng) : Optional.none(); 28329 28330 const getChildrenUntilBlockBoundary = (block, schema) => { 28331 const children = children$1
vendor: 67,761 bytes, lines 28331-29698
28331(block); 28332 return findIndex$2(children, (el) => schema.isBlock(name(el))).fold(constant(children), (index) => children.slice(0, index)); 28333 }; 28334 const extractChildren = (block, schema) => { 28335 const children = getChildrenUntilBlockBoundary(block, schema); 28336 each$e(children, remove$8); 28337 return children; 28338 }; 28339 const removeEmptyRoot = (schema, rootNode, block) => { 28340 const parents = parentsAndSelf(block, rootNode); 28341 return find$2(parents.reverse(), (element) => isEmpty$4(schema, element)).each(remove$8); 28342 }; 28343 const isEmptyBefore = (schema, el) => filter$5(prevSiblings(el), (el) => !isEmpty$4(schema, el)).length === 0; 28344 const nestedBlockMerge = (rootNode, fromBlock, toBlock, schema, insertionPoint) => { 28345 if (isEmpty$4(schema, toBlock)) { 28346 fillWithPaddingBr(toBlock); 28347 return firstPositionIn(toBlock.dom); 28348 } 28349 if (isEmptyBefore(schema, insertionPoint) && isEmpty$4(schema, fromBlock)) { 28350 before$4(insertionPoint, SugarElement.fromTag('br')); 28351 } 28352 const position = prevPosition(toBlock.dom, CaretPosition.before(insertionPoint.dom)); 28353 each$e(extractChildren(fromBlock, schema), (child) => { 28354 before$4(insertionPoint, child); 28355 }); 28356 removeEmptyRoot(schema, rootNode, fromBlock); 28357 return position; 28358 }; 28359 const isInline = (schema, node) => schema.isInline(name(node)); 28360 const sidelongBlockMerge = (rootNode, fromBlock, toBlock, schema) => { 28361 if (isEmpty$4(schema, toBlock)) { 28362 if (isEmpty$4(schema, fromBlock)) { 28363 const getInlineToBlockDescendants = (el) => { 28364 const helper = (node, elements) => firstChild(node).fold(() => elements, (child) => isInline(schema, child) ? helper(child, elements.concat(shallow(child))) : elements); 28365 return helper(el, []); 28366 }; 28367 const newFromBlockDescendants = foldr(getInlineToBlockDescendants(toBlock), (element, descendant) => { 28368 wrap$2(element, descendant); 28369 return descendant; 28370 }, createPaddingBr()); 28371 empty(fromBlock); 28372 append$1(fromBlock, newFromBlockDescendants); 28373 } 28374 remove$8(toBlock); 28375 return firstPositionIn(fromBlock.dom); 28376 } 28377 const position = lastPositionIn(toBlock.dom); 28378 each$e(extractChildren(fromBlock, schema), (child) => { 28379 append$1(toBlock, child); 28380 }); 28381 removeEmptyRoot(schema, rootNode, fromBlock); 28382 return position; 28383 }; 28384 const findInsertionPoint = (toBlock, block) => { 28385 const parentsAndSelf$1 = parentsAndSelf(block, toBlock); 28386 return Optional.from(parentsAndSelf$1[parentsAndSelf$1.length - 1]); 28387 }; 28388 const getInsertionPoint = (fromBlock, toBlock) => contains(toBlock, fromBlock) ? findInsertionPoint(toBlock, fromBlock) : Optional.none(); 28389 const trimBr = (first, block) => { 28390 positionIn(first, block.dom) 28391 .bind((position) => Optional.from(position.getNode())) 28392 .map(SugarElement.fromDom) 28393 .filter(isBr$6) 28394 .each(remove$8); 28395 }; 28396 const mergeBlockInto = (rootNode, fromBlock, toBlock, schema) => { 28397 trimBr(true, fromBlock); 28398 trimBr(false, toBlock); 28399 return getInsertionPoint(fromBlock, toBlock).fold(curry(sidelongBlockMerge, rootNode, fromBlock, toBlock, schema), curry(nestedBlockMerge, rootNode, fromBlock, toBlock, schema)); 28400 }; 28401 const mergeBlocks = (rootNode, forward, block1, block2, schema) => forward ? mergeBlockInto(rootNode, block2, block1, schema) : mergeBlockInto(rootNode, block1, block2, schema); 28402 28403 const backspaceDelete$b = (editor, forward) => { 28404 const rootNode = SugarElement.fromDom(editor.getBody()); 28405 const position = read$1(editor.schema, rootNode.dom, forward, editor.selection.getRng()) 28406 .map((blockBoundary) => () => { 28407 mergeBlocks(rootNode, forward, blockBoundary.from.block, blockBoundary.to.block, editor.schema) 28408 .each((pos) => { 28409 editor.selection.setRng(pos.toRange()); 28410 }); 28411 }); 28412 return position; 28413 }; 28414 28415 const deleteRangeMergeBlocks = (rootNode, selection, schema) => { 28416 const rng = selection.getRng(); 28417 return lift2(getParentBlock$2(rootNode, SugarElement.fromDom(rng.startContainer)), getParentBlock$2(rootNode, SugarElement.fromDom(rng.endContainer)), (block1, block2) => { 28418 if (!eq(block1, block2)) { 28419 return Optional.some(() => { 28420 rng.deleteContents(); 28421 mergeBlocks(rootNode, true, block1, block2, schema).each((pos) => { 28422 selection.setRng(pos.toRange()); 28423 }); 28424 }); 28425 } 28426 else { 28427 return Optional.none(); 28428 } 28429 }).getOr(Optional.none()); 28430 }; 28431 const isRawNodeInTable = (root, rawNode) => { 28432 const node = SugarElement.fromDom(rawNode); 28433 const isRoot = curry(eq, root); 28434 return ancestor$5(node, isTableCell$2, isRoot).isSome(); 28435 }; 28436 const isSelectionInTable = (root, rng) => isRawNodeInTable(root, rng.startContainer) || isRawNodeInTable(root, rng.endContainer); 28437 const isEverythingSelected = (root, rng) => { 28438 const noPrevious = prevPosition(root.dom, CaretPosition.fromRangeStart(rng)).isNone(); 28439 const noNext = nextPosition(root.dom, CaretPosition.fromRangeEnd(rng)).isNone(); 28440 return !isSelectionInTable(root, rng) && noPrevious && noNext; 28441 }; 28442 const emptyEditor = (editor) => { 28443 return Optional.some(() => { 28444 editor.setContent(''); 28445 editor.selection.setCursorLocation(); 28446 }); 28447 }; 28448 const deleteRange$3 = (editor) => { 28449 const rootNode = SugarElement.fromDom(editor.getBody()); 28450 const rng = editor.selection.getRng(); 28451 return isEverythingSelected(rootNode, rng) ? emptyEditor(editor) : deleteRangeMergeBlocks(rootNode, editor.selection, editor.schema); 28452 }; 28453 const backspaceDelete$a = (editor, _forward) => editor.selection.isCollapsed() ? Optional.none() : deleteRange$3(editor); 28454 28455 const showCaret = (direction, editor, node, before, scrollIntoView) => 28456 // TODO: Figure out a better way to handle this dependency 28457 Optional.from(editor._selectionOverrides.showCaret(direction, node, before, scrollIntoView)); 28458 const getNodeRange = (node) => { 28459 const rng = node.ownerDocument.createRange(); 28460 rng.selectNode(node); 28461 return rng; 28462 }; 28463 const selectNode = (editor, node) => { 28464 const e = editor.dispatch('BeforeObjectSelected', { target: node }); 28465 if (e.isDefaultPrevented()) { 28466 return Optional.none(); 28467 } 28468 return Optional.some(getNodeRange(node)); 28469 }; 28470 const renderCaretAtRange = (editor, range, scrollIntoView) => { 28471 const normalizedRange = normalizeRange$2(1, editor.getBody(), range); 28472 const caretPosition = CaretPosition.fromRangeStart(normalizedRange); 28473 const caretPositionNode = caretPosition.getNode(); 28474 if (isInlineFakeCaretTarget(caretPositionNode)) { 28475 return showCaret(1, editor, caretPositionNode, !caretPosition.isAtEnd(), false); 28476 } 28477 const caretPositionBeforeNode = caretPosition.getNode(true); 28478 if (isInlineFakeCaretTarget(caretPositionBeforeNode)) { 28479 return showCaret(1, editor, caretPositionBeforeNode, false, false); 28480 } 28481 // TODO: Should render caret before/after depending on where you click on the page forces after now 28482 const ceRoot = getContentEditableRoot$1(editor.dom.getRoot(), caretPosition.getNode()); 28483 if (isInlineFakeCaretTarget(ceRoot)) { 28484 return showCaret(1, editor, ceRoot, false, scrollIntoView); 28485 } 28486 return Optional.none(); 28487 }; 28488 const renderRangeCaret = (editor, range, scrollIntoView) => range.collapsed ? renderCaretAtRange(editor, range, scrollIntoView).getOr(range) : range; 28489 28490 const isBeforeBoundary = (pos) => isBeforeContentEditableFalse(pos) || isBeforeMedia(pos); 28491 const isAfterBoundary = (pos) => isAfterContentEditableFalse(pos) || isAfterMedia(pos); 28492 const trimEmptyTextNode = (dom, node) => { 28493 if (isText$b(node) && node.data.length === 0) { 28494 dom.remove(node); 28495 } 28496 }; 28497 const deleteContentAndShowCaret = (editor, range, node, direction, forward, peekCaretPosition) => { 28498 showCaret(direction, editor, peekCaretPosition.getNode(!forward), forward, true).each((caretRange) => { 28499 // Delete the selected content 28500 if (range.collapsed) { 28501 const deleteRange = range.cloneRange(); 28502 if (forward) { 28503 deleteRange.setEnd(caretRange.startContainer, caretRange.startOffset); 28504 } 28505 else { 28506 deleteRange.setStart(caretRange.endContainer, caretRange.endOffset); 28507 } 28508 deleteRange.deleteContents(); 28509 } 28510 else { 28511 range.deleteContents(); 28512 } 28513 editor.selection.setRng(caretRange); 28514 }); 28515 trimEmptyTextNode(editor.dom, node); 28516 }; 28517 // If the caret position is next to a fake caret target element (eg cef/media) after a delete operation, then ensure a caret is added 28518 // eg. <span cE=false>a|b -> <span cE=false>|bc 28519 // Note: We also need to handle the actual deletion, as some browsers (eg IE) move the selection to the opposite side of the cef element 28520 const deleteBoundaryText = (editor, forward) => { 28521 const range = editor.selection.getRng(); 28522 if (!isText$b(range.commonAncestorContainer)) { 28523 return Optional.none(); 28524 } 28525 const direction = forward ? 1 /* HDirection.Forwards */ : -1 /* HDirection.Backwards */; 28526 const caretWalker = CaretWalker(editor.getBody()); 28527 const getNextPosFn = curry(getVisualCaretPosition, forward ? caretWalker.next : caretWalker.prev); 28528 const isBeforeFn = forward ? isBeforeBoundary : isAfterBoundary; 28529 // Get the next caret position. ie where it'll be after the delete 28530 const caretPosition = getNormalizedRangeEndPoint(direction, editor.getBody(), range); 28531 const nextCaretPosition = getNextPosFn(caretPosition); 28532 const normalizedNextCaretPosition = nextCaretPosition ? normalizePosition(forward, nextCaretPosition) : nextCaretPosition; 28533 if (!normalizedNextCaretPosition || !isMoveInsideSameBlock(caretPosition, normalizedNextCaretPosition)) { 28534 return Optional.none(); 28535 } 28536 else if (isBeforeFn(normalizedNextCaretPosition)) { 28537 return Optional.some(() => deleteContentAndShowCaret(editor, range, caretPosition.getNode(), direction, forward, normalizedNextCaretPosition)); 28538 } 28539 // Peek ahead and see if the next element is a cef/media element 28540 const peekCaretPosition = getNextPosFn(normalizedNextCaretPosition); 28541 if (peekCaretPosition && isBeforeFn(peekCaretPosition)) { 28542 if (isMoveInsideSameBlock(normalizedNextCaretPosition, peekCaretPosition)) { 28543 return Optional.some(() => deleteContentAndShowCaret(editor, range, caretPosition.getNode(), direction, forward, peekCaretPosition)); 28544 } 28545 } 28546 return Optional.none(); 28547 }; 28548 const backspaceDelete$9 = (editor, forward) => deleteBoundaryText(editor, forward); 28549 28550 const getEdgeCefPosition = (editor, atStart) => { 28551 const root = editor.getBody(); 28552 return atStart ? firstPositionIn(root).filter(isBeforeContentEditableFalse) : 28553 lastPositionIn(root).filter(isAfterContentEditableFalse); 28554 }; 28555 const isCefAtEdgeSelected = (editor) => { 28556 const rng = editor.selection.getRng(); 28557 return !rng.collapsed 28558 && (getEdgeCefPosition(editor, true).exists((pos) => pos.isEqual(CaretPosition.fromRangeStart(rng))) 28559 || getEdgeCefPosition(editor, false).exists((pos) => pos.isEqual(CaretPosition.fromRangeEnd(rng)))); 28560 }; 28561 28562 const isCompoundElement = (node) => isNonNullable(node) && (isTableCell$2(SugarElement.fromDom(node)) || isListItem$2(SugarElement.fromDom(node))); 28563 const DeleteAction = Adt.generate([ 28564 { remove: ['element'] }, 28565 { moveToElement: ['element'] }, 28566 { moveToPosition: ['position'] } 28567 ]); 28568 const isAtContentEditableBlockCaret = (forward, from) => { 28569 const elm = from.getNode(!forward); 28570 const caretLocation = forward ? 'after' : 'before'; 28571 return isElement$7(elm) && elm.getAttribute('data-mce-caret') === caretLocation; 28572 }; 28573 const isDeleteFromCefDifferentBlocks = (root, forward, from, to, schema) => { 28574 const inSameBlock = (elm) => schema.isInline(elm.nodeName.toLowerCase()) && !isInSameBlock(from, to, root); 28575 return getRelativeCefElm(!forward, from).fold(() => getRelativeCefElm(forward, to).fold(never, inSameBlock), inSameBlock); 28576 }; 28577 const deleteEmptyBlockOrMoveToCef = (schema, root, forward, from, to) => { 28578 // TODO: TINY-8865 - This may not be safe to cast as Node below and alternative solutions need to be looked into 28579 const toCefElm = to.getNode(!forward); 28580 return getParentBlock$2(SugarElement.fromDom(root), SugarElement.fromDom(from.getNode())).map((blockElm) => isEmpty$4(schema, blockElm) ? DeleteAction.remove(blockElm.dom) : DeleteAction.moveToElement(toCefElm)).orThunk(() => Optional.some(DeleteAction.moveToElement(toCefElm))); 28581 }; 28582 const findCefPosition = (root, forward, from, schema) => fromPosition(forward, root, from).bind((to) => { 28583 if (isCompoundElement(to.getNode())) { 28584 return Optional.none(); 28585 } 28586 else if (isDeleteFromCefDifferentBlocks(root, forward, from, to, schema)) { 28587 return Optional.none(); 28588 } 28589 else if (forward && isContentEditableFalse$a(to.getNode())) { 28590 return deleteEmptyBlockOrMoveToCef(schema, root, forward, from, to); 28591 } 28592 else if (!forward && isContentEditableFalse$a(to.getNode(true))) { 28593 return deleteEmptyBlockOrMoveToCef(schema, root, forward, from, to); 28594 } 28595 else if (forward && isAfterContentEditableFalse(from)) { 28596 return Optional.some(DeleteAction.moveToPosition(to)); 28597 } 28598 else if (!forward && isBeforeContentEditableFalse(from)) { 28599 return Optional.some(DeleteAction.moveToPosition(to)); 28600 } 28601 else { 28602 return Optional.none(); 28603 } 28604 }); 28605 const getContentEditableBlockAction = (forward, elm) => { 28606 if (isNullable(elm)) { 28607 return Optional.none(); 28608 } 28609 else if (forward && isContentEditableFalse$a(elm.nextSibling)) { 28610 return Optional.some(DeleteAction.moveToElement(elm.nextSibling)); 28611 } 28612 else if (!forward && isContentEditableFalse$a(elm.previousSibling)) { 28613 return Optional.some(DeleteAction.moveToElement(elm.previousSibling)); 28614 } 28615 else { 28616 return Optional.none(); 28617 } 28618 }; 28619 const skipMoveToActionFromInlineCefToContent = (root, from, deleteAction) => deleteAction.fold((elm) => Optional.some(DeleteAction.remove(elm)), (elm) => Optional.some(DeleteAction.moveToElement(elm)), (to) => { 28620 if (isInSameBlock(from, to, root)) { 28621 return Optional.none(); 28622 } 28623 else { 28624 return Optional.some(DeleteAction.moveToPosition(to)); 28625 } 28626 }); 28627 const getContentEditableAction = (root, forward, from, schema) => { 28628 if (isAtContentEditableBlockCaret(forward, from)) { 28629 return getContentEditableBlockAction(forward, from.getNode(!forward)) 28630 .orThunk(() => findCefPosition(root, forward, from, schema)); 28631 } 28632 else { 28633 return findCefPosition(root, forward, from, schema).bind((deleteAction) => skipMoveToActionFromInlineCefToContent(root, from, deleteAction)); 28634 } 28635 }; 28636 const read = (root, forward, rng, schema) => { 28637 const normalizedRange = normalizeRange$2(forward ? 1 : -1, root, rng); 28638 const from = CaretPosition.fromRangeStart(normalizedRange); 28639 const rootElement = SugarElement.fromDom(root); 28640 // TODO: TINY-8865 - This may not be safe to cast as Node below and alternative solutions need to be looked into 28641 if (!forward && isAfterContentEditableFalse(from)) { 28642 return Optional.some(DeleteAction.remove(from.getNode(true))); 28643 } 28644 else if (forward && isBeforeContentEditableFalse(from)) { 28645 return Optional.some(DeleteAction.remove(from.getNode())); 28646 } 28647 else if (!forward && isBeforeContentEditableFalse(from) && isAfterBr(rootElement, from, schema)) { 28648 return findPreviousBr(rootElement, from, schema).map((br) => DeleteAction.remove(br.getNode())); 28649 } 28650 else if (forward && isAfterContentEditableFalse(from) && isBeforeBr$1(rootElement, from, schema)) { 28651 return findNextBr(rootElement, from, schema).map((br) => DeleteAction.remove(br.getNode())); 28652 } 28653 else { 28654 return getContentEditableAction(root, forward, from, schema); 28655 } 28656 }; 28657 28658 const deleteElement$1 = (editor, forward) => (element) => { 28659 editor._selectionOverrides.hideFakeCaret(); 28660 deleteElement$2(editor, forward, SugarElement.fromDom(element)); 28661 return true; 28662 }; 28663 const moveToElement = (editor, forward) => (element) => { 28664 const pos = forward ? CaretPosition.before(element) : CaretPosition.after(element); 28665 editor.selection.setRng(pos.toRange()); 28666 return true; 28667 }; 28668 const moveToPosition = (editor) => (pos) => { 28669 editor.selection.setRng(pos.toRange()); 28670 return true; 28671 }; 28672 const getAncestorCe = (editor, node) => Optional.from(getContentEditableRoot$1(editor.getBody(), node)); 28673 const backspaceDeleteCaret = (editor, forward) => { 28674 const selectedNode = editor.selection.getNode(); // is the parent node if cursor before/after cef 28675 // Cases: 28676 // 1. CEF selectedNode -> return true 28677 // 2. CET selectedNode -> try to delete, return true if possible else false 28678 // 3. CET ancestor -> try to delete, return true if possible else false 28679 // 4. no CET/CEF ancestor -> try to delete, return true if possible else false 28680 // 5. CEF ancestor -> return true 28681 return getAncestorCe(editor, selectedNode).filter(isContentEditableFalse$a).fold(() => read(editor.getBody(), forward, editor.selection.getRng(), editor.schema).map((deleteAction) => () => deleteAction.fold(deleteElement$1(editor, forward), moveToElement(editor, forward), moveToPosition(editor))), () => Optional.some(noop)); 28682 }; 28683 const deleteOffscreenSelection = (rootElement) => { 28684 each$e(descendants(rootElement, '.mce-offscreen-selection'), remove$8); 28685 }; 28686 const backspaceDeleteRange = (editor, forward) => { 28687 const selectedNode = editor.selection.getNode(); // is the cef node if cef is selected 28688 // Cases: 28689 // 1. Table cell -> return false, as this is handled by `TableDelete` instead 28690 // 2. CEF selectedNode 28691 // a. no ancestor CET/CEF || CET ancestor -> run delete code and return true 28692 // b. CEF ancestor -> return true 28693 // 3. non-CEF selectedNode -> return false 28694 if (isContentEditableFalse$a(selectedNode) && !isTableCell$3(selectedNode)) { 28695 const hasCefAncestor = getAncestorCe(editor, selectedNode.parentNode).filter(isContentEditableFalse$a); 28696 return hasCefAncestor.fold(() => Optional.some(() => { 28697 deleteOffscreenSelection(SugarElement.fromDom(editor.getBody())); 28698 deleteElement$2(editor, forward, SugarElement.fromDom(editor.selection.getNode())); 28699 paddEmptyBody(editor); 28700 }), () => Optional.some(noop)); 28701 } 28702 if (isCefAtEdgeSelected(editor)) { 28703 return Optional.some(() => { 28704 deleteRangeContents(editor, editor.selection.getRng(), SugarElement.fromDom(editor.getBody())); 28705 }); 28706 } 28707 return Optional.none(); 28708 }; 28709 const paddEmptyElement = (editor) => { 28710 const dom = editor.dom, selection = editor.selection; 28711 const ceRoot = getContentEditableRoot$1(editor.getBody(), selection.getNode()); 28712 if (isContentEditableTrue$3(ceRoot) && dom.isBlock(ceRoot) && dom.isEmpty(ceRoot)) { 28713 const br = dom.create('br', { 'data-mce-bogus': '1' }); 28714 dom.setHTML(ceRoot, ''); 28715 ceRoot.appendChild(br); 28716 selection.setRng(CaretPosition.before(br).toRange()); 28717 } 28718 return true; 28719 }; 28720 const backspaceDelete$8 = (editor, forward) => { 28721 if (editor.selection.isCollapsed()) { 28722 return backspaceDeleteCaret(editor, forward); 28723 } 28724 else { 28725 return backspaceDeleteRange(editor, forward); 28726 } 28727 }; 28728 28729 const backspaceDelete$7 = (editor, forward) => { 28730 const dom = editor.dom; 28731 const startBlock = dom.getParent(editor.selection.getStart(), dom.isBlock); 28732 const endBlock = dom.getParent(editor.selection.getEnd(), dom.isBlock); 28733 const body = editor.getBody(); 28734 const startBlockName = startBlock?.nodeName?.toLowerCase(); 28735 // Only act on single root div that is not empty 28736 if (startBlockName === 'div' && startBlock && endBlock && startBlock === body.firstChild && endBlock === body.lastChild && !dom.isEmpty(body)) { 28737 const wrapper = startBlock.cloneNode(false); 28738 const deleteAction = () => { 28739 if (forward) { 28740 execNativeForwardDeleteCommand(editor); 28741 } 28742 else { 28743 execNativeDeleteCommand(editor); 28744 } 28745 // Div was deleted by delete operation then lets restore it 28746 if (body.firstChild !== startBlock) { 28747 const bookmark = createBookmark(editor.selection.getRng(), () => document.createElement('span')); 28748 Array.from(body.childNodes).forEach((node) => wrapper.appendChild(node)); 28749 body.appendChild(wrapper); 28750 editor.selection.setRng(resolveBookmark(bookmark)); 28751 } 28752 }; 28753 return Optional.some(deleteAction); 28754 } 28755 return Optional.none(); 28756 }; 28757 28758 const deleteCaret$2 = (editor, forward) => { 28759 const fromPos = CaretPosition.fromRangeStart(editor.selection.getRng()); 28760 return fromPosition(forward, editor.getBody(), fromPos) 28761 .filter((pos) => forward ? isBeforeImageBlock(pos) : isAfterImageBlock(pos)) 28762 .bind((pos) => getChildNodeAtRelativeOffset(forward ? 0 : -1, pos)) 28763 .map((elm) => () => editor.selection.select(elm)); 28764 }; 28765 const backspaceDelete$6 = (editor, forward) => editor.selection.isCollapsed() ? deleteCaret$2(editor, forward) : Optional.none(); 28766 28767 const isText$2 = isText$b; 28768 const startsWithCaretContainer = (node) => isText$2(node) && node.data[0] === ZWSP$1; 28769 const endsWithCaretContainer = (node) => isText$2(node) && node.data[node.data.length - 1] === ZWSP$1; 28770 const createZwsp = (node) => { 28771 const doc = node.ownerDocument ?? document; 28772 return doc.createTextNode(ZWSP$1); 28773 }; 28774 const insertBefore$1 = (node) => { 28775 if (isText$2(node.previousSibling)) { 28776 if (endsWithCaretContainer(node.previousSibling)) { 28777 return node.previousSibling; 28778 } 28779 else { 28780 node.previousSibling.appendData(ZWSP$1); 28781 return node.previousSibling; 28782 } 28783 } 28784 else if (isText$2(node)) { 28785 if (startsWithCaretContainer(node)) { 28786 return node; 28787 } 28788 else { 28789 node.insertData(0, ZWSP$1); 28790 return node; 28791 } 28792 } 28793 else { 28794 const newNode = createZwsp(node); 28795 node.parentNode?.insertBefore(newNode, node); 28796 return newNode; 28797 } 28798 }; 28799 const insertAfter$1 = (node) => { 28800 if (isText$2(node.nextSibling)) { 28801 if (startsWithCaretContainer(node.nextSibling)) { 28802 return node.nextSibling; 28803 } 28804 else { 28805 node.nextSibling.insertData(0, ZWSP$1); 28806 return node.nextSibling; 28807 } 28808 } 28809 else if (isText$2(node)) { 28810 if (endsWithCaretContainer(node)) { 28811 return node; 28812 } 28813 else { 28814 node.appendData(ZWSP$1); 28815 return node; 28816 } 28817 } 28818 else { 28819 const newNode = createZwsp(node); 28820 if (node.nextSibling) { 28821 node.parentNode?.insertBefore(newNode, node.nextSibling); 28822 } 28823 else { 28824 node.parentNode?.appendChild(newNode); 28825 } 28826 return newNode; 28827 } 28828 }; 28829 const insertInline = (before, node) => before ? insertBefore$1(node) : insertAfter$1(node); 28830 const insertInlineBefore = curry(insertInline, true); 28831 const insertInlineAfter = curry(insertInline, false); 28832 28833 const insertInlinePos = (pos, before) => { 28834 if (isText$b(pos.container())) { 28835 return insertInline(before, pos.container()); 28836 } 28837 else { 28838 // TODO: TINY-8865 - This may not be safe to cast as Node and alternative solutions need to be looked into 28839 return insertInline(before, pos.getNode()); 28840 } 28841 }; 28842 const isPosCaretContainer = (pos, caret) => { 28843 const caretNode = caret.get(); 28844 return caretNode && pos.container() === caretNode && isCaretContainerInline(caretNode); 28845 }; 28846 const renderCaret = (caret, location) => location.fold((element) => { 28847 remove$2(caret.get()); 28848 const text = insertInlineBefore(element); 28849 caret.set(text); 28850 return Optional.some(CaretPosition(text, text.length - 1)); 28851 }, (element) => // Start 28852 firstPositionIn(element).map((pos) => { 28853 if (!isPosCaretContainer(pos, caret)) { 28854 remove$2(caret.get()); 28855 const text = insertInlinePos(pos, true); 28856 caret.set(text); 28857 return CaretPosition(text, 1); 28858 } 28859 else { 28860 const node = caret.get(); 28861 return CaretPosition(node, 1); 28862 } 28863 }), (element) => // End 28864 lastPositionIn(element).map((pos) => { 28865 if (!isPosCaretContainer(pos, caret)) { 28866 remove$2(caret.get()); 28867 const text = insertInlinePos(pos, false); 28868 caret.set(text); 28869 return CaretPosition(text, text.length - 1); 28870 } 28871 else { 28872 const node = caret.get(); 28873 return CaretPosition(node, node.length - 1); 28874 } 28875 }), (element) => { 28876 remove$2(caret.get()); 28877 const text = insertInlineAfter(element); 28878 caret.set(text); 28879 return Optional.some(CaretPosition(text, 1)); 28880 }); 28881 28882 const evaluateUntil = (fns, args) => { 28883 for (let i = 0; i < fns.length; i++) { 28884 const result = fns[i].apply(null, args); 28885 if (result.isSome()) { 28886 return result; 28887 } 28888 } 28889 return Optional.none(); 28890 }; 28891 28892 const Location = Adt.generate([ 28893 { before: ['element'] }, 28894 { start: ['element'] }, 28895 { end: ['element'] }, 28896 { after: ['element'] } 28897 ]); 28898 const rescope$1 = (rootNode, node) => { 28899 const parentBlock = getParentBlock$3(node, rootNode); 28900 return parentBlock ? parentBlock : rootNode; 28901 }; 28902 const before = (isInlineTarget, rootNode, pos) => { 28903 const nPos = normalizeForwards(pos); 28904 const scope = rescope$1(rootNode, nPos.container()); 28905 return findRootInline(isInlineTarget, scope, nPos).fold(() => nextPosition(scope, nPos) 28906 .bind(curry(findRootInline, isInlineTarget, scope)) 28907 .map((inline) => Location.before(inline)), Optional.none); 28908 }; 28909 const isNotInsideFormatCaretContainer = (rootNode, elm) => getParentCaretContainer(rootNode, elm) === null; 28910 const findInsideRootInline = (isInlineTarget, rootNode, pos) => findRootInline(isInlineTarget, rootNode, pos).filter(curry(isNotInsideFormatCaretContainer, rootNode)); 28911 const start$1 = (isInlineTarget, rootNode, pos) => { 28912 const nPos = normalizeBackwards(pos); 28913 return findInsideRootInline(isInlineTarget, rootNode, nPos).bind((inline) => { 28914 const prevPos = prevPosition(inline, nPos); 28915 return prevPos.isNone() ? Optional.some(Location.start(inline)) : Optional.none(); 28916 }); 28917 }; 28918 const end = (isInlineTarget, rootNode, pos) => { 28919 const nPos = normalizeForwards(pos); 28920 return findInsideRootInline(isInlineTarget, rootNode, nPos).bind((inline) => { 28921 const nextPos = nextPosition(inline, nPos); 28922 return nextPos.isNone() ? Optional.some(Location.end(inline)) : Optional.none(); 28923 }); 28924 }; 28925 const after = (isInlineTarget, rootNode, pos) => { 28926 const nPos = normalizeBackwards(pos); 28927 const scope = rescope$1(rootNode, nPos.container()); 28928 return findRootInline(isInlineTarget, scope, nPos).fold(() => prevPosition(scope, nPos) 28929 .bind(curry(findRootInline, isInlineTarget, scope)) 28930 .map((inline) => Location.after(inline)), Optional.none); 28931 }; 28932 const isValidLocation = (location) => !isRtl(getElement(location)); 28933 const readLocation = (isInlineTarget, rootNode, pos) => { 28934 const location = evaluateUntil([ 28935 before, 28936 start$1, 28937 end, 28938 after 28939 ], [isInlineTarget, rootNode, pos]); 28940 return location.filter(isValidLocation); 28941 }; 28942 const getElement = (location) => location.fold(identity, // Before 28943 identity, // Start 28944 identity, // End 28945 identity // After 28946 ); 28947 const getName = (location) => location.fold(constant('before'), // Before 28948 constant('start'), // Start 28949 constant('end'), // End 28950 constant('after') // After 28951 ); 28952 const outside = (location) => location.fold(Location.before, // Before 28953 Location.before, // Start 28954 Location.after, // End 28955 Location.after // After 28956 ); 28957 const inside = (location) => location.fold(Location.start, // Before 28958 Location.start, // Start 28959 Location.end, // End 28960 Location.end // After 28961 ); 28962 const isEq = (location1, location2) => getName(location1) === getName(location2) && getElement(location1) === getElement(location2); 28963 const betweenInlines = (forward, isInlineTarget, rootNode, from, to, location) => lift2(findRootInline(isInlineTarget, rootNode, from), findRootInline(isInlineTarget, rootNode, to), (fromInline, toInline) => { 28964 if (fromInline !== toInline && hasSameParentBlock(rootNode, fromInline, toInline)) { 28965 // Force after since some browsers normalize and lean left into the closest inline 28966 return Location.after(forward ? fromInline : toInline); 28967 } 28968 else { 28969 return location; 28970 } 28971 }).getOr(location); 28972 const skipNoMovement = (fromLocation, toLocation) => fromLocation.fold(always, (fromLocation) => !isEq(fromLocation, toLocation)); 28973 const findLocationTraverse = (forward, isInlineTarget, rootNode, fromLocation, pos) => { 28974 const from = normalizePosition(forward, pos); 28975 const to = fromPosition(forward, rootNode, from).map(curry(normalizePosition, forward)); 28976 const location = to.fold(() => fromLocation.map(outside), (to) => readLocation(isInlineTarget, rootNode, to) 28977 .map(curry(betweenInlines, forward, isInlineTarget, rootNode, from, to)) 28978 .filter(curry(skipNoMovement, fromLocation))); 28979 return location.filter(isValidLocation); 28980 }; 28981 const findLocationSimple = (forward, location) => { 28982 if (forward) { 28983 return location.fold(compose(Optional.some, Location.start), // Before -> Start 28984 Optional.none, compose(Optional.some, Location.after), // End -> After 28985 Optional.none); 28986 } 28987 else { 28988 return location.fold(Optional.none, compose(Optional.some, Location.before), // Before <- Start 28989 Optional.none, compose(Optional.some, Location.end) // End <- After 28990 ); 28991 } 28992 }; 28993 const findLocation$1 = (forward, isInlineTarget, rootNode, pos) => { 28994 const from = normalizePosition(forward, pos); 28995 const fromLocation = readLocation(isInlineTarget, rootNode, from); 28996 return readLocation(isInlineTarget, rootNode, from).bind(curry(findLocationSimple, forward)) 28997 .orThunk(() => findLocationTraverse(forward, isInlineTarget, rootNode, fromLocation, pos)); 28998 }; 28999 29000 const hasSelectionModifyApi = (editor) => { 29001 return isFunction(editor.selection.getSel().modify); 29002 }; 29003 const moveRel = (forward, selection, pos) => { 29004 const delta = forward ? 1 : -1; 29005 selection.setRng(CaretPosition(pos.container(), pos.offset() + delta).toRange()); 29006 selection.getSel().modify('move', forward ? 'forward' : 'backward', 'word'); 29007 return true; 29008 }; 29009 const moveByWord = (forward, editor) => { 29010 const rng = editor.selection.getRng(); 29011 const pos = forward ? CaretPosition.fromRangeEnd(rng) : CaretPosition.fromRangeStart(rng); 29012 if (!hasSelectionModifyApi(editor)) { 29013 return false; 29014 } 29015 else if (forward && isBeforeInline(pos)) { 29016 return moveRel(true, editor.selection, pos); 29017 } 29018 else if (!forward && isAfterInline(pos)) { 29019 return moveRel(false, editor.selection, pos); 29020 } 29021 else { 29022 return false; 29023 } 29024 }; 29025 29026 var BreakType; 29027 (function (BreakType) { 29028 BreakType[BreakType["Br"] = 0] = "Br"; 29029 BreakType[BreakType["Block"] = 1] = "Block"; 29030 BreakType[BreakType["Wrap"] = 2] = "Wrap"; 29031 BreakType[BreakType["Eol"] = 3] = "Eol"; 29032 })(BreakType || (BreakType = {})); 29033 const flip = (direction, positions) => direction === -1 /* HDirection.Backwards */ ? reverse(positions) : positions; 29034 const walk$1 = (direction, caretWalker, pos) => direction === 1 /* HDirection.Forwards */ ? caretWalker.next(pos) : caretWalker.prev(pos); 29035 const getBreakType = (scope, direction, currentPos, nextPos) => { 29036 if (isBr$7(nextPos.getNode(direction === 1 /* HDirection.Forwards */))) { 29037 return BreakType.Br; 29038 } 29039 else if (isInSameBlock(currentPos, nextPos) === false) { 29040 return BreakType.Block; 29041 } 29042 else { 29043 return BreakType.Wrap; 29044 } 29045 }; 29046 const getPositionsUntil = (predicate, direction, scope, start) => { 29047 const caretWalker = CaretWalker(scope); 29048 let currentPos = start; 29049 const positions = []; 29050 while (currentPos) { 29051 const nextPos = walk$1(direction, caretWalker, currentPos); 29052 if (!nextPos) { 29053 break; 29054 } 29055 if (isBr$7(nextPos.getNode(false))) { 29056 if (direction === 1 /* HDirection.Forwards */) { 29057 return { positions: flip(direction, positions).concat([nextPos]), breakType: BreakType.Br, breakAt: Optional.some(nextPos) }; 29058 } 29059 else { 29060 return { positions: flip(direction, positions), breakType: BreakType.Br, breakAt: Optional.some(nextPos) }; 29061 } 29062 } 29063 if (!nextPos.isVisible()) { 29064 currentPos = nextPos; 29065 continue; 29066 } 29067 if (predicate(currentPos, nextPos)) { 29068 const breakType = getBreakType(scope, direction, currentPos, nextPos); 29069 return { positions: flip(direction, positions), breakType, breakAt: Optional.some(nextPos) }; 29070 } 29071 positions.push(nextPos); 29072 currentPos = nextPos; 29073 } 29074 return { positions: flip(direction, positions), breakType: BreakType.Eol, breakAt: Optional.none() }; 29075 }; 29076 const getAdjacentLinePositions = (direction, getPositionsUntilBreak, scope, start) => getPositionsUntilBreak(scope, start).breakAt.map((pos) => { 29077 const positions = getPositionsUntilBreak(scope, pos).positions; 29078 return direction === -1 /* HDirection.Backwards */ ? positions.concat(pos) : [pos].concat(positions); 29079 }).getOr([]); 29080 const findClosestHorizontalPositionFromPoint = (positions, x) => foldl(positions, (acc, newPos) => acc.fold(() => Optional.some(newPos), (lastPos) => lift2(head(lastPos.getClientRects()), head(newPos.getClientRects()), (lastRect, newRect) => { 29081 const lastDist = Math.abs(x - lastRect.left); 29082 const newDist = Math.abs(x - newRect.left); 29083 return newDist <= lastDist ? newPos : lastPos; 29084 }).or(acc)), Optional.none()); 29085 const findClosestHorizontalPosition = (positions, pos) => head(pos.getClientRects()).bind((targetRect) => findClosestHorizontalPositionFromPoint(positions, targetRect.left)); 29086 const getPositionsUntilPreviousLine = curry(getPositionsUntil, CaretPosition.isAbove, -1); 29087 const getPositionsUntilNextLine = curry(getPositionsUntil, CaretPosition.isBelow, 1); 29088 const getPositionsAbove = curry(getAdjacentLinePositions, -1, getPositionsUntilPreviousLine); 29089 const getPositionsBelow = curry(getAdjacentLinePositions, 1, getPositionsUntilNextLine); 29090 const isAtFirstLine = (scope, pos) => getPositionsUntilPreviousLine(scope, pos).breakAt.isNone(); 29091 const isAtLastLine = (scope, pos) => getPositionsUntilNextLine(scope, pos).breakAt.isNone(); 29092 const getFirstLinePositions = (scope) => firstPositionIn(scope).map((pos) => [pos].concat(getPositionsUntilNextLine(scope, pos).positions)).getOr([]); 29093 const getLastLinePositions = (scope) => lastPositionIn(scope).map((pos) => getPositionsUntilPreviousLine(scope, pos).positions.concat(pos)).getOr([]); 29094 const getClosestPositionAbove = (scope, pos) => findClosestHorizontalPosition(getPositionsAbove(scope, pos), pos); 29095 const getClosestPositionBelow = (scope, pos) => findClosestHorizontalPosition(getPositionsBelow(scope, pos), pos); 29096 29097 const isContentEditableFalse$4 = isContentEditableFalse$a; 29098 const distanceToRectLeft$1 = (clientRect, clientX) => Math.abs(clientRect.left - clientX); 29099 const distanceToRectRight$1 = (clientRect, clientX) => Math.abs(clientRect.right - clientX); 29100 const isNodeClientRect = (rect) => hasNonNullableKey(rect, 'node'); 29101 const findClosestClientRect = (clientRects, clientX) => reduce(clientRects, (oldClientRect, clientRect) => { 29102 const oldDistance = Math.min(distanceToRectLeft$1(oldClientRect, clientX), distanceToRectRight$1(oldClientRect, clientX)); 29103 const newDistance = Math.min(distanceToRectLeft$1(clientRect, clientX), distanceToRectRight$1(clientRect, clientX)); 29104 // cE=false has higher priority 29105 if (newDistance === oldDistance && isNodeClientRect(clientRect) && isContentEditableFalse$4(clientRect.node)) { 29106 return clientRect; 29107 } 29108 if (newDistance < oldDistance) { 29109 return clientRect; 29110 } 29111 return oldClientRect; 29112 }); 29113 29114 const getNodeClientRects = (node) => { 29115 const toArrayWithNode = (clientRects) => { 29116 return map$3(clientRects, (rect) => { 29117 const clientRect = clone$1(rect); 29118 clientRect.node = node; 29119 return clientRect; 29120 }); 29121 }; 29122 if (isElement$7(node)) { 29123 return toArrayWithNode(node.getClientRects()); 29124 } 29125 else if (isText$b(node)) { 29126 const rng = node.ownerDocument.createRange(); 29127 rng.setStart(node, 0); 29128 rng.setEnd(node, node.data.length); 29129 return toArrayWithNode(rng.getClientRects()); 29130 } 29131 else { 29132 return []; 29133 } 29134 }; 29135 const getClientRects = (nodes) => bind$3(nodes, getNodeClientRects); 29136 29137 var VDirection; 29138 (function (VDirection) { 29139 VDirection[VDirection["Up"] = -1] = "Up"; 29140 VDirection[VDirection["Down"] = 1] = "Down"; 29141 })(VDirection || (VDirection = {})); 29142 const findUntil = (direction, root, predicateFn, node) => { 29143 let currentNode = node; 29144 while ((currentNode = findNode(currentNode, direction, isEditableCaretCandidate$1, root))) { 29145 if (predicateFn(currentNode)) { 29146 return; 29147 } 29148 } 29149 }; 29150 const walkUntil = (direction, isAboveFn, isBeflowFn, root, predicateFn, caretPosition) => { 29151 let line = 0; 29152 const result = []; 29153 const add = (node) => { 29154 let clientRects = getClientRects([node]); 29155 if (direction === VDirection.Up) { 29156 clientRects = clientRects.reverse(); 29157 } 29158 for (let i = 0; i < clientRects.length; i++) { 29159 const clientRect = clientRects[i]; 29160 if (isBeflowFn(clientRect, targetClientRect)) { 29161 continue; 29162 } 29163 if (result.length > 0 && isAboveFn(clientRect, last(result))) { 29164 line++; 29165 } 29166 clientRect.line = line; 29167 if (predicateFn(clientRect)) { 29168 return true; 29169 } 29170 result.push(clientRect); 29171 } 29172 return false; 29173 }; 29174 const targetClientRect = last(caretPosition.getClientRects()); 29175 if (!targetClientRect) { 29176 return result; 29177 } 29178 const node = caretPosition.getNode(); 29179 if (node) { 29180 add(node); 29181 findUntil(direction, root, add, node); 29182 } 29183 return result; 29184 }; 29185 const aboveLineNumber = (lineNumber, clientRect) => clientRect.line > lineNumber; 29186 const isLineNumber = (lineNumber, clientRect) => clientRect.line === lineNumber; 29187 const upUntil = curry(walkUntil, VDirection.Up, isAbove$1, isBelow$1); 29188 const downUntil = curry(walkUntil, VDirection.Down, isBelow$1, isAbove$1); 29189 const getLastClientRect = (caretPosition) => { 29190 // ASSUMPTION: There should always be at least one client rect here 29191 return last(caretPosition.getClientRects()); 29192 }; 29193 const positionsUntil = (direction, root, predicateFn, node) => { 29194 const caretWalker = CaretWalker(root); 29195 let walkFn; 29196 let isBelowFn; 29197 let isAboveFn; 29198 let caretPosition; 29199 const result = []; 29200 let line = 0; 29201 if (direction === VDirection.Down) { 29202 walkFn = caretWalker.next; 29203 isBelowFn = isBelow$1; 29204 isAboveFn = isAbove$1; 29205 caretPosition = CaretPosition.after(node); 29206 } 29207 else { 29208 walkFn = caretWalker.prev; 29209 isBelowFn = isAbove$1; 29210 isAboveFn = isBelow$1; 29211 caretPosition = CaretPosition.before(node); 29212 } 29213 const targetClientRect = getLastClientRect(caretPosition); 29214 do { 29215 if (!caretPosition.isVisible()) { 29216 continue; 29217 } 29218 const rect = getLastClientRect(caretPosition); 29219 if (isAboveFn(rect, targetClientRect)) { 29220 continue; 29221 } 29222 if (result.length > 0 && isBelowFn(rect, last(result))) { 29223 line++; 29224 } 29225 const clientRect = clone$1(rect); 29226 clientRect.position = caretPosition; 29227 clientRect.line = line; 29228 if (predicateFn(clientRect)) { 29229 return result; 29230 } 29231 result.push(clientRect); 29232 } while ((caretPosition = walkFn(caretPosition))); 29233 return result; 29234 }; 29235 const isAboveLine = (lineNumber) => (clientRect) => aboveLineNumber(lineNumber, clientRect); 29236 const isLine = (lineNumber) => (clientRect) => isLineNumber(lineNumber, clientRect); 29237 29238 const moveToRange = (editor, rng) => { 29239 editor.selection.setRng(rng); 29240 // Don't reuse the original range as TinyMCE will adjust it 29241 scrollRangeIntoView(editor, editor.selection.getRng()); 29242 }; 29243 const renderRangeCaretOpt = (editor, range, scrollIntoView) => Optional.some(renderRangeCaret(editor, range, scrollIntoView)); 29244 const getAbsPositionElement = (pos, direction) => { 29245 const node = pos.getNode(direction === -1 /* HDirection.Backwards */); 29246 return isNonNullable(node) && isAbsPositionedElement(node) ? Optional.some(node) : Optional.none(); 29247 }; 29248 const elementToRange = (editor, node) => { 29249 const rng = editor.dom.createRng(); 29250 rng.selectNode(node); 29251 return rng; 29252 }; 29253 const moveHorizontally = (editor, direction, range, isBefore, isAfter, isElement) => { 29254 const forwards = direction === 1 /* HDirection.Forwards */; 29255 const caretWalker = CaretWalker(editor.getBody()); 29256 const getNextPosFn = curry(getVisualCaretPosition, forwards ? caretWalker.next : caretWalker.prev); 29257 const isBeforeFn = forwards ? isBefore : isAfter; 29258 if (!range.collapsed) { 29259 const node = getSelectedNode(range); 29260 if (isElement(node)) { 29261 if (isAbsPositionedElement(node)) { 29262 const caretPosition = getNormalizedRangeEndPoint(direction, editor.getBody(), range); 29263 return Optional.from(getNextPosFn(caretPosition)).map((next) => next.toRange()); 29264 } 29265 else { 29266 return showCaret(direction, editor, node, direction === -1 /* HDirection.Backwards */, false); 29267 } 29268 } 29269 else if (isCefAtEdgeSelected(editor)) { 29270 const newRange = range.cloneRange(); 29271 newRange.collapse(direction === -1 /* HDirection.Backwards */); 29272 return Optional.from(newRange); 29273 } 29274 } 29275 const caretPosition = getNormalizedRangeEndPoint(direction, editor.getBody(), range); 29276 if (isBeforeFn(caretPosition)) { 29277 return selectNode(editor, caretPosition.getNode(!forwards)); 29278 } 29279 let nextCaretPosition = getNextPosFn(caretPosition); 29280 const rangeIsInContainerBlock = isRangeInCaretContainerBlock(range); 29281 if (!nextCaretPosition) { 29282 return rangeIsInContainerBlock ? Optional.some(range) : Optional.none(); 29283 } 29284 else { 29285 nextCaretPosition = normalizePosition(forwards, nextCaretPosition); 29286 } 29287 if (isBeforeFn(nextCaretPosition)) { 29288 return getAbsPositionElement(nextCaretPosition, direction).fold(() => showCaret(direction, editor, nextCaretPosition?.getNode(!forwards), forwards, false), (el) => Optional.some(elementToRange(editor, el))); 29289 } 29290 // Peek ahead for handling of ab|c<span cE=false> -> abc|<span cE=false> 29291 const peekCaretPosition = getNextPosFn(nextCaretPosition); 29292 if (peekCaretPosition && isBeforeFn(peekCaretPosition)) { 29293 if (isMoveInsideSameBlock(nextCaretPosition, peekCaretPosition)) { 29294 return getAbsPositionElement(nextCaretPosition, direction).fold(() => showCaret(direction, editor, peekCaretPosition.getNode(!forwards), forwards, false), (el) => Optional.some(elementToRange(editor, el))); 29295 } 29296 } 29297 if (rangeIsInContainerBlock) { 29298 return renderRangeCaretOpt(editor, nextCaretPosition.toRange(), false); 29299 } 29300 return Optional.none(); 29301 }; 29302 const moveVertically$1 = (editor, direction, range, isBefore, isAfter, isElement) => { 29303 const caretPosition = getNormalizedRangeEndPoint(direction, editor.getBody(), range); 29304 const caretClientRect = last(caretPosition.getClientRects()); 29305 const forwards = direction === VDirection.Down; 29306 const root = editor.getBody(); 29307 if (!caretClientRect) { 29308 return Optional.none(); 29309 } 29310 if (isCefAtEdgeSelected(editor)) { 29311 const caretPosition = forwards ? CaretPosition.fromRangeEnd(range) : CaretPosition.fromRangeStart(range); 29312 const getClosestFn = !forwards ? getClosestPositionAbove : getClosestPositionBelow; 29313 return getClosestFn(root, caretPosition) 29314 .orThunk(() => Optional.from(caretPosition)) 29315 .map((pos) => pos.toRange()); 29316 } 29317 const walkerFn = forwards ? downUntil : upUntil; 29318 const linePositions = walkerFn(root, isAboveLine(1), caretPosition); 29319 const nextLinePositions = filter$5(linePositions, isLine(1)); 29320 const clientX = caretClientRect.left; 29321 const nextLineRect = findClosestClientRect(nextLinePositions, clientX); 29322 if (nextLineRect && isElement(nextLineRect.node)) { 29323 const dist1 = Math.abs(clientX - nextLineRect.left); 29324 const dist2 = Math.abs(clientX - nextLineRect.right); 29325 return showCaret(direction, editor, nextLineRect.node, dist1 < dist2, false); 29326 } 29327 let currentNode; 29328 if (isBefore(caretPosition)) { 29329 currentNode = caretPosition.getNode(); 29330 } 29331 else if (isAfter(caretPosition)) { 29332 currentNode = caretPosition.getNode(true); 29333 } 29334 else { 29335 currentNode = getSelectedNode(range); 29336 } 29337 if (currentNode) { 29338 const caretPositions = positionsUntil(direction, root, isAboveLine(1), currentNode); 29339 let closestNextLineRect = findClosestClientRect(filter$5(caretPositions, isLine(1)), clientX); 29340 if (closestNextLineRect) { 29341 return renderRangeCaretOpt(editor, closestNextLineRect.position.toRange(), false); 29342 } 29343 closestNextLineRect = last(filter$5(caretPositions, isLine(0))); 29344 if (closestNextLineRect) { 29345 return renderRangeCaretOpt(editor, closestNextLineRect.position.toRange(), false); 29346 } 29347 } 29348 if (nextLinePositions.length === 0) { 29349 return getLineEndPoint(editor, forwards).filter(forwards ? isAfter : isBefore) 29350 .map((pos) => renderRangeCaret(editor, pos.toRange(), false)); 29351 } 29352 return Optional.none(); 29353 }; 29354 const getLineEndPoint = (editor, forward) => { 29355 const rng = editor.selection.getRng(); 29356 const from = forward ? CaretPosition.fromRangeEnd(rng) : CaretPosition.fromRangeStart(rng); 29357 const host = getEditingHost(from.container(), editor.getBody()); 29358 if (forward) { 29359 const lineInfo = getPositionsUntilNextLine(host, from); 29360 return last$2(lineInfo.positions); 29361 } 29362 else { 29363 const lineInfo = getPositionsUntilPreviousLine(host, from); 29364 return head(lineInfo.positions); 29365 } 29366 }; 29367 const moveToLineEndPoint$3 = (editor, forward, isElementPosition) => getLineEndPoint(editor, forward).filter(isElementPosition).exists((pos) => { 29368 editor.selection.setRng(pos.toRange()); 29369 return true; 29370 }); 29371 29372 const setCaretPosition = (editor, pos) => { 29373 const rng = editor.dom.createRng(); 29374 rng.setStart(pos.container(), pos.offset()); 29375 rng.setEnd(pos.container(), pos.offset()); 29376 editor.selection.setRng(rng); 29377 }; 29378 const setSelected = (state, elm) => { 29379 if (state) { 29380 elm.setAttribute('data-mce-selected', 'inline-boundary'); 29381 } 29382 else { 29383 elm.removeAttribute('data-mce-selected'); 29384 } 29385 }; 29386 const renderCaretLocation = (editor, caret, location) => renderCaret(caret, location).map((pos) => { 29387 setCaretPosition(editor, pos); 29388 return location; 29389 }); 29390 const getPositionFromRange = (range, root, forward) => { 29391 const start = CaretPosition.fromRangeStart(range); 29392 if (range.collapsed) { 29393 return start; 29394 } 29395 else { 29396 const end = CaretPosition.fromRangeEnd(range); 29397 return forward ? prevPosition(root, end).getOr(end) : nextPosition(root, start).getOr(start); 29398 } 29399 }; 29400 const findLocation = (editor, caret, forward) => { 29401 const rootNode = editor.getBody(); 29402 const from = getPositionFromRange(editor.selection.getRng(), rootNode, forward); 29403 const isInlineTarget$1 = curry(isInlineTarget, editor); 29404 const location = findLocation$1(forward, isInlineTarget$1, rootNode, from); 29405 return location.bind((location) => renderCaretLocation(editor, caret, location)); 29406 }; 29407 const toggleInlines = (isInlineTarget, dom, elms) => { 29408 const inlineBoundaries = map$3(descendants(SugarElement.fromDom(dom.getRoot()), '*[data-mce-selected="inline-boundary"]'), (e) => e.dom); 29409 const selectedInlines = filter$5(inlineBoundaries, isInlineTarget); 29410 const targetInlines = filter$5(elms, isInlineTarget); 29411 each$e(difference(selectedInlines, targetInlines), curry(setSelected, false)); 29412 each$e(difference(targetInlines, selectedInlines), curry(setSelected, true)); 29413 }; 29414 const safeRemoveCaretContainer = (editor, caret) => { 29415 const caretValue = caret.get(); 29416 if (editor.selection.isCollapsed() && !editor.composing && caretValue) { 29417 const pos = CaretPosition.fromRangeStart(editor.selection.getRng()); 29418 if (CaretPosition.isTextPosition(pos) && !isAtZwsp(pos)) { 29419 setCaretPosition(editor, removeAndReposition(caretValue, pos)); 29420 caret.set(null); 29421 } 29422 } 29423 }; 29424 const renderInsideInlineCaret = (isInlineTarget, editor, caret, elms) => { 29425 if (editor.selection.isCollapsed()) { 29426 const inlines = filter$5(elms, isInlineTarget); 29427 each$e(inlines, (_inline) => { 29428 const pos = CaretPosition.fromRangeStart(editor.selection.getRng()); 29429 readLocation(isInlineTarget, editor.getBody(), pos).bind((location) => renderCaretLocation(editor, caret, location)); 29430 }); 29431 } 29432 }; 29433 const move$3 = (editor, caret, forward) => isInlineBoundariesEnabled(editor) ? findLocation(editor, caret, forward).isSome() : false; 29434 const moveWord = (forward, editor, _caret) => isInlineBoundariesEnabled(editor) ? moveByWord(forward, editor) : false; 29435 const setupSelectedState = (editor) => { 29436 const caret = Cell(null); 29437 const isInlineTarget$1 = curry(isInlineTarget, editor); 29438 editor.on('NodeChange', (e) => { 29439 if (isInlineBoundariesEnabled(editor)) { 29440 toggleInlines(isInlineTarget$1, editor.dom, e.parents); 29441 safeRemoveCaretContainer(editor, caret); 29442 renderInsideInlineCaret(isInlineTarget$1, editor, caret, e.parents); 29443 } 29444 }); 29445 return caret; 29446 }; 29447 const moveNextWord = curry(moveWord, true); 29448 const movePrevWord = curry(moveWord, false); 29449 const moveToLineEndPoint$2 = (editor, forward, caret) => { 29450 if (isInlineBoundariesEnabled(editor)) { 29451 // Try to find the line endpoint, however if one isn't found then assume we're already at the end point 29452 const linePoint = getLineEndPoint(editor, forward).getOrThunk(() => { 29453 const rng = editor.selection.getRng(); 29454 return forward ? CaretPosition.fromRangeEnd(rng) : CaretPosition.fromRangeStart(rng); 29455 }); 29456 return readLocation(curry(isInlineTarget, editor), editor.getBody(), linePoint).exists((loc) => { 29457 const outsideLoc = outside(loc); 29458 return renderCaret(caret, outsideLoc).exists((pos) => { 29459 setCaretPosition(editor, pos); 29460 return true; 29461 }); 29462 }); 29463 } 29464 else { 29465 return false; 29466 } 29467 }; 29468 29469 const rangeFromPositions = (from, to) => { 29470 const range = document.createRange(); 29471 range.setStart(from.container(), from.offset()); 29472 range.setEnd(to.container(), to.offset()); 29473 return range; 29474 }; 29475 // Checks for delete at <code>|a</code> when there is only one item left except the zwsp caret container nodes 29476 const hasOnlyTwoOrLessPositionsLeft = (elm) => lift2(firstPositionIn(elm), lastPositionIn(elm), (firstPos, lastPos) => { 29477 const normalizedFirstPos = normalizePosition(true, firstPos); 29478 const normalizedLastPos = normalizePosition(false, lastPos); 29479 return nextPosition(elm, normalizedFirstPos).forall((pos) => pos.isEqual(normalizedLastPos)); 29480 }).getOr(true); 29481 const setCaretLocation = (editor, caret) => (location) => renderCaret(caret, location).map((pos) => () => setCaretPosition(editor, pos)); 29482 const deleteFromTo = (editor, caret, from, to) => { 29483 const rootNode = editor.getBody(); 29484 const isInlineTarget$1 = curry(isInlineTarget, editor); 29485 editor.undoManager.ignore(() => { 29486 editor.selection.setRng(rangeFromPositions(from, to)); 29487 // TODO: TINY-9120 - Investigate if this should be using our custom overrides 29488 execNativeDeleteCommand(editor); 29489 readLocation(isInlineTarget$1, rootNode, CaretPosition.fromRangeStart(editor.selection.getRng())) 29490 .map(inside) 29491 .bind(setCaretLocation(editor, caret)) 29492 .each(call); 29493 }); 29494 editor.nodeChanged(); 29495 }; 29496 const rescope = (rootNode, node) => { 29497 const parentBlock = getParentBlock$3(node, rootNode); 29498 return parentBlock ? parentBlock : rootNode; 29499 }; 29500 const backspaceDeleteCollapsed = (editor, caret, forward, from) => { 29501 const rootNode = rescope(editor.getBody(), from.container()); 29502 const isInlineTarget$1 = curry(isInlineTarget, editor); 29503 const fromLocation = readLocation(isInlineTarget$1, rootNode, from); 29504 const location = fromLocation.bind((location) => { 29505 if (forward) { 29506 return location.fold(constant(Optional.some(inside(location))), // Before 29507 Optional.none, // Start 29508 constant(Optional.some(outside(location))), // End 29509 Optional.none // After 29510 ); 29511 } 29512 else { 29513 return location.fold(Optional.none, // Before 29514 constant(Optional.some(outside(location))), // Start 29515 Optional.none, // End 29516 constant(Optional.some(inside(location))) // After 29517 ); 29518 } 29519 }); 29520 return location.map(setCaretLocation(editor, caret)) 29521 .getOrThunk(() => { 29522 const toPosition = navigate(forward, rootNode, from); 29523 const toLocation = toPosition.bind((pos) => readLocation(isInlineTarget$1, rootNode, pos)); 29524 return lift2(fromLocation, toLocation, () => findRootInline(isInlineTarget$1, rootNode, from).bind((elm) => { 29525 if (hasOnlyTwoOrLessPositionsLeft(elm)) { 29526 return Optional.some(() => { 29527 deleteElement$2(editor, forward, SugarElement.fromDom(elm)); 29528 }); 29529 } 29530 else { 29531 return Optional.none(); 29532 } 29533 })).getOrThunk(() => toLocation.bind(() => toPosition.map((to) => { 29534 return () => { 29535 if (forward) { 29536 deleteFromTo(editor, caret, from, to); 29537 } 29538 else { 29539 deleteFromTo(editor, caret, to, from); 29540 } 29541 }; 29542 }))); 29543 }); 29544 }; 29545 const backspaceDelete$5 = (editor, caret, forward) => { 29546 if (editor.selection.isCollapsed() && isInlineBoundariesEnabled(editor)) { 29547 const from = CaretPosition.fromRangeStart(editor.selection.getRng()); 29548 return backspaceDeleteCollapsed(editor, caret, forward, from); 29549 } 29550 return Optional.none(); 29551 }; 29552 29553 const hasMultipleChildren = (elm) => childNodesCount(elm) > 1; 29554 const getParentsUntil = (editor, pred) => { 29555 const rootElm = SugarElement.fromDom(editor.getBody()); 29556 const startElm = SugarElement.fromDom(editor.selection.getStart()); 29557 const parents = parentsAndSelf(startElm, rootElm); 29558 return findIndex$2(parents, pred).fold(constant(parents), (index) => parents.slice(0, index)); 29559 }; 29560 const hasOnlyOneChild = (elm) => childNodesCount(elm) === 1; 29561 const getParentInlinesUntilMultichildInline = (editor) => getParentsUntil(editor, (elm) => editor.schema.isBlock(name(elm)) || hasMultipleChildren(elm)); 29562 const getParentInlines = (editor) => getParentsUntil(editor, (el) => editor.schema.isBlock(name(el))); 29563 const getFormatNodes = (editor, parentInlines) => { 29564 const isFormatElement$1 = curry(isFormatElement, editor); 29565 return bind$3(parentInlines, (elm) => isFormatElement$1(elm) ? [elm.dom] : []); 29566 }; 29567 const getFormatNodesAtStart = (editor) => { 29568 const parentInlines = getParentInlines(editor); 29569 return getFormatNodes(editor, parentInlines); 29570 }; 29571 const deleteLastPosition = (forward, editor, target, parentInlines) => { 29572 const formatNodes = getFormatNodes(editor, parentInlines); 29573 if (formatNodes.length === 0) { 29574 deleteElement$2(editor, forward, target); 29575 } 29576 else { 29577 const pos = replaceWithCaretFormat(target.dom, formatNodes); 29578 editor.selection.setRng(pos.toRange()); 29579 } 29580 }; 29581 const deleteCaret$1 = (editor, forward) => { 29582 const parentInlines = filter$5(getParentInlinesUntilMultichildInline(editor), hasOnlyOneChild); 29583 return last$2(parentInlines).bind((target) => { 29584 const fromPos = CaretPosition.fromRangeStart(editor.selection.getRng()); 29585 if (willDeleteLastPositionInElement(forward, fromPos, target.dom) && !isEmptyCaretFormatElement(target)) { 29586 return Optional.some(() => deleteLastPosition(forward, editor, target, parentInlines)); 29587 } 29588 else { 29589 return Optional.none(); 29590 } 29591 }); 29592 }; 29593 const isBrInEmptyElement = (editor, elm) => { 29594 const parentElm = elm.parentElement; 29595 return isBr$7(elm) && !isNull(parentElm) && editor.dom.isEmpty(parentElm); 29596 }; 29597 const isEmptyCaret = (elm) => isEmptyCaretFormatElement(SugarElement.fromDom(elm)); 29598 const createCaretFormatAtStart = (editor, formatNodes) => { 29599 const startElm = editor.selection.getStart(); 29600 // replace <br> in empty node or existing caret at start if applicable 29601 // otherwise create new caret format at start 29602 const pos = isBrInEmptyElement(editor, startElm) || isEmptyCaret(startElm) 29603 ? replaceWithCaretFormat(startElm, formatNodes) 29604 : createCaretFormatAtStart$1(editor.selection.getRng(), formatNodes); 29605 editor.selection.setRng(pos.toRange()); 29606 }; 29607 const updateCaretFormat = (editor, updateFormats) => { 29608 // Create a caret format at cursor containing missing formats to ensure all formats 29609 // that are supposed to be retained are retained 29610 const missingFormats = difference(updateFormats, getFormatNodesAtStart(editor)); 29611 if (missingFormats.length > 0) { 29612 createCaretFormatAtStart(editor, missingFormats); 29613 } 29614 }; 29615 const rangeStartsAtTextContainer = (rng) => isText$b(rng.startContainer); 29616 const rangeStartsAtStartOfTextContainer = (rng) => rng.startOffset === 0 && rangeStartsAtTextContainer(rng); 29617 const rangeStartParentIsFormatElement = (editor, rng) => { 29618 const startParent = rng.startContainer.parentElement; 29619 return !isNull(startParent) && isFormatElement(editor, SugarElement.fromDom(startParent)); 29620 }; 29621 const rangeStartAndEndHaveSameParent = (rng) => { 29622 const startParent = rng.startContainer.parentNode; 29623 const endParent = rng.endContainer.parentNode; 29624 return !isNull(startParent) && !isNull(endParent) && startParent.isEqualNode(endParent); 29625 }; 29626 const rangeEndsAtEndOfEndContainer = (rng) => { 29627 const endContainer = rng.endContainer; 29628 return rng.endOffset === (isText$b(endContainer) ? endContainer.length : endContainer.childNodes.length); 29629 }; 29630 const rangeEndsAtEndOfStartContainer = (rng) => rangeStartAndEndHaveSameParent(rng) && rangeEndsAtEndOfEndContainer(rng); 29631 const rangeEndsAfterEndOfStartContainer = (rng) => !rng.endContainer.isEqualNode(rng.commonAncestorContainer); 29632 const rangeEndsAtOrAfterEndOfStartContainer = (rng) => rangeEndsAtEndOfStartContainer(rng) || rangeEndsAfterEndOfStartContainer(rng); 29633 const requiresDeleteRangeOverride = (editor) => { 29634 const rng = editor.selection.getRng(); 29635 return rangeStartsAtStartOfTextContainer(rng) && rangeStartParentIsFormatElement(editor, rng) && rangeEndsAtOrAfterEndOfStartContainer(rng); 29636 }; 29637 const deleteRange$2 = (editor) => { 29638 if (requiresDeleteRangeOverride(editor)) { 29639 const formatNodes = getFormatNodesAtStart(editor); 29640 return Optional.some(() => { 29641 execNativeDeleteCommand(editor); 29642 updateCaretFormat(editor, formatNodes); 29643 }); 29644 } 29645 else { 29646 return Optional.none(); 29647 } 29648 }; 29649 const backspaceDelete$4 = (editor, forward) => editor.selection.isCollapsed() ? deleteCaret$1(editor, forward) : deleteRange$2(editor); 29650 const hasAncestorInlineCaret = (elm, schema) => ancestor$3(elm, (node) => isCaretNode(node.dom), (el) => schema.isBlock(name(el))); 29651 const hasAncestorInlineCaretAtStart = (editor) => hasAncestorInlineCaret(SugarElement.fromDom(editor.selection.getStart()), editor.schema); 29652 const requiresRefreshCaretOverride = (editor) => { 29653 const rng = editor.selection.getRng(); 29654 return rng.collapsed && (rangeStartsAtTextContainer(rng) || editor.dom.isEmpty(rng.startContainer)) && !hasAncestorInlineCaretAtStart(editor); 29655 }; 29656 const refreshCaret = (editor) => { 29657 if (requiresRefreshCaretOverride(editor)) { 29658 createCaretFormatAtStart(editor, []); 29659 } 29660 return true; 29661 }; 29662 29663 const deleteElement = (editor, forward, element) => { 29664 if (isNonNullable(element)) { 29665 return Optional.some(() => { 29666 editor._selectionOverrides.hideFakeCaret(); 29667 deleteElement$2(editor, forward, SugarElement.fromDom(element)); 29668 }); 29669 } 29670 else { 29671 return Optional.none(); 29672 } 29673 }; 29674 const deleteCaret = (editor, forward) => { 29675 const isNearMedia = forward ? isBeforeMedia : isAfterMedia; 29676 const direction = forward ? 1 /* HDirection.Forwards */ : -1 /* HDirection.Backwards */; 29677 const fromPos = getNormalizedRangeEndPoint(direction, editor.getBody(), editor.selection.getRng()); 29678 if (isNearMedia(fromPos)) { 29679 return deleteElement(editor, forward, fromPos.getNode(!forward)); 29680 } 29681 else { 29682 return Optional.from(normalizePosition(forward, fromPos)) 29683 .filter((pos) => isNearMedia(pos) && isMoveInsideSameBlock(fromPos, pos)) 29684 .bind((pos) => deleteElement(editor, forward, pos.getNode(!forward))); 29685 } 29686 }; 29687 const deleteRange$1 = (editor, forward) => { 29688 const selectedNode = editor.selection.getNode(); 29689 return isMedia$2(selectedNode) ? deleteElement(editor, forward, selectedNode) : Optional.none(); 29690 }; 29691 const backspaceDelete$3 = (editor, forward) => editor.selection.isCollapsed() ? deleteCaret(editor, forward) : deleteRange$1(editor, forward); 29692 29693 const isEditable = (target) => closest$5(target, (elm) => isContentEditableTrue$3(elm.dom) || isContentEditableFalse$a(elm.dom)) 29694 .exists((elm) => isContentEditableTrue$3(elm.dom)); 29695 const parseIndentValue = (value) => toInt(value ?? '').getOr(0); 29696 const getIndentStyleName = (useMargin, element) => { 29697 const indentStyleName = useMargin || isTable$1(element) ? 'margin' : 'padding'; 29698 const suffix = get$
vendor: 3,375 bytes, lines 29698-29765
296987(element, 'direction') === 'rtl' ? '-right' : '-left'; 29699 return indentStyleName + suffix; 29700 }; 29701 const indentElement = (dom, command, useMargin, value, unit, element) => { 29702 const indentStyleName = getIndentStyleName(useMargin, SugarElement.fromDom(element)); 29703 const parsedValue = parseIndentValue(dom.getStyle(element, indentStyleName)); 29704 if (command === 'outdent') { 29705 const styleValue = Math.max(0, parsedValue - value); 29706 dom.setStyle(element, indentStyleName, styleValue ? styleValue + unit : ''); 29707 } 29708 else { 29709 const styleValue = (parsedValue + value) + unit; 29710 dom.setStyle(element, indentStyleName, styleValue); 29711 } 29712 }; 29713 const validateBlocks = (editor, blocks) => forall(blocks, (block) => { 29714 const indentStyleName = getIndentStyleName(shouldIndentUseMargin(editor), block); 29715 const intentValue = getRaw$1(block, indentStyleName).map(parseIndentValue).getOr(0); 29716 const contentEditable = editor.dom.getContentEditable(block.dom); 29717 return contentEditable !== 'false' && intentValue > 0; 29718 }); 29719 const canOutdent = (editor) => { 29720 const blocks = getBlocksToIndent(editor); 29721 return !editor.mode.isReadOnly() && (blocks.length > 1 || validateBlocks(editor, blocks)); 29722 }; 29723 const canIndent = (editor) => !editor.mode.isReadOnly() && canIndent$1(editor); 29724 const isListComponent = (el) => isList$1(el) || isListItem$2(el); 29725 const parentIsListComponent = (el) => parent(el).exists(isListComponent); 29726 const getBlocksToIndent = (editor) => filter$5(fromDom$1(editor.selection.getSelectedBlocks()), (el) => !isListComponent(el) && !parentIsListComponent(el) && isEditable(el)); 29727 const handle = (editor, command) => { 29728 if (editor.mode.isReadOnly()) { 29729 return; 29730 } 29731 const { dom } = editor; 29732 const indentation = getIndentation(editor); 29733 const indentUnit = /[a-z%]+$/i.exec(indentation)?.[0] ?? 'px'; 29734 const indentValue = parseIndentValue(indentation); 29735 const useMargin = shouldIndentUseMargin(editor); 29736 each$e(getBlocksToIndent(editor), (block) => { 29737 indentElement(dom, command, useMargin, indentValue, indentUnit, block.dom); 29738 }); 29739 if (command === 'indent') { 29740 indentListSelection(editor); 29741 } 29742 else { 29743 outdentListSelection(editor); 29744 } 29745 }; 29746 const indent = (editor) => handle(editor, 'indent'); 29747 const outdent = (editor) => handle(editor, 'outdent'); 29748 29749 const backspaceDelete$2 = (editor) => { 29750 if (editor.selection.isCollapsed() && canOutdent(editor)) { 29751 const dom = editor.dom; 29752 const rng = editor.selection.getRng(); 29753 const pos = CaretPosition.fromRangeStart(rng); 29754 const block = dom.getParent(rng.startContainer, dom.isBlock); 29755 if (block !== null && isAtStartOfBlock(SugarElement.fromDom(block), pos, editor.schema)) { 29756 return Optional.some(() => outdent(editor)); 29757 } 29758 } 29759 return Optional.none(); 29760 }; 29761 29762 const deleteRange = (editor, forward) => { 29763 const rng = normalize(editor.selection.getRng()); 29764 return isSelectionOverWholeHTMLElement(rng) 29765 ? Optional.some(() => deleteElement$2(editor, forward, SugarElement.fromDom(editor.selection.getNode()
vendor: 25,699 bytes, lines 29765-30361
29765))) 29766 : Optional.none(); 29767 }; 29768 const backspaceDelete$1 = (editor, forward) => editor.selection.isCollapsed() ? Optional.none() : deleteRange(editor, forward); 29769 29770 const findAction = (editor, caret, forward) => findMap([ 29771 backspaceDelete$2, 29772 backspaceDelete$8, 29773 backspaceDelete$9, 29774 (editor, forward) => backspaceDelete$5(editor, caret, forward), 29775 backspaceDelete$b, 29776 backspaceDelete$d, 29777 backspaceDelete$6, 29778 backspaceDelete$3, 29779 backspaceDelete$a, 29780 backspaceDelete$4, 29781 backspaceDelete$7, 29782 backspaceDelete$1, 29783 ], (item) => item(editor, forward)) 29784 .filter((_) => editor.selection.isEditable()); 29785 const deleteCommand = (editor, caret) => { 29786 const result = findAction(editor, caret, false); 29787 result.fold(() => { 29788 // We can't use an `execEditorDeleteCommand` here, otherwise we'd get 29789 // possible infinite recursion (as it would trigger `deleteCommand` again) 29790 if (editor.selection.isEditable()) { 29791 execNativeDeleteCommand(editor); 29792 paddEmptyBody(editor); 29793 } 29794 }, call); 29795 if (hasListSelection(editor)) { 29796 normalizeLists(editor.dom, editor.getBody()); 29797 } 29798 }; 29799 const forwardDeleteCommand = (editor, caret) => { 29800 const result = findAction(editor, caret, true); 29801 result.fold(() => { 29802 if (editor.selection.isEditable()) { 29803 execNativeForwardDeleteCommand(editor); 29804 } 29805 }, call); 29806 if (hasListSelection(editor)) { 29807 normalizeLists(editor.dom, editor.getBody()); 29808 } 29809 }; 29810 const setup$v = (editor, caret) => { 29811 editor.addCommand('delete', () => { 29812 deleteCommand(editor, caret); 29813 }); 29814 editor.addCommand('forwardDelete', () => { 29815 forwardDeleteCommand(editor, caret); 29816 }); 29817 }; 29818 29819 // This is based heavily on Alloy's TapEvent.ts, just modified to use TinyMCE's event system. 29820 const SIGNIFICANT_MOVE = 5; 29821 const LONGPRESS_DELAY = 400; 29822 const getTouch = (event) => { 29823 if (event.touches === undefined || event.touches.length !== 1) { 29824 return Optional.none(); 29825 } 29826 return Optional.some(event.touches[0]); 29827 }; 29828 const isFarEnough = (touch, data) => { 29829 const distX = Math.abs(touch.clientX - data.x); 29830 const distY = Math.abs(touch.clientY - data.y); 29831 return distX > SIGNIFICANT_MOVE || distY > SIGNIFICANT_MOVE; 29832 }; 29833 const setup$u = (editor) => { 29834 const startData = value$1(); 29835 const longpressFired = Cell(false); 29836 const debounceLongpress = last$1((e) => { 29837 editor.dispatch('longpress', { ...e, type: 'longpress' }); 29838 longpressFired.set(true); 29839 }, LONGPRESS_DELAY); 29840 editor.on('touchstart', (e) => { 29841 getTouch(e).each((touch) => { 29842 debounceLongpress.cancel(); 29843 const data = { 29844 x: touch.clientX, 29845 y: touch.clientY, 29846 target: e.target 29847 }; 29848 debounceLongpress.throttle(e); 29849 longpressFired.set(false); 29850 startData.set(data); 29851 }); 29852 }, true); 29853 editor.on('touchmove', (e) => { 29854 debounceLongpress.cancel(); 29855 getTouch(e).each((touch) => { 29856 startData.on((data) => { 29857 if (isFarEnough(touch, data)) { 29858 startData.clear(); 29859 longpressFired.set(false); 29860 editor.dispatch('longpresscancel'); 29861 } 29862 }); 29863 }); 29864 }, true); 29865 editor.on('touchend touchcancel', (e) => { 29866 debounceLongpress.cancel(); 29867 if (e.type === 'touchcancel') { 29868 return; 29869 } 29870 // Cancel the touchend event if a longpress was fired, otherwise fire the tap event 29871 startData.get() 29872 .filter((data) => data.target.isEqualNode(e.target)) 29873 .each(() => { 29874 if (longpressFired.get()) { 29875 e.preventDefault(); 29876 } 29877 else { 29878 editor.dispatch('tap', { ...e, type: 'tap' }); 29879 } 29880 }); 29881 }, true); 29882 }; 29883 29884 /** 29885 * Makes sure that everything gets wrapped in paragraphs. 29886 * 29887 * @private 29888 * @class tinymce.ForceBlocks 29889 */ 29890 const isBlockElement = (blockElements, node) => has$2(blockElements, node.nodeName); 29891 const isValidTarget = (schema, node) => { 29892 if (isText$b(node)) { 29893 return true; 29894 } 29895 else if (isElement$7(node)) { 29896 return !isBlockElement(schema.getBlockElements(), node) && !isBookmarkNode$1(node) && 29897 !isTransparentBlock(schema, node) && !isNonHtmlElementRoot(node) && !isTemplate(node); 29898 } 29899 else { 29900 return false; 29901 } 29902 }; 29903 const hasBlockParent = (blockElements, root, node) => { 29904 return exists(parents(SugarElement.fromDom(node), SugarElement.fromDom(root)), (elm) => { 29905 return isBlockElement(blockElements, elm.dom); 29906 }); 29907 }; 29908 const shouldRemoveTextNode = (blockElements, node) => { 29909 if (isText$b(node)) { 29910 if (node.data.length === 0) { 29911 return true; 29912 } 29913 else if (/^\s+$/.test(node.data)) { 29914 return !node.nextSibling || isBlockElement(blockElements, node.nextSibling) || isNonHtmlElementRoot(node.nextSibling); 29915 } 29916 } 29917 return false; 29918 }; 29919 const createRootBlock = (editor) => editor.dom.create(getForcedRootBlock(editor), getForcedRootBlockAttrs(editor)); 29920 const addRootBlocks = (editor) => { 29921 const dom = editor.dom, selection = editor.selection; 29922 const schema = editor.schema; 29923 const blockElements = schema.getBlockElements(); 29924 const startNode = selection.getStart(); 29925 const rootNode = editor.getBody(); 29926 let rootBlockNode; 29927 let tempNode; 29928 let bm = null; 29929 const forcedRootBlock = getForcedRootBlock(editor); 29930 if (!startNode || !isElement$7(startNode)) { 29931 return; 29932 } 29933 const rootNodeName = rootNode.nodeName.toLowerCase(); 29934 if (!schema.isValidChild(rootNodeName, forcedRootBlock.toLowerCase()) || hasBlockParent(blockElements, rootNode, startNode)) { 29935 return; 29936 } 29937 // Firefox will automatically remove the last BR if you insert nodes next to it and add a BR back if you remove those siblings 29938 // and since the bookmark code inserts temporary nodes an new BR will be constantly removed and added and triggering a selection 29939 // change causing an infinite recursion. So we treat this special case on it's own. 29940 if (rootNode.firstChild === rootNode.lastChild && isBr$7(rootNode.firstChild)) { 29941 rootBlockNode = createRootBlock(editor); 29942 rootBlockNode.appendChild(createPaddingBr().dom); 29943 rootNode.replaceChild(rootBlockNode, rootNode.firstChild); 29944 editor.selection.setCursorLocation(rootBlockNode, 0); 29945 editor.nodeChanged(); 29946 return; 29947 } 29948 // Wrap non block elements and text nodes 29949 let node = rootNode.firstChild; 29950 while (node) { 29951 if (isElement$7(node)) { 29952 updateElement(schema, node); 29953 } 29954 if (isValidTarget(schema, node)) { 29955 // Remove empty text nodes and nodes containing only whitespace 29956 if (shouldRemoveTextNode(blockElements, node)) { 29957 tempNode = node; 29958 node = node.nextSibling; 29959 dom.remove(tempNode); 29960 continue; 29961 } 29962 if (!rootBlockNode) { 29963 if (!bm && editor.hasFocus()) { 29964 bm = createBookmark(editor.selection.getRng(), () => document.createElement('span')); 29965 } 29966 // Firefox will remove the last BR element if you insert nodes next to it using DOM APIs like insertBefore 29967 // so for that weird edge case we stop processing. 29968 if (!node.parentNode) { 29969 node = null; 29970 break; 29971 } 29972 rootBlockNode = createRootBlock(editor); 29973 rootNode.insertBefore(rootBlockNode, node); 29974 } 29975 tempNode = node; 29976 node = node.nextSibling; 29977 rootBlockNode.appendChild(tempNode); 29978 } 29979 else { 29980 rootBlockNode = null; 29981 node = node.nextSibling; 29982 } 29983 } 29984 if (bm) { 29985 editor.selection.setRng(resolveBookmark(bm)); 29986 editor.nodeChanged(); 29987 } 29988 }; 29989 const insertEmptyLine = (editor, root, insertBlock) => { 29990 const block = SugarElement.fromDom(createRootBlock(editor)); 29991 const br = createPaddingBr(); 29992 append$1(block, br); 29993 insertBlock(root, block); 29994 const rng = document.createRange(); 29995 rng.setStartBefore(br.dom); 29996 rng.setEndBefore(br.dom); 29997 return rng; 29998 }; 29999 const setup$t = (editor) => { 30000 editor.on('NodeChange', () => addRootBlocks(editor)); 30001 }; 30002 30003 const hasClass = (checkClassName) => (node) => (' ' + node.attr('class') + ' ').indexOf(checkClassName) !== -1; 30004 const replaceMatchWithSpan = (editor, content, cls) => { 30005 return function (match) { 30006 const args = arguments, index = args[args.length - 2]; 30007 const prevChar = index > 0 ? content.charAt(index - 1) : ''; 30008 // Is value inside an attribute then don't replace 30009 if (prevChar === '"') { 30010 return match; 30011 } 30012 // Is value inside a contentEditable='false' tag 30013 if (prevChar === '>') { 30014 const findStartTagIndex = content.lastIndexOf('<', index); 30015 if (findStartTagIndex !== -1) { 30016 const tagHtml = content.substring(findStartTagIndex, index); 30017 if (tagHtml.indexOf('contenteditable="false"') !== -1) { 30018 return match; 30019 } 30020 } 30021 } 30022 return ('<span class="' + cls + '" data-mce-content="' + editor.dom.encode(args[0]) + '">' + 30023 editor.dom.encode(typeof args[1] === 'string' ? args[1] : args[0]) + '</span>'); 30024 }; 30025 }; 30026 const convertRegExpsToNonEditable = (editor, nonEditableRegExps, e) => { 30027 let i = nonEditableRegExps.length, content = e.content; 30028 // Don't replace the variables when raw is used for example on undo/redo 30029 if (e.format === 'raw') { 30030 return; 30031 } 30032 while (i--) { 30033 content = content.replace(nonEditableRegExps[i], replaceMatchWithSpan(editor, content, getNonEditableClass(editor))); 30034 } 30035 e.content = content; 30036 }; 30037 const isValidContent = (nonEditableRegExps, content) => { 30038 return forall(nonEditableRegExps, (re) => { 30039 const matches = content.match(re); 30040 return matches !== null && matches[0].length === content.length; 30041 }); 30042 }; 30043 const setup$s = (editor) => { 30044 const contentEditableAttrName = 'contenteditable'; 30045 const editClass = ' ' + Tools.trim(getEditableClass(editor)) + ' '; 30046 const nonEditClass = ' ' + Tools.trim(getNonEditableClass(editor)) + ' '; 30047 const hasEditClass = hasClass(editClass); 30048 const hasNonEditClass = hasClass(nonEditClass); 30049 const nonEditableRegExps = getNonEditableRegExps(editor); 30050 if (nonEditableRegExps.length > 0) { 30051 editor.on('BeforeSetContent', (e) => { 30052 convertRegExpsToNonEditable(editor, nonEditableRegExps, e); 30053 }); 30054 } 30055 editor.parser.addAttributeFilter('class', (nodes) => { 30056 let i = nodes.length; 30057 while (i--) { 30058 const node = nodes[i]; 30059 if (hasEditClass(node)) { 30060 node.attr(contentEditableAttrName, 'true'); 30061 } 30062 else if (hasNonEditClass(node)) { 30063 node.attr(contentEditableAttrName, 'false'); 30064 } 30065 } 30066 }); 30067 editor.serializer.addAttributeFilter(contentEditableAttrName, (nodes) => { 30068 let i = nodes.length; 30069 while (i--) { 30070 const node = nodes[i]; 30071 if (!hasEditClass(node) && !hasNonEditClass(node)) { 30072 continue; 30073 } 30074 const content = node.attr('data-mce-content'); 30075 if (nonEditableRegExps.length > 0 && content) { 30076 if (isValidContent(nonEditableRegExps, content)) { 30077 node.name = '#text'; 30078 node.type = 3; 30079 node.raw = true; 30080 node.value = content; 30081 } 30082 else { 30083 node.remove(); 30084 } 30085 } 30086 else { 30087 node.attr(contentEditableAttrName, null); 30088 } 30089 } 30090 }); 30091 }; 30092 30093 /** 30094 * This module shows the invisible block that the caret is currently in when contents is added to that block. 30095 */ 30096 const findBlockCaretContainer = (editor) => descendant$1(SugarElement.fromDom(editor.getBody()), '*[data-mce-caret]') 30097 .map((elm) => elm.dom) 30098 .getOrNull(); 30099 const showBlockCaretContainer = (editor, blockCaretContainer) => { 30100 if (blockCaretContainer.hasAttribute('data-mce-caret')) { 30101 showCaretContainerBlock(blockCaretContainer); 30102 editor.selection.setRng(editor.selection.getRng()); // Clears the fake caret state 30103 editor.selection.scrollIntoView(blockCaretContainer); 30104 } 30105 }; 30106 const handleBlockContainer = (editor, e) => { 30107 const blockCaretContainer = findBlockCaretContainer(editor); 30108 if (!blockCaretContainer) { 30109 return; 30110 } 30111 if (e.type === 'compositionstart') { 30112 e.preventDefault(); 30113 e.stopPropagation(); 30114 showBlockCaretContainer(editor, blockCaretContainer); 30115 return; 30116 } 30117 if (hasContent(blockCaretContainer)) { 30118 showBlockCaretContainer(editor, blockCaretContainer); 30119 editor.undoManager.add(); 30120 } 30121 }; 30122 const setup$r = (editor) => { 30123 editor.on('keyup compositionstart', curry(handleBlockContainer, editor)); 30124 }; 30125 30126 const isContentEditableFalse$3 = isContentEditableFalse$a; 30127 const moveToCeFalseHorizontally = (direction, editor, range) => moveHorizontally(editor, direction, range, isBeforeContentEditableFalse, isAfterContentEditableFalse, isContentEditableFalse$3); 30128 const moveToCeFalseVertically = (direction, editor, range) => { 30129 const isBefore = (caretPosition) => isBeforeContentEditableFalse(caretPosition) || isBeforeTable(caretPosition); 30130 const isAfter = (caretPosition) => isAfterContentEditableFalse(caretPosition) || isAfterTable(caretPosition); 30131 return moveVertically$1(editor, direction, range, isBefore, isAfter, isContentEditableFalse$3); 30132 }; 30133 const createTextBlock = (editor) => { 30134 const textBlock = editor.dom.create(getForcedRootBlock(editor)); 30135 textBlock.innerHTML = '<br data-mce-bogus="1">'; 30136 return textBlock; 30137 }; 30138 const exitPreBlock = (editor, direction, range) => { 30139 const caretWalker = CaretWalker(editor.getBody()); 30140 const getVisualCaretPosition$1 = curry(getVisualCaretPosition, direction === 1 /* HDirection.Forwards */ ? caretWalker.next : caretWalker.prev); 30141 if (range.collapsed) { 30142 const pre = editor.dom.getParent(range.startContainer, 'PRE'); 30143 if (!pre) { 30144 return; 30145 } 30146 const caretPos = getVisualCaretPosition$1(CaretPosition.fromRangeStart(range)); 30147 if (!caretPos) { 30148 const newBlock = SugarElement.fromDom(createTextBlock(editor)); 30149 if (direction === 1 /* HDirection.Forwards */) { 30150 after$4(SugarElement.fromDom(pre), newBlock); 30151 } 30152 else { 30153 before$4(SugarElement.fromDom(pre), newBlock); 30154 } 30155 editor.selection.select(newBlock.dom, true); 30156 editor.selection.collapse(); 30157 } 30158 } 30159 }; 30160 const getHorizontalRange = (editor, forward) => { 30161 const direction = forward ? 1 /* HDirection.Forwards */ : -1 /* HDirection.Backwards */; 30162 const range = editor.selection.getRng(); 30163 return moveToCeFalseHorizontally(direction, editor, range).orThunk(() => { 30164 exitPreBlock(editor, direction, range); 30165 return Optional.none(); 30166 }); 30167 }; 30168 const getVerticalRange = (editor, down) => { 30169 const direction = down ? 1 : -1; 30170 const range = editor.selection.getRng(); 30171 return moveToCeFalseVertically(direction, editor, range).orThunk(() => { 30172 exitPreBlock(editor, direction, range); 30173 return Optional.none(); 30174 }); 30175 }; 30176 const flipDirection = (selection, forward) => { 30177 const elm = forward ? selection.getEnd(true) : selection.getStart(true); 30178 return isRtl(elm) ? !forward : forward; 30179 }; 30180 const moveH$2 = (editor, forward) => getHorizontalRange(editor, flipDirection(editor.selection, forward)).exists((newRange) => { 30181 moveToRange(editor, newRange); 30182 return true; 30183 }); 30184 const moveV$5 = (editor, down) => getVerticalRange(editor, down).exists((newRange) => { 30185 moveToRange(editor, newRange); 30186 return true; 30187 }); 30188 const moveToLineEndPoint$1 = (editor, forward) => { 30189 const isCefPosition = forward ? isAfterContentEditableFalse : isBeforeContentEditableFalse; 30190 return moveToLineEndPoint$3(editor, forward, isCefPosition); 30191 }; 30192 const selectToEndPoint = (editor, forward) => getEdgeCefPosition(editor, !forward) 30193 .map((pos) => { 30194 const rng = pos.toRange(); 30195 const curRng = editor.selection.getRng(); 30196 if (forward) { 30197 rng.setStart(curRng.startContainer, curRng.startOffset); 30198 } 30199 else { 30200 rng.setEnd(curRng.endContainer, curRng.endOffset); 30201 } 30202 return rng; 30203 }) 30204 .exists((rng) => { 30205 moveToRange(editor, rng); 30206 return true; 30207 }); 30208 30209 const getClosestCetBlock = (position, root) => { 30210 const isRoot = (el) => eq(el, root); 30211 const isCet = (el) => isContentEditableTrue$3(el.dom); 30212 const startNode = SugarElement.fromDom(position.container()); 30213 const closestCetBlock = closest$5(startNode, isCet, isRoot); 30214 return closestCetBlock.filter((b) => !isRoot(b)); 30215 }; 30216 const moveVertically = (editor, position, down) => { 30217 const getNextPosition = down ? getClosestPositionBelow : getClosestPositionAbove; 30218 return getNextPosition(editor.getBody(), position).map((nextPosition) => nextPosition.toRange()); 30219 }; 30220 const moveToNextOrPreviousLine = (editor, down) => { 30221 const startPosition = CaretPosition.fromRangeStart(editor.selection.getRng()); 30222 const endPosition = CaretPosition.fromRangeEnd(editor.selection.getRng()); 30223 const root = SugarElement.fromDom(editor.getBody()); 30224 /* I wasn't able to find a way to create a selection between two different contenteditable elements. 30225 However I can't rule out that it is possible. So I am checking if both positions are in the same contenteditable element. 30226 This is a defensive check to ensure selection integrity. 30227 */ 30228 const closestCetBlock = flatten(lift2(getClosestCetBlock(startPosition, root), getClosestCetBlock(endPosition, root), (c1, c2) => eq(c1, c2) ? Optional.some(c1) : Optional.none())); 30229 return closestCetBlock.fold(never, (cetBlock) => { 30230 if ((down && isAtLastLine(cetBlock.dom, endPosition)) || 30231 (!down && isAtFirstLine(cetBlock.dom, startPosition))) { 30232 return moveVertically(editor, down ? endPosition : startPosition, down).exists((newRange) => { 30233 moveToRange(editor, newRange); 30234 return true; 30235 }); 30236 } 30237 return false; 30238 }); 30239 }; 30240 const moveV$4 = (editor, down) => moveToNextOrPreviousLine(editor, down); 30241 30242 const isTarget = (node) => contains$2(['figcaption'], name(node)); 30243 const getClosestTargetBlock = (pos, root, schema) => { 30244 const isRoot = curry(eq, root); 30245 return closest$5(SugarElement.fromDom(pos.container()), (el) => schema.isBlock(name(el)), isRoot).filter(isTarget); 30246 }; 30247 const isAtFirstOrLastLine = (root, forward, pos) => forward ? isAtLastLine(root.dom, pos) : isAtFirstLine(root.dom, pos); 30248 const moveCaretToNewEmptyLine = (editor, forward) => { 30249 const root = SugarElement.fromDom(editor.getBody()); 30250 const pos = CaretPosition.fromRangeStart(editor.selection.getRng()); 30251 return getClosestTargetBlock(pos, root, editor.schema).exists(() => { 30252 if (isAtFirstOrLastLine(root, forward, pos)) { 30253 const insertFn = forward ? append$1 : prepend; 30254 const rng = insertEmptyLine(editor, root, insertFn); 30255 editor.selection.setRng(rng); 30256 return true; 30257 } 30258 else { 30259 return false; 30260 } 30261 }); 30262 }; 30263 const moveV$3 = (editor, forward) => { 30264 if (editor.selection.isCollapsed()) { 30265 return moveCaretToNewEmptyLine(editor, forward); 30266 } 30267 else { 30268 return false; 30269 } 30270 }; 30271 30272 const moveUp = (editor, details, summary) => { 30273 const rng = editor.selection.getRng(); 30274 const pos = CaretPosition.fromRangeStart(rng); 30275 const root = editor.getBody(); 30276 if (root.firstChild === details && isAtFirstLine(summary, pos)) { 30277 editor.execCommand('InsertNewBlockBefore'); 30278 return true; 30279 } 30280 else { 30281 return false; 30282 } 30283 }; 30284 const moveDown = (editor, details) => { 30285 const rng = editor.selection.getRng(); 30286 const pos = CaretPosition.fromRangeStart(rng); 30287 const root = editor.getBody(); 30288 if (root.lastChild === details && isAtLastLine(details, pos)) { 30289 editor.execCommand('InsertNewBlockAfter'); 30290 return true; 30291 } 30292 else { 30293 return false; 30294 } 30295 }; 30296 const move$2 = (editor, forward) => { 30297 if (forward) { 30298 return Optional.from(editor.dom.getParent(editor.selection.getNode(), 'details')) 30299 .map((details) => moveDown(editor, details)) 30300 .getOr(false); 30301 } 30302 else { 30303 return Optional.from(editor.dom.getParent(editor.selection.getNode(), 'summary')) 30304 .bind((summary) => Optional.from(editor.dom.getParent(summary, 'details')) 30305 .map((details) => moveUp(editor, details, summary))).getOr(false); 30306 } 30307 }; 30308 const moveV$2 = (editor, forward) => move$2(editor, forward); 30309 30310 const baseKeyPattern = { 30311 shiftKey: false, 30312 altKey: false, 30313 ctrlKey: false, 30314 metaKey: false, 30315 keyCode: 0 30316 }; 30317 const defaultPatterns = (patterns) => map$3(patterns, (pattern) => ({ 30318 ...baseKeyPattern, 30319 ...pattern 30320 })); 30321 const defaultDelayedPatterns = (patterns) => map$3(patterns, (pattern) => ({ 30322 ...baseKeyPattern, 30323 ...pattern 30324 })); 30325 const matchesEvent = (pattern, evt) => (evt.keyCode === pattern.keyCode && 30326 evt.shiftKey === pattern.shiftKey && 30327 evt.altKey === pattern.altKey && 30328 evt.ctrlKey === pattern.ctrlKey && 30329 evt.metaKey === pattern.metaKey); 30330 const match$1 = (patterns, evt) => bind$3(defaultPatterns(patterns), (pattern) => matchesEvent(pattern, evt) ? [pattern] : []); 30331 const matchDelayed = (patterns, evt) => bind$3(defaultDelayedPatterns(patterns), (pattern) => matchesEvent(pattern, evt) ? [pattern] : []); 30332 const action = (f, ...x) => () => f.apply(null, x); 30333 const execute = (patterns, evt) => find$2(match$1(patterns, evt), (pattern) => pattern.action()); 30334 const executeWithDelayedAction = (patterns, evt) => findMap(matchDelayed(patterns, evt), (pattern) => pattern.action()); 30335 30336 const moveH$1 = (editor, forward) => { 30337 const direction = forward ? 1 /* HDirection.Forwards */ : -1 /* HDirection.Backwards */; 30338 const range = editor.selection.getRng(); 30339 return moveHorizontally(editor, direction, range, isBeforeMedia, isAfterMedia, isMedia$2).exists((newRange) => { 30340 moveToRange(editor, newRange); 30341 return true; 30342 }); 30343 }; 30344 const moveV$1 = (editor, down) => { 30345 const direction = down ? 1 : -1; 30346 const range = editor.selection.getRng(); 30347 return moveVertically$1(editor, direction, range, isBeforeMedia, isAfterMedia, isMedia$2).exists((newRange) => { 30348 moveToRange(editor, newRange); 30349 return true; 30350 }); 30351 }; 30352 const moveToLineEndPoint = (editor, forward) => { 30353 const isNearMedia = forward ? isAfterMedia : isBeforeMedia; 30354 return moveToLineEndPoint$3(editor, forward, isNearMedia); 30355 }; 30356 30357 const firstLayer = (scope, selector) => { 30358 return filterFirstLayer(scope, selector, always); 30359 }; 30360 const filterFirstLayer = (scope, selector, predicate) => { 30361 return bind$3(children$
vendor: 10,021 bytes, lines 30361-30664
303611(scope), (x) => { 30362 if (is$2(x, selector)) { 30363 return predicate(x) ? [x] : []; 30364 } 30365 else { 30366 return filterFirstLayer(x, selector, predicate); 30367 } 30368 }); 30369 }; 30370 30371 // lookup inside this table 30372 const lookup$1 = (tags, element, isRoot = never) => { 30373 // If the element we're inspecting is the root, we definitely don't want it. 30374 if (isRoot(element)) { 30375 return Optional.none(); 30376 } 30377 // This looks a lot like SelectorFind.closest, with one big exception - the isRoot check. 30378 // The code here will look for parents if passed a table, SelectorFind.closest with that specific isRoot check won't. 30379 if (contains$2(tags, name(element))) { 30380 return Optional.some(element); 30381 } 30382 const isRootOrUpperTable = (elm) => is$2(elm, 'table') || isRoot(elm); 30383 return ancestor$4(element, tags.join(','), isRootOrUpperTable); 30384 }; 30385 /* 30386 * Identify the optional cell that element represents. 30387 */ 30388 const cell = (element, isRoot) => lookup$1(['td', 'th'], element, isRoot); 30389 const cells = (ancestor) => firstLayer(ancestor, 'th,td'); 30390 const table = (element, isRoot) => closest$4(element, 'table', isRoot); 30391 30392 const adt = Adt.generate([ 30393 { none: ['current'] }, 30394 { first: ['current'] }, 30395 { middle: ['current', 'target'] }, 30396 { last: ['current'] } 30397 ]); 30398 const none = (current) => adt.none(current); 30399 const CellLocation = { 30400 ...adt, 30401 none 30402 }; 30403 30404 /* 30405 * Walk until the next eligible cell location is found, or the start/end of the table is found. 30406 */ 30407 const walk = (all, current, index, direction, isEligible = always) => { 30408 const forwards = direction === 1 /* Direction.Forwards */; 30409 if (!forwards && index <= 0) { 30410 return CellLocation.first(all[0]); 30411 } 30412 else if (forwards && index >= all.length - 1) { 30413 return CellLocation.last(all[all.length - 1]); 30414 } 30415 else { 30416 const newIndex = index + direction; 30417 const elem = all[newIndex]; 30418 return isEligible(elem) ? CellLocation.middle(current, elem) : walk(all, current, newIndex, direction, isEligible); 30419 } 30420 }; 30421 /* 30422 * Identify the index of the current cell within all the cells, and 30423 * a list of the cells within its table. 30424 */ 30425 const detect = (current, isRoot) => { 30426 return table(current, isRoot).bind((table) => { 30427 const all = cells(table); 30428 const index = findIndex$2(all, (x) => eq(current, x)); 30429 return index.map((index) => ({ index, all })); 30430 }); 30431 }; 30432 /* 30433 * Identify the CellLocation of the cell when navigating forward from current 30434 */ 30435 const next = (current, isEligible, isRoot) => { 30436 const detection = detect(current, isRoot); 30437 return detection.fold(() => { 30438 return CellLocation.none(current); 30439 }, (info) => { 30440 return walk(info.all, current, info.index, 1 /* Direction.Forwards */, isEligible); 30441 }); 30442 }; 30443 /* 30444 * Identify the CellLocation of the cell when navigating back from current 30445 */ 30446 const prev = (current, isEligible, isRoot) => { 30447 const detection = detect(current, isRoot); 30448 return detection.fold(() => { 30449 return CellLocation.none(); 30450 }, (info) => { 30451 return walk(info.all, current, info.index, -1 /* Direction.Backwards */, isEligible); 30452 }); 30453 }; 30454 30455 var TagBoundaries = [ 30456 'body', 30457 'p', 30458 'div', 30459 'article', 30460 'aside', 30461 'figcaption', 30462 'figure', 30463 'footer', 30464 'header', 30465 'nav', 30466 'section', 30467 'ol', 30468 'ul', 30469 'li', 30470 'table', 30471 'thead', 30472 'tbody', 30473 'tfoot', 30474 'caption', 30475 'tr', 30476 'td', 30477 'th', 30478 'h1', 30479 'h2', 30480 'h3', 30481 'h4', 30482 'h5', 30483 'h6', 30484 'blockquote', 30485 'pre', 30486 'address' 30487 ]; 30488 30489 var DomUniverse = () => { 30490 const clone = (element) => { 30491 return SugarElement.fromDom(element.dom.cloneNode(false)); 30492 }; 30493 const document = (element) => documentOrOwner(element).dom; 30494 const isBoundary = (element) => { 30495 if (!isElement$8(element)) { 30496 return false; 30497 } 30498 if (name(element) === 'body') { 30499 return true; 30500 } 30501 return contains$2(TagBoundaries, name(element)); 30502 }; 30503 const isEmptyTag = (element) => { 30504 if (!isElement$8(element)) { 30505 return false; 30506 } 30507 return contains$2(['br', 'img', 'hr', 'input'], name(element)); 30508 }; 30509 const isNonEditable = (element) => isElement$8(element) && get$9(element, 'contenteditable') === 'false'; 30510 const comparePosition = (element, other) => { 30511 return element.dom.compareDocumentPosition(other.dom); 30512 }; 30513 const copyAttributesTo = (source, destination) => { 30514 const as = clone$4(source); 30515 setAll$1(destination, as); 30516 }; 30517 const isSpecial = (element) => { 30518 const tag = name(element); 30519 return contains$2([ 30520 'script', 'noscript', 'iframe', 'noframes', 'noembed', 'title', 'style', 'textarea', 'xmp' 30521 ], tag); 30522 }; 30523 const getLanguage = (element) => isElement$8(element) ? getOpt(element, 'lang') : Optional.none(); 30524 return { 30525 up: constant({ 30526 selector: ancestor$4, 30527 closest: closest$4, 30528 predicate: ancestor$5, 30529 all: parents$1 30530 }), 30531 down: constant({ 30532 selector: descendants, 30533 predicate: descendants$1 30534 }), 30535 styles: constant({ 30536 get: get$7, 30537 getRaw: getRaw$1, 30538 set: set$2, 30539 remove: remove$7 30540 }), 30541 attrs: constant({ 30542 get: get$9, 30543 set: set$4, 30544 remove: remove$9, 30545 copyTo: copyAttributesTo 30546 }), 30547 insert: constant({ 30548 before: before$4, 30549 after: after$4, 30550 afterAll: after$3, 30551 append: append$1, 30552 appendAll: append, 30553 prepend: prepend, 30554 wrap: wrap$2 30555 }), 30556 remove: constant({ 30557 unwrap: unwrap, 30558 remove: remove$8 30559 }), 30560 create: constant({ 30561 nu: SugarElement.fromTag, 30562 clone, 30563 text: SugarElement.fromText 30564 }), 30565 query: constant({ 30566 comparePosition, 30567 prevSibling: prevSibling, 30568 nextSibling: nextSibling 30569 }), 30570 property: constant({ 30571 children: children$1, 30572 name: name, 30573 parent: parent, 30574 document, 30575 isText: isText$c, 30576 isComment: isComment$1, 30577 isElement: isElement$8, 30578 isSpecial, 30579 getLanguage, 30580 getText: get$4, 30581 setText: set$1, 30582 isBoundary, 30583 isEmptyTag, 30584 isNonEditable 30585 }), 30586 eq: eq, 30587 is: is$1 30588 }; 30589 }; 30590 30591 const point$1 = (element, offset) => ({ 30592 element, 30593 offset 30594 }); 30595 30596 /** 30597 * Return the last available cursor position in the node. 30598 */ 30599 const toLast$1 = (universe, node) => { 30600 if (universe.property().isText(node)) { 30601 return point$1(node, universe.property().getText(node).length); 30602 } 30603 else { 30604 const children = universe.property().children(node); 30605 // keep descending if there are children. 30606 return children.length > 0 ? toLast$1(universe, children[children.length - 1]) : point$1(node, children.length); 30607 } 30608 }; 30609 /** 30610 * Descend down to a leaf node at the given offset. 30611 */ 30612 const toLeaf$3 = (universe, element, offset) => { 30613 const children = universe.property().children(element); 30614 if (children.length > 0 && offset < children.length) { 30615 return toLeaf$3(universe, children[offset], 0); 30616 } 30617 else if (children.length > 0 && universe.property().isElement(element) && children.length === offset) { 30618 return toLast$1(universe, children[children.length - 1]); 30619 } 30620 else { 30621 return point$1(element, offset); 30622 } 30623 }; 30624 30625 const toLeaf$2 = toLeaf$3; 30626 30627 const universe = DomUniverse(); 30628 const toLeaf$1 = (element, offset) => { 30629 return toLeaf$2(universe, element, offset); 30630 }; 30631 30632 const imageId = generate('image'); 30633 const getDragImage = (transfer) => { 30634 const dt = transfer; 30635 return Optional.from(dt[imageId]); 30636 }; 30637 const setDragImage = (transfer, imageData) => { 30638 const dt = transfer; 30639 dt[imageId] = imageData; 30640 }; 30641 30642 const eventId = generate('event'); 30643 const getEvent = (transfer) => { 30644 const dt = transfer; 30645 return Optional.from(dt[eventId]); 30646 }; 30647 const mkSetEventFn = (type) => (transfer) => { 30648 const dt = transfer; 30649 dt[eventId] = type; 30650 }; 30651 const setEvent = (transfer, type) => mkSetEventFn(type)(transfer); 30652 const setDragstartEvent = mkSetEventFn(0 /* Event.Dragstart */); 30653 const setDropEvent = mkSetEventFn(2 /* Event.Drop */); 30654 const setDragendEvent = mkSetEventFn(1 /* Event.Dragend */); 30655 const checkEvent = (expectedType) => (transfer) => { 30656 const dt = transfer; 30657 return Optional.from(dt[eventId]).exists((type) => type === expectedType); 30658 }; 30659 const isInDragStartEvent = checkEvent(0 /* Event.Dragstart */); 30660 30661 const createEmptyFileList = () => Object.freeze({ 30662 length: 0, 30663 item: (_) => null 30664 })
vendor: 141,153 bytes, lines 30664-33678
30664; 30665 30666 const modeId = generate('mode'); 30667 const getMode = (transfer) => { 30668 const dt = transfer; 30669 return Optional.from(dt[modeId]); 30670 }; 30671 const mkSetModeFn = (mode) => (transfer) => { 30672 const dt = transfer; 30673 dt[modeId] = mode; 30674 }; 30675 const setMode$1 = (transfer, mode) => mkSetModeFn(mode)(transfer); 30676 const setReadWriteMode = mkSetModeFn(0 /* Mode.ReadWrite */); 30677 const setReadOnlyMode = mkSetModeFn(2 /* Mode.ReadOnly */); 30678 const setProtectedMode = mkSetModeFn(1 /* Mode.Protected */); 30679 const checkMode = (expectedMode) => (transfer) => { 30680 const dt = transfer; 30681 return Optional.from(dt[modeId]).exists((mode) => mode === expectedMode); 30682 }; 30683 const isInReadWriteMode = checkMode(0 /* Mode.ReadWrite */); 30684 const isInProtectedMode = checkMode(1 /* Mode.Protected */); 30685 30686 const normalizeItems = (dataTransfer, itemsImpl) => ({ 30687 ...itemsImpl, 30688 get length() { 30689 return itemsImpl.length; 30690 }, 30691 add: (data, type) => { 30692 if (isInReadWriteMode(dataTransfer)) { 30693 if (isString(data)) { 30694 if (!isUndefined(type)) { 30695 return itemsImpl.add(data, type); 30696 } 30697 } 30698 else { 30699 return itemsImpl.add(data); 30700 } 30701 } 30702 return null; 30703 }, 30704 remove: (idx) => { 30705 if (isInReadWriteMode(dataTransfer)) { 30706 itemsImpl.remove(idx); 30707 } 30708 }, 30709 clear: () => { 30710 if (isInReadWriteMode(dataTransfer)) { 30711 itemsImpl.clear(); 30712 } 30713 } 30714 }); 30715 30716 const validDropEffects = ['none', 'copy', 'link', 'move']; 30717 const validEffectAlloweds = ['none', 'copy', 'copyLink', 'copyMove', 'link', 'linkMove', 'move', 'all', 'uninitialized']; 30718 const createDataTransfer = () => { 30719 const dataTransferImpl = new window.DataTransfer(); 30720 let dropEffect = 'move'; 30721 let effectAllowed = 'all'; 30722 const dataTransfer = { 30723 get dropEffect() { 30724 return dropEffect; 30725 }, 30726 set dropEffect(effect) { 30727 if (contains$2(validDropEffects, effect)) { 30728 dropEffect = effect; 30729 } 30730 }, 30731 get effectAllowed() { 30732 return effectAllowed; 30733 }, 30734 set effectAllowed(allowed) { 30735 // TINY-9601: Only allow setting effectAllowed to a valid value in a dragstart event 30736 // https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/effectAllowed 30737 if (isInDragStartEvent(dataTransfer) && contains$2(validEffectAlloweds, allowed)) { 30738 effectAllowed = allowed; 30739 } 30740 }, 30741 get items() { 30742 return normalizeItems(dataTransfer, dataTransferImpl.items); 30743 }, 30744 get files() { 30745 if (isInProtectedMode(dataTransfer)) { 30746 return createEmptyFileList(); 30747 } 30748 else { 30749 return dataTransferImpl.files; 30750 } 30751 }, 30752 get types() { 30753 return dataTransferImpl.types; 30754 }, 30755 setDragImage: (image, x, y) => { 30756 if (isInReadWriteMode(dataTransfer)) { 30757 setDragImage(dataTransfer, { image, x, y }); 30758 dataTransferImpl.setDragImage(image, x, y); 30759 } 30760 }, 30761 getData: (format) => { 30762 if (isInProtectedMode(dataTransfer)) { 30763 return ''; 30764 } 30765 else { 30766 return dataTransferImpl.getData(format); 30767 } 30768 }, 30769 setData: (format, data) => { 30770 if (isInReadWriteMode(dataTransfer)) { 30771 dataTransferImpl.setData(format, data); 30772 } 30773 }, 30774 clearData: (format) => { 30775 if (isInReadWriteMode(dataTransfer)) { 30776 dataTransferImpl.clearData(format); 30777 } 30778 } 30779 }; 30780 setReadWriteMode(dataTransfer); 30781 return dataTransfer; 30782 }; 30783 const cloneDataTransfer = (original) => { 30784 // Create new DataTransfer object to ensure scope is not shared between original and clone 30785 const clone = createDataTransfer(); 30786 const originalMode = getMode(original); 30787 // Set original to read-only to ensure data can be copied 30788 setReadOnlyMode(original); 30789 // Set clone event to dragstart to ensure effectAllowed can be set 30790 setDragstartEvent(clone); 30791 clone.dropEffect = original.dropEffect; 30792 clone.effectAllowed = original.effectAllowed; 30793 getDragImage(original).each((imageData) => clone.setDragImage(imageData.image, imageData.x, imageData.y)); 30794 each$e(original.types, (type) => { 30795 if (type !== 'Files') { 30796 clone.setData(type, original.getData(type)); 30797 } 30798 }); 30799 each$e(original.files, (file) => clone.items.add(file)); 30800 getEvent(original).each((type) => { 30801 setEvent(clone, type); 30802 }); 30803 originalMode.each((mode) => { 30804 // Reset original mode since it was set to read-only earlier 30805 setMode$1(original, mode); 30806 setMode$1(clone, mode); 30807 }); 30808 return clone; 30809 }; 30810 30811 const getHtmlData = (dataTransfer) => { 30812 const html = dataTransfer.getData('text/html'); 30813 return html === '' ? Optional.none() : Optional.some(html); 30814 }; 30815 const setHtmlData = (dataTransfer, html) => dataTransfer.setData('text/html', html); 30816 30817 const deflate = (rect, delta) => ({ 30818 left: rect.left - delta, 30819 top: rect.top - delta, 30820 right: rect.right + delta * 2, 30821 bottom: rect.bottom + delta * 2, 30822 width: rect.width + delta, 30823 height: rect.height + delta 30824 }); 30825 const getCorners = (getYAxisValue, tds) => bind$3(tds, (td) => { 30826 const rect = deflate(clone$1(td.getBoundingClientRect()), -1); 30827 return [ 30828 { x: rect.left, y: getYAxisValue(rect), cell: td }, 30829 { x: rect.right, y: getYAxisValue(rect), cell: td } 30830 ]; 30831 }); 30832 const findClosestCorner = (corners, x, y) => foldl(corners, (acc, newCorner) => acc.fold(() => Optional.some(newCorner), (oldCorner) => { 30833 const oldDist = Math.sqrt(Math.abs(oldCorner.x - x) + Math.abs(oldCorner.y - y)); 30834 const newDist = Math.sqrt(Math.abs(newCorner.x - x) + Math.abs(newCorner.y - y)); 30835 return Optional.some(newDist < oldDist ? newCorner : oldCorner); 30836 }), Optional.none()); 30837 const getClosestCell = (getYAxisValue, isTargetCorner, table, x, y) => { 30838 const cells = descendants(SugarElement.fromDom(table), 'td,th,caption').map((e) => e.dom); 30839 const corners = filter$5(getCorners(getYAxisValue, cells), (corner) => isTargetCorner(corner, y)); 30840 return findClosestCorner(corners, x, y).map((corner) => corner.cell); 30841 }; 30842 const getBottomValue = (rect) => rect.bottom; 30843 const getTopValue = (rect) => rect.top; 30844 const isAbove = (corner, y) => corner.y < y; 30845 const isBelow = (corner, y) => corner.y > y; 30846 const getClosestCellAbove = curry(getClosestCell, getBottomValue, isAbove); 30847 const getClosestCellBelow = curry(getClosestCell, getTopValue, isBelow); 30848 const findClosestPositionInAboveCell = (table, pos) => head(pos.getClientRects()) 30849 .bind((rect) => getClosestCellAbove(table, rect.left, rect.top)) 30850 .bind((cell) => findClosestHorizontalPosition(getLastLinePositions(cell), pos)); 30851 const findClosestPositionInBelowCell = (table, pos) => last$2(pos.getClientRects()) 30852 .bind((rect) => getClosestCellBelow(table, rect.left, rect.top)) 30853 .bind((cell) => findClosestHorizontalPosition(getFirstLinePositions(cell), pos)); 30854 30855 const hasNextBreak = (getPositionsUntil, scope, lineInfo) => lineInfo.breakAt.exists((breakPos) => getPositionsUntil(scope, breakPos).breakAt.isSome()); 30856 const startsWithWrapBreak = (lineInfo) => lineInfo.breakType === BreakType.Wrap && lineInfo.positions.length === 0; 30857 const startsWithBrBreak = (lineInfo) => lineInfo.breakType === BreakType.Br && lineInfo.positions.length === 1; 30858 const isAtTableCellLine = (getPositionsUntil, scope, pos) => { 30859 const lineInfo = getPositionsUntil(scope, pos); 30860 // Since we can't determine if the caret is on the above or below line in a word wrap break we asume it's always 30861 // on the below/above line based on direction. This will make the caret jump one line if you are at the end of the last 30862 // line and moving down or at the beginning of the second line moving up. 30863 if (startsWithWrapBreak(lineInfo) || (!isBr$7(pos.getNode()) && startsWithBrBreak(lineInfo))) { 30864 return !hasNextBreak(getPositionsUntil, scope, lineInfo); 30865 } 30866 else { 30867 return lineInfo.breakAt.isNone(); 30868 } 30869 }; 30870 const isAtFirstTableCellLine = curry(isAtTableCellLine, getPositionsUntilPreviousLine); 30871 const isAtLastTableCellLine = curry(isAtTableCellLine, getPositionsUntilNextLine); 30872 const isCaretAtStartOrEndOfTable = (forward, rng, table) => { 30873 const caretPos = CaretPosition.fromRangeStart(rng); 30874 return positionIn(!forward, table).exists((pos) => pos.isEqual(caretPos)); 30875 }; 30876 const navigateHorizontally = (editor, forward, table, _td) => { 30877 const rng = editor.selection.getRng(); 30878 const direction = forward ? 1 : -1; 30879 if (isFakeCaretTableBrowser() && isCaretAtStartOrEndOfTable(forward, rng, table)) { 30880 showCaret(direction, editor, table, !forward, false).each((newRng) => { 30881 moveToRange(editor, newRng); 30882 }); 30883 return true; 30884 } 30885 return false; 30886 }; 30887 const getClosestAbovePosition = (root, table, start) => findClosestPositionInAboveCell(table, start).orThunk(() => head(start.getClientRects()).bind((rect) => findClosestHorizontalPositionFromPoint(getPositionsAbove(root, CaretPosition.before(table)), rect.left))).getOr(CaretPosition.before(table)); 30888 const getClosestBelowPosition = (root, table, start) => findClosestPositionInBelowCell(table, start).orThunk(() => head(start.getClientRects()).bind((rect) => findClosestHorizontalPositionFromPoint(getPositionsBelow(root, CaretPosition.after(table)), rect.left))).getOr(CaretPosition.after(table)); 30889 const getTable = (previous, pos) => { 30890 const node = pos.getNode(previous); 30891 return isTable$2(node) ? Optional.some(node) : Optional.none(); 30892 }; 30893 const renderBlock = (down, editor, table) => { 30894 editor.undoManager.transact(() => { 30895 const insertFn = down ? after$4 : before$4; 30896 const rng = insertEmptyLine(editor, SugarElement.fromDom(table), insertFn); 30897 moveToRange(editor, rng); 30898 }); 30899 }; 30900 const moveCaret = (editor, down, pos) => { 30901 const table = down ? getTable(true, pos) : getTable(false, pos); 30902 const last = down === false; 30903 table.fold(() => moveToRange(editor, pos.toRange()), (table) => positionIn(last, editor.getBody()).filter((lastPos) => lastPos.isEqual(pos)).fold(() => moveToRange(editor, pos.toRange()), (_) => renderBlock(down, editor, table))); 30904 }; 30905 const navigateVertically = (editor, down, table, td) => { 30906 const rng = editor.selection.getRng(); 30907 const pos = CaretPosition.fromRangeStart(rng); 30908 const root = editor.getBody(); 30909 if (!down && isAtFirstTableCellLine(td, pos)) { 30910 const newPos = getClosestAbovePosition(root, table, pos); 30911 moveCaret(editor, down, newPos); 30912 return true; 30913 } 30914 else if (down && isAtLastTableCellLine(td, pos)) { 30915 const newPos = getClosestBelowPosition(root, table, pos); 30916 moveCaret(editor, down, newPos); 30917 return true; 30918 } 30919 else { 30920 return false; 30921 } 30922 }; 30923 const move$1 = (editor, forward, mover) => Optional.from(editor.dom.getParent(editor.selection.getNode(), 'td,th')) 30924 .bind((td) => Optional.from(editor.dom.getParent(td, 'table')) 30925 .map((table) => mover(editor, forward, table, td))).getOr(false); 30926 const moveH = (editor, forward) => move$1(editor, forward, navigateHorizontally); 30927 const moveV = (editor, forward) => move$1(editor, forward, navigateVertically); 30928 const getCellFirstCursorPosition = (cell) => { 30929 const selection = SimSelection.exact(cell, 0, cell, 0); 30930 return toNative(selection); 30931 }; 30932 const rowHasEditableCell = (cell) => { 30933 return isCellEditable(cell) || siblings(cell).some((element) => isHTMLElement$1(element) && isCellEditable(element)); 30934 }; 30935 const tabGo = (editor, isRoot, cell) => { 30936 return cell.fold(Optional.none, Optional.none, (_current, next) => { 30937 return first(next).map((cell) => { 30938 return getCellFirstCursorPosition(cell); 30939 }); 30940 }, (current) => { 30941 if (editor.mode.isReadOnly() || !isCellInEditableTable(current) || !rowHasEditableCell(current)) { 30942 return Optional.none(); 30943 } 30944 editor.execCommand('mceTableInsertRowAfter'); 30945 // Move forward from the last cell so that we move into the first valid position in the new row 30946 return tabForward(editor, isRoot, current); 30947 }); 30948 }; 30949 const isCellInEditableTable = (cell) => closest$5(cell, isTag('table')).exists(isEditable$2); 30950 const tabForward = (editor, isRoot, cell) => tabGo(editor, isRoot, next(cell, isCellEditable)); 30951 const tabBackward = (editor, isRoot, cell) => tabGo(editor, isRoot, prev(cell, isCellEditable)); 30952 const isCellEditable = (cell) => isEditable$2(cell) || descendant(cell, isEditableHTMLElement); 30953 const isEditableHTMLElement = (node) => isHTMLElement$1(node) && isEditable$2(node); 30954 const handleTab = (editor, forward) => { 30955 const rootElements = ['table', 'li', 'dl']; 30956 const body = SugarElement.fromDom(editor.getBody()); 30957 const isRoot = (element) => { 30958 const name$1 = name(element); 30959 return eq(element, body) || contains$2(rootElements, name$1); 30960 }; 30961 const rng = editor.selection.getRng(); 30962 // If navigating backwards, use the start of the ranged selection 30963 const container = SugarElement.fromDom(!forward ? rng.startContainer : rng.endContainer); 30964 return cell(container, isRoot).map((cell) => { 30965 // Clear fake ranged selection because our new selection will always be collapsed 30966 table(cell, isRoot).each((table) => { 30967 editor.model.table.clearSelectedCells(table.dom); 30968 }); 30969 // Collapse selection to start or end based on shift key 30970 editor.selection.collapse(!forward); 30971 const navigation = !forward ? tabBackward : tabForward; 30972 const rng = navigation(editor, isRoot, cell); 30973 rng.each((range) => { 30974 editor.selection.setRng(range); 30975 }); 30976 return true; 30977 }).getOr(false); 30978 }; 30979 30980 const executeKeydownOverride$4 = (editor, caret, evt) => { 30981 const isMac = Env.os.isMacOS() || Env.os.isiOS(); 30982 const isFirefox = Env.browser.isFirefox(); 30983 execute([ 30984 { keyCode: VK.RIGHT, action: action(moveH$2, editor, true) }, 30985 { keyCode: VK.LEFT, action: action(moveH$2, editor, false) }, 30986 { keyCode: VK.UP, action: action(moveV$5, editor, false) }, 30987 { keyCode: VK.DOWN, action: action(moveV$5, editor, true) }, 30988 ...(isMac ? [ 30989 { keyCode: VK.UP, action: action(selectToEndPoint, editor, false), metaKey: true, shiftKey: true }, 30990 { keyCode: VK.DOWN, action: action(selectToEndPoint, editor, true), metaKey: true, shiftKey: true } 30991 ] : []), 30992 { keyCode: VK.RIGHT, action: action(moveH, editor, true) }, 30993 { keyCode: VK.LEFT, action: action(moveH, editor, false) }, 30994 { keyCode: VK.UP, action: action(moveV, editor, false) }, 30995 { keyCode: VK.DOWN, action: action(moveV, editor, true) }, 30996 { keyCode: VK.UP, action: action(moveV, editor, false) }, 30997 { keyCode: VK.UP, action: action(moveV$2, editor, false) }, 30998 { keyCode: VK.DOWN, action: action(moveV$2, editor, true) }, 30999 { keyCode: VK.RIGHT, action: action(moveH$1, editor, true) }, 31000 { keyCode: VK.LEFT, action: action(moveH$1, editor, false) }, 31001 { keyCode: VK.UP, action: action(moveV$1, editor, false) }, 31002 { keyCode: VK.DOWN, action: action(moveV$1, editor, true) }, 31003 { keyCode: VK.RIGHT, action: action(move$3, editor, caret, true) }, 31004 { keyCode: VK.LEFT, action: action(move$3, editor, caret, false) }, 31005 { keyCode: VK.RIGHT, ctrlKey: !isMac, altKey: isMac, action: action(moveNextWord, editor, caret) }, 31006 { keyCode: VK.LEFT, ctrlKey: !isMac, altKey: isMac, action: action(movePrevWord, editor, caret) }, 31007 { keyCode: VK.UP, action: action(moveV$3, editor, false) }, 31008 { keyCode: VK.DOWN, action: action(moveV$3, editor, true) }, 31009 ...(isFirefox ? [ 31010 { keyCode: VK.UP, action: action(moveV$4, editor, false) }, 31011 { keyCode: VK.DOWN, action: action(moveV$4, editor, true) } 31012 ] : []) 31013 ], evt).each((_) => { 31014 evt.preventDefault(); 31015 }); 31016 }; 31017 const setup$q = (editor, caret) => { 31018 editor.on('keydown', (evt) => { 31019 if (!evt.isDefaultPrevented()) { 31020 executeKeydownOverride$4(editor, caret, evt); 31021 } 31022 }); 31023 }; 31024 31025 const point = (container, offset) => ({ 31026 container, 31027 offset 31028 }); 31029 31030 const DOM$7 = DOMUtils.DOM; 31031 const alwaysNext = (startNode) => (node) => startNode === node ? -1 : 0; 31032 // This largely is derived from robins isBoundary check, however it also treats contenteditable=false elements as a boundary 31033 // See robins `Structure.isEmptyTag` for the list of quasi block elements 31034 const isBoundary = (dom) => (node) => dom.isBlock(node) || contains$2(['BR', 'IMG', 'HR', 'INPUT'], node.nodeName) || dom.getContentEditable(node) === 'false'; 31035 // Finds the text node before the specified node, or just returns the node if it's already on a text node 31036 const textBefore = (node, offset, rootNode) => { 31037 if (isText$b(node) && offset >= 0) { 31038 return Optional.some(point(node, offset)); 31039 } 31040 else { 31041 const textSeeker = TextSeeker(DOM$7); 31042 return Optional.from(textSeeker.backwards(node, offset, alwaysNext(node), rootNode)).map((prev) => point(prev.container, prev.container.data.length)); 31043 } 31044 }; 31045 const textAfter = (node, offset, rootNode) => { 31046 if (isText$b(node) && offset >= node.length) { 31047 return Optional.some(point(node, offset)); 31048 } 31049 else { 31050 const textSeeker = TextSeeker(DOM$7); 31051 return Optional.from(textSeeker.forwards(node, offset, alwaysNext(node), rootNode)).map((prev) => point(prev.container, 0)); 31052 } 31053 }; 31054 const scanLeft = (node, offset, rootNode) => { 31055 if (!isText$b(node)) { 31056 return Optional.none(); 31057 } 31058 const text = node.data; 31059 if (offset >= 0 && offset <= text.length) { 31060 return Optional.some(point(node, offset)); 31061 } 31062 else { 31063 const textSeeker = TextSeeker(DOM$7); 31064 return Optional.from(textSeeker.backwards(node, offset, alwaysNext(node), rootNode)).bind((prev) => { 31065 const prevText = prev.container.data; 31066 return scanLeft(prev.container, offset + prevText.length, rootNode); 31067 }); 31068 } 31069 }; 31070 const scanRight = (node, offset, rootNode) => { 31071 if (!isText$b(node)) { 31072 return Optional.none(); 31073 } 31074 const text = node.data; 31075 if (offset <= text.length) { 31076 return Optional.some(point(node, offset)); 31077 } 31078 else { 31079 const textSeeker = TextSeeker(DOM$7); 31080 return Optional.from(textSeeker.forwards(node, offset, alwaysNext(node), rootNode)).bind((next) => scanRight(next.container, offset - text.length, rootNode)); 31081 } 31082 }; 31083 const repeatLeft = (dom, node, offset, process, rootNode) => { 31084 const search = TextSeeker(dom, isBoundary(dom)); 31085 return Optional.from(search.backwards(node, offset, process, rootNode)); 31086 }; 31087 31088 const isValidTextRange = (rng) => rng.collapsed && isText$b(rng.startContainer); 31089 // Normalize the text by replacing non-breaking spaces with regular spaces and stripping zero-width spaces (fake carets). 31090 const getText = (rng) => trim$2(rng.toString().replace(/\u00A0/g, ' ')); 31091 const isWhitespace = (chr) => chr !== '' && ' \u00a0\ufeff\f\n\r\t\v'.indexOf(chr) !== -1; 31092 31093 const stripTrigger = (text, trigger) => text.substring(trigger.length); 31094 const findTrigger = (text, index, trigger, includeWhitespace = false) => { 31095 // Identify the `char` in, and start the text from that point forward. If there is ever any whitespace, fail 31096 let i; 31097 const firstChar = trigger.charAt(0); 31098 for (i = index - 1; i >= 0; i--) { 31099 const char = text.charAt(i); 31100 if (!includeWhitespace && isWhitespace(char)) { 31101 return Optional.none(); 31102 } 31103 if (firstChar === char && contains$1(text, trigger, i, index)) { 31104 break; 31105 } 31106 } 31107 return Optional.some(i); 31108 }; 31109 const getContext = (dom, initRange, trigger, includeWhitespace = false) => { 31110 if (!isValidTextRange(initRange)) { 31111 return Optional.none(); 31112 } 31113 const buffer = { text: '', offset: 0 }; 31114 const findTriggerIndex = (element, offset, text) => { 31115 buffer.text = text + buffer.text; 31116 buffer.offset += offset; 31117 // Stop searching by just returning the current offset if whitespace was found (eg Optional.none()) 31118 // and we'll handle the final checks below instead 31119 return findTrigger(buffer.text, buffer.offset, trigger, includeWhitespace).getOr(offset); 31120 }; 31121 const root = dom.getParent(initRange.startContainer, dom.isBlock) || dom.getRoot(); 31122 return repeatLeft(dom, initRange.startContainer, initRange.startOffset, findTriggerIndex, root).bind((spot) => { 31123 const range = initRange.cloneRange(); 31124 range.setStart(spot.container, spot.offset); 31125 range.setEnd(initRange.endContainer, initRange.endOffset); 31126 // If the range is collapsed then we didn't find a match so abort 31127 if (range.collapsed) { 31128 return Optional.none(); 31129 } 31130 const text = getText(range); 31131 const triggerIndex = text.lastIndexOf(trigger); 31132 // If the match doesn't start with the trigger (eg whitespace found) 31133 if (triggerIndex !== 0) { 31134 return Optional.none(); 31135 } 31136 else { 31137 return Optional.some({ text: stripTrigger(text, trigger), range, trigger }); 31138 } 31139 }); 31140 }; 31141 31142 const isText$1 = (node) => node.nodeType === TEXT; 31143 const isElement = (node) => node.nodeType === ELEMENT; 31144 const toLast = (node) => { 31145 if (isText$1(node)) { 31146 return point(node, node.data.length); 31147 } 31148 else { 31149 const children = node.childNodes; 31150 // keep descending if there are children. 31151 return children.length > 0 ? toLast(children[children.length - 1]) : point(node, children.length); 31152 } 31153 }; 31154 const toLeaf = (node, offset) => { 31155 const children = node.childNodes; 31156 if (children.length > 0 && offset < children.length) { 31157 return toLeaf(children[offset], 0); 31158 } 31159 else if (children.length > 0 && isElement(node) && children.length === offset) { 31160 return toLast(children[children.length - 1]); 31161 } 31162 else { 31163 return point(node, offset); 31164 } 31165 }; 31166 31167 const isPreviousCharContent = (dom, leaf) => { 31168 // If at the start of the range, then we need to look backwards one more place. Otherwise we just need to look at the current text 31169 const root = dom.getParent(leaf.container, dom.isBlock) ?? dom.getRoot(); 31170 return repeatLeft(dom, leaf.container, leaf.offset, (_element, offset) => offset === 0 ? -1 : offset, root).filter((spot) => { 31171 const char = spot.container.data.charAt(spot.offset - 1); 31172 return !isWhitespace(char); 31173 }).isSome(); 31174 }; 31175 const isStartOfWord = (dom) => (rng) => { 31176 const leaf = toLeaf(rng.startContainer, rng.startOffset); 31177 return !isPreviousCharContent(dom, leaf); 31178 }; 31179 const getTriggerContext = (dom, initRange, database) => findMap(database.triggers, (trigger) => getContext(dom, initRange, trigger)); 31180 const lookup = (editor, getDatabase) => { 31181 const database = getDatabase(); 31182 const rng = editor.selection.getRng(); 31183 return getTriggerContext(editor.dom, rng, database).bind((context) => lookupWithContext(editor, getDatabase, context)); 31184 }; 31185 const lookupWithContext = (editor, getDatabase, context, fetchOptions = {}) => { 31186 const database = getDatabase(); 31187 const rng = editor.selection.getRng(); 31188 const startText = rng.startContainer.nodeValue ?? ''; 31189 const autocompleters = filter$5(database.lookupByTrigger(context.trigger), (autocompleter) => context.text.length >= autocompleter.minChars && autocompleter.matches.getOrThunk(() => isStartOfWord(editor.dom))(context.range, startText, context.text)); 31190 if (autocompleters.length === 0) { 31191 return Optional.none(); 31192 } 31193 const lookupData = Promise.all(map$3(autocompleters, (ac) => { 31194 // TODO: Find a sensible way to do maxResults 31195 const fetchResult = ac.fetch(context.text, ac.maxResults, fetchOptions); 31196 return fetchResult.then((results) => ({ 31197 matchText: context.text, 31198 items: results, 31199 columns: ac.columns, 31200 onAction: ac.onAction, 31201 highlightOn: ac.highlightOn 31202 })); 31203 })); 31204 return Optional.some({ 31205 lookupData, 31206 context 31207 }); 31208 }; 31209 31210 const type = requiredString('type'); 31211 const fetch$1 = requiredFunction('fetch'); 31212 const onAction = requiredFunction('onAction'); 31213 optionString('name'); 31214 optionString('text'); 31215 optionString('role'); 31216 optionString('icon'); 31217 optionString('url'); 31218 optionString('tooltip'); 31219 optionString('chevronTooltip'); 31220 optionString('label'); 31221 optionString('shortcut'); 31222 const defaultedColumns = (num) => defaulted('columns', num); 31223 31224 const autocompleterSchema = objOf([ 31225 type, 31226 requiredString('trigger'), 31227 defaultedNumber('minChars', 1), 31228 defaultedColumns(1), 31229 defaultedNumber('maxResults', 10), 31230 optionFunction('matches'), 31231 fetch$1, 31232 onAction, 31233 defaultedArrayOf('highlightOn', [], string) 31234 ]); 31235 const createAutocompleter = (spec) => asRaw('Autocompleter', autocompleterSchema, spec); 31236 31237 const create$6 = () => { 31238 const buttons = {}; 31239 const menuItems = {}; 31240 const popups = {}; 31241 const icons = {}; 31242 const contextMenus = {}; 31243 const contextToolbars = {}; 31244 const contexts = {}; 31245 const sidebars = {}; 31246 const views = {}; 31247 const add = (collection, type) => (name, spec) => { 31248 collection[name.toLowerCase()] = { ...spec, type }; 31249 }; 31250 const addDefaulted = (collection, type) => (name, spec) => { 31251 collection[name.toLowerCase()] = { type, ...spec }; 31252 }; 31253 const addIcon = (name, svgData) => icons[name.toLowerCase()] = svgData; 31254 const addContext = (name, pred) => contexts[name.toLowerCase()] = pred; 31255 return { 31256 addButton: add(buttons, 'button'), 31257 addGroupToolbarButton: add(buttons, 'grouptoolbarbutton'), 31258 addToggleButton: add(buttons, 'togglebutton'), 31259 addMenuButton: add(buttons, 'menubutton'), 31260 addSplitButton: add(buttons, 'splitbutton'), 31261 addMenuItem: add(menuItems, 'menuitem'), 31262 addNestedMenuItem: add(menuItems, 'nestedmenuitem'), 31263 addToggleMenuItem: add(menuItems, 'togglemenuitem'), 31264 addAutocompleter: add(popups, 'autocompleter'), 31265 addContextMenu: add(contextMenus, 'contextmenu'), 31266 addContextToolbar: add(contextToolbars, 'contexttoolbar'), 31267 addContextForm: addDefaulted(contextToolbars, 'contextform'), 31268 addSidebar: add(sidebars, 'sidebar'), 31269 addView: add(views, 'views'), 31270 addIcon, 31271 addContext, 31272 getAll: () => ({ 31273 buttons, 31274 menuItems, 31275 icons, 31276 // TODO: should popups be combined with context menus? We'd need to make a new add function. 31277 // Right now using `add` shares the key namespace, which prevents registering both 31278 // a completer and a context menu with the same name 31279 popups, 31280 contextMenus, 31281 contextToolbars, 31282 sidebars, 31283 views, 31284 contexts 31285 }) 31286 }; 31287 }; 31288 31289 const register$2 = (editor) => { 31290 const popups = editor.ui.registry.getAll().popups; 31291 const dataset = map$2(popups, (popup) => createAutocompleter(popup).fold((err) => { 31292 throw new Error(formatError(err)); 31293 }, identity)); 31294 const triggers = stringArray(mapToArray(dataset, (v) => v.trigger)); 31295 const datasetValues = values(dataset); 31296 const lookupByTrigger = (trigger) => filter$5(datasetValues, (dv) => dv.trigger === trigger); 31297 return { 31298 dataset, 31299 triggers, 31300 lookupByTrigger 31301 }; 31302 }; 31303 31304 const setupEditorInput = (editor, api) => { 31305 const update = last$1(api.load, 50); 31306 editor.on('input', (e) => { 31307 // TINY-10715: Firefox on Android using Korean Gboard will produce stray composition events when you move the caret by tapping inside the composed text 31308 if (e.inputType === 'insertCompositionText' && !editor.composing) { 31309 return; 31310 } 31311 update.throttle(); 31312 }); 31313 editor.on('keydown', (e) => { 31314 const keyCode = e.which; 31315 // Pressing <backspace> updates the autocompleter 31316 if (keyCode === 8) { 31317 update.throttle(); 31318 // Pressing <esc> closes the autocompleter 31319 } 31320 else if (keyCode === 27) { 31321 update.cancel(); // We need to cancel here since Esc cancels the IME composition and triggers an input event 31322 api.cancelIfNecessary(); 31323 } 31324 else if (keyCode === 38 || keyCode === 40) { 31325 // Arrow up and down keys needs to cancel the update since while composing arrow up or down will end the compose and issue a input event 31326 // that causes the list to update and then the focus moves up to the first item in the auto completer list. 31327 update.cancel(); 31328 } 31329 }, true); // Need to add this to the top so that it exectued before the silver keyboard event 31330 editor.on('remove', update.cancel); 31331 }; 31332 const setup$p = (editor) => { 31333 const activeAutocompleter = value$1(); 31334 const uiActive = Cell(false); 31335 const isActive = activeAutocompleter.isSet; 31336 const cancelIfNecessary = () => { 31337 if (isActive()) { 31338 fireAutocompleterEnd(editor); 31339 uiActive.set(false); 31340 activeAutocompleter.clear(); 31341 } 31342 }; 31343 const commenceIfNecessary = (context) => { 31344 if (!isActive()) { 31345 // store the element/context 31346 activeAutocompleter.set({ 31347 trigger: context.trigger, 31348 matchLength: context.text.length 31349 }); 31350 } 31351 }; 31352 // This needs to be calculated once things are ready, but the key events must be bound 31353 // before `init` or other keydown / keypress listeners will fire first. Therefore, 31354 // this is a thunk so that its value is calculated just once when it is used for the 31355 // first time, and after that it's value is stored. 31356 const getAutocompleters = cached(() => register$2(editor)); 31357 const doLookup = (fetchOptions) => activeAutocompleter.get().map((ac) => getContext(editor.dom, editor.selection.getRng(), ac.trigger, true) 31358 .bind((newContext) => lookupWithContext(editor, getAutocompleters, newContext, fetchOptions))).getOrThunk(() => lookup(editor, getAutocompleters)); 31359 const load = (fetchOptions) => { 31360 doLookup(fetchOptions).fold(cancelIfNecessary, (lookupInfo) => { 31361 commenceIfNecessary(lookupInfo.context); 31362 // Wait for the results to return and then display the menu 31363 // eslint-disable-next-line @typescript-eslint/no-floating-promises 31364 lookupInfo.lookupData.then((lookupData) => { 31365 // Lookup the active autocompleter to make sure it's still active, if it isn't then do nothing 31366 activeAutocompleter.get().map((ac) => { 31367 const context = lookupInfo.context; 31368 // Ensure the active autocompleter trigger matches, as the old one may have closed 31369 // and a new one may have opened. If it doesn't match, then do nothing. 31370 if (ac.trigger !== context.trigger) { 31371 return; 31372 } 31373 activeAutocompleter.set({ 31374 ...ac, 31375 matchLength: context.text.length 31376 }); 31377 if (uiActive.get()) { 31378 fireAutocompleterUpdateActiveRange(editor, { range: context.range }); 31379 fireAutocompleterUpdate(editor, { lookupData }); 31380 } 31381 else { 31382 uiActive.set(true); 31383 fireAutocompleterUpdateActiveRange(editor, { range: context.range }); 31384 fireAutocompleterStart(editor, { lookupData }); 31385 } 31386 }); 31387 }); 31388 }); 31389 }; 31390 const isRangeInsideOrEqual = (innerRange, outerRange) => { 31391 const startComparison = innerRange.compareBoundaryPoints(window.Range.START_TO_START, outerRange); 31392 const endComparison = innerRange.compareBoundaryPoints(window.Range.END_TO_END, outerRange); 31393 return startComparison >= 0 && endComparison <= 0; 31394 }; 31395 const readActiveRange = () => { 31396 return activeAutocompleter.get().bind(({ trigger }) => { 31397 const selRange = editor.selection.getRng(); 31398 return getContext(editor.dom, selRange, trigger, uiActive.get()) 31399 .filter(({ range }) => isRangeInsideOrEqual(selRange, range)) 31400 .map(({ range }) => range); 31401 }); 31402 }; 31403 editor.addCommand('mceAutocompleterReload', (_ui, value) => { 31404 const fetchOptions = isObject(value) ? value.fetchOptions : {}; 31405 load(fetchOptions); 31406 }); 31407 editor.addCommand('mceAutocompleterClose', cancelIfNecessary); 31408 editor.addCommand('mceAutocompleterRefreshActiveRange', () => { 31409 readActiveRange().each((range) => { 31410 fireAutocompleterUpdateActiveRange(editor, { range }); 31411 }); 31412 }); 31413 editor.editorCommands.addQueryStateHandler('mceAutoCompleterInRange', () => readActiveRange().isSome()); 31414 setupEditorInput(editor, { 31415 cancelIfNecessary, 31416 load 31417 }); 31418 }; 31419 31420 const browser$1 = detect$1().browser; 31421 const isSafari = browser$1.isSafari(); 31422 const emptyNodeContents = (node) => fillWithPaddingBr(SugarElement.fromDom(node)); 31423 const isEntireNodeSelected = (rng, node) => rng.startOffset === 0 && rng.endOffset === node.textContent?.length; 31424 const getParentDetailsElementAtPos = (dom, pos) => Optional.from(dom.getParent(pos.container(), 'details')); 31425 const isInDetailsElement = (dom, pos) => getParentDetailsElementAtPos(dom, pos).isSome(); 31426 const getDetailsElements = (dom, rng) => { 31427 const startDetails = Optional.from(dom.getParent(rng.startContainer, 'details')); 31428 const endDetails = Optional.from(dom.getParent(rng.endContainer, 'details')); 31429 if (startDetails.isSome() || endDetails.isSome()) { 31430 const startSummary = startDetails.bind((details) => Optional.from(dom.select('summary', details)[0])); 31431 return Optional.some({ startSummary, startDetails, endDetails }); 31432 } 31433 else { 31434 return Optional.none(); 31435 } 31436 }; 31437 const isCaretInTheBeginningOf = (caretPos, element) => firstPositionIn(element).exists((pos) => pos.isEqual(caretPos)); 31438 const isCaretInTheEndOf = (caretPos, element) => { 31439 return lastPositionIn(element).exists((pos) => { 31440 // Summary may or may not have a trailing BR 31441 if (isBr$7(pos.getNode())) { 31442 return prevPosition(element, pos).exists((pos2) => pos2.isEqual(caretPos)) || pos.isEqual(caretPos); 31443 } 31444 else { 31445 return pos.isEqual(caretPos); 31446 } 31447 }); 31448 }; 31449 const isCaretAtStartOfSummary = (caretPos, detailsElements) => detailsElements.startSummary.exists((summary) => isCaretInTheBeginningOf(caretPos, summary)); 31450 const isCaretAtEndOfSummary = (caretPos, detailsElements) => detailsElements.startSummary.exists((summary) => isCaretInTheEndOf(caretPos, summary)); 31451 const isCaretInFirstPositionInBody = (caretPos, detailsElements) => detailsElements.startDetails.exists((details) => prevPosition(details, caretPos).forall((pos) => detailsElements.startSummary.exists((summary) => !summary.contains(caretPos.container()) && summary.contains(pos.container())))); 31452 const isCaretInLastPositionInBody = (root, caretPos, detailsElements) => detailsElements.startDetails.exists((details) => nextPosition(root, caretPos).forall((pos) => !details.contains(pos.container()))); 31453 const setCaretToPosition = (editor, position) => { 31454 const node = position.getNode(); 31455 if (!isUndefined(node)) { 31456 editor.selection.setCursorLocation(node, position.offset()); 31457 } 31458 }; 31459 const moveCaretToDetailsPos = (editor, pos, forward) => { 31460 const details = editor.dom.getParent(pos.container(), 'details'); 31461 if (details && !details.open) { 31462 const summary = editor.dom.select('summary', details)[0]; 31463 if (summary) { 31464 const newPos = forward ? firstPositionIn(summary) : lastPositionIn(summary); 31465 newPos.each((pos) => setCaretToPosition(editor, pos)); 31466 } 31467 } 31468 else { 31469 setCaretToPosition(editor, pos); 31470 } 31471 }; 31472 const isPartialDelete = (rng, detailsElements) => { 31473 const containsStart = (element) => element.contains(rng.startContainer); 31474 const containsEnd = (element) => element.contains(rng.endContainer); 31475 const startInSummary = detailsElements.startSummary.exists(containsStart); 31476 const endInSummary = detailsElements.startSummary.exists(containsEnd); 31477 const isPartiallySelectedDetailsElements = detailsElements.startDetails.forall((startDetails) => detailsElements.endDetails.forall((endDetails) => startDetails !== endDetails)); 31478 const isInPartiallySelectedSummary = (startInSummary || endInSummary) && !(startInSummary && endInSummary); 31479 return isInPartiallySelectedSummary || isPartiallySelectedDetailsElements; 31480 }; 31481 const shouldPreventDeleteIntoDetails = (editor, forward, granularity) => { 31482 const { dom, selection } = editor; 31483 const root = editor.getBody(); 31484 if (granularity === 'character') { 31485 const caretPos = CaretPosition.fromRangeStart(selection.getRng()); 31486 const parentBlock = dom.getParent(caretPos.container(), dom.isBlock); 31487 const parentDetailsAtCaret = getParentDetailsElementAtPos(dom, caretPos); 31488 const inEmptyParentBlock = parentBlock && dom.isEmpty(parentBlock); 31489 const isFirstBlock = isNull(parentBlock?.previousSibling); 31490 const isLastBlock = isNull(parentBlock?.nextSibling); 31491 // Pressing backspace or delete in an first or last empty block before or after details 31492 if (inEmptyParentBlock) { 31493 const firstOrLast = forward ? isLastBlock : isFirstBlock; 31494 if (firstOrLast) { 31495 const isBeforeAfterDetails = navigate(!forward, root, caretPos).exists((pos) => { 31496 return isInDetailsElement(dom, pos) && !equals(parentDetailsAtCaret, getParentDetailsElementAtPos(dom, pos)); 31497 }); 31498 if (isBeforeAfterDetails) { 31499 return true; 31500 } 31501 } 31502 } 31503 return navigate(forward, root, caretPos).fold(never, (pos) => { 31504 const parentDetailsAtNewPos = getParentDetailsElementAtPos(dom, pos); 31505 if (isInDetailsElement(dom, pos) && !equals(parentDetailsAtCaret, parentDetailsAtNewPos)) { 31506 if (!forward) { 31507 moveCaretToDetailsPos(editor, pos, false); 31508 } 31509 if (parentBlock && inEmptyParentBlock) { 31510 if (forward && isFirstBlock) { 31511 return true; 31512 } 31513 else if (!forward && isLastBlock) { 31514 return true; 31515 } 31516 moveCaretToDetailsPos(editor, pos, forward); 31517 editor.dom.remove(parentBlock); 31518 } 31519 return true; 31520 } 31521 else { 31522 return false; 31523 } 31524 }); 31525 } 31526 else { 31527 return false; 31528 } 31529 }; 31530 const shouldPreventDeleteSummaryAction = (editor, detailElements, forward, granularity) => { 31531 const selection = editor.selection; 31532 const rng = selection.getRng(); 31533 const caretPos = CaretPosition.fromRangeStart(rng); 31534 const root = editor.getBody(); 31535 if (granularity === 'selection') { 31536 return isPartialDelete(rng, detailElements); 31537 } 31538 else if (forward) { 31539 return isCaretAtEndOfSummary(caretPos, detailElements) || isCaretInLastPositionInBody(root, caretPos, detailElements); 31540 } 31541 else { 31542 return isCaretAtStartOfSummary(caretPos, detailElements) || isCaretInFirstPositionInBody(caretPos, detailElements); 31543 } 31544 }; 31545 const shouldPreventDeleteAction = (editor, forward, granularity) => getDetailsElements(editor.dom, editor.selection.getRng()).fold(() => shouldPreventDeleteIntoDetails(editor, forward, granularity), (detailsElements) => shouldPreventDeleteSummaryAction(editor, detailsElements, forward, granularity) || shouldPreventDeleteIntoDetails(editor, forward, granularity)); 31546 const handleDeleteActionSafari = (editor, forward, granularity) => { 31547 const selection = editor.selection; 31548 const node = selection.getNode(); 31549 const rng = selection.getRng(); 31550 const caretPos = CaretPosition.fromRangeStart(rng); 31551 if (isSummary$1(node)) { 31552 // TINY-9951: Safari bug, deleting within the summary causes all content to be removed and no caret position to be left 31553 // https://bugs.webkit.org/show_bug.cgi?id=257745 31554 if (granularity === 'selection' && isEntireNodeSelected(rng, node) || willDeleteLastPositionInElement(forward, caretPos, node)) { 31555 emptyNodeContents(node); 31556 } 31557 else { 31558 editor.undoManager.transact(() => { 31559 // Wrap all summary children in a temporary container to execute Backspace/Delete there, then unwrap 31560 const sel = selection.getSel(); 31561 let { anchorNode, anchorOffset, focusNode, focusOffset } = sel ?? {}; 31562 const applySelection = () => { 31563 if (isNonNullable(anchorNode) && isNonNullable(anchorOffset) && isNonNullable(focusNode) && isNonNullable(focusOffset)) { 31564 sel?.setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset); 31565 } 31566 }; 31567 const updateSelection = () => { 31568 anchorNode = sel?.anchorNode; 31569 anchorOffset = sel?.anchorOffset; 31570 focusNode = sel?.focusNode; 31571 focusOffset = sel?.focusOffset; 31572 }; 31573 const appendAllChildNodes = (from, to) => { 31574 each$e(from.childNodes, (child) => { 31575 if (isNode(child)) { 31576 to.appendChild(child); 31577 } 31578 }); 31579 }; 31580 const container = editor.dom.create('span', { 'data-mce-bogus': '1' }); 31581 appendAllChildNodes(node, container); 31582 node.appendChild(container); 31583 applySelection(); 31584 // Manually perform deletion with modified granularities 31585 if (granularity === 'word' || granularity === 'line') { 31586 sel?.modify('extend', forward ? 'right' : 'left', granularity); 31587 } 31588 if (!selection.isCollapsed() && isEntireNodeSelected(selection.getRng(), container)) { 31589 emptyNodeContents(node); 31590 } 31591 else { 31592 editor.execCommand(forward ? 'ForwardDelete' : 'Delete'); 31593 updateSelection(); 31594 appendAllChildNodes(container, node); 31595 applySelection(); 31596 } 31597 editor.dom.remove(container); 31598 }); 31599 } 31600 return true; 31601 } 31602 else { 31603 return false; 31604 } 31605 }; 31606 const backspaceDelete = (editor, forward, granularity) => shouldPreventDeleteAction(editor, forward, granularity) || isSafari && handleDeleteActionSafari(editor, forward, granularity) 31607 ? Optional.some(noop) : Optional.none(); 31608 31609 const platform$2 = detect$1(); 31610 const os = platform$2.os; 31611 const isMacOSOriOS = os.isMacOS() || os.isiOS(); 31612 const browser = platform$2.browser; 31613 const isFirefox = browser.isFirefox(); 31614 const executeKeydownOverride$3 = (editor, caret, evt) => { 31615 const inputType = evt.keyCode === VK.BACKSPACE ? 'deleteContentBackward' : 'deleteContentForward'; 31616 const isCollapsed = editor.selection.isCollapsed(); 31617 const unmodifiedGranularity = isCollapsed ? 'character' : 'selection'; 31618 const getModifiedGranularity = (isWord) => { 31619 if (isCollapsed) { 31620 return isWord ? 'word' : 'line'; 31621 } 31622 else { 31623 return 'selection'; 31624 } 31625 }; 31626 executeWithDelayedAction([ 31627 { keyCode: VK.BACKSPACE, action: action(backspaceDelete$2, editor) }, 31628 { keyCode: VK.BACKSPACE, action: action(backspaceDelete$8, editor, false) }, 31629 { keyCode: VK.DELETE, action: action(backspaceDelete$8, editor, true) }, 31630 { keyCode: VK.BACKSPACE, action: action(backspaceDelete$9, editor, false) }, 31631 { keyCode: VK.DELETE, action: action(backspaceDelete$9, editor, true) }, 31632 { keyCode: VK.BACKSPACE, action: action(backspaceDelete$5, editor, caret, false) }, 31633 { keyCode: VK.DELETE, action: action(backspaceDelete$5, editor, caret, true) }, 31634 { keyCode: VK.BACKSPACE, action: action(backspaceDelete$d, editor, false) }, 31635 { keyCode: VK.DELETE, action: action(backspaceDelete$d, editor, true) }, 31636 { keyCode: VK.BACKSPACE, action: action(backspaceDelete, editor, false, unmodifiedGranularity) }, 31637 { keyCode: VK.DELETE, action: action(backspaceDelete, editor, true, unmodifiedGranularity) }, 31638 ...isMacOSOriOS ? [ 31639 { keyCode: VK.BACKSPACE, altKey: true, action: action(backspaceDelete, editor, false, getModifiedGranularity(true)) }, 31640 { keyCode: VK.DELETE, altKey: true, action: action(backspaceDelete, editor, true, getModifiedGranularity(true)) }, 31641 { keyCode: VK.BACKSPACE, metaKey: true, action: action(backspaceDelete, editor, false, getModifiedGranularity(false)) }, 31642 ] : [ 31643 { keyCode: VK.BACKSPACE, ctrlKey: true, action: action(backspaceDelete, editor, false, getModifiedGranularity(true)) }, 31644 { keyCode: VK.DELETE, ctrlKey: true, action: action(backspaceDelete, editor, true, getModifiedGranularity(true)) } 31645 ], 31646 { keyCode: VK.BACKSPACE, action: action(backspaceDelete$6, editor, false) }, 31647 { keyCode: VK.DELETE, action: action(backspaceDelete$6, editor, true) }, 31648 { keyCode: VK.BACKSPACE, action: action(backspaceDelete$3, editor, false) }, 31649 { keyCode: VK.DELETE, action: action(backspaceDelete$3, editor, true) }, 31650 { keyCode: VK.BACKSPACE, action: action(backspaceDelete$a, editor, false) }, 31651 { keyCode: VK.DELETE, action: action(backspaceDelete$a, editor, true) }, 31652 { keyCode: VK.BACKSPACE, action: action(backspaceDelete$b, editor, false) }, 31653 { keyCode: VK.DELETE, action: action(backspaceDelete$b, editor, true) }, 31654 { keyCode: VK.BACKSPACE, action: action(backspaceDelete$4, editor, false) }, 31655 { keyCode: VK.DELETE, action: action(backspaceDelete$4, editor, true) }, 31656 { keyCode: VK.BACKSPACE, action: action(backspaceDelete$7, editor, false) }, 31657 { keyCode: VK.DELETE, action: action(backspaceDelete$7, editor, true) }, 31658 { keyCode: VK.BACKSPACE, action: action(backspaceDelete$1, editor, false) }, 31659 { keyCode: VK.DELETE, action: action(backspaceDelete$1, editor, true) }, 31660 ], evt) 31661 .filter((_) => editor.selection.isEditable()) 31662 .each((applyAction) => { 31663 evt.preventDefault(); 31664 const beforeInput = fireBeforeInputEvent(editor, inputType); 31665 if (!beforeInput.isDefaultPrevented()) { 31666 applyAction(); 31667 fireInputEvent(editor, inputType); 31668 } 31669 }); 31670 }; 31671 const executeKeyupOverride = (editor, evt, isBackspaceKeydown, formatNodes) => execute([ 31672 { keyCode: VK.BACKSPACE, action: action(paddEmptyElement, editor) }, 31673 { keyCode: VK.DELETE, action: action(paddEmptyElement, editor) }, 31674 ...isMacOSOriOS ? [ 31675 { keyCode: VK.BACKSPACE, altKey: true, action: action(refreshCaret, editor) }, 31676 { keyCode: VK.DELETE, altKey: true, action: action(refreshCaret, editor) }, 31677 // macOS surpresses keyup events for most keys including Backspace when Meta key is engaged 31678 // To emulate Meta + Backspace on macOS, add a pattern for the meta key when backspace was 31679 // detected on keydown 31680 ...isBackspaceKeydown ? [{ 31681 // Firefox detects macOS Command key code as "Command" not "Meta" 31682 keyCode: isFirefox ? 224 : 91, 31683 action: action(() => { 31684 updateCaretFormat(editor, formatNodes); 31685 return refreshCaret(editor); 31686 }) 31687 }] : [] 31688 ] : [ 31689 { keyCode: VK.BACKSPACE, ctrlKey: true, action: action(refreshCaret, editor) }, 31690 { keyCode: VK.DELETE, ctrlKey: true, action: action(refreshCaret, editor) } 31691 ] 31692 ], evt); 31693 const setup$o = (editor, caret) => { 31694 // track backspace keydown state for emulating Meta + Backspace keyup detection on macOS 31695 let isBackspaceKeydown = false; 31696 let formatNodes = []; 31697 editor.on('init', () => { 31698 editor.on('keydown', (evt) => { 31699 isBackspaceKeydown = evt.keyCode === VK.BACKSPACE; 31700 formatNodes = getFormatNodesAtStart(editor); 31701 if (!evt.isDefaultPrevented()) { 31702 executeKeydownOverride$3(editor, caret, evt); 31703 } 31704 }); 31705 editor.on('keyup', (evt) => { 31706 if (!evt.isDefaultPrevented()) { 31707 executeKeyupOverride(editor, evt, isBackspaceKeydown, formatNodes); 31708 formatNodes.length = 0; 31709 } 31710 isBackspaceKeydown = false; 31711 }); 31712 }); 31713 }; 31714 31715 const reduceFontStyleNesting = (block, node) => { 31716 const blockSugar = SugarElement.fromDom(block); 31717 const nodeSugar = SugarElement.fromDom(node); 31718 const isSpan = isTag('span'); 31719 const isEndBlock = curry(eq, blockSugar); 31720 const hasFontSize = (element) => isElement$8(element) && getRaw$1(element, 'font-size').isSome(); 31721 const elementsWithFontSize = [ 31722 ...(hasFontSize(nodeSugar) ? [nodeSugar] : []), 31723 ...ancestors$1(nodeSugar, hasFontSize, isEndBlock) 31724 ]; 31725 each$e(elementsWithFontSize.slice(1), (element) => { 31726 remove$7(element, 'font-size'); 31727 remove$9(element, 'data-mce-style'); 31728 if (isSpan(element) && hasNone(element)) { 31729 unwrap(element); 31730 } 31731 }); 31732 }; 31733 31734 const firstNonWhiteSpaceNodeSibling = (node) => { 31735 while (node) { 31736 if (isElement$7(node) || (isText$b(node) && node.data && /[\r\n\s]/.test(node.data))) { 31737 return Optional.from(SugarElement.fromDom(node)); 31738 } 31739 node = node.nextSibling; 31740 } 31741 return Optional.none(); 31742 }; 31743 const moveToCaretPosition = (editor, root) => { 31744 const dom = editor.dom; 31745 const moveCaretBeforeOnEnterElementsMap = editor.schema.getMoveCaretBeforeOnEnterElements(); 31746 if (!root) { 31747 return; 31748 } 31749 if (isListItem$2(SugarElement.fromDom(root))) { 31750 const findFirstList = (e) => isList$1(e) ? Optional.from(e) : descendant$2(e, isList$1); 31751 const isEmpty = (e) => dom.isEmpty(e.dom); 31752 firstNonWhiteSpaceNodeSibling(root.firstChild).each((firstChild) => { 31753 findFirstList(firstChild).fold(() => { 31754 if (isEmpty(firstChild)) { 31755 const element = toLeaf$1(firstChild, 0).element; 31756 if (isElement$8(element) && !isBr$6(element)) { 31757 append$1(element, SugarElement.fromHtml('<br data-mce-bogus="1" />')); 31758 } 31759 } 31760 }, (firstList) => { 31761 before$4(firstList, SugarElement.fromText(nbsp)); 31762 }); 31763 }); 31764 } 31765 const rng = dom.createRng(); 31766 root.normalize(); 31767 if (root.hasChildNodes()) { 31768 const walker = new DomTreeWalker(root, root); 31769 let lastNode = root; 31770 let node; 31771 while ((node = walker.current())) { 31772 if (isText$b(node)) { 31773 rng.setStart(node, 0); 31774 rng.setEnd(node, 0); 31775 break; 31776 } 31777 if (moveCaretBeforeOnEnterElementsMap[node.nodeName.toLowerCase()]) { 31778 rng.setStartBefore(node); 31779 rng.setEndBefore(node); 31780 break; 31781 } 31782 lastNode = node; 31783 node = walker.next(); 31784 } 31785 if (!node) { 31786 rng.setStart(lastNode, 0); 31787 rng.setEnd(lastNode, 0); 31788 } 31789 } 31790 else { 31791 if (isBr$7(root)) { 31792 if (root.nextSibling && dom.isBlock(root.nextSibling)) { 31793 rng.setStartBefore(root); 31794 rng.setEndBefore(root); 31795 } 31796 else { 31797 rng.setStartAfter(root); 31798 rng.setEndAfter(root); 31799 } 31800 } 31801 else { 31802 rng.setStart(root, 0); 31803 rng.setEnd(root, 0); 31804 } 31805 } 31806 editor.selection.setRng(rng); 31807 scrollRangeIntoView(editor, rng); 31808 }; 31809 const getEditableRoot = (dom, node) => { 31810 const root = dom.getRoot(); 31811 let editableRoot; 31812 // Get all parents until we hit a non editable parent or the root 31813 let parent = node; 31814 while (parent !== root && parent && dom.getContentEditable(parent) !== 'false') { 31815 if (dom.getContentEditable(parent) === 'true') { 31816 editableRoot = parent; 31817 break; 31818 } 31819 parent = parent.parentNode; 31820 } 31821 return parent !== root ? editableRoot : root; 31822 }; 31823 const getParentBlock$1 = (editor) => { 31824 return Optional.from(editor.dom.getParent(editor.selection.getStart(true), editor.dom.isBlock)); 31825 }; 31826 const getParentBlockName = (editor) => { 31827 return getParentBlock$1(editor).fold(constant(''), (parentBlock) => { 31828 return parentBlock.nodeName.toUpperCase(); 31829 }); 31830 }; 31831 const isListItemParentBlock = (editor) => { 31832 return getParentBlock$1(editor).filter((elm) => { 31833 return isListItem$2(SugarElement.fromDom(elm)); 31834 }).isSome(); 31835 }; 31836 const emptyBlock = (elm) => { 31837 elm.innerHTML = '<br data-mce-bogus="1">'; 31838 }; 31839 const applyAttributes = (editor, node, forcedRootBlockAttrs) => { 31840 const dom = editor.dom; 31841 // Merge and apply style attribute 31842 Optional.from(forcedRootBlockAttrs.style) 31843 .map(dom.parseStyle) 31844 .each((attrStyles) => { 31845 const currentStyles = getAllRaw(SugarElement.fromDom(node)); 31846 const newStyles = { ...currentStyles, ...attrStyles }; 31847 dom.setStyles(node, newStyles); 31848 }); 31849 // Merge and apply class attribute 31850 const attrClassesOpt = Optional.from(forcedRootBlockAttrs.class).map((attrClasses) => attrClasses.split(/\s+/)); 31851 const currentClassesOpt = Optional.from(node.className).map((currentClasses) => filter$5(currentClasses.split(/\s+/), (clazz) => clazz !== '')); 31852 lift2(attrClassesOpt, currentClassesOpt, (attrClasses, currentClasses) => { 31853 const filteredClasses = filter$5(currentClasses, (clazz) => !contains$2(attrClasses, clazz)); 31854 const newClasses = [...attrClasses, ...filteredClasses]; 31855 dom.setAttrib(node, 'class', newClasses.join(' ')); 31856 }); 31857 // Apply any remaining forced root block attributes 31858 const appliedAttrs = ['style', 'class']; 31859 const remainingAttrs = filter$4(forcedRootBlockAttrs, (_, attrs) => !contains$2(appliedAttrs, attrs)); 31860 dom.setAttribs(node, remainingAttrs); 31861 }; 31862 const setForcedBlockAttrs = (editor, node) => { 31863 const forcedRootBlockName = getForcedRootBlock(editor); 31864 if (forcedRootBlockName.toLowerCase() === node.tagName.toLowerCase()) { 31865 const forcedRootBlockAttrs = getForcedRootBlockAttrs(editor); 31866 applyAttributes(editor, node, forcedRootBlockAttrs); 31867 } 31868 }; 31869 // Creates a new block element by cloning the current one or creating a new one if the name is specified 31870 // This function will also copy any text formatting from the parent block and add it to the new one 31871 const createNewBlock = (editor, container, parentBlock, editableRoot, keepStyles = true, name, styles) => { 31872 const dom = editor.dom; 31873 const schema = editor.schema; 31874 const newBlockName = getForcedRootBlock(editor); 31875 const parentBlockName = parentBlock ? parentBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5 31876 let node = container; 31877 const textInlineElements = schema.getTextInlineElements(); 31878 let block; 31879 if (name || parentBlockName === 'TABLE' || parentBlockName === 'HR') { 31880 block = dom.create(name || newBlockName, styles || {}); 31881 } 31882 else { 31883 block = parentBlock.cloneNode(false); 31884 } 31885 let caretNode = block; 31886 if (!keepStyles) { 31887 dom.setAttrib(block, 'style', null); // wipe out any styles that came over with the block 31888 dom.setAttrib(block, 'class', null); 31889 } 31890 else { 31891 // Clone any parent styles 31892 do { 31893 if (textInlineElements[node.nodeName]) { 31894 // Ignore caret or bookmark nodes when cloning 31895 if (isCaretNode(node) || isBookmarkNode$1(node)) { 31896 continue; 31897 } 31898 const clonedNode = node.cloneNode(false); 31899 dom.setAttrib(clonedNode, 'id', ''); // Remove ID since it needs to be document unique 31900 if (block.hasChildNodes()) { 31901 clonedNode.appendChild(block.firstChild); 31902 block.appendChild(clonedNode); 31903 } 31904 else { 31905 caretNode = clonedNode; 31906 block.appendChild(clonedNode); 31907 } 31908 } 31909 } while ((node = node.parentNode) && node !== editableRoot); 31910 // Not omitting font sizes of list items otherwise their font size doesn't match its content 31911 if (block.nodeName !== 'LI') { 31912 reduceFontStyleNesting(block, caretNode); 31913 } 31914 } 31915 setForcedBlockAttrs(editor, block); 31916 emptyBlock(caretNode); 31917 return block; 31918 }; 31919 31920 const getDetailsRoot = (editor, element) => editor.dom.getParent(element, isDetails); 31921 const isAtDetailsEdge = (root, element, isTextBlock) => { 31922 let node = element; 31923 while (node && node !== root && isNull(node.nextSibling)) { 31924 const parent = node.parentElement; 31925 if (!parent || !isTextBlock(parent)) { 31926 return isDetails(parent); 31927 } 31928 node = parent; 31929 } 31930 return false; 31931 }; 31932 const isLastEmptyBlockInDetails = (editor, shiftKey, element) => !shiftKey && 31933 element.nodeName.toLowerCase() === getForcedRootBlock(editor) && 31934 editor.dom.isEmpty(element) && 31935 isAtDetailsEdge(editor.getBody(), element, (el) => has$2(editor.schema.getTextBlockElements(), el.nodeName.toLowerCase())); 31936 const insertNewLine = (editor, createNewBlock, parentBlock) => { 31937 const newBlock = createNewBlock(getForcedRootBlock(editor)); 31938 const root = getDetailsRoot(editor, parentBlock); 31939 if (!root) { 31940 return; 31941 } 31942 editor.dom.insertAfter(newBlock, root); 31943 moveToCaretPosition(editor, newBlock); 31944 // TODO: This now only works with our Accordions not details with multiple root level children should we support that 31945 if ((parentBlock.parentElement?.childNodes?.length ?? 0) > 1) { 31946 editor.dom.remove(parentBlock); 31947 } 31948 }; 31949 31950 const hasFirstChild = (elm, name) => { 31951 return elm.firstChild && elm.firstChild.nodeName === name; 31952 }; 31953 const isFirstChild = (elm) => { 31954 return elm.parentNode?.firstChild === elm; 31955 }; 31956 const hasParent = (elm, parentName) => { 31957 const parentNode = elm?.parentNode; 31958 return isNonNullable(parentNode) && parentNode.nodeName === parentName; 31959 }; 31960 const isListBlock = (elm) => { 31961 return isNonNullable(elm) && /^(OL|UL|LI)$/.test(elm.nodeName); 31962 }; 31963 const isListItem = (elm) => { 31964 return isNonNullable(elm) && /^(LI|DT|DD)$/.test(elm.nodeName); 31965 }; 31966 const isNestedList = (elm) => { 31967 return isListBlock(elm) && isListBlock(elm.parentNode); 31968 }; 31969 const getContainerBlock = (containerBlock) => { 31970 const containerBlockParent = containerBlock.parentNode; 31971 return isListItem(containerBlockParent) ? containerBlockParent : containerBlock; 31972 }; 31973 const isFirstOrLastLi = (containerBlock, parentBlock, first) => { 31974 let node = containerBlock[first ? 'firstChild' : 'lastChild']; 31975 // Find first/last element since there might be whitespace there 31976 while (node) { 31977 if (isElement$7(node)) { 31978 break; 31979 } 31980 node = node[first ? 'nextSibling' : 'previousSibling']; 31981 } 31982 return node === parentBlock; 31983 }; 31984 const getStyles = (elm) => foldl(mapToArray(getAllRaw(SugarElement.fromDom(elm)), (style, styleName) => `${styleName}: ${style};`), (acc, s) => acc + s, ''); 31985 // Inserts a block or br before/after or in the middle of a split list of the LI is empty 31986 const insert$4 = (editor, createNewBlock, containerBlock, parentBlock, newBlockName) => { 31987 const dom = editor.dom; 31988 const rng = editor.selection.getRng(); 31989 const containerParent = containerBlock.parentNode; 31990 if (containerBlock === editor.getBody() || !containerParent) { 31991 return; 31992 } 31993 if (isNestedList(containerBlock)) { 31994 newBlockName = 'LI'; 31995 } 31996 const parentBlockStyles = isListItem(parentBlock) ? getStyles(parentBlock) : undefined; 31997 let newBlock = isListItem(parentBlock) && parentBlockStyles 31998 ? createNewBlock(newBlockName, { style: getStyles(parentBlock) }) 31999 : createNewBlock(newBlockName); 32000 if (isFirstOrLastLi(containerBlock, parentBlock, true) && isFirstOrLastLi(containerBlock, parentBlock, false)) { 32001 if (hasParent(containerBlock, 'LI')) { 32002 // Nested list is inside a LI 32003 const containerBlockParent = getContainerBlock(containerBlock); 32004 dom.insertAfter(newBlock, containerBlockParent); 32005 if (isFirstChild(containerBlock)) { 32006 dom.remove(containerBlockParent); 32007 } 32008 else { 32009 dom.remove(containerBlock); 32010 } 32011 } 32012 else { 32013 // Is first and last list item then replace the OL/UL with a text block 32014 dom.replace(newBlock, containerBlock); 32015 } 32016 } 32017 else if (isFirstOrLastLi(containerBlock, parentBlock, true)) { 32018 if (hasParent(containerBlock, 'LI')) { 32019 // List nested in an LI then move the list to a new sibling LI 32020 dom.insertAfter(newBlock, getContainerBlock(containerBlock)); 32021 newBlock.appendChild(dom.doc.createTextNode(' ')); // Needed for IE so the caret can be placed 32022 newBlock.appendChild(containerBlock); 32023 } 32024 else { 32025 // First LI in list then remove LI and add text block before list 32026 containerParent.insertBefore(newBlock, containerBlock); 32027 } 32028 dom.remove(parentBlock); 32029 } 32030 else if (isFirstOrLastLi(containerBlock, parentBlock, false)) { 32031 // Last LI in list then remove LI and add text block after list 32032 dom.insertAfter(newBlock, getContainerBlock(containerBlock)); 32033 dom.remove(parentBlock); 32034 } 32035 else { 32036 // Middle LI in list then split the list and insert a text block in the middle 32037 // Extract after fragment and insert it after the current block 32038 containerBlock = getContainerBlock(containerBlock); 32039 const tmpRng = rng.cloneRange(); 32040 tmpRng.setStartAfter(parentBlock); 32041 tmpRng.setEndAfter(containerBlock); 32042 const fragment = tmpRng.extractContents(); 32043 if (newBlockName === 'LI' && hasFirstChild(fragment, 'LI')) { 32044 const previousChildren = filter$5(map$3(newBlock.children, SugarElement.fromDom), not(isTag('br'))); 32045 newBlock = fragment.firstChild; 32046 dom.insertAfter(fragment, containerBlock); 32047 each$e(previousChildren, (child) => prepend(SugarElement.fromDom(newBlock), child)); 32048 if (parentBlockStyles) { 32049 newBlock.setAttribute('style', parentBlockStyles); 32050 } 32051 } 32052 else { 32053 dom.insertAfter(fragment, containerBlock); 32054 dom.insertAfter(newBlock, containerBlock); 32055 } 32056 dom.remove(parentBlock); 32057 } 32058 moveToCaretPosition(editor, newBlock); 32059 }; 32060 32061 const trimZwsp = (fragment) => { 32062 each$e(descendants$1(SugarElement.fromDom(fragment), isText$c), (text) => { 32063 const rawNode = text.dom; 32064 rawNode.nodeValue = trim$2(rawNode.data); 32065 }); 32066 }; 32067 const isWithinNonEditableList = (editor, node) => { 32068 const parentList = editor.dom.getParent(node, 'ol,ul,dl'); 32069 return parentList !== null && editor.dom.getContentEditableParent(parentList) === 'false'; 32070 }; 32071 const isEmptyAnchor = (dom, elm) => { 32072 return elm && elm.nodeName === 'A' && dom.isEmpty(elm); 32073 }; 32074 const containerAndPreviousSiblingName = (container, nodeName) => { 32075 return container.nodeName === nodeName || (container.previousSibling && container.previousSibling.nodeName === nodeName); 32076 }; 32077 const containerAndNextSiblingName = (container, nodeName) => { 32078 return container.nodeName === nodeName || (container.nextSibling && container.nextSibling.nodeName === nodeName); 32079 }; 32080 // Returns true if the block can be split into two blocks or not 32081 const canSplitBlock = (dom, node) => { 32082 return isNonNullable(node) && 32083 dom.isBlock(node) && 32084 !/^(TD|TH|CAPTION|FORM)$/.test(node.nodeName) && 32085 !/^(fixed|absolute)/i.test(node.style.position) && 32086 dom.isEditable(node.parentNode) && dom.getContentEditable(node) !== 'false'; 32087 }; 32088 // Remove the first empty inline element of the block so this: <p><b><em></em></b>x</p> becomes this: <p>x</p> 32089 const trimInlineElementsOnLeftSideOfBlock = (dom, nonEmptyElementsMap, block) => { 32090 const firstChilds = []; 32091 if (!block) { 32092 return; 32093 } 32094 // Find inner most first child ex: <p><i><b>*</b></i></p> 32095 let currentNode = block; 32096 while ((currentNode = currentNode.firstChild)) { 32097 if (dom.isBlock(currentNode)) { 32098 return; 32099 } 32100 if (isElement$7(currentNode) && !nonEmptyElementsMap[currentNode.nodeName.toLowerCase()]) { 32101 firstChilds.push(currentNode); 32102 } 32103 } 32104 let i = firstChilds.length; 32105 while (i--) { 32106 currentNode = firstChilds[i]; 32107 if (!currentNode.hasChildNodes() || (currentNode.firstChild === currentNode.lastChild && currentNode.firstChild?.nodeValue === '')) { 32108 dom.remove(currentNode); 32109 } 32110 else { 32111 if (isEmptyAnchor(dom, currentNode)) { 32112 dom.remove(currentNode); 32113 } 32114 } 32115 } 32116 }; 32117 const normalizeZwspOffset = (start, container, offset) => { 32118 if (!isText$b(container)) { 32119 return offset; 32120 } 32121 else if (start) { 32122 return offset === 1 && container.data.charAt(offset - 1) === ZWSP$1 ? 0 : offset; 32123 } 32124 else { 32125 return offset === container.data.length - 1 && container.data.charAt(offset) === ZWSP$1 ? container.data.length : offset; 32126 } 32127 }; 32128 const includeZwspInRange = (rng) => { 32129 const newRng = rng.cloneRange(); 32130 newRng.setStart(rng.startContainer, normalizeZwspOffset(true, rng.startContainer, rng.startOffset)); 32131 newRng.setEnd(rng.endContainer, normalizeZwspOffset(false, rng.endContainer, rng.endOffset)); 32132 return newRng; 32133 }; 32134 // Trims any linebreaks at the beginning of node user for example when pressing enter in a PRE element 32135 const trimLeadingLineBreaks = (node) => { 32136 let currentNode = node; 32137 do { 32138 if (isText$b(currentNode)) { 32139 currentNode.data = currentNode.data.replace(/^[\r\n]+/, ''); 32140 } 32141 currentNode = currentNode.firstChild; 32142 } while (currentNode); 32143 }; 32144 // Wraps any text nodes or inline elements in the specified forced root block name 32145 const wrapSelfAndSiblingsInDefaultBlock = (editor, newBlockName, rng, container, offset) => { 32146 const dom = editor.dom; 32147 const editableRoot = getEditableRoot(dom, container) ?? dom.getRoot(); 32148 // Not in a block element or in a table cell or caption 32149 let parentBlock = dom.getParent(container, dom.isBlock); 32150 if (!parentBlock || !canSplitBlock(dom, parentBlock)) { 32151 parentBlock = parentBlock || editableRoot; 32152 if (!parentBlock.hasChildNodes()) { 32153 const newBlock = dom.create(newBlockName); 32154 setForcedBlockAttrs(editor, newBlock); 32155 parentBlock.appendChild(newBlock); 32156 rng.setStart(newBlock, 0); 32157 rng.setEnd(newBlock, 0); 32158 return newBlock; 32159 } 32160 // Find parent that is the first child of parentBlock 32161 let node = container; 32162 while (node && node.parentNode !== parentBlock) { 32163 node = node.parentNode; 32164 } 32165 // Loop left to find start node start wrapping at 32166 let startNode; 32167 while (node && !dom.isBlock(node)) { 32168 startNode = node; 32169 node = node.previousSibling; 32170 } 32171 const startNodeName = startNode?.parentElement?.nodeName; 32172 if (startNode && startNodeName && editor.schema.isValidChild(startNodeName, newBlockName.toLowerCase())) { 32173 // This should never be null since we check it above 32174 const startNodeParent = startNode.parentNode; 32175 const newBlock = dom.create(newBlockName); 32176 setForcedBlockAttrs(editor, newBlock); 32177 startNodeParent.insertBefore(newBlock, startNode); 32178 // Start wrapping until we hit a block 32179 node = startNode; 32180 while (node && !dom.isBlock(node)) { 32181 const next = node.nextSibling; 32182 newBlock.appendChild(node); 32183 node = next; 32184 } 32185 // Restore range to it's past location 32186 rng.setStart(container, offset); 32187 rng.setEnd(container, offset); 32188 } 32189 } 32190 return container; 32191 }; 32192 // Adds a BR at the end of blocks that only contains an IMG or INPUT since 32193 // these might be floated and then they won't expand the block 32194 const addBrToBlockIfNeeded = (dom, block) => { 32195 // IE will render the blocks correctly other browsers needs a BR 32196 block.normalize(); // Remove empty text nodes that got left behind by the extract 32197 // Check if the block is empty or contains a floated last child 32198 const lastChild = block.lastChild; 32199 if (!lastChild || isElement$7(lastChild) && (/^(left|right)$/gi.test(dom.getStyle(lastChild, 'float', true)))) { 32200 dom.add(block, 'br'); 32201 } 32202 }; 32203 const shouldEndContainer = (editor, container) => { 32204 const optionValue = shouldEndContainerOnEmptyBlock(editor); 32205 if (isNullable(container)) { 32206 return false; 32207 } 32208 else if (isString(optionValue)) { 32209 return contains$2(Tools.explode(optionValue), container.nodeName.toLowerCase()); 32210 } 32211 else { 32212 return optionValue; 32213 } 32214 }; 32215 const insert$3 = (editor, evt) => { 32216 let container; 32217 let offset; 32218 let parentBlockName; 32219 let containerBlock; 32220 let isAfterLastNodeInContainer = false; 32221 const dom = editor.dom; 32222 const schema = editor.schema, nonEmptyElementsMap = schema.getNonEmptyElements(); 32223 const rng = editor.selection.getRng(); 32224 const newBlockName = getForcedRootBlock(editor); 32225 const start = SugarElement.fromDom(rng.startContainer); 32226 const child = child$1(start, rng.startOffset); 32227 const isCef = child.exists((element) => isHTMLElement$1(element) && !isEditable$2(element)); 32228 const collapsedAndCef = rng.collapsed && isCef; 32229 const createNewBlock$1 = (name, styles) => { 32230 return createNewBlock(editor, container, parentBlock, editableRoot, shouldKeepStyles(editor), name, styles); 32231 }; 32232 // Returns true/false if the caret is at the start/end of the parent block element 32233 const isCaretAtStartOrEndOfBlock = (start) => { 32234 const normalizedOffset = normalizeZwspOffset(start, container, offset); 32235 // Caret is in the middle of a text node like "a|b" 32236 if (isText$b(container) && (start ? normalizedOffset > 0 : normalizedOffset < container.data.length)) { 32237 return false; 32238 } 32239 // If after the last element in block node edge case for #5091 32240 if ((container.parentNode === parentBlock || container === parentBlock) && isAfterLastNodeInContainer && !start) { 32241 return true; 32242 } 32243 // If the caret if before the first element in parentBlock 32244 if (start && isElement$7(container) && container === parentBlock.firstChild) { 32245 return true; 32246 } 32247 // Caret can be before/after a table or a hr 32248 if (containerAndPreviousSiblingName(container, 'TABLE') || containerAndPreviousSiblingName(container, 'HR')) { 32249 if (containerAndNextSiblingName(container, 'BR')) { 32250 return !start; 32251 } 32252 return (isAfterLastNodeInContainer && !start) || (!isAfterLastNodeInContainer && start); 32253 } 32254 // Walk the DOM and look for text nodes or non empty elements 32255 const walker = new DomTreeWalker(container, parentBlock); 32256 // If caret is in beginning or end of a text block then jump to the next/previous node 32257 if (isText$b(container)) { 32258 if (start && normalizedOffset === 0) { 32259 walker.prev(); 32260 } 32261 else if (!start && normalizedOffset === container.data.length) { 32262 walker.next(); 32263 } 32264 } 32265 let node; 32266 while ((node = walker.current())) { 32267 if (isElement$7(node)) { 32268 // Ignore bogus elements 32269 if (!node.getAttribute('data-mce-bogus')) { 32270 // Keep empty elements like <img /> <input /> but not trailing br:s like <p>text|<br></p> 32271 const name = node.nodeName.toLowerCase(); 32272 if (nonEmptyElementsMap[name] && name !== 'br') { 32273 return false; 32274 } 32275 } 32276 } 32277 else if (isText$b(node) && !isWhitespaceText(node.data)) { 32278 return false; 32279 } 32280 if (start) { 32281 walker.prev(); 32282 } 32283 else { 32284 walker.next(); 32285 } 32286 } 32287 return true; 32288 }; 32289 const isInsideLiBeforeAList = (newBlock) => { 32290 const nextSibling$1 = firstChild(newBlock).bind(nextSibling); 32291 return isListItem$2(newBlock) && nextSibling$1.exists(isList$1); 32292 }; 32293 const trimEmptySpacesInLeftLeaf = (newBlock) => { 32294 const leaf = toLeaf$1(SugarElement.fromDom(newBlock), 0).element; 32295 if (isText$c(leaf) && dom.isEmpty(leaf.dom)) { 32296 leaf.dom.remove(); 32297 } 32298 }; 32299 const insertNewBlockAfter = () => { 32300 let block; 32301 // If the caret is at the end of a header we produce a P tag after it similar to Word unless we are in a hgroup 32302 if (/^(H[1-6]|PRE|FIGURE)$/.test(parentBlockName) && containerBlockName !== 'HGROUP') { 32303 block = createNewBlock$1(newBlockName); 32304 } 32305 else { 32306 block = createNewBlock$1(); 32307 } 32308 // Split the current container block element if enter is pressed inside an empty inner block element 32309 if (shouldEndContainer(editor, containerBlock) && canSplitBlock(dom, containerBlock) && dom.isEmpty(parentBlock, undefined, { includeZwsp: true })) { 32310 // Split container block for example a BLOCKQUOTE at the current blockParent location for example a P 32311 block = dom.split(containerBlock, parentBlock); 32312 } 32313 else { 32314 dom.insertAfter(block, parentBlock); 32315 } 32316 moveToCaretPosition(editor, block); 32317 return block; 32318 }; 32319 // Setup range items and newBlockName 32320 normalize$2(dom, rng).each((normRng) => { 32321 rng.setStart(normRng.startContainer, normRng.startOffset); 32322 rng.setEnd(normRng.endContainer, normRng.endOffset); 32323 }); 32324 container = rng.startContainer; 32325 offset = rng.startOffset; 32326 const shiftKey = !!(evt && evt.shiftKey); 32327 const ctrlKey = !!(evt && evt.ctrlKey); 32328 // Resolve node index 32329 if (isElement$7(container) && container.hasChildNodes() && !collapsedAndCef) { 32330 isAfterLastNodeInContainer = offset > container.childNodes.length - 1; 32331 container = container.childNodes[Math.min(offset, container.childNodes.length - 1)] || container; 32332 if (isAfterLastNodeInContainer && isText$b(container)) { 32333 offset = container.data.length; 32334 } 32335 else { 32336 offset = 0; 32337 } 32338 } 32339 // Get editable root node, normally the body element but sometimes a div or span 32340 const editableRoot = getEditableRoot(dom, container); 32341 // If there is no editable root then enter is done inside a contentEditable false element 32342 if (!editableRoot || isWithinNonEditableList(editor, container)) { 32343 return; 32344 } 32345 // Wrap the current node and it's sibling in a default block if it's needed. 32346 // for example this <td>text|<b>text2</b></td> will become this <td><p>text|<b>text2</p></b></td> 32347 // This won't happen if root blocks are disabled or the shiftKey is pressed 32348 if (!shiftKey) { 32349 container = wrapSelfAndSiblingsInDefaultBlock(editor, newBlockName, rng, container, offset); 32350 } 32351 // Find parent block and setup empty block paddings 32352 let parentBlock = dom.getParent(container, dom.isBlock) || dom.getRoot(); 32353 containerBlock = isNonNullable(parentBlock?.parentNode) ? dom.getParent(parentBlock.parentNode, dom.isBlock) : null; 32354 // Setup block names 32355 parentBlockName = parentBlock ? parentBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5 32356 const containerBlockName = containerBlock ? containerBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5 32357 // Enter inside block contained within a LI then split or insert before/after LI 32358 if (containerBlockName === 'LI' && !ctrlKey) { 32359 const liBlock = containerBlock; 32360 parentBlock = liBlock; 32361 containerBlock = liBlock.parentNode; 32362 parentBlockName = containerBlockName; 32363 } 32364 if (isElement$7(containerBlock) && isLastEmptyBlockInDetails(editor, shiftKey, parentBlock)) { 32365 return insertNewLine(editor, createNewBlock$1, parentBlock); 32366 } 32367 // Handle enter in list item 32368 if (/^(LI|DT|DD)$/.test(parentBlockName) && isElement$7(containerBlock)) { 32369 // Handle enter inside an empty list item 32370 if (dom.isEmpty(parentBlock)) { 32371 insert$4(editor, createNewBlock$1, containerBlock, parentBlock, newBlockName); 32372 return; 32373 } 32374 } 32375 // Never split the body or blocks that we can't split like noneditable host elements 32376 if (!collapsedAndCef && (parentBlock === editor.getBody() || !canSplitBlock(dom, parentBlock))) { 32377 return; 32378 } 32379 const parentBlockParent = parentBlock.parentNode; 32380 // Insert new block before/after the parent block depending on caret location 32381 let newBlock; 32382 if (collapsedAndCef) { 32383 newBlock = createNewBlock$1(newBlockName); 32384 child.fold(() => { 32385 append$1(start, SugarElement.fromDom(newBlock)); 32386 }, (child) => { 32387 before$4(child, SugarElement.fromDom(newBlock)); 32388 }); 32389 editor.selection.setCursorLocation(newBlock, 0); 32390 } 32391 else if (isCaretContainerBlock$1(parentBlock)) { 32392 // TODO: TINY-10384 NOTE: Added logic to make sure pressing enter is consistent between browsers. 32393 // As an example a fake caret is used before/after tables on Firefox but not on Chrome. So different behaviour could occur 32394 newBlock = showCaretContainerBlock(parentBlock); 32395 if (dom.isEmpty(parentBlock)) { 32396 emptyBlock(parentBlock); 32397 } 32398 setForcedBlockAttrs(editor, newBlock); 32399 moveToCaretPosition(editor, newBlock); 32400 } 32401 else if (isCaretAtStartOrEndOfBlock(false)) { 32402 // Caret is moved to the new block in the insertNewBlockAfter fn 32403 newBlock = insertNewBlockAfter(); 32404 } 32405 else if (isCaretAtStartOrEndOfBlock(true) && parentBlockParent) { 32406 // Check where caret is positioned before it is potentially moved by 'insertBefore' fn 32407 const caretPos = CaretPosition.fromRangeStart(rng); 32408 const afterTable = isAfterTable(caretPos); 32409 const parentBlockSugar = SugarElement.fromDom(parentBlock); 32410 const afterBr = isAfterBr(parentBlockSugar, caretPos, editor.schema); 32411 const prevBrOpt = afterBr 32412 ? findPreviousBr(parentBlockSugar, caretPos, editor.schema).bind((pos) => Optional.from(pos.getNode())) 32413 : Optional.none(); 32414 newBlock = parentBlockParent.insertBefore(createNewBlock$1(), parentBlock); 32415 const root = containerAndPreviousSiblingName(parentBlock, 'HR') || afterTable ? newBlock : prevBrOpt.getOr(parentBlock); 32416 moveToCaretPosition(editor, root); 32417 } 32418 else { 32419 // Extract after fragment and insert it after the current block 32420 const tmpRng = includeZwspInRange(rng).cloneRange(); 32421 tmpRng.setEndAfter(parentBlock); 32422 const fragment = tmpRng.extractContents(); 32423 trimZwsp(fragment); 32424 trimLeadingLineBreaks(fragment); 32425 newBlock = fragment.firstChild; 32426 if (parentBlock === newBlock) { // Can't add yourself to yourself. Additionally the newBlock is removed from the DOM earlier, so even if you could, it'd still not work. 32427 if (isNonNullable(parentBlockParent)) { 32428 dom.insertAfter(fragment, parentBlockParent); 32429 } 32430 } 32431 else { 32432 dom.insertAfter(fragment, parentBlock); 32433 } 32434 if (!isInsideLiBeforeAList(SugarElement.fromDom(newBlock))) { 32435 trimInlineElementsOnLeftSideOfBlock(dom, nonEmptyElementsMap, newBlock); 32436 addBrToBlockIfNeeded(dom, parentBlock); 32437 } 32438 else { 32439 trimEmptySpacesInLeftLeaf(newBlock); 32440 } 32441 if (dom.isEmpty(parentBlock)) { 32442 emptyBlock(parentBlock); 32443 } 32444 newBlock.normalize(); 32445 // New block might become empty if it's <p><b>a |</b></p> 32446 if (dom.isEmpty(newBlock)) { 32447 dom.remove(newBlock); 32448 insertNewBlockAfter(); 32449 } 32450 else { 32451 setForcedBlockAttrs(editor, newBlock); 32452 moveToCaretPosition(editor, newBlock); 32453 } 32454 } 32455 dom.setAttrib(newBlock, 'id', ''); // Remove ID since it needs to be document unique 32456 // Allow custom handling of new blocks 32457 editor.dispatch('NewBlock', { newBlock }); 32458 }; 32459 const fakeEventName$1 = 'insertParagraph'; 32460 const blockbreak = { 32461 insert: insert$3, 32462 fakeEventName: fakeEventName$1 32463 }; 32464 32465 // Walks the parent block to the right and look for BR elements 32466 const hasRightSideContent = (schema, container, parentBlock) => { 32467 const walker = new DomTreeWalker(container, parentBlock); 32468 let node; 32469 const nonEmptyElementsMap = schema.getNonEmptyElements(); 32470 while ((node = walker.next())) { 32471 if (nonEmptyElementsMap[node.nodeName.toLowerCase()] || isText$b(node) && node.length > 0) { 32472 return true; 32473 } 32474 } 32475 return false; 32476 }; 32477 const moveSelectionToBr = (editor, brElm, extraBr) => { 32478 const rng = editor.dom.createRng(); 32479 if (!extraBr) { 32480 rng.setStartAfter(brElm); 32481 rng.setEndAfter(brElm); 32482 } 32483 else { 32484 rng.setStartBefore(brElm); 32485 rng.setEndBefore(brElm); 32486 } 32487 editor.selection.setRng(rng); 32488 scrollRangeIntoView(editor, rng); 32489 }; 32490 const insertBrAtCaret = (editor, evt) => { 32491 // We load the current event in from EnterKey.js when appropriate to heed 32492 // certain event-specific variations such as ctrl-enter in a list 32493 const selection = editor.selection; 32494 const dom = editor.dom; 32495 const rng = selection.getRng(); 32496 let brElm; 32497 let extraBr = false; 32498 normalize$2(dom, rng).each((normRng) => { 32499 rng.setStart(normRng.startContainer, normRng.startOffset); 32500 rng.setEnd(normRng.endContainer, normRng.endOffset); 32501 }); 32502 let offset = rng.startOffset; 32503 let container = rng.startContainer; 32504 // Resolve node index 32505 if (isElement$7(container) && container.hasChildNodes()) { 32506 const isAfterLastNodeInContainer = offset > container.childNodes.length - 1; 32507 container = container.childNodes[Math.min(offset, container.childNodes.length - 1)] || container; 32508 if (isAfterLastNodeInContainer && isText$b(container)) { 32509 offset = container.data.length; 32510 } 32511 else { 32512 offset = 0; 32513 } 32514 } 32515 let parentBlock = dom.getParent(container, dom.isBlock); 32516 const containerBlock = parentBlock && parentBlock.parentNode ? dom.getParent(parentBlock.parentNode, dom.isBlock) : null; 32517 const containerBlockName = containerBlock ? containerBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5 32518 // Enter inside block contained within a LI then split or insert before/after LI 32519 const isControlKey = !!(evt && evt.ctrlKey); 32520 if (containerBlockName === 'LI' && !isControlKey) { 32521 parentBlock = containerBlock; 32522 } 32523 if (isText$b(container) && offset >= container.data.length) { 32524 // Insert extra BR element at the end block elements 32525 if (!hasRightSideContent(editor.schema, container, parentBlock || dom.getRoot())) { 32526 brElm = dom.create('br'); 32527 rng.insertNode(brElm); 32528 rng.setStartAfter(brElm); 32529 rng.setEndAfter(brElm); 32530 extraBr = true; 32531 } 32532 } 32533 brElm = dom.create('br'); 32534 rangeInsertNode(dom, rng, brElm); 32535 moveSelectionToBr(editor, brElm, extraBr); 32536 editor.undoManager.add(); 32537 }; 32538 const insertBrBefore = (editor, inline) => { 32539 const br = SugarElement.fromTag('br'); 32540 before$4(SugarElement.fromDom(inline), br); 32541 editor.undoManager.add(); 32542 }; 32543 const insertBrAfter = (editor, inline) => { 32544 if (!hasBrAfter(editor.getBody(), inline)) { 32545 after$4(SugarElement.fromDom(inline), SugarElement.fromTag('br')); 32546 } 32547 const br = SugarElement.fromTag('br'); 32548 after$4(SugarElement.fromDom(inline), br); 32549 moveSelectionToBr(editor, br.dom, false); 32550 editor.undoManager.add(); 32551 }; 32552 const isBeforeBr = (pos) => { 32553 return isBr$7(pos.getNode()); 32554 }; 32555 const hasBrAfter = (rootNode, startNode) => { 32556 if (isBeforeBr(CaretPosition.after(startNode))) { 32557 return true; 32558 } 32559 else { 32560 return nextPosition(rootNode, CaretPosition.after(startNode)).map((pos) => { 32561 return isBr$7(pos.getNode()); 32562 }).getOr(false); 32563 } 32564 }; 32565 const isAnchorLink = (elm) => { 32566 return elm && elm.nodeName === 'A' && 'href' in elm; 32567 }; 32568 const isInsideAnchor = (location) => { 32569 return location.fold(never, isAnchorLink, isAnchorLink, never); 32570 }; 32571 const readInlineAnchorLocation = (editor) => { 32572 const isInlineTarget$1 = curry(isInlineTarget, editor); 32573 const position = CaretPosition.fromRangeStart(editor.selection.getRng()); 32574 return readLocation(isInlineTarget$1, editor.getBody(), position).filter(isInsideAnchor); 32575 }; 32576 const insertBrOutsideAnchor = (editor, location) => { 32577 location.fold(noop, curry(insertBrBefore, editor), curry(insertBrAfter, editor), noop); 32578 }; 32579 const insert$2 = (editor, evt) => { 32580 const anchorLocation = readInlineAnchorLocation(editor); 32581 if (anchorLocation.isSome()) { 32582 anchorLocation.each(curry(insertBrOutsideAnchor, editor)); 32583 } 32584 else { 32585 insertBrAtCaret(editor, evt); 32586 } 32587 }; 32588 const fakeEventName = 'insertLineBreak'; 32589 const linebreak = { 32590 insert: insert$2, 32591 fakeEventName 32592 }; 32593 32594 const matchesSelector = (editor, selector) => { 32595 return getParentBlock$1(editor).filter((parentBlock) => { 32596 return selector.length > 0 && is$2(SugarElement.fromDom(parentBlock), selector); 32597 }).isSome(); 32598 }; 32599 const shouldInsertBr = (editor) => { 32600 return matchesSelector(editor, getBrNewLineSelector(editor)); 32601 }; 32602 const shouldBlockNewLine$1 = (editor) => { 32603 return matchesSelector(editor, getNoNewLineSelector(editor)); 32604 }; 32605 32606 const newLineAction = Adt.generate([ 32607 { br: [] }, 32608 { block: [] }, 32609 { none: [] } 32610 ]); 32611 const shouldBlockNewLine = (editor, _shiftKey) => { 32612 return shouldBlockNewLine$1(editor); 32613 }; 32614 const inListBlock = (requiredState) => { 32615 return (editor, _shiftKey) => { 32616 return isListItemParentBlock(editor) === requiredState; 32617 }; 32618 }; 32619 const inBlock = (blockName, requiredState) => (editor, _shiftKey) => { 32620 const state = getParentBlockName(editor) === blockName.toUpperCase(); 32621 return state === requiredState; 32622 }; 32623 const inCefBlock = (editor) => { 32624 const editableRoot = getEditableRoot(editor.dom, editor.selection.getStart()); 32625 return isNullable(editableRoot); 32626 }; 32627 const inPreBlock = (requiredState) => inBlock('pre', requiredState); 32628 const inSummaryBlock = () => inBlock('summary', true); 32629 const shouldPutBrInPre = (requiredState) => { 32630 return (editor, _shiftKey) => { 32631 return shouldPutBrInPre$1(editor) === requiredState; 32632 }; 32633 }; 32634 const inBrContext = (editor, _shiftKey) => { 32635 return shouldInsertBr(editor); 32636 }; 32637 const hasShiftKey = (_editor, shiftKey) => { 32638 return shiftKey; 32639 }; 32640 const canInsertIntoEditableRoot = (editor) => { 32641 const forcedRootBlock = getForcedRootBlock(editor); 32642 const rootEditable = getEditableRoot(editor.dom, editor.selection.getStart()); 32643 return isNonNullable(rootEditable) && editor.schema.isValidChild(rootEditable.nodeName, forcedRootBlock); 32644 }; 32645 const isInRootWithEmptyOrCEF = (editor) => { 32646 const rng = editor.selection.getRng(); 32647 const start = SugarElement.fromDom(rng.startContainer); 32648 const child = child$1(start, rng.startOffset); 32649 const isCefOpt = child.map((element) => isHTMLElement$1(element) && !isEditable$2(element)); 32650 return rng.collapsed && isCefOpt.getOr(true); 32651 }; 32652 const match = (predicates, action) => { 32653 return (editor, shiftKey) => { 32654 const isMatch = foldl(predicates, (res, p) => { 32655 return res && p(editor, shiftKey); 32656 }, true); 32657 return isMatch ? Optional.some(action) : Optional.none(); 32658 }; 32659 }; 32660 const getAction = (editor, evt) => { 32661 return evaluateUntil([ 32662 match([shouldBlockNewLine], newLineAction.none()), 32663 // If the pre block is cef, do not try to insert a new line (or delete contents) 32664 match([inPreBlock(true), inCefBlock], newLineAction.none()), 32665 match([inSummaryBlock()], newLineAction.br()), 32666 match([inPreBlock(true), shouldPutBrInPre(false), hasShiftKey], newLineAction.br()), 32667 match([inPreBlock(true), shouldPutBrInPre(false)], newLineAction.block()), 32668 match([inPreBlock(true), shouldPutBrInPre(true), hasShiftKey], newLineAction.block()), 32669 match([inPreBlock(true), shouldPutBrInPre(true)], newLineAction.br()), 32670 // TODO: TINY-9127 investigate if the list handling (and pre) is correct here. 32671 match([inListBlock(true), hasShiftKey], newLineAction.br()), 32672 match([inListBlock(true)], newLineAction.block()), 32673 match([inBrContext], newLineAction.br()), 32674 match([hasShiftKey], newLineAction.br()), 32675 match([canInsertIntoEditableRoot], newLineAction.block()), 32676 match([isInRootWithEmptyOrCEF], newLineAction.block()) 32677 ], [editor, !!(evt && evt.shiftKey)]).getOr(newLineAction.none()); 32678 }; 32679 32680 const insertBreak = (breakType, editor, evt) => { 32681 if (editor.mode.isReadOnly()) { 32682 return; 32683 } 32684 if (!editor.selection.isCollapsed()) { 32685 // IMPORTANT: We want to use the editor execCommand here, so that our `delete` execCommand 32686 // overrides will be considered. 32687 execEditorDeleteCommand(editor); 32688 } 32689 if (isNonNullable(evt)) { 32690 const event = fireBeforeInputEvent(editor, breakType.fakeEventName); 32691 if (event.isDefaultPrevented()) { 32692 return; 32693 } 32694 } 32695 breakType.insert(editor, evt); 32696 if (isNonNullable(evt)) { 32697 fireInputEvent(editor, breakType.fakeEventName); 32698 } 32699 }; 32700 const insert$1 = (editor, evt) => { 32701 if (editor.mode.isReadOnly()) { 32702 return; 32703 } 32704 const br = () => insertBreak(linebreak, editor, evt); 32705 const block = () => insertBreak(blockbreak, editor, evt); 32706 const logicalAction = getAction(editor, evt); 32707 switch (getNewlineBehavior(editor)) { 32708 case 'linebreak': 32709 logicalAction.fold(br, br, noop); 32710 break; 32711 case 'block': 32712 logicalAction.fold(block, block, noop); 32713 break; 32714 case 'invert': 32715 logicalAction.fold(block, br, noop); 32716 break; 32717 // implied by the options processor, unnecessary 32718 // case 'default': 32719 default: 32720 logicalAction.fold(br, block, noop); 32721 break; 32722 } 32723 }; 32724 32725 const platform$1 = detect$1(); 32726 const isIOSSafari = platform$1.os.isiOS() && platform$1.browser.isSafari(); 32727 const handleEnterKeyEvent = (editor, event) => { 32728 if (event.isDefaultPrevented()) { 32729 return; 32730 } 32731 event.preventDefault(); 32732 endTypingLevelIgnoreLocks(editor.undoManager); 32733 editor.undoManager.transact(() => { 32734 insert$1(editor, event); 32735 }); 32736 }; 32737 const isCaretAfterKoreanCharacter = (rng) => { 32738 if (!rng.collapsed) { 32739 return false; 32740 } 32741 const startContainer = rng.startContainer; 32742 if (isText$b(startContainer)) { 32743 // Hangul: \uAC00-\uD7AF 32744 // Hangul Jamo: \u1100-\u11FF 32745 // Hangul Compatibility Jamo: \u3130-\u318F 32746 // Hangul Jamo Extended-A: \uA960-\uA97F 32747 // Hangul Jamo Extended-B: \uD7B0-\uD7FF 32748 const koreanCharRegex = /^[\uAC00-\uD7AF\u1100-\u11FF\u3130-\u318F\uA960-\uA97F\uD7B0-\uD7FF]$/; 32749 const char = startContainer.data.charAt(rng.startOffset - 1); 32750 return koreanCharRegex.test(char); 32751 } 32752 else { 32753 return false; 32754 } 32755 }; 32756 const setup$n = (editor) => { 32757 let iOSSafariKeydownBookmark = Optional.none(); 32758 const iOSSafariKeydownOverride = (editor) => { 32759 iOSSafariKeydownBookmark = Optional.some(editor.selection.getBookmark()); 32760 editor.undoManager.add(); 32761 }; 32762 const iOSSafariKeyupOverride = (editor, event) => { 32763 editor.undoManager.undo(); 32764 iOSSafariKeydownBookmark.fold(noop, (b) => editor.selection.moveToBookmark(b)); 32765 handleEnterKeyEvent(editor, event); 32766 iOSSafariKeydownBookmark = Optional.none(); 32767 }; 32768 editor.on('keydown', (event) => { 32769 if (event.keyCode === VK.ENTER) { 32770 if (isIOSSafari && isCaretAfterKoreanCharacter(editor.selection.getRng())) { 32771 // TINY-9746: iOS Safari composes Korean characters by deleting the previous partial character and inserting 32772 // the composed character. If the native Enter keypress event is not fired, iOS Safari will continue to compose across 32773 // our custom newline by deleting it and inserting the composed character on the previous line, causing a bug. The workaround 32774 // is to save a bookmark and an undo level on keydown while not preventing default to allow the native Enter keypress. 32775 // Then on keyup, the effects of the native Enter keypress is undone and our own Enter key handler is called. 32776 iOSSafariKeydownOverride(editor); 32777 } 32778 else { 32779 handleEnterKeyEvent(editor, event); 32780 } 32781 } 32782 }); 32783 editor.on('keyup', (event) => { 32784 if (event.keyCode === VK.ENTER) { 32785 iOSSafariKeydownBookmark.each(() => iOSSafariKeyupOverride(editor, event)); 32786 } 32787 }); 32788 }; 32789 32790 const executeKeydownOverride$2 = (editor, caret, evt) => { 32791 const isMac = Env.os.isMacOS() || Env.os.isiOS(); 32792 execute([ 32793 { keyCode: VK.END, action: action(moveToLineEndPoint$1, editor, true) }, 32794 { keyCode: VK.HOME, action: action(moveToLineEndPoint$1, editor, false) }, 32795 ...(!isMac ? [ 32796 { keyCode: VK.HOME, action: action(selectToEndPoint, editor, false), ctrlKey: true, shiftKey: true }, 32797 { keyCode: VK.END, action: action(selectToEndPoint, editor, true), ctrlKey: true, shiftKey: true } 32798 ] : []), 32799 { keyCode: VK.END, action: action(moveToLineEndPoint, editor, true) }, 32800 { keyCode: VK.HOME, action: action(moveToLineEndPoint, editor, false) }, 32801 { keyCode: VK.END, action: action(moveToLineEndPoint$2, editor, true, caret) }, 32802 { keyCode: VK.HOME, action: action(moveToLineEndPoint$2, editor, false, caret) } 32803 ], evt).each((_) => { 32804 evt.preventDefault(); 32805 }); 32806 }; 32807 const setup$m = (editor, caret) => { 32808 editor.on('keydown', (evt) => { 32809 if (!evt.isDefaultPrevented()) { 32810 executeKeydownOverride$2(editor, caret, evt); 32811 } 32812 }); 32813 }; 32814 32815 const setup$l = (editor) => { 32816 editor.on('input', (e) => { 32817 // We only care about non composing inputs since moving the caret or modifying the text node will blow away the IME 32818 if (!e.isComposing) { 32819 normalizeNbspsInEditor(editor); 32820 } 32821 }); 32822 }; 32823 32824 const platform = detect$1(); 32825 const executeKeyupAction = (editor, caret, evt) => { 32826 execute([ 32827 { keyCode: VK.PAGE_UP, action: action(moveToLineEndPoint$2, editor, false, caret) }, 32828 { keyCode: VK.PAGE_DOWN, action: action(moveToLineEndPoint$2, editor, true, caret) } 32829 ], evt); 32830 }; 32831 const stopImmediatePropagation = (e) => e.stopImmediatePropagation(); 32832 const isPageUpDown = (evt) => evt.keyCode === VK.PAGE_UP || evt.keyCode === VK.PAGE_DOWN; 32833 const setNodeChangeBlocker = (blocked, editor, block) => { 32834 // Node change event is only blocked while the user is holding down the page up/down key it would have limited effects on other things 32835 // Prevents a flickering UI while caret move in and out of the inline boundary element 32836 if (block && !blocked.get()) { 32837 editor.on('NodeChange', stopImmediatePropagation, true); 32838 } 32839 else if (!block && blocked.get()) { 32840 editor.off('NodeChange', stopImmediatePropagation); 32841 } 32842 blocked.set(block); 32843 }; 32844 // Determining the correct placement on key up/down is very complicated and would require handling many edge cases, 32845 // which we don't have the resources to handle currently. As such, we allow the browser to change the selection and then make adjustments later. 32846 const setup$k = (editor, caret) => { 32847 // Mac OS doesn't move the selection when pressing page up/down and as such TinyMCE shouldn't be moving it either 32848 if (platform.os.isMacOS()) { 32849 return; 32850 } 32851 const blocked = Cell(false); 32852 editor.on('keydown', (evt) => { 32853 if (isPageUpDown(evt)) { 32854 setNodeChangeBlocker(blocked, editor, true); 32855 } 32856 }); 32857 editor.on('keyup', (evt) => { 32858 if (!evt.isDefaultPrevented()) { 32859 executeKeyupAction(editor, caret, evt); 32860 } 32861 if (isPageUpDown(evt) && blocked.get()) { 32862 setNodeChangeBlocker(blocked, editor, false); 32863 editor.nodeChanged(); 32864 } 32865 }); 32866 }; 32867 32868 const isValidContainer = (root, container) => root === container || root.contains(container); 32869 const isInEditableRange = (editor, range) => { 32870 // If the range is not in the body then it's in a shadow root and we should allow that more details in: TINY-11446 32871 if (!isValidContainer(editor.getBody(), range.startContainer) || !isValidContainer(editor.getBody(), range.endContainer)) { 32872 return true; 32873 } 32874 return isEditableRange(editor.dom, range); 32875 }; 32876 const setup$j = (editor) => { 32877 editor.on('beforeinput', (e) => { 32878 // Normally input is blocked on non-editable elements that have contenteditable="false" however we are also treating 32879 // SVG elements as non-editable and deleting inside or into is possible in some browsers so we need to detect that and prevent that. 32880 if (!editor.selection.isEditable() || exists(e.getTargetRanges(), (rng) => !isInEditableRange(editor, rng))) { 32881 e.preventDefault(); 32882 } 32883 }); 32884 }; 32885 32886 const insertTextAtPosition = (text, pos) => { 32887 const container = pos.container(); 32888 const offset = pos.offset(); 32889 if (isText$b(container)) { 32890 container.insertData(offset, text); 32891 return Optional.some(CaretPosition(container, offset + text.length)); 32892 } 32893 else { 32894 return getElementFromPosition(pos).map((elm) => { 32895 const textNode = SugarElement.fromText(text); 32896 if (pos.isAtEnd()) { 32897 after$4(elm, textNode); 32898 } 32899 else { 32900 before$4(elm, textNode); 32901 } 32902 return CaretPosition(textNode.dom, text.length); 32903 }); 32904 } 32905 }; 32906 const insertNbspAtPosition = curry(insertTextAtPosition, nbsp); 32907 const insertSpaceAtPosition = curry(insertTextAtPosition, ' '); 32908 32909 const insertSpaceOrNbspAtPosition = (root, pos, schema) => needsToHaveNbsp(root, pos, schema) ? insertNbspAtPosition(pos) : insertSpaceAtPosition(pos); 32910 const locationToCaretPosition = (root) => (location) => location.fold((element) => prevPosition(root.dom, CaretPosition.before(element)), (element) => firstPositionIn(element), (element) => lastPositionIn(element), (element) => nextPosition(root.dom, CaretPosition.after(element))); 32911 const insertInlineBoundarySpaceOrNbsp = (root, pos, schema) => (checkPos) => needsToHaveNbsp(root, checkPos, schema) ? insertNbspAtPosition(pos) : insertSpaceAtPosition(pos); 32912 const setSelection = (editor) => (pos) => { 32913 editor.selection.setRng(pos.toRange()); 32914 editor.nodeChanged(); 32915 }; 32916 const isInsideSummary = (domUtils, node) => domUtils.isEditable(domUtils.getParent(node, 'summary')); 32917 const insertSpaceOrNbspAtSelection = (editor) => { 32918 const pos = CaretPosition.fromRangeStart(editor.selection.getRng()); 32919 const root = SugarElement.fromDom(editor.getBody()); 32920 if (editor.selection.isCollapsed()) { 32921 const isInlineTarget$1 = curry(isInlineTarget, editor); 32922 const caretPosition = CaretPosition.fromRangeStart(editor.selection.getRng()); 32923 return readLocation(isInlineTarget$1, editor.getBody(), caretPosition) 32924 .bind(locationToCaretPosition(root)) 32925 .map((checkPos) => () => insertInlineBoundarySpaceOrNbsp(root, pos, editor.schema)(checkPos).each(setSelection(editor))); 32926 } 32927 else { 32928 return Optional.none(); 32929 } 32930 }; 32931 // TINY-9964: Firefox has a bug where the space key is toggling the open state instead of inserting a space in a summary element 32932 const insertSpaceInSummaryAtSelectionOnFirefox = (editor) => { 32933 const insertSpaceThunk = () => { 32934 const root = SugarElement.fromDom(editor.getBody()); 32935 if (!editor.selection.isCollapsed()) { 32936 editor.getDoc().execCommand('Delete'); 32937 } 32938 const pos = CaretPosition.fromRangeStart(editor.selection.getRng()); 32939 insertSpaceOrNbspAtPosition(root, pos, editor.schema).each(setSelection(editor)); 32940 }; 32941 return someIf(Env.browser.isFirefox() && editor.selection.isEditable() && isInsideSummary(editor.dom, editor.selection.getRng().startContainer), insertSpaceThunk); 32942 }; 32943 32944 const executeKeydownOverride$1 = (editor, evt) => { 32945 executeWithDelayedAction([ 32946 { keyCode: VK.SPACEBAR, action: action(insertSpaceOrNbspAtSelection, editor) }, 32947 { keyCode: VK.SPACEBAR, action: action(insertSpaceInSummaryAtSelectionOnFirefox, editor) } 32948 ], evt).each((applyAction) => { 32949 evt.preventDefault(); 32950 const event = fireBeforeInputEvent(editor, 'insertText', { data: ' ' }); 32951 if (!event.isDefaultPrevented()) { 32952 applyAction(); 32953 // Browsers sends space in data even if the dom ends up with a nbsp so we should always be sending a space 32954 fireInputEvent(editor, 'insertText', { data: ' ' }); 32955 } 32956 }); 32957 }; 32958 const setup$i = (editor) => { 32959 editor.on('keydown', (evt) => { 32960 if (!evt.isDefaultPrevented()) { 32961 executeKeydownOverride$1(editor, evt); 32962 } 32963 }); 32964 }; 32965 32966 const tableTabNavigation = (editor) => { 32967 if (hasTableTabNavigation(editor)) { 32968 return [ 32969 { keyCode: VK.TAB, action: action(handleTab, editor, true) }, 32970 { keyCode: VK.TAB, shiftKey: true, action: action(handleTab, editor, false) }, 32971 ]; 32972 } 32973 else { 32974 return []; 32975 } 32976 }; 32977 const executeKeydownOverride = (editor, evt) => { 32978 execute([ 32979 ...tableTabNavigation(editor) 32980 ], evt).each((_) => { 32981 evt.preventDefault(); 32982 }); 32983 }; 32984 const setup$h = (editor) => { 32985 editor.on('keydown', (evt) => { 32986 if (!evt.isDefaultPrevented()) { 32987 executeKeydownOverride(editor, evt); 32988 } 32989 }); 32990 }; 32991 32992 const setup$g = (editor) => { 32993 editor.addShortcut('Meta+P', '', 'mcePrint'); 32994 setup$p(editor); 32995 if (isRtc(editor)) { 32996 return Cell(null); 32997 } 32998 else { 32999 const caret = setupSelectedState(editor); 33000 setup$j(editor); 33001 setup$r(editor); 33002 setup$q(editor, caret); 33003 setup$o(editor, caret); 33004 setup$n(editor); 33005 setup$i(editor); 33006 setup$l(editor); 33007 setup$h(editor); 33008 setup$m(editor, caret); 33009 setup$k(editor, caret); 33010 return caret; 33011 } 33012 }; 33013 33014 const updateList = (editor, update) => { 33015 const parentList = getParentList(editor); 33016 if (parentList === null || isWithinNonEditableList$1(editor, parentList)) { 33017 return; 33018 } 33019 editor.undoManager.transact(() => { 33020 if (isObject(update.styles)) { 33021 editor.dom.setStyles(parentList, update.styles); 33022 } 33023 if (isObject(update.attrs)) { 33024 each$d(update.attrs, (v, k) => editor.dom.setAttrib(parentList, k, v)); 33025 } 33026 }); 33027 }; 33028 33029 const queryListCommandState = (editor, listName) => () => { 33030 const parentList = getParentList(editor); 33031 return isNonNullable(parentList) && parentList.nodeName === listName; 33032 }; 33033 const setup$f = (editor) => { 33034 editor.addCommand('InsertUnorderedList', (ui, detail) => { 33035 toggleList(editor, 'UL', detail); 33036 }); 33037 editor.addCommand('InsertOrderedList', (ui, detail) => { 33038 toggleList(editor, 'OL', detail); 33039 }); 33040 editor.addCommand('InsertDefinitionList', (ui, detail) => { 33041 toggleList(editor, 'DL', detail); 33042 }); 33043 editor.addCommand('RemoveList', () => { 33044 flattenListSelection(editor); 33045 }); 33046 editor.addCommand('mceListUpdate', (ui, detail) => { 33047 if (isObject(detail)) { 33048 updateList(editor, detail); 33049 } 33050 }); 33051 editor.addCommand('mceListBackspaceDelete', (_ui, forward) => { 33052 backspaceDelete$c(editor, forward); 33053 }); 33054 editor.addQueryStateHandler('InsertUnorderedList', queryListCommandState(editor, 'UL')); 33055 editor.addQueryStateHandler('InsertOrderedList', queryListCommandState(editor, 'OL')); 33056 editor.addQueryStateHandler('InsertDefinitionList', queryListCommandState(editor, 'DL')); 33057 }; 33058 33059 const setup$e = (editor) => { 33060 editor.on('init', () => { 33061 // Init is done after the editor's setup. This places our keydown after any keydowns registered in the setup function, so they can do default prevent. 33062 editor.on('keydown', (e) => { 33063 if (e.defaultPrevented) { 33064 return; 33065 } 33066 if (e.keyCode === VK.BACKSPACE) { 33067 if (backspaceDelete$c(editor, false)) { 33068 e.preventDefault(); 33069 } 33070 } 33071 else if (e.keyCode === VK.DELETE) { 33072 if (backspaceDelete$c(editor, true)) { 33073 e.preventDefault(); 33074 } 33075 } 33076 }); 33077 }); 33078 }; 33079 33080 const isTextNode = (node) => node.type === 3; 33081 const isEmpty = (nodeBuffer) => nodeBuffer.length === 0; 33082 const wrapInvalidChildren = (list) => { 33083 const insertListItem = (buffer, refNode) => { 33084 const li = AstNode.create('li'); 33085 each$e(buffer, (node) => li.append(node)); 33086 if (refNode) { 33087 list.insert(li, refNode, true); 33088 } 33089 else { 33090 list.append(li); 33091 } 33092 }; 33093 const reducer = (buffer, node) => { 33094 if (isTextNode(node)) { 33095 return [...buffer, node]; 33096 } 33097 else if (!isEmpty(buffer) && !isTextNode(node)) { 33098 insertListItem(buffer, node); 33099 return []; 33100 } 33101 else { 33102 return buffer; 33103 } 33104 }; 33105 const restBuffer = foldl(list.children(), reducer, []); 33106 if (!isEmpty(restBuffer)) { 33107 insertListItem(restBuffer); 33108 } 33109 }; 33110 const setup$d = (editor) => { 33111 editor.on('PreInit', () => { 33112 const { parser } = editor; 33113 parser.addNodeFilter('ul,ol', (nodes) => each$e(nodes, wrapInvalidChildren)); 33114 }); 33115 }; 33116 33117 const setupTabKey = (editor) => { 33118 editor.on('keydown', (e) => { 33119 // Check for tab but not ctrl/cmd+tab since it switches browser tabs 33120 if (e.keyCode !== VK.TAB || VK.metaKeyPressed(e)) { 33121 return; 33122 } 33123 editor.undoManager.transact(() => { 33124 if (e.shiftKey ? outdentListSelection(editor) : indentListSelection(editor)) { 33125 e.preventDefault(); 33126 } 33127 }); 33128 }); 33129 }; 33130 const setup$c = (editor) => { 33131 if (shouldIndentOnTab(editor)) { 33132 setupTabKey(editor); 33133 } 33134 }; 33135 33136 const setup$b = (editor) => { 33137 setup$e(editor); 33138 setup$f(editor); 33139 setup$d(editor); 33140 setup$c(editor); 33141 }; 33142 33143 /** 33144 * This class handles the nodechange event dispatching both manual and through selection change events. 33145 * 33146 * @class tinymce.NodeChange 33147 * @private 33148 */ 33149 class NodeChange { 33150 editor; 33151 lastPath = []; 33152 constructor(editor) { 33153 this.editor = editor; 33154 let lastRng; 33155 const self = this; 33156 // Gecko doesn't support the "selectionchange" event 33157 if (!('onselectionchange' in editor.getDoc())) { 33158 editor.on('NodeChange click mouseup keyup focus', (e) => { 33159 // Since DOM Ranges mutate on modification 33160 // of the DOM we need to clone it's contents 33161 const nativeRng = editor.selection.getRng(); 33162 const fakeRng = { 33163 startContainer: nativeRng.startContainer, 33164 startOffset: nativeRng.startOffset, 33165 endContainer: nativeRng.endContainer, 33166 endOffset: nativeRng.endOffset 33167 }; 33168 // Always treat nodechange as a selectionchange since applying 33169 // formatting to the current range wouldn't update the range but it's parent 33170 if (e.type === 'nodechange' || !isEq$4(fakeRng, lastRng)) { 33171 editor.dispatch('SelectionChange'); 33172 } 33173 lastRng = fakeRng; 33174 }); 33175 } 33176 // IE has a bug where it fires a selectionchange on right click that has a range at the start of the body 33177 // When the contextmenu event fires the selection is located at the right location 33178 editor.on('contextmenu', () => { 33179 store(editor); 33180 editor.dispatch('SelectionChange'); 33181 }); 33182 // Selection change is delayed ~200ms on IE when you click inside the current range 33183 editor.on('SelectionChange', () => { 33184 const startElm = editor.selection.getStart(true); 33185 // When focusout from after cef element to other input element the startelm can be undefined 33186 if (!startElm) { 33187 return; 33188 } 33189 if (hasAnyRanges(editor) && !self.isSameElementPath(startElm) && editor.dom.isChildOf(startElm, editor.getBody())) { 33190 editor.nodeChanged({ selectionChange: true }); 33191 } 33192 }); 33193 // Fire an extra nodeChange on mouseup for compatibility reasons 33194 editor.on('mouseup', (e) => { 33195 if (!e.isDefaultPrevented() && hasAnyRanges(editor)) { 33196 // Delay nodeChanged call for WebKit edge case issue where the range 33197 // isn't updated until after you click outside a selected image 33198 if (editor.selection.getNode().nodeName === 'IMG') { 33199 Delay.setEditorTimeout(editor, () => { 33200 editor.nodeChanged(); 33201 }); 33202 } 33203 else { 33204 editor.nodeChanged(); 33205 } 33206 } 33207 }); 33208 } 33209 /** 33210 * Dispatches out a onNodeChange event to all observers. This method should be called when you 33211 * need to update the UI states or element path etc. 33212 * 33213 * @method nodeChanged 33214 * @param {Object} args Optional args to pass to NodeChange event handlers. 33215 */ 33216 nodeChanged(args = {}) { 33217 const editor = this.editor; 33218 const selection = editor.selection; 33219 let node; 33220 // Fix for bug #1896577 it seems that this can not be fired while the editor is loading 33221 if (editor.initialized && selection && !shouldDisableNodeChange(editor) && !isDisabled$1(editor)) { 33222 // Get start node 33223 const root = editor.getBody(); 33224 node = selection.getStart(true) || root; 33225 // Make sure the node is within the editor root or is the editor root 33226 if (node.ownerDocument !== editor.getDoc() || !editor.dom.isChildOf(node, root)) { 33227 node = root; 33228 } 33229 // Get parents and add them to object 33230 const parents = []; 33231 editor.dom.getParent(node, (node) => { 33232 if (node === root) { 33233 return true; 33234 } 33235 else { 33236 parents.push(node); 33237 return false; 33238 } 33239 }); 33240 editor.dispatch('NodeChange', { 33241 ...args, 33242 element: node, 33243 parents 33244 }); 33245 } 33246 } 33247 /** 33248 * Returns true/false if the current element path has been changed or not. 33249 * 33250 * @private 33251 * @return {Boolean} True if the element path is the same false if it's not. 33252 */ 33253 isSameElementPath(startElm) { 33254 let i; 33255 const editor = this.editor; 33256 const currentPath = reverse(editor.dom.getParents(startElm, always, editor.getBody())); 33257 if (currentPath.length === this.lastPath.length) { 33258 for (i = currentPath.length; i >= 0; i--) { 33259 if (currentPath[i] !== this.lastPath[i]) { 33260 break; 33261 } 33262 } 33263 if (i === -1) { 33264 this.lastPath = currentPath; 33265 return true; 33266 } 33267 } 33268 this.lastPath = currentPath; 33269 return false; 33270 } 33271 } 33272 33273 const internalMimeType = 'x-tinymce/html'; 33274 const internalHtmlMime = constant(internalMimeType); 33275 const internalMark = '<!-- ' + internalMimeType + ' -->'; 33276 const mark = (html) => internalMark + html; 33277 const unmark = (html) => html.replace(internalMark, ''); 33278 const isMarked = (html) => html.indexOf(internalMark) !== -1; 33279 33280 /* 33281 * This module contains utilities to convert newlines (\n or \r\n) to BRs or to a combination of the specified block element and BRs 33282 */ 33283 const isPlainText = (text) => { 33284 // so basically any tag that is not one of the "p, div, span, br", or is one of them, but is followed 33285 // by some additional characters qualifies the text as not a plain text (having some HTML tags) 33286 // <span style="white-space:pre"> and <br /> are added as separate exceptions to the rule 33287 return !/<(?:\/?(?!(?:div|p|br|span)>)\w+|(?:(?!(?:span style="white-space:\s?pre;?">)|br\s?\/>))\w+\s[^>]+)>/i.test(text); 33288 }; 33289 const openContainer = (rootTag, rootAttrs) => { 33290 let tag = '<' + rootTag; 33291 const attrs = mapToArray(rootAttrs, (value, key) => key + '="' + Entities.encodeAllRaw(value) + '"'); 33292 if (attrs.length) { 33293 tag += ' ' + attrs.join(' '); 33294 } 33295 return tag + '>'; 33296 }; 33297 const toBlockElements = (text, rootTag, rootAttrs) => { 33298 const blocks = text.split(/\n\n/); 33299 const tagOpen = openContainer(rootTag, rootAttrs); 33300 const tagClose = '</' + rootTag + '>'; 33301 const paragraphs = map$3(blocks, (p) => { 33302 return p.split(/\n/).join('<br />'); 33303 }); 33304 const stitch = (p) => { 33305 return tagOpen + p + tagClose; 33306 }; 33307 return paragraphs.length === 1 ? paragraphs[0] : map$3(paragraphs, stitch).join(''); 33308 }; 33309 33310 const pasteBinDefaultContent = '%MCEPASTEBIN%'; 33311 /* 33312 * Creates a paste bin element as close as possible to the current caret location and places the focus inside that element 33313 * so that when the real paste event occurs the contents gets inserted into this element 33314 * instead of the current editor selection element. 33315 */ 33316 const create$5 = (editor, lastRngCell) => { 33317 const { dom, selection } = editor; 33318 const body = editor.getBody(); 33319 lastRngCell.set(selection.getRng()); 33320 // Create a pastebin 33321 const pasteBinElm = dom.add(editor.getBody(), 'div', { 33322 'id': 'mcepastebin', 33323 'class': 'mce-pastebin', 33324 'contentEditable': true, 33325 'data-mce-bogus': 'all', 33326 'style': 'position: fixed; top: 50%; width: 10px; height: 10px; overflow: hidden; opacity: 0' 33327 }, pasteBinDefaultContent); 33328 // Move paste bin out of sight since the controlSelection rect gets displayed otherwise on Gecko 33329 if (Env.browser.isFirefox()) { 33330 dom.setStyle(pasteBinElm, 'left', dom.getStyle(body, 'direction', true) === 'rtl' ? 0xFFFF : -0xFFFF); 33331 } 33332 // Prevent focus events from bubbling fixed FocusManager issues 33333 dom.bind(pasteBinElm, 'beforedeactivate focusin focusout', (e) => { 33334 e.stopPropagation(); 33335 }); 33336 pasteBinElm.focus(); 33337 selection.select(pasteBinElm, true); 33338 }; 33339 /* 33340 * Removes the paste bin if it exists. 33341 */ 33342 const remove = (editor, lastRngCell) => { 33343 const dom = editor.dom; 33344 if (getEl(editor)) { 33345 let pasteBinClone; 33346 const lastRng = lastRngCell.get(); 33347 // WebKit/Blink might clone the div so 33348 // lets make sure we remove all clones 33349 // TODO: Man o man is this ugly. WebKit is the new IE! Remove this if they ever fix it! 33350 while ((pasteBinClone = getEl(editor))) { 33351 dom.remove(pasteBinClone); 33352 dom.unbind(pasteBinClone); 33353 } 33354 if (lastRng) { 33355 editor.selection.setRng(lastRng); 33356 } 33357 } 33358 lastRngCell.set(null); 33359 }; 33360 const getEl = (editor) => editor.dom.get('mcepastebin'); 33361 const isPasteBin = (elm) => isNonNullable(elm) && elm.id === 'mcepastebin'; 33362 /* 33363 * Returns the contents of the paste bin as a HTML string. 33364 */ 33365 const getHtml = (editor) => { 33366 const dom = editor.dom; 33367 // Since WebKit/Chrome might clone the paste bin when pasting 33368 // for example: <img style="float: right"> we need to check if any of them contains some useful html. 33369 // TODO: Man o man is this ugly. WebKit is the new IE! Remove this if they ever fix it! 33370 const copyAndRemove = (toElm, fromElm) => { 33371 toElm.appendChild(fromElm); 33372 dom.remove(fromElm, true); // remove, but keep children 33373 }; 33374 // find only top level elements (there might be more nested inside them as well, see TINY-1162) 33375 const [pasteBinElm, ...pasteBinClones] = filter$5(editor.getBody().childNodes, isPasteBin); 33376 // if clones were found, move their content into the first bin 33377 each$e(pasteBinClones, (pasteBinClone) => { 33378 copyAndRemove(pasteBinElm, pasteBinClone); 33379 }); 33380 // TINY-1162: when copying plain text (from notepad for example) WebKit clones 33381 // paste bin (with styles and attributes) and uses it as a default wrapper for 33382 // the chunks of the content, here we cycle over the whole paste bin and replace 33383 // those wrappers with a basic div 33384 const dirtyWrappers = dom.select('div[id=mcepastebin]', pasteBinElm); 33385 for (let i = dirtyWrappers.length - 1; i >= 0; i--) { 33386 const cleanWrapper = dom.create('div'); 33387 pasteBinElm.insertBefore(cleanWrapper, dirtyWrappers[i]); 33388 copyAndRemove(cleanWrapper, dirtyWrappers[i]); 33389 } 33390 return pasteBinElm ? pasteBinElm.innerHTML : ''; 33391 }; 33392 const isDefaultPasteBinContent = (content) => content === pasteBinDefaultContent; 33393 const PasteBin = (editor) => { 33394 const lastRng = Cell(null); 33395 return { 33396 create: () => create$5(editor, lastRng), 33397 remove: () => remove(editor, lastRng), 33398 getEl: () => getEl(editor), 33399 getHtml: () => getHtml(editor), 33400 getLastRng: lastRng.get 33401 }; 33402 }; 33403 33404 /* 33405 * This module contains various utility functions for the paste logic. 33406 */ 33407 const filter = (content, items) => { 33408 Tools.each(items, (v) => { 33409 if (is$5(v, RegExp)) { 33410 content = content.replace(v, ''); 33411 } 33412 else { 33413 content = content.replace(v[0], v[1]); 33414 } 33415 }); 33416 return content; 33417 }; 33418 /* 33419 * Gets the innerText of the specified element. It will handle edge cases 33420 * and works better than textContent on Gecko. 33421 */ 33422 const innerText = (html) => { 33423 const schema = Schema(); 33424 const domParser = DomParser({}, schema); 33425 let text = ''; 33426 const voidElements = schema.getVoidElements(); 33427 const ignoreElements = Tools.makeMap('script noscript style textarea video audio iframe object', ' '); 33428 const blockElements = schema.getBlockElements(); 33429 const walk = (node) => { 33430 const name = node.name, currentNode = node; 33431 if (name === 'br') { 33432 text += '\n'; 33433 return; 33434 } 33435 // Ignore wbr, to replicate innerText on Chrome/Firefox 33436 if (name === 'wbr') { 33437 return; 33438 } 33439 // img/input/hr but ignore wbr as it's just a potential word break 33440 if (voidElements[name]) { 33441 text += ' '; 33442 } 33443 // Ignore script, video contents 33444 if (ignoreElements[name]) { 33445 text += ' '; 33446 return; 33447 } 33448 if (node.type === 3) { 33449 text += node.value; 33450 } 33451 // Walk all children 33452 if (!(node.name in schema.getVoidElements())) { 33453 let currentNode = node.firstChild; 33454 if (currentNode) { 33455 do { 33456 walk(currentNode); 33457 } while ((currentNode = currentNode.next)); 33458 } 33459 } 33460 // Add \n or \n\n for blocks or P 33461 if (blockElements[name] && currentNode.next) { 33462 text += '\n'; 33463 if (name === 'p') { 33464 text += '\n'; 33465 } 33466 } 33467 }; 33468 html = filter(html, [ 33469 /<!\[[^\]]+\]>/g // Conditional comments 33470 ]); 33471 walk(domParser.parse(html)); 33472 return text; 33473 }; 33474 /* 33475 * Trims the specified HTML by removing all WebKit fragments, all elements wrapping the body trailing BR elements etc. 33476 */ 33477 const trimHtml = (html) => { 33478 const trimSpaces = (all, s1, s2) => { 33479 // WebKit meant to preserve multiple spaces but instead inserted around all inline tags, 33480 // including the spans with inline styles created on paste 33481 if (!s1 && !s2) { 33482 return ' '; 33483 } 33484 return nbsp; 33485 }; 33486 html = filter(html, [ 33487 /^[\s\S]*<body[^>]*>\s*|\s*<\/body[^>]*>[\s\S]*$/ig, // Remove anything but the contents within the BODY element 33488 /<!--StartFragment-->|<!--EndFragment-->/g, // Inner fragments (tables from excel on mac) 33489 [/( ?)<span class="Apple-converted-space">\u00a0<\/span>( ?)/g, trimSpaces], 33490 /<br class="Apple-interchange-newline">/g, 33491 /<br>$/i // Trailing BR elements 33492 ]); 33493 return html; 33494 }; 33495 // TODO: Should be in some global class 33496 const createIdGenerator = (prefix) => { 33497 let count = 0; 33498 return () => { 33499 return prefix + (count++); 33500 }; 33501 }; 33502 const getImageMimeType = (ext) => { 33503 const lowerExt = ext.toLowerCase(); 33504 const mimeOverrides = { 33505 jpg: 'jpeg', 33506 jpe: 'jpeg', 33507 jfi: 'jpeg', 33508 jif: 'jpeg', 33509 jfif: 'jpeg', 33510 pjpeg: 'jpeg', 33511 pjp: 'jpeg', 33512 svg: 'svg+xml' 33513 }; 33514 return Tools.hasOwn(mimeOverrides, lowerExt) ? 'image/' + mimeOverrides[lowerExt] : 'image/' + lowerExt; 33515 }; 33516 33517 const preProcess = (editor, html) => { 33518 const parser = DomParser({ 33519 sanitize: shouldSanitizeXss(editor), 33520 sandbox_iframes: shouldSandboxIframes(editor), 33521 sandbox_iframes_exclusions: getSandboxIframesExclusions(editor), 33522 convert_unsafe_embeds: shouldConvertUnsafeEmbeds(editor) 33523 }, editor.schema); 33524 // Strip meta elements 33525 parser.addNodeFilter('meta', (nodes) => { 33526 Tools.each(nodes, (node) => { 33527 node.remove(); 33528 }); 33529 }); 33530 const fragment = parser.parse(html, { forced_root_block: false, isRootContent: true }); 33531 return HtmlSerializer({ validate: true }, editor.schema).serialize(fragment); 33532 }; 33533 const processResult = (content, cancelled) => ({ content, cancelled }); 33534 const postProcessFilter = (editor, html, internal) => { 33535 const tempBody = editor.dom.create('div', { style: 'display:none' }, html); 33536 const postProcessArgs = firePastePostProcess(editor, tempBody, internal); 33537 return processResult(postProcessArgs.node.innerHTML, postProcessArgs.isDefaultPrevented()); 33538 }; 33539 const filterContent = (editor, content, internal) => { 33540 const preProcessArgs = firePastePreProcess(editor, content, internal); 33541 // Filter the content to remove potentially dangerous content (eg scripts) 33542 const filteredContent = preProcess(editor, preProcessArgs.content); 33543 if (editor.hasEventListeners('PastePostProcess') && !preProcessArgs.isDefaultPrevented()) { 33544 return postProcessFilter(editor, filteredContent, internal); 33545 } 33546 else { 33547 return processResult(filteredContent, preProcessArgs.isDefaultPrevented()); 33548 } 33549 }; 33550 const process = (editor, html, internal) => { 33551 return filterContent(editor, html, internal); 33552 }; 33553 33554 const pasteHtml$1 = (editor, html) => { 33555 editor.insertContent(html, { 33556 merge: shouldPasteMergeFormats(editor), 33557 paste: true 33558 }); 33559 return true; 33560 }; 33561 const isAbsoluteUrl = (url) => /^https?:\/\/[\w\-\/+=.,!;:&%@^~(){}?#]+$/i.test(url); 33562 const isImageUrl = (editor, url) => { 33563 return isAbsoluteUrl(url) && exists(getAllowedImageFileTypes(editor), (type) => endsWith(url.toLowerCase(), `.${type.toLowerCase()}`)); 33564 }; 33565 const createImage = (editor, url, pasteHtmlFn) => { 33566 editor.undoManager.extra(() => { 33567 pasteHtmlFn(editor, url); 33568 }, () => { 33569 editor.insertContent('<img src="' + url + '">'); 33570 }); 33571 return true; 33572 }; 33573 const createLink = (editor, url, pasteHtmlFn) => { 33574 editor.undoManager.extra(() => { 33575 pasteHtmlFn(editor, url); 33576 }, () => { 33577 editor.execCommand('mceInsertLink', false, url); 33578 }); 33579 return true; 33580 }; 33581 const linkSelection = (editor, html, pasteHtmlFn) => !editor.selection.isCollapsed() && isAbsoluteUrl(html) ? createLink(editor, html, pasteHtmlFn) : false; 33582 const insertImage = (editor, html, pasteHtmlFn) => isImageUrl(editor, html) ? createImage(editor, html, pasteHtmlFn) : false; 33583 const smartInsertContent = (editor, html) => { 33584 Tools.each([ 33585 linkSelection, 33586 insertImage, 33587 pasteHtml$1 33588 ], (action) => { 33589 return !action(editor, html, pasteHtml$1); 33590 }); 33591 }; 33592 const insertContent = (editor, html, pasteAsText) => { 33593 if (pasteAsText || !isSmartPasteEnabled(editor)) { 33594 pasteHtml$1(editor, html); 33595 } 33596 else { 33597 smartInsertContent(editor, html); 33598 } 33599 }; 33600 33601 const uniqueId = createIdGenerator('mceclip'); 33602 const createPasteDataTransfer = (html) => { 33603 const dataTransfer = createDataTransfer(); 33604 setHtmlData(dataTransfer, html); 33605 // TINY-9829: Set to read-only mode as per https://www.w3.org/TR/input-events-2/ 33606 setReadOnlyMode(dataTransfer); 33607 return dataTransfer; 33608 }; 33609 const doPaste = (editor, content, internal, pasteAsText, shouldSimulateInputEvent) => { 33610 const res = process(editor, content, internal); 33611 if (!res.cancelled) { 33612 const content = res.content; 33613 const doPasteAction = () => insertContent(editor, content, pasteAsText); 33614 if (shouldSimulateInputEvent) { 33615 const args = fireBeforeInputEvent(editor, 'insertFromPaste', { dataTransfer: createPasteDataTransfer(content) }); 33616 if (!args.isDefaultPrevented()) { 33617 doPasteAction(); 33618 fireInputEvent(editor, 'insertFromPaste'); 33619 } 33620 } 33621 else { 33622 doPasteAction(); 33623 } 33624 } 33625 }; 33626 /* 33627 * Pastes the specified HTML. This means that the HTML is filtered and then 33628 * inserted at the current selection in the editor. It will also fire paste events 33629 * for custom user filtering. 33630 */ 33631 const pasteHtml = (editor, html, internalFlag, shouldSimulateInputEvent) => { 33632 const internal = internalFlag ? internalFlag : isMarked(html); 33633 doPaste(editor, unmark(html), internal, false, shouldSimulateInputEvent); 33634 }; 33635 /* 33636 * Pastes the specified text. This means that the plain text is processed 33637 * and converted into BR and P elements. It will fire paste events for custom filtering. 33638 */ 33639 const pasteText = (editor, text, shouldSimulateInputEvent) => { 33640 const encodedText = editor.dom.encode(text).replace(/\r\n/g, '\n'); 33641 const normalizedText = normalize$4(encodedText, getPasteTabSpaces(editor)); 33642 const html = toBlockElements(normalizedText, getForcedRootBlock(editor), getForcedRootBlockAttrs(editor)); 33643 doPaste(editor, html, false, true, shouldSimulateInputEvent); 33644 }; 33645 /* 33646 * Gets various content types out of a datatransfer object. 33647 */ 33648 const getDataTransferItems = (dataTransfer) => { 33649 const items = {}; 33650 if (dataTransfer && dataTransfer.types) { 33651 for (let i = 0; i < dataTransfer.types.length; i++) { 33652 const contentType = dataTransfer.types[i]; 33653 try { // IE11 throws exception when contentType is Files (type is present but data cannot be retrieved via getData()) 33654 items[contentType] = dataTransfer.getData(contentType); 33655 } 33656 catch { 33657 items[contentType] = ''; // useless in general, but for consistency across browsers 33658 } 33659 } 33660 } 33661 return items; 33662 }; 33663 const hasContentType = (clipboardContent, mimeType) => mimeType in clipboardContent && clipboardContent[mimeType].length > 0; 33664 const hasHtmlOrText = (content) => hasContentType(content, 'text/html') || hasContentType(content, 'text/plain'); 33665 const extractFilename = (editor, str) => { 33666 const m = str.match(/([\s\S]+?)(?:\.[a-z0-9.]+)$/i); 33667 return isNonNullable(m) ? editor.dom.encode(m[1]) : undefined; 33668 }; 33669 const createBlobInfo = (editor, blobCache, file, base64) => { 33670 const id = uniqueId(); 33671 const useFileName = shouldReuseFileName(editor) && isNonNullable(file.name); 33672 const name = useFileName ? extractFilename(editor, file.name) : id; 33673 const filename = useFileName ? file.name : undefined; 33674 const blobInfo = blobCache.create(id, file, base64, name, filename); 33675 blobCache.add(blobInfo); 33676 return blobInfo; 33677 }; 33678 const pasteImage =
33678(editor, imageItem) => { 33679 parseDataUri(imageItem.uri).each(({ data, type, base64Encoded }) => { 33680 const base64 = base64Encoded ? data : btoa(data); 33681 const file = imageItem.file; 33682 // TODO: Move the bulk of the cache logic to EditorUpload 33683 const blobCache = editor.editorUpload.blobCache; 33684 const existingBlobInfo = blobCache.getByData(base64, type); 33685 const blobInfo = existingBlobInfo ?? createBlobInfo(editor, blobCache, file, base64); 33686 pasteHtml(editor, `<img src="${blobInfo.blobUri()}">`, false, true); 33687 }); 33688 }; 33689 const isClipboardEvent = (event) => event.type === 'paste';
vendor: 1,757 bytes, lines 33690-33723
33690 const readFilesAsDataUris = (items) => Promise.all(map$3(items, (file) => { 33691 return blobToDataUri(file).then((uri) => ({ file, uri })); 33692 })); 33693 const isImage = (editor) => { 33694 const allowedExtensions = getAllowedImageFileTypes(editor); 33695 return (file) => startsWith(file.type, 'image/') && exists(allowedExtensions, (extension) => { 33696 return getImageMimeType(extension) === file.type; 33697 }); 33698 }; 33699 const getImagesFromDataTransfer = (editor, dataTransfer) => { 33700 const items = dataTransfer.items ? bind$3(from(dataTransfer.items), (item) => { 33701 return item.kind === 'file' ? [item.getAsFile()] : []; 33702 }) : []; 33703 const files = dataTransfer.files ? from(dataTransfer.files) : []; 33704 return filter$5(items.length > 0 ? items : files, isImage(editor)); 33705 }; 33706 /* 33707 * Checks if the clipboard contains image data if it does it will take that data 33708 * and convert it into a data url image and paste that image at the caret location. 33709 */ 33710 const pasteImageData = (editor, e, rng) => { 33711 const dataTransfer = isClipboardEvent(e) ? e.clipboardData : e.dataTransfer; 33712 if (shouldPasteDataImages(editor) && dataTransfer) { 33713 const images = getImagesFromDataTransfer(editor, dataTransfer); 33714 if (images.length > 0) { 33715 e.preventDefault(); 33716 // eslint-disable-next-line @typescript-eslint/no-floating-promises 33717 readFilesAsDataUris(images).then((fileResults) => { 33718 if (rng) { 33719 editor.selection.setRng(rng); 33720 } 33721 each$e(fileResults, (result) => { 33722 pasteImage(editor, result); 33723 });
vendor: 67,018 bytes, lines 33723-35106
33723 33724 }); 33725 return true; 33726 } 33727 } 33728 return false; 33729 }; 33730 // Chrome on Android doesn't support proper clipboard access so we have no choice but to allow the browser default behavior. 33731 const isBrokenAndroidClipboardEvent = (e) => Env.os.isAndroid() && e.clipboardData?.items?.length === 0; 33732 // Ctrl+V or Shift+Insert 33733 const isKeyboardPasteEvent = (e) => (VK.metaKeyPressed(e) && e.keyCode === 86) || (e.shiftKey && e.keyCode === 45); 33734 const insertClipboardContent = (editor, clipboardContent, html, plainTextMode, shouldSimulateInputEvent) => { 33735 let content = trimHtml(html); 33736 const isInternal = hasContentType(clipboardContent, internalHtmlMime()) || isMarked(html); 33737 const isPlainTextHtml = !isInternal && isPlainText(content); 33738 const isAbsoluteUrl$1 = isAbsoluteUrl(content); 33739 // If the paste bin is empty try using plain text mode since that is better than nothing right? 33740 // Also if we got nothing from clipboard API/pastebin or the content is a plain text (with only 33741 // some BRs, Ps or DIVs as newlines) then we fallback to plain/text 33742 if (isDefaultPasteBinContent(content) || !content.length || (isPlainTextHtml && !isAbsoluteUrl$1)) { 33743 plainTextMode = true; 33744 } 33745 // Grab plain text from Clipboard API or convert existing HTML to plain text 33746 if (plainTextMode || isAbsoluteUrl$1) { 33747 // Use plain text contents from Clipboard API unless the HTML contains paragraphs then 33748 // we should convert the HTML to plain text since works better when pasting HTML/Word contents as plain text 33749 if (hasContentType(clipboardContent, 'text/plain') && isPlainTextHtml) { 33750 content = clipboardContent['text/plain']; 33751 } 33752 else { 33753 content = innerText(content); 33754 } 33755 } 33756 // If the content is the paste bin default HTML then it was impossible to get the clipboard data out. 33757 if (isDefaultPasteBinContent(content)) { 33758 return; 33759 } 33760 if (plainTextMode) { 33761 pasteText(editor, content, shouldSimulateInputEvent); 33762 } 33763 else { 33764 pasteHtml(editor, content, isInternal, shouldSimulateInputEvent); 33765 } 33766 }; 33767 const registerEventHandlers = (editor, pasteBin, pasteFormat) => { 33768 let keyboardPastePlainTextState; 33769 const getLastRng = () => pasteBin.getLastRng() || editor.selection.getRng(); 33770 editor.on('keydown', (e) => { 33771 if (isKeyboardPasteEvent(e) && !e.isDefaultPrevented()) { 33772 keyboardPastePlainTextState = e.shiftKey && e.keyCode === 86; 33773 } 33774 }); 33775 editor.on('paste', (e) => { 33776 if (e.isDefaultPrevented() || isBrokenAndroidClipboardEvent(e)) { 33777 return; 33778 } 33779 const plainTextMode = pasteFormat.get() === 'text' || keyboardPastePlainTextState; 33780 keyboardPastePlainTextState = false; 33781 const clipboardContent = getDataTransferItems(e.clipboardData); 33782 if (!hasHtmlOrText(clipboardContent) && pasteImageData(editor, e, getLastRng())) { 33783 return; 33784 } 33785 // If the clipboard API has HTML then use that directly 33786 if (hasContentType(clipboardContent, 'text/html')) { 33787 e.preventDefault(); 33788 insertClipboardContent(editor, clipboardContent, clipboardContent['text/html'], plainTextMode, true); 33789 } 33790 else if (hasContentType(clipboardContent, 'text/plain') && hasContentType(clipboardContent, 'text/uri-list')) { 33791 /* 33792 Safari adds the uri-list attribute to links copied within it. 33793 When pasting something with the url-list within safari using the default functionality it will convert it from www.example.com to <a href="www.example.com">www.example.com</a> when pasting into the pasteBin-div. 33794 This causes issues. To solve this we bypass the default paste functionality for this situation. 33795 */ 33796 e.preventDefault(); 33797 insertClipboardContent(editor, clipboardContent, clipboardContent['text/plain'], plainTextMode, true); 33798 } 33799 else { 33800 // We can't extract the HTML content from the clipboard so we need to allow the paste 33801 // to run via the pastebin and then extract from there 33802 pasteBin.create(); 33803 Delay.setEditorTimeout(editor, () => { 33804 // Get the pastebin content and then remove it so the selection is restored 33805 const html = pasteBin.getHtml(); 33806 pasteBin.remove(); 33807 insertClipboardContent(editor, clipboardContent, html, plainTextMode, false); 33808 }, 0); 33809 } 33810 }); 33811 }; 33812 const registerDataImageFilter = (editor) => { 33813 const isWebKitFakeUrl = (src) => startsWith(src, 'webkit-fake-url'); 33814 const isDataUri = (src) => startsWith(src, 'data:'); 33815 const isPasteInsert = (args) => args.data?.paste === true; 33816 // Remove all data images from paste for example from Gecko 33817 // except internal images like video elements 33818 editor.parser.addNodeFilter('img', (nodes, name, args) => { 33819 if (!shouldPasteDataImages(editor) && isPasteInsert(args)) { 33820 for (const node of nodes) { 33821 const src = node.attr('src'); 33822 if (isString(src) && !node.attr('data-mce-object') && src !== Env.transparentSrc) { 33823 // Safari on Mac produces webkit-fake-url see: https://bugs.webkit.org/show_bug.cgi?id=49141 33824 if (isWebKitFakeUrl(src)) { 33825 node.remove(); 33826 } 33827 else if (!shouldAllowHtmlDataUrls(editor) && isDataUri(src)) { 33828 node.remove(); 33829 } 33830 } 33831 } 33832 } 33833 }); 33834 }; 33835 /* 33836 * This class contains logic for getting HTML contents out of the clipboard. 33837 * 33838 * This by default will attempt to use the W3C clipboard API to get HTML content. 33839 * If that can't be used then fallback to letting the browser paste natively with 33840 * some logic to clean up what the browser generated, as it can mutate the content. 33841 * 33842 * Current implementation steps: 33843 * 1. On keydown determine if we should paste as plain text. 33844 * 2. Wait for the browser to fire a "paste" event and get the contents out of clipboard. 33845 * 3. If no content is available, then attach the paste bin and change the selection to be inside the bin. 33846 * 4. Extract the contents from the bin in the next event loop. 33847 * 5. If no HTML is found or we're using plain text paste mode then convert the HTML or lookup the clipboard to get the plain text. 33848 * 6. Process the content from the clipboard or pastebin and insert it into the editor. 33849 */ 33850 const registerEventsAndFilters = (editor, pasteBin, pasteFormat) => { 33851 registerEventHandlers(editor, pasteBin, pasteFormat); 33852 registerDataImageFilter(editor); 33853 }; 33854 33855 const togglePlainTextPaste = (editor, pasteFormat) => { 33856 if (pasteFormat.get() === 'text') { 33857 pasteFormat.set('html'); 33858 firePastePlainTextToggle(editor, false); 33859 } 33860 else { 33861 pasteFormat.set('text'); 33862 firePastePlainTextToggle(editor, true); 33863 } 33864 editor.focus(); 33865 }; 33866 const register$1 = (editor, pasteFormat) => { 33867 editor.addCommand('mceTogglePlainTextPaste', () => { 33868 togglePlainTextPaste(editor, pasteFormat); 33869 }); 33870 editor.addCommand('mceInsertClipboardContent', (ui, value) => { 33871 if (value.html) { 33872 // TINY-9997: Input events are not simulated when using paste commands, similar to how the 'mceInsertContent' 33873 // and 'Delete' commands work. 33874 pasteHtml(editor, value.html, value.internal, false); 33875 } 33876 if (value.text) { 33877 pasteText(editor, value.text, false); 33878 } 33879 }); 33880 }; 33881 33882 const setHtml5Clipboard = (clipboardData, html, text) => { 33883 if (clipboardData) { 33884 try { 33885 clipboardData.clearData(); 33886 clipboardData.setData('text/html', html); 33887 clipboardData.setData('text/plain', text); 33888 clipboardData.setData(internalHtmlMime(), html); 33889 return true; 33890 } 33891 catch { 33892 return false; 33893 } 33894 } 33895 else { 33896 return false; 33897 } 33898 }; 33899 const setClipboardData = (evt, data, fallback, done) => { 33900 if (setHtml5Clipboard(evt.clipboardData, data.html, data.text)) { 33901 evt.preventDefault(); 33902 done(); 33903 } 33904 else { 33905 fallback(data.html, done); 33906 } 33907 }; 33908 const fallback = (editor) => (html, done) => { 33909 const { dom, selection } = editor; 33910 const outer = dom.create('div', { 'contenteditable': 'false', 'data-mce-bogus': 'all' }); 33911 const inner = dom.create('div', { contenteditable: 'true' }, html); 33912 dom.setStyles(outer, { 33913 position: 'fixed', 33914 top: '0', 33915 left: '-3000px', 33916 width: '1000px', 33917 overflow: 'hidden' 33918 }); 33919 outer.appendChild(inner); 33920 dom.add(editor.getBody(), outer); 33921 const range = selection.getRng(); 33922 inner.focus(); 33923 const offscreenRange = dom.createRng(); 33924 offscreenRange.selectNodeContents(inner); 33925 selection.setRng(offscreenRange); 33926 Delay.setEditorTimeout(editor, () => { 33927 selection.setRng(range); 33928 dom.remove(outer); 33929 done(); 33930 }, 0); 33931 }; 33932 const getData = (editor) => ({ 33933 html: mark(editor.selection.getContent({ contextual: true })), 33934 text: editor.selection.getContent({ format: 'text' }) 33935 }); 33936 const isTableSelection = (editor) => !!editor.dom.getParent(editor.selection.getStart(), 'td[data-mce-selected],th[data-mce-selected]', editor.getBody()); 33937 const hasSelectedContent = (editor) => !editor.selection.isCollapsed() || isTableSelection(editor); 33938 const cut = (editor, caret) => (evt) => { 33939 if (!evt.isDefaultPrevented() && hasSelectedContent(editor) && editor.selection.isEditable()) { 33940 setClipboardData(evt, getData(editor), fallback(editor), () => { 33941 if (Env.browser.isChromium() || Env.browser.isFirefox()) { 33942 const rng = editor.selection.getRng(); 33943 // Chrome fails to execCommand from another execCommand with this message: 33944 // "We don't execute document.execCommand() this time, because it is called recursively."" 33945 // Firefox 82 now also won't run recursive commands, but it doesn't log an error 33946 Delay.setEditorTimeout(editor, () => { 33947 // Restore the range before deleting, as Chrome on Android will 33948 // collapse the selection after a cut event has fired. 33949 editor.selection.setRng(rng); 33950 // Delete command is called directly without using editor.execCommand to avoid running editor.focus() which side effect was selection normalization and additional undo level 33951 deleteCommand(editor, caret); 33952 }, 0); 33953 } 33954 else { 33955 // Delete command is called directly without using editor.execCommand to avoid running editor.focus() which side effect was selection normalization and additional undo level 33956 deleteCommand(editor, caret); 33957 } 33958 }); 33959 } 33960 }; 33961 const copy = (editor) => (evt) => { 33962 if (!evt.isDefaultPrevented() && hasSelectedContent(editor)) { 33963 setClipboardData(evt, getData(editor), fallback(editor), noop); 33964 } 33965 }; 33966 const register = (editor, caret) => { 33967 editor.on('cut', cut(editor, caret)); 33968 editor.on('copy', copy(editor)); 33969 }; 33970 33971 const getCaretRangeFromEvent = (editor, e) => 33972 // TODO: TINY-7075 Remove the "?? 0" here when agar passes valid client coords 33973 RangeUtils.getCaretRangeFromPoint(e.clientX ?? 0, e.clientY ?? 0, editor.getDoc()); 33974 const isPlainTextFileUrl = (content) => { 33975 const plainTextContent = content['text/plain']; 33976 return plainTextContent ? plainTextContent.indexOf('file://') === 0 : false; 33977 }; 33978 const setFocusedRange = (editor, rng) => { 33979 if (rng) { 33980 editor.selection.setRng(rng); 33981 } 33982 editor.focus(); 33983 }; 33984 const hasImage = (dataTransfer) => exists(dataTransfer.files, (file) => /^image\//.test(file.type)); 33985 const needsCustomInternalDrop = (dom, schema, target, dropContent) => { 33986 const parentTransparent = dom.getParent(target, (node) => isTransparentBlock(schema, node)); 33987 const inSummary = !isNull(dom.getParent(target, 'summary')); 33988 if (inSummary) { 33989 return true; 33990 } 33991 else if (parentTransparent && has$2(dropContent, 'text/html')) { 33992 const fragment = new DOMParser().parseFromString(dropContent['text/html'], 'text/html').body; 33993 return !isNull(fragment.querySelector(parentTransparent.nodeName.toLowerCase())); 33994 } 33995 else { 33996 return false; 33997 } 33998 }; 33999 const setupSummaryDeleteByDragFix = (editor) => { 34000 editor.on('input', (e) => { 34001 const hasNoSummary = (el) => isNull(el.querySelector('summary')); 34002 if (e.inputType === 'deleteByDrag') { 34003 const brokenDetailElements = filter$5(editor.dom.select('details'), hasNoSummary); 34004 each$e(brokenDetailElements, (details) => { 34005 // Firefox leaves a BR 34006 if (isBr$7(details.firstChild)) { 34007 details.firstChild.remove(); 34008 } 34009 const summary = editor.dom.create('summary'); 34010 summary.appendChild(createPaddingBr().dom); 34011 details.prepend(summary); 34012 }); 34013 } 34014 }); 34015 }; 34016 const setup$a = (editor, draggingInternallyState) => { 34017 // Block all drag/drop events 34018 if (shouldPasteBlockDrop(editor)) { 34019 editor.on('dragend dragover draggesture dragdrop drop drag', (e) => { 34020 e.preventDefault(); 34021 e.stopPropagation(); 34022 }); 34023 } 34024 // Prevent users from dropping data images on Gecko 34025 if (!shouldPasteDataImages(editor)) { 34026 editor.on('drop', (e) => { 34027 const dataTransfer = e.dataTransfer; 34028 if (dataTransfer && hasImage(dataTransfer)) { 34029 e.preventDefault(); 34030 } 34031 }); 34032 } 34033 editor.on('drop', (e) => { 34034 if (e.isDefaultPrevented()) { 34035 return; 34036 } 34037 const rng = getCaretRangeFromEvent(editor, e); 34038 if (isNullable(rng)) { 34039 return; 34040 } 34041 const dropContent = getDataTransferItems(e.dataTransfer); 34042 const internal = hasContentType(dropContent, internalHtmlMime()); 34043 if ((!hasHtmlOrText(dropContent) || isPlainTextFileUrl(dropContent)) && pasteImageData(editor, e, rng)) { 34044 return; 34045 } 34046 const internalContent = dropContent[internalHtmlMime()]; 34047 const content = internalContent || dropContent['text/html'] || dropContent['text/plain']; 34048 const needsInternalDrop = needsCustomInternalDrop(editor.dom, editor.schema, rng.startContainer, dropContent); 34049 const isInternalDrop = draggingInternallyState.get(); 34050 if (isInternalDrop && !needsInternalDrop) { 34051 return; 34052 } 34053 if (content) { 34054 e.preventDefault(); 34055 // FF 45 doesn't paint a caret when dragging in text in due to focus call by execCommand 34056 Delay.setEditorTimeout(editor, () => { 34057 editor.undoManager.transact(() => { 34058 if (internalContent || (isInternalDrop && needsInternalDrop)) { 34059 editor.execCommand('Delete'); 34060 } 34061 setFocusedRange(editor, rng); 34062 const trimmedContent = trimHtml(content); 34063 if (dropContent['text/html']) { 34064 pasteHtml(editor, trimmedContent, internal, true); 34065 } 34066 else { 34067 pasteText(editor, trimmedContent, true); 34068 } 34069 }); 34070 }); 34071 } 34072 }); 34073 editor.on('dragstart', (_e) => { 34074 draggingInternallyState.set(true); 34075 }); 34076 editor.on('dragover dragend', (e) => { 34077 if (shouldPasteDataImages(editor) && !draggingInternallyState.get()) { 34078 e.preventDefault(); 34079 setFocusedRange(editor, getCaretRangeFromEvent(editor, e)); 34080 } 34081 if (e.type === 'dragend') { 34082 draggingInternallyState.set(false); 34083 } 34084 }); 34085 setupSummaryDeleteByDragFix(editor); 34086 }; 34087 34088 const setup$9 = (editor) => { 34089 const processEvent = (f) => (e) => { 34090 f(editor, e); 34091 }; 34092 const preProcess = getPastePreProcess(editor); 34093 if (isFunction(preProcess)) { 34094 editor.on('PastePreProcess', processEvent(preProcess)); 34095 } 34096 const postProcess = getPastePostProcess(editor); 34097 if (isFunction(postProcess)) { 34098 editor.on('PastePostProcess', processEvent(postProcess)); 34099 } 34100 }; 34101 34102 /* 34103 * This module contains various fixes for browsers. These issues can not be feature 34104 * detected since we have no direct control over the clipboard. However we might be able 34105 * to remove some of these fixes once the browsers gets updated/fixed. 34106 */ 34107 const addPreProcessFilter = (editor, filterFunc) => { 34108 editor.on('PastePreProcess', (e) => { 34109 e.content = filterFunc(editor, e.content, e.internal); 34110 }); 34111 }; 34112 const rgbRegExp = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/gi; 34113 const rgbToHex = (value) => Tools.trim(value).replace(rgbRegExp, rgbaToHexString).toLowerCase(); 34114 /* 34115 * WebKit has a nasty quirk where the all computed styles gets added to style attributes when copy/pasting contents. 34116 * This fix solves that by simply removing the whole style attribute. 34117 * 34118 * The paste_webkit_styles option can be set to specify what to keep: 34119 * paste_webkit_styles: "none" // Keep no styles 34120 * paste_webkit_styles: "all", // Keep all of them 34121 * paste_webkit_styles: "font-weight color" // Keep specific ones 34122 */ 34123 const removeWebKitStyles = (editor, content, internal) => { 34124 const webKitStylesOption = getPasteWebkitStyles(editor); 34125 // If the content is internal or if we're keeping all styles then we don't need any processing 34126 if (internal || webKitStylesOption === 'all' || !shouldPasteRemoveWebKitStyles(editor)) { 34127 return content; 34128 } 34129 const webKitStyles = webKitStylesOption ? webKitStylesOption.split(/[, ]/) : []; 34130 // Keep specific styles that don't match the current node computed style 34131 if (webKitStyles && webKitStylesOption !== 'none') { 34132 const dom = editor.dom, node = editor.selection.getNode(); 34133 content = content.replace(/(<[^>]+) style="([^"]*)"([^>]*>)/gi, (all, before, value, after) => { 34134 const inputStyles = dom.parseStyle(dom.decode(value)); 34135 const outputStyles = {}; 34136 for (let i = 0; i < webKitStyles.length; i++) { 34137 const inputValue = inputStyles[webKitStyles[i]]; 34138 let compareInput = inputValue; 34139 let currentValue = dom.getStyle(node, webKitStyles[i], true); 34140 if (/color/.test(webKitStyles[i])) { 34141 compareInput = rgbToHex(compareInput); 34142 currentValue = rgbToHex(currentValue); 34143 } 34144 if (currentValue !== compareInput) { 34145 outputStyles[webKitStyles[i]] = inputValue; 34146 } 34147 } 34148 const outputStyle = dom.serializeStyle(outputStyles, 'span'); 34149 if (outputStyle) { 34150 return before + ' style="' + outputStyle + '"' + after; 34151 } 34152 return before + after; 34153 }); 34154 } 34155 else { 34156 // Remove all external styles 34157 content = content.replace(/(<[^>]+) style="([^"]*)"([^>]*>)/gi, '$1$3'); 34158 } 34159 // Keep internal styles 34160 content = content.replace(/(<[^>]+) data-mce-style="([^"]+)"([^>]*>)/gi, (all, before, value, after) => { 34161 return before + ' style="' + value + '"' + after; 34162 }); 34163 return content; 34164 }; 34165 const setup$8 = (editor) => { 34166 if (Env.browser.isChromium() || Env.browser.isSafari()) { 34167 addPreProcessFilter(editor, removeWebKitStyles); 34168 } 34169 }; 34170 34171 const setup$7 = (editor, caret) => { 34172 const draggingInternallyState = Cell(false); 34173 const pasteFormat = Cell(isPasteAsTextEnabled(editor) ? 'text' : 'html'); 34174 const pasteBin = PasteBin(editor); 34175 setup$8(editor); 34176 register$1(editor, pasteFormat); 34177 setup$9(editor); 34178 editor.addQueryStateHandler('mceTogglePlainTextPaste', () => pasteFormat.get() === 'text'); 34179 // IMPORTANT: The following event hooks need to be setup later so that other things 34180 // can hook in and prevent the event so core paste doesn't handle them. 34181 editor.on('PreInit', () => { 34182 register(editor, caret); 34183 setup$a(editor, draggingInternallyState); 34184 registerEventsAndFilters(editor, pasteBin, pasteFormat); 34185 }); 34186 }; 34187 34188 const preventSummaryToggle = (editor) => { 34189 editor.on('click', (e) => { 34190 if (editor.dom.getParent(e.target, 'details')) { 34191 e.preventDefault(); 34192 } 34193 }); 34194 }; 34195 const filterDetails = (editor) => { 34196 editor.parser.addNodeFilter('details', (elms) => { 34197 const initialStateOption = getDetailsInitialState(editor); 34198 each$e(elms, (details) => { 34199 if (initialStateOption === 'expanded') { 34200 details.attr('open', 'open'); 34201 } 34202 else if (initialStateOption === 'collapsed') { 34203 details.attr('open', null); 34204 } 34205 }); 34206 }); 34207 editor.serializer.addNodeFilter('details', (elms) => { 34208 const serializedStateOption = getDetailsSerializedState(editor); 34209 each$e(elms, (details) => { 34210 if (serializedStateOption === 'expanded') { 34211 details.attr('open', 'open'); 34212 } 34213 else if (serializedStateOption === 'collapsed') { 34214 details.attr('open', null); 34215 } 34216 }); 34217 }); 34218 }; 34219 const setup$6 = (editor) => { 34220 preventSummaryToggle(editor); 34221 filterDetails(editor); 34222 }; 34223 34224 const isBr = isBr$7; 34225 const isText = isText$b; 34226 const isContentEditableFalse$2 = (elm) => isContentEditableFalse$a(elm.dom); 34227 const isContentEditableTrue = (elm) => isContentEditableTrue$3(elm.dom); 34228 const isRoot = (rootNode) => (elm) => eq(SugarElement.fromDom(rootNode), elm); 34229 const getClosestScope = (node, rootNode, schema) => closest$5(SugarElement.fromDom(node), (elm) => isContentEditableTrue(elm) || schema.isBlock(name(elm)), isRoot(rootNode)) 34230 .getOr(SugarElement.fromDom(rootNode)).dom; 34231 const getClosestCef = (node, rootNode) => closest$5(SugarElement.fromDom(node), isContentEditableFalse$2, isRoot(rootNode)); 34232 const findEdgeCaretCandidate = (startNode, scope, forward) => { 34233 const walker = new DomTreeWalker(startNode, scope); 34234 const next = forward ? walker.next.bind(walker) : walker.prev.bind(walker); 34235 let result = startNode; 34236 for (let current = forward ? startNode : next(); current && !isBr(current); current = next()) { 34237 if (isCaretCandidate$3(current)) { 34238 result = current; 34239 } 34240 } 34241 return result; 34242 }; 34243 const findClosestBlockRange = (startRng, rootNode, schema) => { 34244 const startPos = CaretPosition.fromRangeStart(startRng); 34245 // TODO: TINY-8865 - This may not be safe to cast as Node and alternative solutions need to be looked into 34246 const clickNode = startPos.getNode(); 34247 const scope = getClosestScope(clickNode, rootNode, schema); 34248 const startNode = findEdgeCaretCandidate(clickNode, scope, false); 34249 const endNode = findEdgeCaretCandidate(clickNode, scope, true); 34250 const rng = document.createRange(); 34251 getClosestCef(startNode, scope).fold(() => { 34252 if (isText(startNode)) { 34253 rng.setStart(startNode, 0); 34254 } 34255 else { 34256 rng.setStartBefore(startNode); 34257 } 34258 }, (cef) => rng.setStartBefore(cef.dom)); 34259 getClosestCef(endNode, scope).fold(() => { 34260 if (isText(endNode)) { 34261 rng.setEnd(endNode, endNode.data.length); 34262 } 34263 else { 34264 rng.setEndAfter(endNode); 34265 } 34266 }, (cef) => rng.setEndAfter(cef.dom)); 34267 return rng; 34268 }; 34269 const onTripleClickSelect = (editor) => { 34270 const rng = findClosestBlockRange(editor.selection.getRng(), editor.getBody(), editor.schema); 34271 editor.selection.setRng(normalize(rng)); 34272 }; 34273 const setup$5 = (editor) => { 34274 editor.on('mousedown', (e) => { 34275 if (e.detail >= 3) { 34276 e.preventDefault(); 34277 onTripleClickSelect(editor); 34278 } 34279 }); 34280 }; 34281 34282 var FakeCaretPosition; 34283 (function (FakeCaretPosition) { 34284 FakeCaretPosition["Before"] = "before"; 34285 FakeCaretPosition["After"] = "after"; 34286 })(FakeCaretPosition || (FakeCaretPosition = {})); 34287 const distanceToRectLeft = (clientRect, clientX) => Math.abs(clientRect.left - clientX); 34288 const distanceToRectRight = (clientRect, clientX) => Math.abs(clientRect.right - clientX); 34289 const isInsideY = (clientY, clientRect) => clientY >= clientRect.top && clientY <= clientRect.bottom; 34290 const collidesY = (r1, r2) => r1.top < r2.bottom && r1.bottom > r2.top; 34291 const isOverlapping = (r1, r2) => { 34292 // Rectangles might overlap a bit so this checks if the overlap is more than 50% then we count that as on the same line 34293 const overlap = overlapY(r1, r2) / Math.min(r1.height, r2.height); 34294 return collidesY(r1, r2) && overlap > 0.5; 34295 }; 34296 const splitRectsPerAxis = (rects, y) => { 34297 const intersectingRects = filter$5(rects, (rect) => isInsideY(y, rect)); 34298 return boundingClientRectFromRects(intersectingRects).fold(() => ([[], rects]), (boundingRect) => { 34299 const { pass: horizontal, fail: vertical } = partition$2(rects, (rect) => isOverlapping(rect, boundingRect)); 34300 return [horizontal, vertical]; 34301 }); 34302 }; 34303 const clientInfo = (rect, clientX) => { 34304 return { 34305 node: rect.node, 34306 position: distanceToRectLeft(rect, clientX) < distanceToRectRight(rect, clientX) ? FakeCaretPosition.Before : FakeCaretPosition.After 34307 }; 34308 }; 34309 // Measure the distance between the x and the closest edge of the rect. 34310 // If the x is inside the rect then always return 0. 34311 const horizontalDistance = (rect, x, _y) => x > rect.left && x < rect.right ? 0 : Math.min(Math.abs(rect.left - x), Math.abs(rect.right - x)); 34312 const closestChildCaretCandidateNodeRect = (children, clientX, clientY, findCloserTextNode) => { 34313 const caretCandidateRect = (rect) => { 34314 if (isCaretCandidate$3(rect.node)) { 34315 return Optional.some(rect); 34316 } 34317 else if (isElement$7(rect.node)) { 34318 return closestChildCaretCandidateNodeRect(from(rect.node.childNodes), clientX, clientY, false); 34319 } 34320 else { 34321 return Optional.none(); 34322 } 34323 }; 34324 // If an element and a text node has nearly equal distance then favor the text node over the element to make it easier to select text 34325 // since setting the selection range will cancel any text select operation. 34326 const tryFindSecondBestTextNode = (closest, sndClosest, distance) => { 34327 return caretCandidateRect(sndClosest).filter((rect) => { 34328 const deltaDistance = Math.abs(distance(closest, clientX, clientY) - distance(rect, clientX, clientY)); 34329 return deltaDistance < 2 && isText$b(rect.node); 34330 }); 34331 }; 34332 const findClosestCaretCandidateNodeRect = (rects, distance) => { 34333 const sortedRects = sort(rects, (r1, r2) => distance(r1, clientX, clientY) - distance(r2, clientX, clientY)); 34334 return findMap(sortedRects, caretCandidateRect).map((closest) => { 34335 // If the closest rect is not a text node then lets try to see if the second rect has a text node that is close enough 34336 if (findCloserTextNode && !isText$b(closest.node) && sortedRects.length > 1) { 34337 return tryFindSecondBestTextNode(closest, sortedRects[1], distance).getOr(closest); 34338 } 34339 else { 34340 return closest; 34341 } 34342 }); 34343 }; 34344 const [horizontalRects, verticalRects] = splitRectsPerAxis(getClientRects(children), clientY); 34345 const { pass: above, fail: below } = partition$2(verticalRects, (rect) => rect.top < clientY); 34346 return findClosestCaretCandidateNodeRect(horizontalRects, horizontalDistance) 34347 .orThunk(() => findClosestCaretCandidateNodeRect(below, distanceToRectEdgeFromXY)) 34348 .orThunk(() => findClosestCaretCandidateNodeRect(above, distanceToRectEdgeFromXY)); 34349 }; 34350 const traverseUp = (rootElm, scope, clientX, clientY) => { 34351 const helper = (scope, prevScope) => { 34352 const isDragGhostContainer = (node) => isElement$7(node) && node.classList.contains('mce-drag-container'); 34353 const childNodesWithoutGhost = filter$5(scope.dom.childNodes, not(isDragGhostContainer)); 34354 return prevScope.fold(() => closestChildCaretCandidateNodeRect(childNodesWithoutGhost, clientX, clientY, true), (prevScope) => { 34355 const uncheckedChildren = filter$5(childNodesWithoutGhost, (node) => node !== prevScope.dom); 34356 return closestChildCaretCandidateNodeRect(uncheckedChildren, clientX, clientY, true); 34357 }).orThunk(() => { 34358 const parent = eq(scope, rootElm) ? Optional.none() : parentElement(scope); 34359 return parent.bind((newScope) => helper(newScope, Optional.some(scope))); 34360 }); 34361 }; 34362 return helper(scope, Optional.none()); 34363 }; 34364 // Rough description of how this algorithm works: 34365 // 1. It starts by finding the element at the specified X, Y coordinate. 34366 // 2. Then it checks its children for the closest one and traverses down into those repeating step 2, 3 until it finds a caret candidate. 34367 // 3. If no caret candidate is found in the closest child node then it checks the second closest and so on until all decendants have been checked. 34368 // 4. If no caret candidate is found, it traverses up skips the element it already checked and checks its siblings using steps 2, 3. 34369 // 5. If no caret candidate is found, it continues step 4 until it finds the root. Then we have checked all the nodes in the document. 34370 // 34371 // This is less accurate but more performant, since for the common case you are likely to find a caret candidate close to where you are clicking. 34372 // The more accurate algorithm would be to read all caret candidates rects in the whole document in and in one big step to find the closest one, but that is just too slow for bigger documents. 34373 const closestCaretCandidateNodeRect = (root, clientX, clientY) => { 34374 const rootElm = SugarElement.fromDom(root); 34375 const ownerDoc = documentOrOwner(rootElm); 34376 const elementAtPoint = SugarElement.fromPoint(ownerDoc, clientX, clientY).filter((elm) => contains(rootElm, elm)); 34377 const element = elementAtPoint.getOr(rootElm); 34378 return traverseUp(rootElm, element, clientX, clientY); 34379 }; 34380 const closestFakeCaretCandidate = (root, clientX, clientY) => closestCaretCandidateNodeRect(root, clientX, clientY) 34381 .filter((rect) => isFakeCaretTarget(rect.node)) 34382 .map((rect) => clientInfo(rect, clientX)); 34383 34384 const getAbsolutePosition = (elm) => { 34385 const clientRect = elm.getBoundingClientRect(); 34386 const doc = elm.ownerDocument; 34387 const docElem = doc.documentElement; 34388 const win = doc.defaultView; 34389 return { 34390 top: clientRect.top + (win?.scrollY ?? 0) - docElem.clientTop, 34391 left: clientRect.left + (win?.scrollX ?? 0) - docElem.clientLeft 34392 }; 34393 }; 34394 const getBodyPosition = (editor) => editor.inline ? getAbsolutePosition(editor.getBody()) : { left: 0, top: 0 }; 34395 const getScrollPosition = (editor) => { 34396 const body = editor.getBody(); 34397 return editor.inline ? { left: body.scrollLeft, top: body.scrollTop } : { left: 0, top: 0 }; 34398 }; 34399 const getBodyScroll = (editor) => { 34400 const body = editor.getBody(), docElm = editor.getDoc().documentElement; 34401 const inlineScroll = { left: body.scrollLeft, top: body.scrollTop }; 34402 const iframeScroll = { left: body.scrollLeft || docElm.scrollLeft, top: body.scrollTop || docElm.scrollTop }; 34403 return editor.inline ? inlineScroll : iframeScroll; 34404 }; 34405 const getMousePosition = (editor, event) => { 34406 if (event.target.ownerDocument !== editor.getDoc()) { 34407 const iframePosition = getAbsolutePosition(editor.getContentAreaContainer()); 34408 const scrollPosition = getBodyScroll(editor); 34409 return { 34410 left: event.pageX - iframePosition.left + scrollPosition.left, 34411 top: event.pageY - iframePosition.top + scrollPosition.top 34412 }; 34413 } 34414 return { 34415 left: event.pageX, 34416 top: event.pageY 34417 }; 34418 }; 34419 const calculatePosition = (bodyPosition, scrollPosition, mousePosition) => ({ 34420 pageX: (mousePosition.left - bodyPosition.left) + scrollPosition.left, 34421 pageY: (mousePosition.top - bodyPosition.top) + scrollPosition.top 34422 }); 34423 const calc = (editor, event) => calculatePosition(getBodyPosition(editor), getScrollPosition(editor), getMousePosition(editor, event)); 34424 34425 const getTargetProps = (target) => ({ target, srcElement: target }); 34426 const makeDndEventFromMouseEvent = (type, mouseEvent, target, dataTransfer) => ({ 34427 ...mouseEvent, 34428 dataTransfer, 34429 type, 34430 ...getTargetProps(target) 34431 }); 34432 const makeDndEvent = (type, target, dataTransfer) => { 34433 const fail = die('Function not supported on simulated event.'); 34434 const event = { 34435 // Event 34436 bubbles: true, 34437 cancelBubble: false, 34438 cancelable: true, 34439 composed: false, 34440 currentTarget: null, 34441 defaultPrevented: false, 34442 eventPhase: 0, 34443 isTrusted: true, 34444 returnValue: false, 34445 timeStamp: 0, 34446 type, 34447 composedPath: fail, 34448 initEvent: fail, 34449 preventDefault: noop, 34450 stopImmediatePropagation: noop, 34451 stopPropagation: noop, 34452 AT_TARGET: window.Event.AT_TARGET, 34453 BUBBLING_PHASE: window.Event.BUBBLING_PHASE, 34454 CAPTURING_PHASE: window.Event.CAPTURING_PHASE, 34455 NONE: window.Event.NONE, 34456 // UIEvent 34457 altKey: false, 34458 button: 0, 34459 buttons: 0, 34460 clientX: 0, 34461 clientY: 0, 34462 ctrlKey: false, 34463 layerX: 0, 34464 layerY: 0, 34465 metaKey: false, 34466 movementX: 0, 34467 movementY: 0, 34468 offsetX: 0, 34469 offsetY: 0, 34470 pageX: 0, 34471 pageY: 0, 34472 relatedTarget: null, 34473 screenX: 0, 34474 screenY: 0, 34475 shiftKey: false, 34476 x: 0, 34477 y: 0, 34478 detail: 0, 34479 view: null, 34480 which: 0, 34481 initUIEvent: fail, 34482 initMouseEvent: fail, 34483 getModifierState: fail, 34484 // DragEvent 34485 dataTransfer, 34486 ...getTargetProps(target) 34487 }; 34488 return event; 34489 }; 34490 const makeDataTransferCopyForDragEvent = (dataTransfer, eventType) => { 34491 const copy = cloneDataTransfer(dataTransfer); 34492 // TINY-9601: Set mode as per https://html.spec.whatwg.org/dev/dnd.html#concept-dnd-rw 34493 if (eventType === 'dragstart') { 34494 setDragstartEvent(copy); 34495 setReadWriteMode(copy); 34496 } 34497 else if (eventType === 'drop') { 34498 setDropEvent(copy); 34499 setReadOnlyMode(copy); 34500 } 34501 else { 34502 setDragendEvent(copy); 34503 setProtectedMode(copy); 34504 } 34505 return copy; 34506 }; 34507 const makeDragEvent = (type, target, dataTransfer, mouseEvent) => { 34508 // TINY-9601: Get copy for each new event to prevent undesired mutations on dispatched DataTransfer objects 34509 const dataTransferForDispatch = makeDataTransferCopyForDragEvent(dataTransfer, type); 34510 return isUndefined(mouseEvent) ? makeDndEvent(type, target, dataTransferForDispatch) : makeDndEventFromMouseEvent(type, mouseEvent, target, dataTransferForDispatch); 34511 }; 34512 34513 /** 34514 * This module contains logic overriding the drag/drop logic of the editor. 34515 * 34516 * @private 34517 * @class tinymce.DragDropOverrides 34518 */ 34519 // Arbitrary values needed when scrolling CEF elements 34520 const scrollPixelsPerInterval = 32; 34521 const scrollIntervalValue = 100; 34522 const mouseRangeToTriggerScrollInsideEditor = 8; 34523 const mouseRangeToTriggerScrollOutsideEditor = 16; 34524 const isContentEditableFalse$1 = isContentEditableFalse$a; 34525 const isContentEditable = or(isContentEditableFalse$1, isContentEditableTrue$3); 34526 const isDraggable = (dom, rootElm, elm) => isContentEditableFalse$1(elm) && elm !== rootElm && dom.isEditable(elm.parentElement); 34527 const isValidDropTarget = (editor, targetElement, dragElement) => { 34528 if (isNullable(targetElement)) { 34529 return false; 34530 } 34531 else if (targetElement === dragElement || editor.dom.isChildOf(targetElement, dragElement)) { 34532 return false; 34533 } 34534 else { 34535 return editor.dom.isEditable(targetElement); 34536 } 34537 }; 34538 const createGhost = (editor, elm, width, height) => { 34539 const dom = editor.dom; 34540 const clonedElm = elm.cloneNode(true); 34541 dom.setStyles(clonedElm, { width, height }); 34542 dom.setAttrib(clonedElm, 'data-mce-selected', null); 34543 const ghostElm = dom.create('div', { 34544 'class': 'mce-drag-container', 34545 'data-mce-bogus': 'all', 34546 'unselectable': 'on', 34547 'contenteditable': 'false' 34548 }); 34549 dom.setStyles(ghostElm, { 34550 position: 'absolute', 34551 opacity: 0.5, 34552 overflow: 'hidden', 34553 border: 0, 34554 padding: 0, 34555 margin: 0, 34556 width, 34557 height 34558 }); 34559 dom.setStyles(clonedElm, { 34560 margin: 0, 34561 boxSizing: 'border-box' 34562 }); 34563 ghostElm.appendChild(clonedElm); 34564 return ghostElm; 34565 }; 34566 const appendGhostToBody = (ghostElm, bodyElm) => { 34567 if (ghostElm.parentNode !== bodyElm) { 34568 bodyElm.appendChild(ghostElm); 34569 } 34570 }; 34571 // Helper function needed for scrolling the editor inside moveGhost function 34572 const scrollEditor = (direction, amount) => (win) => () => { 34573 const current = direction === 'left' ? win.scrollX : win.scrollY; 34574 win.scroll({ 34575 [direction]: current + amount, 34576 behavior: 'smooth', 34577 }); 34578 }; 34579 const scrollLeft = scrollEditor('left', -scrollPixelsPerInterval); 34580 const scrollRight = scrollEditor('left', scrollPixelsPerInterval); 34581 const scrollUp = scrollEditor('top', -scrollPixelsPerInterval); 34582 const scrollDown = scrollEditor('top', scrollPixelsPerInterval); 34583 const moveGhost = (ghostElm, position, width, height, maxX, maxY, mouseY, mouseX, contentAreaContainer, win, state, mouseEventOriginatedFromWithinTheEditor) => { 34584 let overflowX = 0, overflowY = 0; 34585 ghostElm.style.left = position.pageX + 'px'; 34586 ghostElm.style.top = position.pageY + 'px'; 34587 if (position.pageX + width > maxX) { 34588 overflowX = (position.pageX + width) - maxX; 34589 } 34590 if (position.pageY + height > maxY) { 34591 overflowY = (position.pageY + height) - maxY; 34592 } 34593 ghostElm.style.width = (width - overflowX) + 'px'; 34594 ghostElm.style.height = (height - overflowY) + 'px'; 34595 // Code needed for dragging CEF elements (specifically fixing TINY-8874) 34596 // The idea behind the algorithm is that the user will start dragging the 34597 // CEF element to the edge of the editor and that would cause scrolling. 34598 // The way that happens is that the user will trigger a mousedown event, 34599 // then a mousemove event until they reach the edge of the editor. Then 34600 // no event triggers. That's when I set an interval to keep scrolling the editor. 34601 // Once a new event triggers I clear the existing interval and set it back to none. 34602 const clientHeight = contentAreaContainer.clientHeight; 34603 const clientWidth = contentAreaContainer.clientWidth; 34604 const outerMouseY = mouseY + contentAreaContainer.getBoundingClientRect().top; 34605 const outerMouseX = mouseX + contentAreaContainer.getBoundingClientRect().left; 34606 state.on((state) => { 34607 state.intervalId.clear(); 34608 if (state.dragging && mouseEventOriginatedFromWithinTheEditor) { 34609 // This basically means that the mouse is close to the bottom edge 34610 // (within MouseRange pixels of the bottom edge) 34611 if (mouseY + mouseRangeToTriggerScrollInsideEditor >= clientHeight) { 34612 state.intervalId.set(scrollDown(win)); 34613 // This basically means that the mouse is close to the top edge 34614 // (within MouseRange pixels) 34615 } 34616 else if (mouseY - mouseRangeToTriggerScrollInsideEditor <= 0) { 34617 state.intervalId.set(scrollUp(win)); 34618 // This basically means that the mouse is close to the right edge 34619 // (within MouseRange pixels of the right edge) 34620 } 34621 else if (mouseX + mouseRangeToTriggerScrollInsideEditor >= clientWidth) { 34622 state.intervalId.set(scrollRight(win)); 34623 // This basically means that the mouse is close to the left edge 34624 // (within MouseRange pixels of the left edge) 34625 } 34626 else if (mouseX - mouseRangeToTriggerScrollInsideEditor <= 0) { 34627 state.intervalId.set(scrollLeft(win)); 34628 // This basically means that the mouse is close to the bottom edge 34629 // of the page (within MouseRange pixels) when the bottom of 34630 // the editor is offscreen 34631 } 34632 else if (outerMouseY + mouseRangeToTriggerScrollOutsideEditor >= window.innerHeight) { 34633 state.intervalId.set(scrollDown(window)); 34634 // This basically means that the mouse is close to the upper edge 34635 // of the page (within MouseRange pixels) when the top of 34636 // the editor is offscreen 34637 } 34638 else if (outerMouseY - mouseRangeToTriggerScrollOutsideEditor <= 0) { 34639 state.intervalId.set(scrollUp(window)); 34640 // This basically means that the mouse is close to the right edge 34641 // of the page (within MouseRange pixels) when the right edge of 34642 // the editor is offscreen 34643 } 34644 else if (outerMouseX + mouseRangeToTriggerScrollOutsideEditor >= window.innerWidth) { 34645 state.intervalId.set(scrollRight(window)); 34646 // This basically means that the mouse is close to the left edge 34647 // of the page (within MouseRange pixels) when the left edge of 34648 // the editor is offscreen 34649 } 34650 else if (outerMouseX - mouseRangeToTriggerScrollOutsideEditor <= 0) { 34651 state.intervalId.set(scrollLeft(window)); 34652 } 34653 } 34654 }); 34655 }; 34656 const removeElement = (elm) => { 34657 if (elm && elm.parentNode) { 34658 elm.parentNode.removeChild(elm); 34659 } 34660 }; 34661 const removeElementWithPadding = (dom, elm) => { 34662 const parentBlock = dom.getParent(elm.parentNode, dom.isBlock); 34663 removeElement(elm); 34664 if (parentBlock && parentBlock !== dom.getRoot() && dom.isEmpty(parentBlock)) { 34665 fillWithPaddingBr(SugarElement.fromDom(parentBlock)); 34666 } 34667 }; 34668 const isLeftMouseButtonPressed = (e) => e.button === 0; 34669 const applyRelPos = (state, position) => ({ 34670 pageX: position.pageX - state.relX, 34671 pageY: position.pageY + 5 34672 }); 34673 const start = (state, editor) => (e) => { 34674 if (isLeftMouseButtonPressed(e)) { 34675 const ceElm = find$2(editor.dom.getParents(e.target), isContentEditable).getOr(null); 34676 if (isNonNullable(ceElm) && isDraggable(editor.dom, editor.getBody(), ceElm)) { 34677 const elmPos = editor.dom.getPos(ceElm); 34678 const bodyElm = editor.getBody(); 34679 const docElm = editor.getDoc().documentElement; 34680 state.set({ 34681 element: ceElm, 34682 dataTransfer: createDataTransfer(), 34683 dragging: false, 34684 screenX: e.screenX, 34685 screenY: e.screenY, 34686 maxX: (editor.inline ? bodyElm.scrollWidth : docElm.offsetWidth) - 2, 34687 maxY: (editor.inline ? bodyElm.scrollHeight : docElm.offsetHeight) - 2, 34688 relX: e.pageX - elmPos.x, 34689 relY: e.pageY - elmPos.y, 34690 width: ceElm.offsetWidth, 34691 height: ceElm.offsetHeight, 34692 ghost: createGhost(editor, ceElm, ceElm.offsetWidth, ceElm.offsetHeight), 34693 intervalId: repeatable(scrollIntervalValue) 34694 }); 34695 } 34696 } 34697 }; 34698 const placeCaretAt = (editor, clientX, clientY) => { 34699 editor._selectionOverrides.hideFakeCaret(); 34700 closestFakeCaretCandidate(editor.getBody(), clientX, clientY).fold(() => editor.selection.placeCaretAt(clientX, clientY), (caretInfo) => { 34701 const range = editor._selectionOverrides.showCaret(1, caretInfo.node, caretInfo.position === FakeCaretPosition.Before, false); 34702 if (range) { 34703 editor.selection.setRng(range); 34704 } 34705 else { 34706 editor.selection.placeCaretAt(clientX, clientY); 34707 } 34708 }); 34709 }; 34710 const dispatchDragEvent = (editor, type, target, dataTransfer, mouseEvent) => { 34711 if (type === 'dragstart') { 34712 setHtmlData(dataTransfer, editor.dom.getOuterHTML(target)); 34713 } 34714 const event = makeDragEvent(type, target, dataTransfer, mouseEvent); 34715 const args = editor.dispatch(type, event); 34716 return args; 34717 }; 34718 const move = (state, editor) => { 34719 // Reduces laggy drag behavior on Gecko 34720 const throttledPlaceCaretAt = first$1((clientX, clientY) => placeCaretAt(editor, clientX, clientY), 0); 34721 editor.on('remove', throttledPlaceCaretAt.cancel); 34722 const state_ = state; 34723 return (e) => state.on((state) => { 34724 const movement = Math.max(Math.abs(e.screenX - state.screenX), Math.abs(e.screenY - state.screenY)); 34725 if (!state.dragging && movement > 10) { 34726 const args = dispatchDragEvent(editor, 'dragstart', state.element, state.dataTransfer, e); 34727 // TINY-9601: dataTransfer is writable in dragstart, so keep it up-to-date 34728 if (isNonNullable(args.dataTransfer)) { 34729 state.dataTransfer = args.dataTransfer; 34730 } 34731 if (args.isDefaultPrevented()) { 34732 return; 34733 } 34734 state.dragging = true; 34735 editor.focus(); 34736 } 34737 if (state.dragging) { 34738 const mouseEventOriginatedFromWithinTheEditor = e.currentTarget === editor.getDoc().documentElement; 34739 const targetPos = applyRelPos(state, calc(editor, e)); 34740 appendGhostToBody(state.ghost, editor.getBody()); 34741 moveGhost(state.ghost, targetPos, state.width, state.height, state.maxX, state.maxY, e.clientY, e.clientX, editor.getContentAreaContainer(), editor.getWin(), state_, mouseEventOriginatedFromWithinTheEditor); 34742 throttledPlaceCaretAt.throttle(e.clientX, e.clientY); 34743 } 34744 }); 34745 }; 34746 // Returns the raw element instead of the fake cE=false element 34747 const getRawTarget = (selection) => { 34748 const sel = selection.getSel(); 34749 if (isNonNullable(sel)) { 34750 const rng = sel.getRangeAt(0); 34751 const startContainer = rng.startContainer; 34752 return isText$b(startContainer) ? startContainer.parentNode : startContainer; 34753 } 34754 else { 34755 return null; 34756 } 34757 }; 34758 const drop = (state, editor) => (e) => { 34759 state.on((state) => { 34760 state.intervalId.clear(); 34761 if (state.dragging) { 34762 if (isValidDropTarget(editor, getRawTarget(editor.selection), state.element)) { 34763 const dropTarget = editor.getDoc().elementFromPoint(e.clientX, e.clientY) ?? editor.getBody(); 34764 const args = dispatchDragEvent(editor, 'drop', dropTarget, state.dataTransfer, e); 34765 if (!args.isDefaultPrevented()) { 34766 editor.undoManager.transact(() => { 34767 removeElementWithPadding(editor.dom, state.element); 34768 // TINY-9601: Use dataTransfer property to determine inserted content on drop. This allows users to 34769 // manipulate drop content by modifying dataTransfer in the dragstart event. 34770 getHtmlData(state.dataTransfer).each((content) => editor.insertContent(content)); 34771 editor._selectionOverrides.hideFakeCaret(); 34772 }); 34773 } 34774 } 34775 // Use body as the target since the element we are dragging no longer exists. Native drag/drop works in a similar way. 34776 dispatchDragEvent(editor, 'dragend', editor.getBody(), state.dataTransfer, e); 34777 } 34778 }); 34779 removeDragState(state); 34780 }; 34781 const stopDragging = (state, editor, e) => { 34782 state.on((state) => { 34783 state.intervalId.clear(); 34784 if (state.dragging) { 34785 e.fold(() => dispatchDragEvent(editor, 'dragend', state.element, state.dataTransfer), (mouseEvent) => dispatchDragEvent(editor, 'dragend', state.element, state.dataTransfer, mouseEvent)); 34786 } 34787 }); 34788 removeDragState(state); 34789 }; 34790 const stop = (state, editor) => (e) => stopDragging(state, editor, Optional.some(e)); 34791 const removeDragState = (state) => { 34792 state.on((state) => { 34793 state.intervalId.clear(); 34794 removeElement(state.ghost); 34795 }); 34796 state.clear(); 34797 }; 34798 const bindFakeDragEvents = (editor) => { 34799 const state = value$1(); 34800 const pageDom = DOMUtils.DOM; 34801 const rootDocument = document; 34802 const dragStartHandler = start(state, editor); 34803 const dragHandler = move(state, editor); 34804 const dropHandler = drop(state, editor); 34805 const dragEndHandler = stop(state, editor); 34806 editor.on('mousedown', dragStartHandler); 34807 editor.on('mousemove', dragHandler); 34808 editor.on('mouseup', dropHandler); 34809 pageDom.bind(rootDocument, 'mousemove', dragHandler); 34810 pageDom.bind(rootDocument, 'mouseup', dragEndHandler); 34811 editor.on('remove', () => { 34812 pageDom.unbind(rootDocument, 'mousemove', dragHandler); 34813 pageDom.unbind(rootDocument, 'mouseup', dragEndHandler); 34814 }); 34815 editor.on('keydown', (e) => { 34816 // Fire 'dragend' when the escape key is pressed 34817 if (e.keyCode === VK.ESC) { 34818 stopDragging(state, editor, Optional.none()); 34819 } 34820 }); 34821 }; 34822 // Block files being dropped within the editor to prevent accidentally navigating away 34823 // while editing. Note that we can't use the `editor.on` API here, as we want these 34824 // to run after the editor event handlers have run. We also bind to the document 34825 // so that it'll try to ensure it's the last thing that runs, as it bubbles up the dom. 34826 const blockUnsupportedFileDrop = (editor) => { 34827 const preventFileDrop = (e) => { 34828 if (!e.isDefaultPrevented()) { 34829 // Prevent file drop events within the editor, as they'll cause the browser to navigate away 34830 const dataTransfer = e.dataTransfer; 34831 if (dataTransfer && (contains$2(dataTransfer.types, 'Files') || dataTransfer.files.length > 0)) { 34832 e.preventDefault(); 34833 if (e.type === 'drop') { 34834 displayError(editor, 'Dropped file type is not supported'); 34835 } 34836 } 34837 } 34838 }; 34839 const preventFileDropIfUIElement = (e) => { 34840 if (isUIElement(editor, e.target)) { 34841 preventFileDrop(e); 34842 } 34843 }; 34844 const setup = () => { 34845 const pageDom = DOMUtils.DOM; 34846 const dom = editor.dom; 34847 const doc = document; 34848 const editorRoot = editor.inline ? editor.getBody() : editor.getDoc(); 34849 const eventNames = ['drop', 'dragover']; 34850 each$e(eventNames, (name) => { 34851 pageDom.bind(doc, name, preventFileDropIfUIElement); 34852 dom.bind(editorRoot, name, preventFileDrop); 34853 }); 34854 editor.on('remove', () => { 34855 each$e(eventNames, (name) => { 34856 pageDom.unbind(doc, name, preventFileDropIfUIElement); 34857 dom.unbind(editorRoot, name, preventFileDrop); 34858 }); 34859 }); 34860 }; 34861 editor.on('init', () => { 34862 // Use a timeout to ensure this fires after all other init callbacks 34863 Delay.setEditorTimeout(editor, setup, 0); 34864 }); 34865 }; 34866 const init$2 = (editor) => { 34867 bindFakeDragEvents(editor); 34868 if (shouldBlockUnsupportedDrop(editor)) { 34869 blockUnsupportedFileDrop(editor); 34870 } 34871 }; 34872 34873 const setup$4 = (editor) => { 34874 const renderFocusCaret = first$1(() => { 34875 // AP-24 Added the second condition in this if because of a race condition with setting focus on the PowerPaste 34876 // remove/keep formatting dialog on paste in IE11. Without this, because we paste twice on IE11, focus ends up set 34877 // in the editor, not the dialog buttons. Specifically, we focus, blur, focus, blur, focus then enter this throttled 34878 // code before the next blur has been able to run. With this check, this function doesn't run at all in this case, 34879 // so focus goes to the dialog's buttons correctly. 34880 if (!editor.removed && editor.getBody().contains(document.activeElement)) { 34881 const rng = editor.selection.getRng(); 34882 if (rng.collapsed) { // see TINY-1479 34883 const caretRange = renderRangeCaret(editor, rng, false); 34884 editor.selection.setRng(caretRange); 34885 } 34886 } 34887 }, 0); 34888 editor.on('focus', () => { 34889 renderFocusCaret.throttle(); 34890 }); 34891 editor.on('blur', () => { 34892 renderFocusCaret.cancel(); 34893 }); 34894 }; 34895 34896 const setup$3 = (editor) => { 34897 editor.on('init', () => { 34898 // Audio elements don't fire mousedown/click events and only fire a focus event so 34899 // we need to capture that event being fired and use it to update the selection. 34900 editor.on('focusin', (e) => { 34901 const target = e.target; 34902 if (isMedia$2(target)) { 34903 const ceRoot = getContentEditableRoot$1(editor.getBody(), target); 34904 const node = isContentEditableFalse$a(ceRoot) ? ceRoot : target; 34905 if (editor.selection.getNode() !== node) { 34906 selectNode(editor, node).each((rng) => editor.selection.setRng(rng)); 34907 } 34908 } 34909 }); 34910 }); 34911 }; 34912 34913 const isContentEditableFalse = isContentEditableFalse$a; 34914 const getContentEditableRoot = (editor, node) => getContentEditableRoot$1(editor.getBody(), node); 34915 const SelectionOverrides = (editor) => { 34916 const selection = editor.selection, dom = editor.dom; 34917 const rootNode = editor.getBody(); 34918 const fakeCaret = FakeCaret(editor, rootNode, dom.isBlock, () => hasFocus(editor)); 34919 const realSelectionId = 'sel-' + dom.uniqueId(); 34920 const elementSelectionAttr = 'data-mce-selected'; 34921 let selectedElement; 34922 const isFakeSelectionElement = (node) => isNonNullable(node) && dom.hasClass(node, 'mce-offscreen-selection'); 34923 // Note: isChildOf will return true if node === rootNode, so we need an additional check for that 34924 const isFakeSelectionTargetElement = (node) => node !== rootNode && (isContentEditableFalse(node) || isMedia$2(node)) && dom.isChildOf(node, rootNode) && dom.isEditable(node.parentNode); 34925 const setRange = (range) => { 34926 if (range) { 34927 selection.setRng(range); 34928 } 34929 }; 34930 const showCaret = (direction, node, before, scrollIntoView = true) => { 34931 const e = editor.dispatch('ShowCaret', { 34932 target: node, 34933 direction, 34934 before 34935 }); 34936 if (e.isDefaultPrevented()) { 34937 return null; 34938 } 34939 if (scrollIntoView) { 34940 selection.scrollIntoView(node, direction === -1); 34941 } 34942 return fakeCaret.show(before, node); 34943 }; 34944 const showBlockCaretContainer = (blockCaretContainer) => { 34945 if (blockCaretContainer.hasAttribute('data-mce-caret')) { 34946 showCaretContainerBlock(blockCaretContainer); 34947 selection.scrollIntoView(blockCaretContainer); 34948 } 34949 }; 34950 const registerEvents = () => { 34951 editor.on('click', (e) => { 34952 // Prevent clicks on links in a cE=false element 34953 if (!dom.isEditable(e.target)) { 34954 e.preventDefault(); 34955 editor.focus(); 34956 } 34957 }); 34958 editor.on('blur NewBlock', removeElementSelection); 34959 editor.on('ResizeWindow FullscreenStateChanged', fakeCaret.reposition); 34960 editor.on('tap', (e) => { 34961 const targetElm = e.target; 34962 const contentEditableRoot = getContentEditableRoot(editor, targetElm); 34963 if (isContentEditableFalse(contentEditableRoot)) { 34964 e.preventDefault(); 34965 selectNode(editor, contentEditableRoot).each(setElementSelection); 34966 } 34967 else if (isFakeSelectionTargetElement(targetElm)) { 34968 selectNode(editor, targetElm).each(setElementSelection); 34969 } 34970 }, true); 34971 editor.on('mousedown', (e) => { 34972 const targetElm = e.target; 34973 if (targetElm !== rootNode && targetElm.nodeName !== 'HTML' && !dom.isChildOf(targetElm, rootNode)) { 34974 return; 34975 } 34976 if (!isXYInContentArea(editor, e.clientX, e.clientY)) { 34977 return; 34978 } 34979 // Remove needs to be called here since the mousedown might alter the selection without calling selection.setRng 34980 // and therefore not fire the AfterSetSelectionRange event. 34981 removeElementSelection(); 34982 hideFakeCaret(); 34983 const closestContentEditable = getContentEditableRoot(editor, targetElm); 34984 if (isContentEditableFalse(closestContentEditable)) { 34985 e.preventDefault(); 34986 selectNode(editor, closestContentEditable).each(setElementSelection); 34987 } 34988 else { 34989 closestFakeCaretCandidate(rootNode, e.clientX, e.clientY).each((caretInfo) => { 34990 e.preventDefault(); 34991 const range = showCaret(1, caretInfo.node, caretInfo.position === FakeCaretPosition.Before, false); 34992 setRange(range); 34993 // Set the focus after the range has been set to avoid potential issues where the body has no selection 34994 if (isHTMLElement(closestContentEditable)) { 34995 closestContentEditable.focus(); 34996 } 34997 else { 34998 editor.getBody().focus(); 34999 } 35000 }); 35001 } 35002 }); 35003 editor.on('keypress', (e) => { 35004 if (VK.modifierPressed(e)) { 35005 return; 35006 } 35007 if (isContentEditableFalse(selection.getNode())) { 35008 e.preventDefault(); 35009 } 35010 }); 35011 editor.on('GetSelectionRange', (e) => { 35012 let rng = e.range; 35013 if (selectedElement) { 35014 if (!selectedElement.parentNode) { 35015 selectedElement = null; 35016 return; 35017 } 35018 rng = rng.cloneRange(); 35019 rng.selectNode(selectedElement); 35020 e.range = rng; 35021 } 35022 }); 35023 editor.on('focusin', (e) => { 35024 // for medias the selection is already managed in `MediaFocus.ts` 35025 if (isMedia$2(e.target)) { 35026 return; 35027 } 35028 if (editor.getBody().contains(e.target) && e.target !== editor.getBody() && !editor.dom.isEditable(e.target.parentNode)) { 35029 if (fakeCaret.isShowing()) { 35030 fakeCaret.hide(); 35031 } 35032 if (!e.target.contains(editor.selection.getNode())) { 35033 editor.selection.select(e.target, true); 35034 editor.selection.collapse(true); 35035 } 35036 const rng = setElementSelection(editor.selection.getRng(), true); 35037 if (rng) { 35038 editor.selection.setRng(rng); 35039 } 35040 } 35041 }); 35042 editor.on('SetSelectionRange', (e) => { 35043 // If the range is set inside a short ended element, then move it 35044 // to the side as IE for example will try to add content inside 35045 e.range = normalizeVoidElementSelection(e.range); 35046 const rng = setElementSelection(e.range, e.forward); 35047 if (rng) { 35048 e.range = rng; 35049 } 35050 }); 35051 const isPasteBin = (node) => isElement$7(node) && node.id === 'mcepastebin'; 35052 editor.on('AfterSetSelectionRange', (e) => { 35053 const rng = e.range; 35054 const parent = rng.startContainer.parentElement; 35055 if (!isRangeInCaretContainer(rng) && !isPasteBin(parent)) { 35056 hideFakeCaret(); 35057 } 35058 if (!isFakeSelectionElement(parent)) { 35059 removeElementSelection(); 35060 } 35061 }); 35062 init$2(editor); 35063 setup$4(editor); 35064 setup$3(editor); 35065 }; 35066 const isWithinCaretContainer = (node) => (isCaretContainer$2(node) || 35067 startsWithCaretContainer$1(node) || 35068 endsWithCaretContainer$1(node)); 35069 const isRangeInCaretContainer = (rng) => isWithinCaretContainer(rng.startContainer) || isWithinCaretContainer(rng.endContainer); 35070 const normalizeVoidElementSelection = (rng) => { 35071 const voidElements = editor.schema.getVoidElements(); 35072 const newRng = dom.createRng(); 35073 const startContainer = rng.startContainer; 35074 const startOffset = rng.startOffset; 35075 const endContainer = rng.endContainer; 35076 const endOffset = rng.endOffset; 35077 if (has$2(voidElements, startContainer.nodeName.toLowerCase())) { 35078 if (startOffset === 0) { 35079 newRng.setStartBefore(startContainer); 35080 } 35081 else { 35082 newRng.setStartAfter(startContainer); 35083 } 35084 } 35085 else { 35086 newRng.setStart(startContainer, startOffset); 35087 } 35088 if (has$2(voidElements, endContainer.nodeName.toLowerCase())) { 35089 if (endOffset === 0) { 35090 newRng.setEndBefore(endContainer); 35091 } 35092 else { 35093 newRng.setEndAfter(endContainer); 35094 } 35095 } 35096 else { 35097 newRng.setEnd(endContainer, endOffset); 35098 } 35099 return newRng; 35100 }; 35101 const setupOffscreenSelection = (node, targetClone) => { 35102 const body = SugarElement.fromDom(editor.getBody()); 35103 const doc = editor.getDoc(); 35104 const realSelectionContainer = descendant$1(body, '#' + realSelectionId).getOrThunk(() => { 35105 const newContainer = SugarElement.fromHtml('<div data-mce-bogus="all" class="mce-offscreen-selection"></div>', doc); 35106 set$
351064
vendor: 28,469 bytes, lines 35106-35698
35106(newContainer, 'id', realSelectionId); 35107 append$1(body, newContainer); 35108 return newContainer; 35109 }); 35110 const newRange = dom.createRng(); 35111 empty(realSelectionContainer); 35112 append(realSelectionContainer, [ 35113 SugarElement.fromText(nbsp, doc), 35114 SugarElement.fromDom(targetClone), 35115 SugarElement.fromText(nbsp, doc) 35116 ]); 35117 newRange.setStart(realSelectionContainer.dom.firstChild, 1); 35118 newRange.setEnd(realSelectionContainer.dom.lastChild, 0); 35119 setAll(realSelectionContainer, { 35120 top: dom.getPos(node, editor.getBody()).y + 'px' 35121 }); 35122 focus$1(realSelectionContainer); 35123 const sel = selection.getSel(); 35124 if (sel) { 35125 sel.removeAllRanges(); 35126 sel.addRange(newRange); 35127 } 35128 return newRange; 35129 }; 35130 const getUcVideoClone = (ucVideo) => { 35131 const newElm = editor.getDoc().createElement('div'); 35132 newElm.style.width = ucVideo.style.width; 35133 newElm.style.height = ucVideo.style.height; 35134 const ucVideoWidth = ucVideo.getAttribute('width'); 35135 if (ucVideoWidth) { 35136 newElm.setAttribute('width', ucVideoWidth); 35137 } 35138 const ucVideoHeight = ucVideo.getAttribute('height'); 35139 if (ucVideoHeight) { 35140 newElm.setAttribute('height', ucVideoHeight); 35141 } 35142 return newElm; 35143 }; 35144 const selectElement = (elm) => { 35145 const targetClone = isUcVideo(elm) ? getUcVideoClone(elm) : elm.cloneNode(true); 35146 const e = editor.dispatch('ObjectSelected', { target: elm, targetClone }); 35147 if (e.isDefaultPrevented()) { 35148 return null; 35149 } 35150 // Setup the offscreen selection 35151 const range = setupOffscreenSelection(elm, e.targetClone); 35152 // We used to just remove all data-mce-selected values and set 1 on node. 35153 // But data-mce-selected can be values other than 1 so keep existing value if 35154 // node has one, and remove data-mce-selected from everything else 35155 const nodeElm = SugarElement.fromDom(elm); 35156 each$e(descendants(SugarElement.fromDom(editor.getBody()), `*[${elementSelectionAttr}]`), (elm) => { 35157 if (!eq(nodeElm, elm)) { 35158 remove$9(elm, elementSelectionAttr); 35159 } 35160 }); 35161 if (!dom.getAttrib(elm, elementSelectionAttr)) { 35162 elm.setAttribute(elementSelectionAttr, '1'); 35163 } 35164 selectedElement = elm; 35165 hideFakeCaret(); 35166 return range; 35167 }; 35168 const setElementSelection = (range, forward) => { 35169 if (!range) { 35170 return null; 35171 } 35172 if (range.collapsed) { 35173 if (!isRangeInCaretContainer(range)) { 35174 const dir = forward ? 1 : -1; 35175 const caretPosition = getNormalizedRangeEndPoint(dir, rootNode, range); 35176 const beforeNode = caretPosition.getNode(!forward); 35177 if (isNonNullable(beforeNode)) { 35178 if (isFakeCaretTarget(beforeNode)) { 35179 return showCaret(dir, beforeNode, forward ? !caretPosition.isAtEnd() : false, false); 35180 } 35181 if (isCaretContainerInline(beforeNode) && isContentEditableFalse$a(beforeNode.nextSibling)) { 35182 const rng = dom.createRng(); 35183 rng.setStart(beforeNode, 0); 35184 rng.setEnd(beforeNode, 0); 35185 return rng; 35186 } 35187 } 35188 const afterNode = caretPosition.getNode(forward); 35189 if (isNonNullable(afterNode)) { 35190 if (isFakeCaretTarget(afterNode)) { 35191 return showCaret(dir, afterNode, forward ? false : !caretPosition.isAtEnd(), false); 35192 } 35193 if (isCaretContainerInline(afterNode) && isContentEditableFalse$a(afterNode.previousSibling)) { 35194 const rng = dom.createRng(); 35195 rng.setStart(afterNode, 1); 35196 rng.setEnd(afterNode, 1); 35197 return rng; 35198 } 35199 } 35200 } 35201 return null; 35202 } 35203 let startContainer = range.startContainer; 35204 let startOffset = range.startOffset; 35205 const endOffset = range.endOffset; 35206 // Normalizes <span cE=false>[</span>] to [<span cE=false></span>] 35207 if (isText$b(startContainer) && startOffset === 0 && isContentEditableFalse(startContainer.parentNode)) { 35208 startContainer = startContainer.parentNode; 35209 startOffset = dom.nodeIndex(startContainer); 35210 startContainer = startContainer.parentNode; 35211 } 35212 if (!isElement$7(startContainer)) { 35213 return null; 35214 } 35215 if (endOffset === startOffset + 1 && startContainer === range.endContainer) { 35216 const node = startContainer.childNodes[startOffset]; 35217 if (isFakeSelectionTargetElement(node)) { 35218 return selectElement(node); 35219 } 35220 } 35221 return null; 35222 }; 35223 const removeElementSelection = () => { 35224 if (selectedElement) { 35225 selectedElement.removeAttribute(elementSelectionAttr); 35226 } 35227 descendant$1(SugarElement.fromDom(editor.getBody()), '#' + realSelectionId).each(remove$8); 35228 selectedElement = null; 35229 }; 35230 const destroy = () => { 35231 fakeCaret.destroy(); 35232 selectedElement = null; 35233 }; 35234 const hideFakeCaret = () => { 35235 fakeCaret.hide(); 35236 }; 35237 if (!isRtc(editor)) { 35238 registerEvents(); 35239 } 35240 return { 35241 showCaret, 35242 showBlockCaretContainer, 35243 hideFakeCaret, 35244 destroy 35245 }; 35246 }; 35247 35248 const getNormalizedTextOffset = (container, offset) => { 35249 let normalizedOffset = offset; 35250 for (let node = container.previousSibling; isText$b(node); node = node.previousSibling) { 35251 normalizedOffset += node.data.length; 35252 } 35253 return normalizedOffset; 35254 }; 35255 const generatePath = (dom, root, node, offset, normalized) => { 35256 if (isText$b(node) && (offset < 0 || offset > node.data.length)) { 35257 return []; 35258 } 35259 const p = normalized && isText$b(node) ? [getNormalizedTextOffset(node, offset)] : [offset]; 35260 let current = node; 35261 while (current !== root && current.parentNode) { 35262 p.push(dom.nodeIndex(current, normalized)); 35263 current = current.parentNode; 35264 } 35265 return current === root ? p.reverse() : []; 35266 }; 35267 const generatePathRange = (dom, root, startNode, startOffset, endNode, endOffset, normalized = false) => { 35268 const start = generatePath(dom, root, startNode, startOffset, normalized); 35269 const end = generatePath(dom, root, endNode, endOffset, normalized); 35270 return { start, end }; 35271 }; 35272 const resolvePath = (root, path) => { 35273 const nodePath = path.slice(); 35274 const offset = nodePath.pop(); 35275 if (!isNumber(offset)) { 35276 return Optional.none(); 35277 } 35278 else { 35279 const resolvedNode = foldl(nodePath, (optNode, index) => optNode.bind((node) => Optional.from(node.childNodes[index])), Optional.some(root)); 35280 return resolvedNode.bind((node) => { 35281 if (isText$b(node) && (offset < 0 || offset > node.data.length)) { 35282 return Optional.none(); 35283 } 35284 else { 35285 return Optional.some({ node, offset }); 35286 } 35287 }); 35288 } 35289 }; 35290 const resolvePathRange = (root, range) => resolvePath(root, range.start) 35291 .bind(({ node: startNode, offset: startOffset }) => resolvePath(root, range.end).map(({ node: endNode, offset: endOffset }) => { 35292 const rng = document.createRange(); 35293 rng.setStart(startNode, startOffset); 35294 rng.setEnd(endNode, endOffset); 35295 return rng; 35296 })); 35297 const generatePathRangeFromRange = (dom, root, range, normalized = false) => generatePathRange(dom, root, range.startContainer, range.startOffset, range.endContainer, range.endOffset, normalized); 35298 35299 const cleanEmptyNodes = (dom, node, isRoot) => { 35300 // Recursively walk up the tree while we have a parent and the node is empty. If the node is empty, then remove it. 35301 if (node && dom.isEmpty(node) && !isRoot(node)) { 35302 const parent = node.parentNode; 35303 dom.remove(node, isText$b(node.firstChild) && isWhitespaceText(node.firstChild.data)); 35304 cleanEmptyNodes(dom, parent, isRoot); 35305 } 35306 }; 35307 const deleteRng = (dom, rng, isRoot, clean = true) => { 35308 const startParent = rng.startContainer.parentNode; 35309 const endParent = rng.endContainer.parentNode; 35310 rng.deleteContents(); 35311 // Clean up any empty nodes if required 35312 if (clean && !isRoot(rng.startContainer)) { 35313 if (isText$b(rng.startContainer) && rng.startContainer.data.length === 0) { 35314 dom.remove(rng.startContainer); 35315 } 35316 if (isText$b(rng.endContainer) && rng.endContainer.data.length === 0) { 35317 dom.remove(rng.endContainer); 35318 } 35319 cleanEmptyNodes(dom, startParent, isRoot); 35320 if (startParent !== endParent) { 35321 cleanEmptyNodes(dom, endParent, isRoot); 35322 } 35323 } 35324 }; 35325 const getParentBlock = (editor, rng) => Optional.from(editor.dom.getParent(rng.startContainer, editor.dom.isBlock)); 35326 const resolveFromDynamicPatterns = (patternSet, block, beforeText) => { 35327 const dynamicPatterns = patternSet.dynamicPatternsLookup({ 35328 text: beforeText, 35329 block 35330 }); 35331 // dynamic patterns take precedence here 35332 return { 35333 ...patternSet, 35334 blockPatterns: getBlockPatterns(dynamicPatterns).concat(patternSet.blockPatterns), 35335 inlinePatterns: getInlinePatterns(dynamicPatterns).concat(patternSet.inlinePatterns) 35336 }; 35337 }; 35338 const getBeforeText = (dom, block, node, offset) => { 35339 const rng = dom.createRng(); 35340 rng.setStart(block, 0); 35341 rng.setEnd(node, offset); 35342 return rng.toString(); 35343 }; 35344 35345 const newMarker = (dom, id) => dom.create('span', { 'data-mce-type': 'bookmark', id }); 35346 const rangeFromMarker = (dom, marker) => { 35347 const rng = dom.createRng(); 35348 rng.setStartAfter(marker.start); 35349 rng.setEndBefore(marker.end); 35350 return rng; 35351 }; 35352 const createMarker = (dom, markerPrefix, pathRange) => { 35353 // Resolve the path range 35354 const rng = resolvePathRange(dom.getRoot(), pathRange).getOrDie('Unable to resolve path range'); 35355 const startNode = rng.startContainer; 35356 const endNode = rng.endContainer; 35357 // Create the marker 35358 const textEnd = rng.endOffset === 0 ? endNode : endNode.splitText(rng.endOffset); 35359 const textStart = rng.startOffset === 0 ? startNode : startNode.splitText(rng.startOffset); 35360 const startParentNode = textStart.parentNode; 35361 const endParentNode = textEnd.parentNode; 35362 return { 35363 prefix: markerPrefix, 35364 end: endParentNode.insertBefore(newMarker(dom, markerPrefix + '-end'), textEnd), 35365 start: startParentNode.insertBefore(newMarker(dom, markerPrefix + '-start'), textStart) 35366 }; 35367 }; 35368 const removeMarker = (dom, marker, isRoot) => { 35369 // Note: Use dom.get() here instead of marker.end/start, as applying the format/command can 35370 // clone the nodes meaning the old reference isn't usable 35371 cleanEmptyNodes(dom, dom.get(marker.prefix + '-end'), isRoot); 35372 cleanEmptyNodes(dom, dom.get(marker.prefix + '-start'), isRoot); 35373 }; 35374 35375 const isReplacementPattern = (pattern) => pattern.start.length === 0; 35376 const matchesPattern = (patternContent) => (element, offset) => { 35377 const text = element.data; 35378 const searchText = text.substring(0, offset); 35379 const startEndIndex = searchText.lastIndexOf(patternContent.charAt(patternContent.length - 1)); 35380 const startIndex = searchText.lastIndexOf(patternContent); 35381 if (startIndex !== -1) { 35382 // Complete string found 35383 return startIndex + patternContent.length; 35384 } 35385 else if (startEndIndex !== -1) { 35386 // Potential partial string found 35387 return startEndIndex + 1; 35388 } 35389 else { 35390 // No match in current node, so continue 35391 return -1; 35392 } 35393 }; 35394 const findPatternStartFromSpot = (dom, pattern, block, spot) => { 35395 const startPattern = pattern.start; 35396 const startSpot = repeatLeft(dom, spot.container, spot.offset, matchesPattern(startPattern), block); 35397 return startSpot.bind((spot) => { 35398 const startPatternIndex = block.textContent?.indexOf(startPattern) ?? -1; 35399 const isCompleteMatch = startPatternIndex !== -1 && spot.offset >= startPatternIndex + startPattern.length; 35400 if (isCompleteMatch) { 35401 // Complete match 35402 const rng = dom.createRng(); 35403 rng.setStart(spot.container, spot.offset - startPattern.length); 35404 rng.setEnd(spot.container, spot.offset); 35405 return Optional.some(rng); 35406 } 35407 else { 35408 // Partial match so lean left to see if the string exists over fragmented text nodes 35409 const offset = spot.offset - startPattern.length; 35410 return scanLeft(spot.container, offset, block).map((nextSpot) => { 35411 // Build up the range between the last char and the first char 35412 const rng = dom.createRng(); 35413 rng.setStart(nextSpot.container, nextSpot.offset); 35414 rng.setEnd(spot.container, spot.offset); 35415 return rng; 35416 }).filter((rng) => 35417 // Ensure the range content matches the start 35418 rng.toString() === startPattern).orThunk(() => 35419 // No match found, so continue searching 35420 findPatternStartFromSpot(dom, pattern, block, point(spot.container, 0))); 35421 } 35422 }); 35423 }; 35424 const findPatternStart = (dom, pattern, node, offset, block, requireGap = false) => { 35425 if (pattern.start.length === 0 && !requireGap) { 35426 const rng = dom.createRng(); 35427 rng.setStart(node, offset); 35428 rng.setEnd(node, offset); 35429 return Optional.some(rng); 35430 } 35431 return textBefore(node, offset, block).bind((spot) => { 35432 const start = findPatternStartFromSpot(dom, pattern, block, spot); 35433 return start.bind((startRange) => { 35434 if (requireGap) { 35435 if (startRange.endContainer === spot.container && startRange.endOffset === spot.offset) { 35436 return Optional.none(); 35437 } 35438 else if (spot.offset === 0 && startRange.endContainer.textContent?.length === startRange.endOffset) { 35439 return Optional.none(); 35440 } 35441 } 35442 return Optional.some(startRange); 35443 }); 35444 }); 35445 }; 35446 const findPattern$3 = (editor, block, details, normalizedMatches) => { 35447 const dom = editor.dom; 35448 const root = dom.getRoot(); 35449 const pattern = details.pattern; 35450 const endNode = details.position.container; 35451 const endOffset = details.position.offset; 35452 // Lean left to find the start of the end pattern, as it could be across fragmented nodes 35453 return scanLeft(endNode, endOffset - details.pattern.end.length, block).bind((spot) => { 35454 const endPathRng = generatePathRange(dom, root, spot.container, spot.offset, endNode, endOffset, normalizedMatches); 35455 // If we have a replacement pattern, then it can't have nested patterns so just return immediately 35456 if (isReplacementPattern(pattern)) { 35457 return Optional.some({ 35458 matches: [{ 35459 pattern, 35460 startRng: endPathRng, 35461 endRng: endPathRng 35462 }], 35463 position: spot 35464 }); 35465 } 35466 else { 35467 // Find any nested patterns, making sure not to process the current pattern again 35468 const resultsOpt = findPatternsRec(editor, details.remainingPatterns, spot.container, spot.offset, block, normalizedMatches); 35469 const results = resultsOpt.getOr({ matches: [], position: spot }); 35470 const pos = results.position; 35471 // Find the start of the matched pattern 35472 const start = findPatternStart(dom, pattern, pos.container, pos.offset, block, resultsOpt.isNone()); 35473 return start.map((startRng) => { 35474 const startPathRng = generatePathRangeFromRange(dom, root, startRng, normalizedMatches); 35475 return { 35476 matches: results.matches.concat([{ 35477 pattern, 35478 startRng: startPathRng, 35479 endRng: endPathRng 35480 }]), 35481 position: point(startRng.startContainer, startRng.startOffset) 35482 }; 35483 }); 35484 } 35485 }); 35486 }; 35487 // Assumptions: 35488 // 0. Patterns are sorted by priority so we should preferentially match earlier entries 35489 // 1. Patterns may be nested but may only occur once 35490 // 2. Patterns will not have matching prefixes which contain space or standard punctuation ',', '.', ';', ':', '!', '?' 35491 // 3. Patterns will not extend outside of the root element 35492 // 4. All pattern ends must be directly before the cursor (represented by node + offset) 35493 // 5. Only text nodes matter 35494 const findPatternsRec = (editor, patterns, node, offset, block, normalizedMatches) => { 35495 const dom = editor.dom; 35496 return textBefore(node, offset, dom.getRoot()).bind((endSpot) => { 35497 const text = getBeforeText(dom, block, node, offset); 35498 for (let i = 0; i < patterns.length; i++) { 35499 const pattern = patterns[i]; 35500 // If the text does not end with the same string as the pattern, then we can exit 35501 // early, because this pattern isn't going to match this text. This saves us doing more 35502 // expensive matching calls. 35503 if (!endsWith(text, pattern.end)) { 35504 continue; 35505 } 35506 // Generate a new array without the current pattern 35507 const patternsWithoutCurrent = patterns.slice(); 35508 patternsWithoutCurrent.splice(i, 1); 35509 // Try to find the current pattern 35510 const result = findPattern$3(editor, block, { 35511 pattern, 35512 remainingPatterns: patternsWithoutCurrent, 35513 position: endSpot 35514 }, normalizedMatches); 35515 if (result.isNone() && offset > 0) { 35516 return findPatternsRec(editor, patterns, node, offset - 1, block, normalizedMatches); 35517 } 35518 // If a match was found then return that 35519 if (result.isSome()) { 35520 return result; 35521 } 35522 } 35523 return Optional.none(); 35524 }); 35525 }; 35526 const applyPattern$2 = (editor, pattern, patternRange) => { 35527 editor.selection.setRng(patternRange); 35528 if (pattern.type === 'inline-format') { 35529 each$e(pattern.format, (format) => { 35530 editor.formatter.apply(format); 35531 }); 35532 } 35533 else { 35534 editor.execCommand(pattern.cmd, false, pattern.value); 35535 } 35536 }; 35537 const applyReplacementPattern = (editor, pattern, marker, isRoot) => { 35538 // Remove the original text 35539 const markerRange = rangeFromMarker(editor.dom, marker); 35540 deleteRng(editor.dom, markerRange, isRoot); 35541 // Apply the replacement 35542 applyPattern$2(editor, pattern, markerRange); 35543 }; 35544 const applyPatternWithContent = (editor, pattern, startMarker, endMarker, isRoot) => { 35545 const dom = editor.dom; 35546 // Create the marker ranges for the patterns start/end content 35547 const markerEndRange = rangeFromMarker(dom, endMarker); 35548 const markerStartRange = rangeFromMarker(dom, startMarker); 35549 // Clean up the pattern start/end content 35550 deleteRng(dom, markerStartRange, isRoot); 35551 deleteRng(dom, markerEndRange, isRoot); 35552 // Apply the pattern 35553 const patternMarker = { prefix: startMarker.prefix, start: startMarker.end, end: endMarker.start }; 35554 const patternRange = rangeFromMarker(dom, patternMarker); 35555 applyPattern$2(editor, pattern, patternRange); 35556 }; 35557 const addMarkers = (dom, matches) => { 35558 const markerPrefix = generate('mce_textpattern'); 35559 // Add end markers 35560 const matchesWithEnds = foldr(matches, (acc, match) => { 35561 const endMarker = createMarker(dom, markerPrefix + `_end${acc.length}`, match.endRng); 35562 return acc.concat([{ 35563 ...match, 35564 endMarker 35565 }]); 35566 }, []); 35567 // Add start markers 35568 return foldr(matchesWithEnds, (acc, match) => { 35569 const idx = matchesWithEnds.length - acc.length - 1; 35570 const startMarker = isReplacementPattern(match.pattern) ? match.endMarker : createMarker(dom, markerPrefix + `_start${idx}`, match.startRng); 35571 return acc.concat([{ 35572 ...match, 35573 startMarker 35574 }]); 35575 }, []); 35576 }; 35577 const sortPatterns$1 = (patterns) => sort(patterns, (a, b) => b.end.length - a.end.length); 35578 const getBestMatches = (matches, matchesWithSortedPatterns) => { 35579 const hasSameMatches = forall(matches, (match) => exists(matchesWithSortedPatterns, (sortedMatch) => match.pattern.start === sortedMatch.pattern.start && match.pattern.end === sortedMatch.pattern.end)); 35580 if (matches.length === matchesWithSortedPatterns.length) { 35581 if (hasSameMatches) { 35582 return matches; 35583 } 35584 else { 35585 return matchesWithSortedPatterns; 35586 } 35587 } 35588 return matches.length > matchesWithSortedPatterns.length ? matches : matchesWithSortedPatterns; 35589 }; 35590 const findPatterns$2 = (editor, block, node, offset, patternSet, normalizedMatches) => { 35591 const matches = findPatternsRec(editor, patternSet.inlinePatterns, node, offset, block, normalizedMatches).fold(() => [], (result) => result.matches); 35592 const matchesWithSortedPatterns = findPatternsRec(editor, sortPatterns$1(patternSet.inlinePatterns), node, offset, block, normalizedMatches).fold(() => [], (result) => result.matches); 35593 return getBestMatches(matches, matchesWithSortedPatterns); 35594 }; 35595 const applyMatches$2 = (editor, matches) => { 35596 if (matches.length === 0) { 35597 return; 35598 } 35599 // Store the current selection 35600 const dom = editor.dom; 35601 const bookmark = editor.selection.getBookmark(); 35602 // Add markers for the matched patterns 35603 const matchesWithMarkers = addMarkers(dom, matches); 35604 // Do the replacements 35605 each$e(matchesWithMarkers, (match) => { 35606 const block = dom.getParent(match.startMarker.start, dom.isBlock); 35607 const isRoot = (node) => node === block; 35608 if (isReplacementPattern(match.pattern)) { 35609 applyReplacementPattern(editor, match.pattern, match.endMarker, isRoot); 35610 } 35611 else { 35612 applyPatternWithContent(editor, match.pattern, match.startMarker, match.endMarker, isRoot); 35613 } 35614 // Remove the markers 35615 removeMarker(dom, match.endMarker, isRoot); 35616 removeMarker(dom, match.startMarker, isRoot); 35617 }); 35618 // Restore the selection 35619 editor.selection.moveToBookmark(bookmark); 35620 }; 35621 35622 const stripPattern$1 = (dom, block, pattern) => { 35623 // The pattern could be across fragmented text nodes, so we need to find the end 35624 // of the pattern and then remove all elements between the start/end range 35625 return textAfter(block, 0, block).map((spot) => { 35626 const node = spot.container; 35627 scanRight(node, pattern.start.length, block).each((end) => { 35628 const rng = dom.createRng(); 35629 rng.setStart(node, 0); 35630 rng.setEnd(end.container, end.offset); 35631 deleteRng(dom, rng, (e) => e === block); 35632 }); 35633 return node; 35634 }); 35635 }; 35636 const createApplyPattern = (stripPattern) => (editor, match) => { 35637 const dom = editor.dom; 35638 const pattern = match.pattern; 35639 const rng = resolvePathRange(dom.getRoot(), match.range).getOrDie('Unable to resolve path range'); 35640 const isBlockFormatName = (name, formatter) => { 35641 const formatSet = formatter.get(name); 35642 return isArray$1(formatSet) && head(formatSet).exists((format) => has$2(format, 'block')); 35643 }; 35644 getParentBlock(editor, rng).each((block) => { 35645 if (pattern.type === 'block-format') { 35646 if (isBlockFormatName(pattern.format, editor.formatter)) { 35647 editor.undoManager.transact(() => { 35648 stripPattern(editor.dom, block, pattern); 35649 editor.formatter.apply(pattern.format); 35650 }); 35651 } 35652 } 35653 else if (pattern.type === 'block-command') { 35654 editor.undoManager.transact(() => { 35655 stripPattern(editor.dom, block, pattern); 35656 editor.execCommand(pattern.cmd, false, pattern.value); 35657 }); 35658 } 35659 }); 35660 return true; 35661 }; 35662 const sortPatterns = (patterns) => sort(patterns, (a, b) => b.start.length - a.start.length); 35663 // Finds a matching pattern to the specified text 35664 const findPattern$2 = (predicate) => (patterns, text) => { 35665 const sortedPatterns = sortPatterns(patterns); 35666 const nuText = text.replace(nbsp, ' '); 35667 return find$2(sortedPatterns, (pattern) => predicate(pattern, text, nuText)); 35668 }; 35669 const createFindPatterns = (findPattern, skipFullMatch) => (editor, block, patternSet, normalizedMatches, text = block.textContent ?? '') => { 35670 const dom = editor.dom; 35671 const forcedRootBlock = getForcedRootBlock(editor); 35672 if (!dom.is(block, forcedRootBlock)) { 35673 return []; 35674 } 35675 return findPattern(patternSet.blockPatterns, text).map((pattern) => { 35676 if (skipFullMatch && Tools.trim(text).length === pattern.start.length) { 35677 return []; 35678 } 35679 return [{ 35680 pattern, 35681 range: generatePathRange(dom, dom.getRoot(), block, 0, block, 0, normalizedMatches) 35682 }]; 35683 }).getOr([]); 35684 }; 35685 35686 const startsWithSingleSpace = (s) => /^\s[^\s]/.test(s); 35687 const stripPattern = (dom, block, pattern) => { 35688 stripPattern$1(dom, block, pattern).each((node) => { 35689 /** 35690 * TINY-9603: If there is a single space between pattern.start and text (e.g. # 1) 35691 * then it will be left in the text content and then can appear in certain circumstances. 35692 * This is not an issue with multiple spaces because they are transformed to non-breaking ones. 35693 * 35694 * In this specific case we've decided to remove this single space whatsoever 35695 * as it feels to be the expected behavior. 35696 */ 35697 const text = SugarElement.fromDom(node); 35698 const textContent = get$
vendor: 8,569 bytes, lines 35698-35870
356984(text); 35699 if (startsWithSingleSpace(textContent)) { 35700 set$1(text, textContent.slice(1)); 35701 } 35702 }); 35703 }; 35704 const applyPattern$1 = createApplyPattern(stripPattern); 35705 const findPattern$1 = findPattern$2((pattern, text, nuText) => text.indexOf(pattern.start) === 0 || nuText.indexOf(pattern.start) === 0); 35706 const findPatterns$1 = createFindPatterns(findPattern$1, true); 35707 const getMatches$1 = (editor, patternSet) => { 35708 const rng = editor.selection.getRng(); 35709 return getParentBlock(editor, rng).map((block) => { 35710 const offset = Math.max(0, rng.startOffset); 35711 const dynamicPatternSet = resolveFromDynamicPatterns(patternSet, block, block.textContent ?? ''); 35712 // IMPORTANT: We need to get normalized match results since undoing and redoing the editor state 35713 // via undoManager.extra() will result in the DOM being normalized. 35714 const inlineMatches = findPatterns$2(editor, block, rng.startContainer, offset, dynamicPatternSet, true); 35715 const blockMatches = findPatterns$1(editor, block, dynamicPatternSet, true); 35716 return { inlineMatches, blockMatches }; 35717 }).filter(({ inlineMatches, blockMatches }) => blockMatches.length > 0 || inlineMatches.length > 0); 35718 }; 35719 const applyMatches$1 = (editor, matches) => { 35720 if (matches.length === 0) { 35721 return; 35722 } 35723 // Store the current selection and then apply the matched patterns 35724 const bookmark = editor.selection.getBookmark(); 35725 each$e(matches, (match) => applyPattern$1(editor, match)); 35726 editor.selection.moveToBookmark(bookmark); 35727 }; 35728 35729 const applyPattern = createApplyPattern(stripPattern$1); 35730 const findPattern = findPattern$2((pattern, text, nuText) => text === pattern.start || nuText === pattern.start); 35731 const findPatterns = createFindPatterns(findPattern, false); 35732 const getMatches = (editor, patternSet) => { 35733 const rng = editor.selection.getRng(); 35734 return getParentBlock(editor, rng).map((block) => { 35735 const offset = Math.max(0, rng.startOffset); 35736 const beforeText = getBeforeText(editor.dom, block, rng.startContainer, offset); 35737 const dynamicPatternSet = resolveFromDynamicPatterns(patternSet, block, beforeText); 35738 return findPatterns(editor, block, dynamicPatternSet, false, beforeText); 35739 }).filter((matches) => matches.length > 0); 35740 }; 35741 const applyMatches = (editor, matches) => { 35742 each$e(matches, (match) => applyPattern(editor, match)); 35743 }; 35744 35745 const handleEnter = (editor, patternSet) => getMatches$1(editor, patternSet).fold(never, ({ inlineMatches, blockMatches }) => { 35746 editor.undoManager.add(); 35747 editor.undoManager.extra(() => { 35748 editor.execCommand('mceInsertNewLine'); 35749 }, () => { 35750 // create a cursor position that we can move to avoid the inline formats 35751 insert$5(editor); 35752 applyMatches$2(editor, inlineMatches); 35753 applyMatches$1(editor, blockMatches); 35754 // find the spot before the cursor position 35755 const range = editor.selection.getRng(); 35756 const spot = textBefore(range.startContainer, range.startOffset, editor.dom.getRoot()); 35757 editor.execCommand('mceInsertNewLine'); 35758 // clean up the cursor position we used to preserve the format 35759 spot.each((s) => { 35760 const node = s.container; 35761 if (node.data.charAt(s.offset - 1) === zeroWidth) { 35762 node.deleteData(s.offset - 1, 1); 35763 cleanEmptyNodes(editor.dom, node.parentNode, (e) => e === editor.dom.getRoot()); 35764 } 35765 }); 35766 }); 35767 return true; 35768 }); 35769 const handleInlineKey = (editor, patternSet) => { 35770 const rng = editor.selection.getRng(); 35771 getParentBlock(editor, rng).map((block) => { 35772 const offset = Math.max(0, rng.startOffset - 1); 35773 const beforeText = getBeforeText(editor.dom, block, rng.startContainer, offset); 35774 const dynamicPatternSet = resolveFromDynamicPatterns(patternSet, block, beforeText); 35775 const inlineMatches = findPatterns$2(editor, block, rng.startContainer, offset, dynamicPatternSet, false); 35776 if (inlineMatches.length > 0) { 35777 editor.undoManager.transact(() => { 35778 applyMatches$2(editor, inlineMatches); 35779 }); 35780 } 35781 }); 35782 }; 35783 const handleBlockPatternOnSpace = (editor, patternSet) => getMatches(editor, patternSet).fold(never, (matches) => { 35784 editor.undoManager.transact(() => { 35785 applyMatches(editor, matches); 35786 }); 35787 return true; 35788 }); 35789 const checkKeyEvent = (codes, event, predicate) => { 35790 for (let i = 0; i < codes.length; i++) { 35791 if (predicate(codes[i], event)) { 35792 return true; 35793 } 35794 } 35795 return false; 35796 }; 35797 const checkKeyCode = (codes, event) => checkKeyEvent(codes, event, (code, event) => { 35798 return code === event.keyCode && !VK.modifierPressed(event); 35799 }); 35800 const checkCharCode = (chars, event) => checkKeyEvent(chars, event, (chr, event) => { 35801 return chr.charCodeAt(0) === event.charCode; 35802 }); 35803 35804 const setup$2 = (editor) => { 35805 const charCodes = [',', '.', ';', ':', '!', '?']; 35806 const keyCodes = [32]; 35807 // This is a thunk so that they reflect changes in the underlying options each time they are requested. 35808 const getPatternSet = () => createPatternSet(getTextPatterns(editor) 35809 .filter((pattern) => { 35810 if (pattern.type === 'inline-command' || pattern.type === 'block-command') { 35811 return editor.queryCommandSupported(pattern.cmd); 35812 } 35813 return true; 35814 }), getTextPatternsLookup(editor)); 35815 // Only used for skipping text pattern matching altogether if nothing has been defined. 35816 const hasDynamicPatterns = () => hasTextPatternsLookup(editor); 35817 editor.on('keydown', (e) => { 35818 if (e.keyCode === 13 && !VK.modifierPressed(e) && editor.selection.isCollapsed() && editor.selection.isEditable()) { 35819 const patternSet = filterByTrigger(getPatternSet(), 'enter'); 35820 // Do not process anything if we don't have any inline patterns, block patterns, 35821 // or dynamic lookup defined 35822 const hasPatterns = patternSet.inlinePatterns.length > 0 || 35823 patternSet.blockPatterns.length > 0 || 35824 hasDynamicPatterns(); 35825 if (hasPatterns && handleEnter(editor, patternSet)) { 35826 e.preventDefault(); 35827 } 35828 } 35829 }, true); 35830 editor.on('keydown', (e) => { 35831 if (e.keyCode === 32 && editor.selection.isCollapsed() && editor.selection.isEditable()) { 35832 const patternSet = filterByTrigger(getPatternSet(), 'space'); 35833 const hasPatterns = patternSet.blockPatterns.length > 0 || hasDynamicPatterns(); 35834 if (hasPatterns && handleBlockPatternOnSpace(editor, patternSet)) { 35835 e.preventDefault(); 35836 } 35837 } 35838 }, true); 35839 const handleInlineTrigger = () => { 35840 if (editor.selection.isCollapsed() && editor.selection.isEditable()) { 35841 const patternSet = filterByTrigger(getPatternSet(), 'space'); 35842 // Do not process anything if we don't have any inline patterns or dynamic lookup defined 35843 const hasPatterns = patternSet.inlinePatterns.length > 0 || hasDynamicPatterns(); 35844 if (hasPatterns) { 35845 handleInlineKey(editor, patternSet); 35846 } 35847 } 35848 }; 35849 editor.on('keyup', (e) => { 35850 if (checkKeyCode(keyCodes, e)) { 35851 handleInlineTrigger(); 35852 } 35853 }); 35854 editor.on('keypress', (e) => { 35855 if (checkCharCode(charCodes, e)) { 35856 Delay.setEditorTimeout(editor, handleInlineTrigger); 35857 } 35858 }); 35859 }; 35860 35861 const setup$1 = (editor) => { 35862 setup$2(editor); 35863 }; 35864 35865 const Quirks = (editor) => { 35866 const each = Tools.each; 35867 const BACKSPACE = VK.BACKSPACE, DELETE = VK.DELETE, dom = editor.dom, selection = editor.selection, parser = editor.parser; 35868 const browser = Env.browser; 35869 const isGecko = browser.isFirefox(); 35870 const isWebKit = browser.isChromium() ||
vendor: 28,234 bytes, lines 35870-36477
35870 browser.isSafari(); 35871 const isiOS = Env.deviceType.isiPhone() || Env.deviceType.isiPad(); 35872 const isMac = Env.os.isMacOS() || Env.os.isiOS(); 35873 /** 35874 * Executes a command with a specific state this can be to enable/disable browser editing features. 35875 */ 35876 const setEditorCommandState = (cmd, state) => { 35877 try { 35878 editor.getDoc().execCommand(cmd, false, String(state)); 35879 } 35880 catch { 35881 // Ignore 35882 } 35883 }; 35884 /** 35885 * Returns true/false if the event is prevented or not. 35886 * 35887 * @private 35888 * @param {Event} e Event object. 35889 * @return {Boolean} true/false if the event is prevented or not. 35890 */ 35891 const isDefaultPrevented = (e) => { 35892 return e.isDefaultPrevented(); 35893 }; 35894 /** 35895 * Makes sure that the editor body becomes empty when backspace or delete is pressed in empty editors. 35896 * 35897 * For example: 35898 * <p><b>|</b></p> 35899 * 35900 * Or: 35901 * <h1>|</h1> 35902 * 35903 * Or: 35904 * [<h1></h1>] 35905 */ 35906 const emptyEditorWhenDeleting = () => { 35907 const serializeRng = (rng) => { 35908 const body = dom.create('body'); 35909 const contents = rng.cloneContents(); 35910 body.appendChild(contents); 35911 return selection.serializer.serialize(body, { format: 'html' }); 35912 }; 35913 const allContentsSelected = (rng) => { 35914 const selection = serializeRng(rng); 35915 const allRng = dom.createRng(); 35916 allRng.selectNode(editor.getBody()); 35917 const allSelection = serializeRng(allRng); 35918 return selection === allSelection; 35919 }; 35920 editor.on('keydown', (e) => { 35921 const keyCode = e.keyCode; 35922 // Empty the editor if it's needed for example backspace at <p><b>|</b></p> 35923 if (!isDefaultPrevented(e) && (keyCode === DELETE || keyCode === BACKSPACE) && editor.selection.isEditable()) { 35924 const isCollapsed = editor.selection.isCollapsed(); 35925 const body = editor.getBody(); 35926 // Selection is collapsed but the editor isn't empty 35927 if (isCollapsed && !isEmptyNode(editor.schema, body)) { 35928 return; 35929 } 35930 // Selection isn't collapsed but not all the contents is selected 35931 if (!isCollapsed && !allContentsSelected(editor.selection.getRng())) { 35932 return; 35933 } 35934 // Manually empty the editor 35935 e.preventDefault(); 35936 if (symulateDelete(editor, keyCode === DELETE, () => editor.setContent(''))) { 35937 if (body.firstChild && dom.isBlock(body.firstChild)) { 35938 editor.selection.setCursorLocation(body.firstChild, 0); 35939 } 35940 else { 35941 editor.selection.setCursorLocation(body, 0); 35942 } 35943 } 35944 } 35945 }); 35946 }; 35947 /** 35948 * WebKit doesn't select all the nodes in the body when you press Ctrl+A. 35949 * IE selects more than the contents <body>[<p>a</p>]</body> instead of <body><p>[a]</p]</body> see bug #6438 35950 * This selects the whole body so that backspace/delete logic will delete everything 35951 */ 35952 const selectAll = () => { 35953 editor.shortcuts.add('meta+a', null, 'SelectAll'); 35954 }; 35955 /** 35956 * It seems that Chrome doesn't place the caret if you click on the documentElement in iframe mode 35957 * something that is very easy to do by accident so this problem is now more generic than the original issue. 35958 * 35959 * Original IME specific issue: 35960 * WebKit has a weird issue where it some times fails to properly convert keypresses to input method keystrokes. 35961 * The IME on Mac doesn't initialize when it doesn't fire a proper focus event. 35962 * 35963 * This seems to happen when the user manages to click the documentElement element then the window doesn't get proper focus until 35964 * you enter a character into the editor. 35965 * 35966 * See: https://bugs.webkit.org/show_bug.cgi?id=83566 35967 */ 35968 const documentElementEditingFocus = () => { 35969 if (!editor.inline) { 35970 // Needs to be both down/up due to weird rendering bug on Chrome Windows 35971 dom.bind(editor.getDoc(), 'mousedown mouseup', (e) => { 35972 let rng; 35973 if (e.target === editor.getDoc().documentElement) { 35974 rng = selection.getRng(); 35975 // TINY-12245: this is needed to avoid the scroll back to the top when the content is scrolled, there is no selection and the user is clicking on a non selectable editor element 35976 // example content scrolled by browser search and user click on the horizontal scroll bar 35977 if (editor.getDoc().getSelection()?.anchorNode !== null) { 35978 editor.getBody().focus(); 35979 } 35980 if (e.type === 'mousedown') { 35981 if (isCaretContainer$2(rng.startContainer)) { 35982 return; 35983 } 35984 // Edge case for mousedown, drag select and mousedown again within selection on Chrome Windows to render caret 35985 selection.placeCaretAt(e.clientX, e.clientY); 35986 } 35987 else { 35988 selection.setRng(rng); 35989 } 35990 } 35991 }); 35992 } 35993 }; 35994 /** 35995 * Backspacing in FireFox/IE from a paragraph into a horizontal rule results in a floating text node because the 35996 * browser just deletes the paragraph - the browser fails to merge the text node with a horizontal rule so it is 35997 * left there. TinyMCE sees a floating text node and wraps it in a paragraph on the key up event (ForceBlocks.js 35998 * addRootBlocks), meaning the action does nothing. With this code, FireFox/IE matche the behaviour of other 35999 * browsers. 36000 * 36001 * It also fixes a bug on Firefox where it's impossible to delete HR elements. 36002 */ 36003 const removeHrOnBackspace = () => { 36004 editor.on('keydown', (e) => { 36005 if (!isDefaultPrevented(e) && e.keyCode === BACKSPACE) { 36006 // Check if there is any HR elements this is faster since getRng on IE 7 & 8 is slow 36007 if (!editor.getBody().getElementsByTagName('hr').length) { 36008 return; 36009 } 36010 if (selection.isCollapsed() && selection.getRng().startOffset === 0) { 36011 const node = selection.getNode(); 36012 const previousSibling = node.previousSibling; 36013 if (node.nodeName === 'HR') { 36014 dom.remove(node); 36015 e.preventDefault(); 36016 return; 36017 } 36018 if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === 'hr') { 36019 dom.remove(previousSibling); 36020 e.preventDefault(); 36021 } 36022 } 36023 } 36024 }); 36025 }; 36026 /** 36027 * Firefox 3.x has an issue where the body element won't get proper focus if you click out 36028 * side it's rectangle. 36029 */ 36030 const focusBody = () => { 36031 // Fix for a focus bug in FF 3.x where the body element 36032 // wouldn't get proper focus if the user clicked on the HTML element 36033 if (!Range.prototype.getClientRects) { // Detect getClientRects got introduced in FF 4 36034 editor.on('mousedown', (e) => { 36035 if (!isDefaultPrevented(e) && e.target.nodeName === 'HTML') { 36036 const body = editor.getBody(); 36037 // Blur the body it's focused but not correctly focused 36038 body.blur(); 36039 // Refocus the body after a little while 36040 Delay.setEditorTimeout(editor, () => { 36041 body.focus(); 36042 }); 36043 } 36044 }); 36045 } 36046 }; 36047 /** 36048 * WebKit has a bug where it isn't possible to select image, hr or anchor elements 36049 * by clicking on them so we need to fake that. 36050 */ 36051 const selectControlElements = () => { 36052 const visualAidsAnchorClass = getVisualAidsAnchorClass(editor); 36053 editor.on('click', (e) => { 36054 const target = e.target; 36055 // Workaround for bug, http://bugs.webkit.org/show_bug.cgi?id=12250 36056 // WebKit can't even do simple things like selecting an image 36057 // Needs to be the setBaseAndExtend or it will fail to select floated images 36058 if (/^(IMG|HR)$/.test(target.nodeName) && dom.isEditable(target)) { 36059 e.preventDefault(); 36060 editor.selection.select(target); 36061 editor.nodeChanged(); 36062 } 36063 if (target.nodeName === 'A' && dom.hasClass(target, visualAidsAnchorClass) && target.childNodes.length === 0 && dom.isEditable(target.parentNode)) { 36064 e.preventDefault(); 36065 selection.select(target); 36066 } 36067 }); 36068 }; 36069 /** 36070 * Fixes a Gecko a selection bug where if there is a floating image 36071 * more details here: https://bugzilla.mozilla.org/show_bug.cgi?id=1959606 36072 */ 36073 const fixFirefoxImageSelection = () => { 36074 const isEditableImage = (node) => node.nodeName === 'IMG' && editor.dom.isEditable(node); 36075 editor.on('mousedown', (e) => { 36076 lift2(Optional.from(e.clientX), Optional.from(e.clientY), (clientX, clientY) => { 36077 const caretPos = editor.getDoc().caretPositionFromPoint(clientX, clientY); 36078 const img = caretPos?.offsetNode?.childNodes[caretPos.offset - (caretPos.offset > 0 ? 1 : 0)] || caretPos?.offsetNode; 36079 if (isNonNullable(img) && isEditableImage(img)) { 36080 const rect = img.getBoundingClientRect(); 36081 e.preventDefault(); 36082 if (!editor.hasFocus()) { 36083 editor.focus(); 36084 } 36085 editor.selection.select(img); 36086 if (e.clientX < rect.left || e.clientY < rect.top) { 36087 editor.selection.collapse(true); 36088 } 36089 else if (e.clientX > rect.right || e.clientY > rect.bottom) { 36090 editor.selection.collapse(false); 36091 } 36092 } 36093 }); 36094 }); 36095 }; 36096 /** 36097 * Fixes a Gecko bug where the style attribute gets added to the wrong element when deleting between two block elements. 36098 * 36099 * Fixes do backspace/delete on this: 36100 * <p>bla[ck</p><p style="color:red">r]ed</p> 36101 * 36102 * Would become: 36103 * <p>bla|ed</p> 36104 * 36105 * Instead of: 36106 * <p style="color:red">bla|ed</p> 36107 */ 36108 const removeStylesWhenDeletingAcrossBlockElements = () => { 36109 const getAttributeApplyFunction = () => { 36110 const template = dom.getAttribs(selection.getStart().cloneNode(false)); 36111 return () => { 36112 const target = selection.getStart(); 36113 if (target !== editor.getBody()) { 36114 dom.setAttrib(target, 'style', null); 36115 each(template, (attr) => { 36116 target.setAttributeNode(attr.cloneNode(true)); 36117 }); 36118 } 36119 }; 36120 }; 36121 const isSelectionAcrossElements = () => { 36122 return !selection.isCollapsed() && 36123 dom.getParent(selection.getStart(), dom.isBlock) !== dom.getParent(selection.getEnd(), dom.isBlock); 36124 }; 36125 editor.on('keypress', (e) => { 36126 let applyAttributes; 36127 if (!isDefaultPrevented(e) && (e.keyCode === 8 || e.keyCode === 46) && isSelectionAcrossElements()) { 36128 applyAttributes = getAttributeApplyFunction(); 36129 editor.getDoc().execCommand('delete', false); 36130 applyAttributes(); 36131 e.preventDefault(); 36132 return false; 36133 } 36134 else { 36135 return true; 36136 } 36137 }); 36138 dom.bind(editor.getDoc(), 'cut', (e) => { 36139 if (!isDefaultPrevented(e) && isSelectionAcrossElements()) { 36140 const applyAttributes = getAttributeApplyFunction(); 36141 Delay.setEditorTimeout(editor, () => { 36142 applyAttributes(); 36143 }); 36144 } 36145 }); 36146 }; 36147 /** 36148 * Backspacing into a table behaves differently depending upon browser type. 36149 * Therefore, disable Backspace when cursor immediately follows a table. 36150 */ 36151 const disableBackspaceIntoATable = () => { 36152 editor.on('keydown', (e) => { 36153 if (!isDefaultPrevented(e) && e.keyCode === BACKSPACE) { 36154 if (selection.isCollapsed() && selection.getRng().startOffset === 0) { 36155 const previousSibling = selection.getNode().previousSibling; 36156 if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === 'table') { 36157 e.preventDefault(); 36158 return false; 36159 } 36160 } 36161 } 36162 return true; 36163 }); 36164 }; 36165 /** 36166 * Removes a blockquote when backspace is pressed at the beginning of it. 36167 * 36168 * For example: 36169 * <blockquote><p>|x</p></blockquote> 36170 * 36171 * Becomes: 36172 * <p>|x</p> 36173 */ 36174 const removeBlockQuoteOnBackSpace = () => { 36175 // Add block quote deletion handler 36176 editor.on('keydown', (e) => { 36177 if (isDefaultPrevented(e) || e.keyCode !== VK.BACKSPACE) { 36178 return; 36179 } 36180 let rng = selection.getRng(); 36181 const container = rng.startContainer; 36182 const offset = rng.startOffset; 36183 const root = dom.getRoot(); 36184 let parent = container; 36185 if (!rng.collapsed || offset !== 0) { 36186 return; 36187 } 36188 while (parent.parentNode && parent.parentNode.firstChild === parent && parent.parentNode !== root) { 36189 parent = parent.parentNode; 36190 } 36191 // Is the cursor at the beginning of a blockquote? 36192 if (parent.nodeName === 'BLOCKQUOTE') { 36193 // Remove the blockquote 36194 editor.formatter.toggle('blockquote', undefined, parent); 36195 // Move the caret to the beginning of container 36196 rng = dom.createRng(); 36197 rng.setStart(container, 0); 36198 rng.setEnd(container, 0); 36199 selection.setRng(rng); 36200 } 36201 }); 36202 }; 36203 /* 36204 * Firefox-specific fix for arrow key navigation. In Firefox, users can't move the caret out of a 36205 * `<figcaption>` element using the left and right arrow keys. This function handles those keystrokes 36206 * to allow navigation to the previous/next sibling of the figure element. 36207 */ 36208 const arrowInFigcaption = () => { 36209 const isFigcaption = isTag('figcaption'); 36210 editor.on('keydown', (e) => { 36211 if (e.keyCode === VK.LEFT || e.keyCode === VK.RIGHT) { 36212 const currentNode = SugarElement.fromDom(editor.selection.getNode()); 36213 if (isFigcaption(currentNode) && editor.selection.isCollapsed()) { 36214 parent(currentNode).bind((parent) => { 36215 if (editor.selection.getRng().startOffset === 0 && e.keyCode === VK.LEFT) { 36216 return prevSibling(parent); 36217 } 36218 else if (editor.selection.getRng().endOffset === currentNode.dom.textContent?.length && e.keyCode === VK.RIGHT) { 36219 return nextSibling(parent); 36220 } 36221 else { 36222 return Optional.none(); 36223 } 36224 }).each((targetSibling) => { 36225 editor.selection.setCursorLocation(targetSibling.dom, 0); 36226 }); 36227 } 36228 } 36229 }); 36230 }; 36231 /** 36232 * Sets various Gecko editing options on mouse down and before a execCommand to disable inline table editing that is broken etc. 36233 */ 36234 const setGeckoEditingOptions = () => { 36235 const setOpts = () => { 36236 setEditorCommandState('StyleWithCSS', false); 36237 setEditorCommandState('enableInlineTableEditing', false); 36238 if (!getObjectResizing(editor)) { 36239 setEditorCommandState('enableObjectResizing', false); 36240 } 36241 }; 36242 if (!isReadOnly$1(editor)) { 36243 editor.on('BeforeExecCommand mousedown', setOpts); 36244 } 36245 }; 36246 /** 36247 * Fixes a gecko link bug, when a link is placed at the end of block elements there is 36248 * no way to move the caret behind the link. This fix adds a bogus br element after the link. 36249 * 36250 * For example this: 36251 * <p><b><a href="#">x</a></b></p> 36252 * 36253 * Becomes this: 36254 * <p><b><a href="#">x</a></b><br></p> 36255 */ 36256 const addBrAfterLastLinks = () => { 36257 const fixLinks = () => { 36258 each(dom.select('a:not([data-mce-block])'), (node) => { 36259 let parentNode = node.parentNode; 36260 const root = dom.getRoot(); 36261 if (parentNode?.lastChild === node) { 36262 while (parentNode && !dom.isBlock(parentNode)) { 36263 if (parentNode.parentNode?.lastChild !== parentNode || parentNode === root) { 36264 return; 36265 } 36266 parentNode = parentNode.parentNode; 36267 } 36268 dom.add(parentNode, 'br', { 'data-mce-bogus': 1 }); 36269 } 36270 }); 36271 }; 36272 editor.on('SetContent ExecCommand', (e) => { 36273 if (e.type === 'setcontent' || e.command === 'mceInsertLink') { 36274 fixLinks(); 36275 } 36276 }); 36277 }; 36278 /** 36279 * WebKit will produce DIV elements here and there by default. But since TinyMCE uses paragraphs by 36280 * default we want to change that behavior. 36281 */ 36282 const setDefaultBlockType = () => { 36283 editor.on('init', () => { 36284 setEditorCommandState('DefaultParagraphSeparator', getForcedRootBlock(editor)); 36285 }); 36286 }; 36287 const isAllContentSelected = (editor) => { 36288 const body = editor.getBody(); 36289 const rng = editor.selection.getRng(); 36290 return rng.startContainer === rng.endContainer && rng.startContainer === body && rng.startOffset === 0 && rng.endOffset === body.childNodes.length; 36291 }; 36292 /** 36293 * Fixes selection issues where the caret can be placed between two inline elements like <b>a</b>|<b>b</b> 36294 * this fix will lean the caret right into the closest inline element. 36295 */ 36296 const normalizeSelection = () => { 36297 // Normalize selection for example <b>a</b><i>|a</i> becomes <b>a|</b><i>a</i> 36298 editor.on('keyup focusin mouseup', (e) => { 36299 // no point to exclude Ctrl+A, since normalization will still run after Ctrl will be unpressed 36300 // better exclude any key combinations with the modifiers to avoid double normalization 36301 // (also addresses TINY-1130) 36302 // The use of isAllContentSelected addresses TINY-4550 36303 if (!VK.modifierPressed(e) && !isAllContentSelected(editor)) { 36304 selection.normalize(); 36305 } 36306 }, true); 36307 }; 36308 /** 36309 * Forces Gecko to render a broken image icon if it fails to load an image. 36310 */ 36311 const showBrokenImageIcon = () => { 36312 editor.contentStyles.push('img:-moz-broken {' + 36313 '-moz-force-broken-image-icon:1;' + 36314 'min-width:24px;' + 36315 'min-height:24px' + 36316 '}'); 36317 }; 36318 /** 36319 * iOS has a bug where it's impossible to type if the document has a touchstart event 36320 * bound and the user touches the document while having the on screen keyboard visible. 36321 * 36322 * The touch event moves the focus to the parent document while having the caret inside the iframe 36323 * this fix moves the focus back into the iframe document. 36324 */ 36325 const restoreFocusOnKeyDown = () => { 36326 if (!editor.inline) { 36327 editor.on('keydown', () => { 36328 if (document.activeElement === document.body) { 36329 editor.getWin().focus(); 36330 } 36331 }); 36332 } 36333 }; 36334 /** 36335 * IE 11 has an annoying issue where you can't move focus into the editor 36336 * by clicking on the white area HTML element. We used to be able to fix this with 36337 * the fixCaretSelectionOfDocumentElementOnIe fix. But since M$ removed the selection 36338 * object it's not possible anymore. So we need to hack in a ungly CSS to force the 36339 * body to be at least 150px. If the user clicks the HTML element out side this 150px region 36340 * we simply move the focus into the first paragraph. Not ideal since you loose the 36341 * positioning of the caret but goot enough for most cases. 36342 */ 36343 const bodyHeight = () => { 36344 if (!editor.inline) { 36345 editor.contentStyles.push('body {min-height: 150px}'); 36346 editor.on('click', (e) => { 36347 let rng; 36348 if (e.target.nodeName === 'HTML') { 36349 // Need to store away non collapsed ranges since the focus call will mess that up see #7382 36350 rng = editor.selection.getRng(); 36351 editor.getBody().focus(); 36352 editor.selection.setRng(rng); 36353 editor.selection.normalize(); 36354 editor.nodeChanged(); 36355 } 36356 }); 36357 } 36358 }; 36359 /** 36360 * Firefox on Mac OS will move the browser back to the previous page if you press CMD+Left arrow. 36361 * You might then loose all your work so we need to block that behavior and replace it with our own. 36362 */ 36363 const blockCmdArrowNavigation = () => { 36364 if (isMac) { 36365 editor.on('keydown', (e) => { 36366 if (VK.metaKeyPressed(e) && !e.shiftKey && (e.keyCode === 37 || e.keyCode === 39)) { 36367 e.preventDefault(); 36368 // The modify component isn't part of the standard spec, so we need to add the type here 36369 const selection = editor.selection.getSel(); 36370 selection.modify('move', e.keyCode === 37 ? 'backward' : 'forward', 'lineboundary'); 36371 } 36372 }); 36373 } 36374 }; 36375 /** 36376 * iOS 7.1 introduced two new bugs: 36377 * 1) It's possible to open links within a contentEditable area by clicking on them. 36378 * 2) If you hold down the finger it will display the link/image touch callout menu. 36379 */ 36380 const tapLinksAndImages = () => { 36381 editor.on('click', (e) => { 36382 let elm = e.target; 36383 do { 36384 if (elm.tagName === 'A') { 36385 e.preventDefault(); 36386 return; 36387 } 36388 } while ((elm = elm.parentNode)); 36389 }); 36390 editor.contentStyles.push('.mce-content-body {-webkit-touch-callout: none}'); 36391 }; 36392 /** 36393 * iOS Safari and possible other browsers have a bug where it won't fire 36394 * a click event when a contentEditable is focused. This function fakes click events 36395 * by using touchstart/touchend and measuring the time and distance travelled. 36396 */ 36397 /* 36398 function touchClickEvent() { 36399 editor.on('touchstart', function(e) { 36400 var elm, time, startTouch, changedTouches; 36401 36402 elm = e.target; 36403 time = new Date().getTime(); 36404 changedTouches = e.changedTouches; 36405 36406 if (!changedTouches || changedTouches.length > 1) { 36407 return; 36408 } 36409 36410 startTouch = changedTouches[0]; 36411 36412 editor.once('touchend', function(e) { 36413 var endTouch = e.changedTouches[0], args; 36414 36415 if (new Date().getTime() - time > 500) { 36416 return; 36417 } 36418 36419 if (Math.abs(startTouch.clientX - endTouch.clientX) > 5) { 36420 return; 36421 } 36422 36423 if (Math.abs(startTouch.clientY - endTouch.clientY) > 5) { 36424 return; 36425 } 36426 36427 args = { 36428 target: elm 36429 }; 36430 36431 each('pageX pageY clientX clientY screenX screenY'.split(' '), function(key) { 36432 args[key] = endTouch[key]; 36433 }); 36434 36435 args = editor.dispatch('click', args); 36436 36437 if (!args.isDefaultPrevented()) { 36438 // iOS WebKit can't place the caret properly once 36439 // you bind touch events so we need to do this manually 36440 // TODO: Expand to the closest word? Touble tap still works. 36441 editor.selection.placeCaretAt(endTouch.clientX, endTouch.clientY); 36442 editor.nodeChanged(); 36443 } 36444 }); 36445 }); 36446 } 36447 */ 36448 /** 36449 * WebKit has a bug where it will allow forms to be submitted if they are inside a contentEditable element. 36450 * For example this: <form><button></form> 36451 */ 36452 const blockFormSubmitInsideEditor = () => { 36453 editor.on('init', () => { 36454 editor.dom.bind(editor.getBody(), 'submit', (e) => { 36455 e.preventDefault(); 36456 }); 36457 }); 36458 }; 36459 /** 36460 * Sometimes WebKit/Blink generates BR elements with the Apple-interchange-newline class. 36461 * 36462 * Scenario: 36463 * 1) Create a table 2x2. 36464 * 2) Select and copy cells A2-B2. 36465 * 3) Paste and it will add BR element to table cell. 36466 */ 36467 const removeAppleInterchangeBrs = () => { 36468 parser.addNodeFilter('br', (nodes) => { 36469 let i = nodes.length; 36470 while (i--) { 36471 if (nodes[i].attr('class') === 'Apple-interchange-newline') { 36472 nodes[i].remove(); 36473 } 36474 } 36475 }); 36476 }; 36477 /
36477/ No-op since Mozilla seems to have fixed the caret repaint issues 36478 const refreshContentEditable = noop; 36479 const isHidden = () => { 36480 if (!isGecko || editor.removed) { 36481 return false;
vendor: 28,648 bytes, lines 36482-37164
36482 } 36483 // Weird, wheres that cursor selection? 36484 const sel = editor.selection.getSel(); 36485 return (!sel || !sel.rangeCount || sel.rangeCount === 0); 36486 }; 36487 const setupRtc = () => { 36488 if (isWebKit) { 36489 documentElementEditingFocus(); 36490 selectControlElements(); 36491 blockFormSubmitInsideEditor(); 36492 selectAll(); 36493 if (isiOS) { 36494 restoreFocusOnKeyDown(); 36495 bodyHeight(); 36496 tapLinksAndImages(); 36497 } 36498 } 36499 if (isGecko) { 36500 focusBody(); 36501 setGeckoEditingOptions(); 36502 showBrokenImageIcon(); 36503 blockCmdArrowNavigation(); 36504 } 36505 }; 36506 const setup = () => { 36507 // All browsers 36508 removeBlockQuoteOnBackSpace(); 36509 emptyEditorWhenDeleting(); 36510 // Windows phone will return a range like [body, 0] on mousedown so 36511 // it will always normalize to the wrong location 36512 if (!Env.windowsPhone) { 36513 normalizeSelection(); 36514 } 36515 // WebKit 36516 if (isWebKit) { 36517 documentElementEditingFocus(); 36518 selectControlElements(); 36519 setDefaultBlockType(); 36520 blockFormSubmitInsideEditor(); 36521 disableBackspaceIntoATable(); 36522 removeAppleInterchangeBrs(); 36523 // touchClickEvent(); 36524 // iOS 36525 if (isiOS) { 36526 restoreFocusOnKeyDown(); 36527 bodyHeight(); 36528 tapLinksAndImages(); 36529 } 36530 else { 36531 selectAll(); 36532 } 36533 } 36534 // Gecko 36535 if (isGecko) { 36536 arrowInFigcaption(); 36537 fixFirefoxImageSelection(); 36538 removeHrOnBackspace(); 36539 focusBody(); 36540 removeStylesWhenDeletingAcrossBlockElements(); 36541 setGeckoEditingOptions(); 36542 addBrAfterLastLinks(); 36543 showBrokenImageIcon(); 36544 blockCmdArrowNavigation(); 36545 disableBackspaceIntoATable(); 36546 } 36547 }; 36548 if (isRtc(editor)) { 36549 setupRtc(); 36550 } 36551 else { 36552 setup(); 36553 } 36554 return { 36555 refreshContentEditable, 36556 isHidden 36557 }; 36558 }; 36559 36560 class ComponentLoadError extends Error { 36561 url; 36562 constructor(message, url) { 36563 super(message); 36564 this.url = url; 36565 } 36566 } 36567 const hostWindowComponentScripts = {}; 36568 const loadScript = (url, doc, extraAtts) => { 36569 return new Promise((resolve, reject) => { 36570 const script = SugarElement.fromTag('script'); 36571 setAll$1(script, { 36572 type: 'text/javascript', 36573 src: url, 36574 ...extraAtts 36575 }); 36576 const clean = () => { 36577 remove$8(script); 36578 }; 36579 bind$1(script, 'load', () => { 36580 clean(); 36581 resolve(); 36582 }); 36583 bind$1(script, 'error', () => { 36584 clean(); 36585 reject(new Error(`Failed to load script url: ${url}`)); 36586 }); 36587 append$1(getHead(doc), script); 36588 }); 36589 }; 36590 const loadComponent = async (url, doc) => { 36591 const extraAtts = ScriptLoader.ScriptLoader.getScriptAttributes(url); 36592 await loadScript(url, doc, extraAtts).catch(() => Promise.reject(new ComponentLoadError(`Failed to load component url: ${url}`, url))); 36593 return url; 36594 }; 36595 const loadComponentsForInlineEditor = (componentUrls) => { 36596 return mapToArray(componentUrls, (url, elementName) => { 36597 return get$a(hostWindowComponentScripts, url).getOrThunk(() => { 36598 // Only load the component if it hasn't already been loaded in inline mode the page might have already loaded it 36599 if (isNullable(window.customElements.get(elementName))) { 36600 const loadPromise = loadComponent(url, getDocument()); 36601 hostWindowComponentScripts[url] = loadPromise; 36602 return loadPromise; 36603 } 36604 else { 36605 return Promise.resolve(url); 36606 } 36607 }).catch((err) => { 36608 // Remove from cache if the component failed to load so we can try again later 36609 delete hostWindowComponentScripts[url]; 36610 return Promise.reject(err); 36611 }); 36612 }); 36613 }; 36614 const loadComponentsForIframeEditor = (componentUrls, doc) => { 36615 const urls = unique$1(values(componentUrls)); 36616 return map$3(urls, (url) => loadComponent(url, SugarElement.fromDom(doc))); 36617 }; 36618 const loadComponentsForEditor = (editor) => { 36619 const componentUrls = editor.schema.getComponentUrls(); 36620 if (editor.inline) { 36621 return loadComponentsForInlineEditor(componentUrls); 36622 } 36623 else { 36624 return loadComponentsForIframeEditor(componentUrls, editor.getDoc()); 36625 } 36626 }; 36627 const loadComponentsAsync = async (editor) => { 36628 const loadPromises = loadComponentsForEditor(editor); 36629 const rejected = filter$5(await Promise.allSettled(loadPromises), (r) => r.status === 'rejected'); 36630 if (rejected.length > 0) { 36631 each$e(rejected, (rejection) => { 36632 if (rejection.reason instanceof ComponentLoadError) { 36633 const { url } = rejection.reason; 36634 componentLoadError(editor, url); 36635 } 36636 }); 36637 } 36638 }; 36639 const loadComponents = (editor) => { 36640 // Since we are handling the errors in the promise rejections inside the loadComponentsAsync we can ignore the errors here 36641 // eslint-disable-next-line @typescript-eslint/no-floating-promises 36642 loadComponentsAsync(editor); 36643 }; 36644 36645 const DOM$6 = DOMUtils.DOM; 36646 const appendStyle = (editor, text) => { 36647 const body = SugarElement.fromDom(editor.getBody()); 36648 const container = getStyleContainer(getRootNode(body)); 36649 const style = SugarElement.fromTag('style'); 36650 set$4(style, 'type', 'text/css'); 36651 append$1(style, SugarElement.fromText(text)); 36652 append$1(container, style); 36653 editor.on('remove', () => { 36654 remove$8(style); 36655 }); 36656 }; 36657 const getRootName = (editor) => editor.inline ? editor.getElement().nodeName.toLowerCase() : undefined; 36658 const removeUndefined = (obj) => filter$4(obj, (v) => isUndefined(v) === false); 36659 const mkParserSettings = (editor) => { 36660 const getOption = editor.options.get; 36661 const blobCache = editor.editorUpload.blobCache; 36662 return removeUndefined({ 36663 allow_conditional_comments: getOption('allow_conditional_comments'), 36664 allow_html_data_urls: getOption('allow_html_data_urls'), 36665 allow_svg_data_urls: getOption('allow_svg_data_urls'), 36666 allow_html_in_named_anchor: getOption('allow_html_in_named_anchor'), 36667 allow_script_urls: getOption('allow_script_urls'), 36668 allow_html_in_comments: getOption('allow_html_in_comments'), 36669 allow_mathml_annotation_encodings: getOption('allow_mathml_annotation_encodings'), 36670 allow_unsafe_link_target: getOption('allow_unsafe_link_target'), 36671 convert_unsafe_embeds: getOption('convert_unsafe_embeds'), 36672 convert_fonts_to_spans: getOption('convert_fonts_to_spans'), 36673 extended_mathml_attributes: getOption('extended_mathml_attributes'), 36674 extended_mathml_elements: getOption('extended_mathml_elements'), 36675 fix_list_elements: getOption('fix_list_elements'), 36676 font_size_legacy_values: getOption('font_size_legacy_values'), 36677 forced_root_block: getOption('forced_root_block'), 36678 forced_root_block_attrs: getOption('forced_root_block_attrs'), 36679 preserve_cdata: getOption('preserve_cdata'), 36680 inline_styles: getOption('inline_styles'), 36681 root_name: getRootName(editor), 36682 sandbox_iframes: getOption('sandbox_iframes'), 36683 sandbox_iframes_exclusions: getSandboxIframesExclusions(editor), 36684 sanitize: getOption('xss_sanitization'), 36685 validate: true, 36686 blob_cache: blobCache, 36687 document: editor.getDoc() 36688 }); 36689 }; 36690 const mkSchemaSettings = (editor) => { 36691 const getOption = editor.options.get; 36692 return removeUndefined({ 36693 custom_elements: getOption('custom_elements'), 36694 extended_valid_elements: getOption('extended_valid_elements'), 36695 invalid_elements: getOption('invalid_elements'), 36696 invalid_styles: getOption('invalid_styles'), 36697 schema: getOption('schema'), 36698 valid_children: getOption('valid_children'), 36699 valid_classes: getOption('valid_classes'), 36700 valid_elements: getOption('valid_elements'), 36701 valid_styles: getOption('valid_styles'), 36702 verify_html: getOption('verify_html'), 36703 padd_empty_block_inline_children: getOption('format_empty_lines') 36704 }); 36705 }; 36706 const mkSerializerSettings = (editor) => { 36707 const getOption = editor.options.get; 36708 return { 36709 ...mkParserSettings(editor), 36710 ...mkSchemaSettings(editor), 36711 ...removeUndefined({ 36712 // SerializerSettings 36713 remove_trailing_brs: getOption('remove_trailing_brs'), 36714 pad_empty_with_br: getOption('pad_empty_with_br'), 36715 url_converter: getOption('url_converter'), 36716 url_converter_scope: getOption('url_converter_scope'), 36717 // Writer settings 36718 element_format: getOption('element_format'), 36719 entities: getOption('entities'), 36720 entity_encoding: getOption('entity_encoding'), 36721 indent: getOption('indent'), 36722 indent_after: getOption('indent_after'), 36723 indent_before: getOption('indent_before') 36724 }) 36725 }; 36726 }; 36727 const createParser = (editor) => { 36728 const parser = DomParser(mkParserSettings(editor), editor.schema); 36729 // Convert src and href into data-mce-src, data-mce-href and data-mce-style 36730 parser.addAttributeFilter('src,href,style,tabindex', (nodes, name) => { 36731 const dom = editor.dom; 36732 const internalName = 'data-mce-' + name; 36733 let i = nodes.length; 36734 while (i--) { 36735 const node = nodes[i]; 36736 let value = node.attr(name); 36737 // Add internal attribute if we need to we don't on a refresh of the document 36738 if (value && !node.attr(internalName)) { 36739 // Don't duplicate these since they won't get modified by any browser 36740 if (value.indexOf('data:') === 0 || value.indexOf('blob:') === 0) { 36741 continue; 36742 } 36743 if (name === 'style') { 36744 value = dom.serializeStyle(dom.parseStyle(value), node.name); 36745 if (!value.length) { 36746 value = null; 36747 } 36748 node.attr(internalName, value); 36749 node.attr(name, value); 36750 } 36751 else if (name === 'tabindex') { 36752 node.attr(internalName, value); 36753 node.attr(name, null); 36754 } 36755 else { 36756 node.attr(internalName, editor.convertURL(value, name, node.name)); 36757 } 36758 } 36759 } 36760 }); 36761 // Keep scripts from executing 36762 parser.addNodeFilter('script', (nodes) => { 36763 let i = nodes.length; 36764 while (i--) { 36765 const node = nodes[i]; 36766 const type = node.attr('type') || 'no/type'; 36767 if (type.indexOf('mce-') !== 0) { 36768 node.attr('type', 'mce-' + type); 36769 } 36770 } 36771 }); 36772 if (shouldPreserveCData(editor)) { 36773 parser.addNodeFilter('#cdata', (nodes) => { 36774 let i = nodes.length; 36775 while (i--) { 36776 const node = nodes[i]; 36777 node.type = 8; 36778 node.name = '#comment'; 36779 node.value = '[CDATA[' + editor.dom.encode(node.value ?? '') + ']]'; 36780 } 36781 }); 36782 } 36783 parser.addNodeFilter('p,h1,h2,h3,h4,h5,h6,div', (nodes) => { 36784 let i = nodes.length; 36785 const nonEmptyElements = editor.schema.getNonEmptyElements(); 36786 while (i--) { 36787 const node = nodes[i]; 36788 if (node.isEmpty(nonEmptyElements) && node.getAll('br').length === 0) { 36789 node.append(new AstNode('br', 1)); 36790 } 36791 } 36792 }); 36793 return parser; 36794 }; 36795 const autoFocus = (editor) => { 36796 const autoFocus = getAutoFocus(editor); 36797 if (autoFocus) { 36798 Delay.setEditorTimeout(editor, () => { 36799 let focusEditor; 36800 if (autoFocus === true) { 36801 focusEditor = editor; 36802 } 36803 else { 36804 focusEditor = editor.editorManager.get(autoFocus); 36805 } 36806 if (focusEditor && !focusEditor.destroyed) { 36807 focusEditor.focus(); 36808 focusEditor.selection.scrollIntoView(); 36809 } 36810 }, 100); 36811 } 36812 }; 36813 const moveSelectionToFirstCaretPosition = (editor) => { 36814 // If not inline and no useful selection, we want to set selection to the first valid cursor position 36815 // We don't do this on inline because then it selects the editor container 36816 // This must run AFTER editor.focus! 36817 const root = editor.dom.getRoot(); 36818 if (!editor.inline && (!hasAnyRanges(editor) || editor.selection.getStart(true) === root)) { 36819 firstPositionIn(root).each((pos) => { 36820 const node = pos.getNode(); 36821 // If a table is the first caret pos, then walk down one more level 36822 const caretPos = isTable$2(node) ? firstPositionIn(node).getOr(pos) : pos; 36823 editor.selection.setRng(caretPos.toRange()); 36824 }); 36825 } 36826 }; 36827 const initEditor = (editor) => { 36828 editor.bindPendingEventDelegates(); 36829 editor.initialized = true; 36830 fireInit(editor); 36831 editor.focus(true); 36832 moveSelectionToFirstCaretPosition(editor); 36833 editor.nodeChanged({ initial: true }); 36834 const initInstanceCallback = getInitInstanceCallback(editor); 36835 if (isFunction(initInstanceCallback)) { 36836 initInstanceCallback.call(editor, editor); 36837 } 36838 autoFocus(editor); 36839 if (isDisabled(editor)) { 36840 toggleDisabled(editor, true); 36841 } 36842 }; 36843 const getStyleSheetLoader$1 = (editor) => editor.inline ? editor.ui.styleSheetLoader : editor.dom.styleSheetLoader; 36844 const makeStylesheetLoadingPromises = (editor, css, framedFonts) => { 36845 const { pass: bundledCss, fail: normalCss } = partition$2(css, (key) => tinymce.Resource.has(key)); 36846 const bundledPromises = bundledCss.map((key) => { 36847 const css = tinymce.Resource.get(key); 36848 if (isString(css)) { 36849 return Promise.resolve(getStyleSheetLoader$1(editor).loadRawCss(key, css)); 36850 } 36851 return Promise.resolve(); 36852 }); 36853 const promises = [...bundledPromises, 36854 getStyleSheetLoader$1(editor).loadAll(normalCss), 36855 ]; 36856 if (editor.inline) { 36857 return promises; 36858 } 36859 else { 36860 return promises.concat([ 36861 editor.ui.styleSheetLoader.loadAll(framedFonts) 36862 ]); 36863 } 36864 }; 36865 const loadContentCss = (editor) => { 36866 const styleSheetLoader = getStyleSheetLoader$1(editor); 36867 const fontCss = getFontCss(editor); 36868 const css = editor.contentCSS; 36869 const removeCss = () => { 36870 styleSheetLoader.unloadAll(css); 36871 if (!editor.inline) { 36872 editor.ui.styleSheetLoader.unloadAll(fontCss); 36873 } 36874 }; 36875 const loaded = () => { 36876 if (editor.removed) { 36877 removeCss(); 36878 } 36879 else { 36880 editor.on('remove', removeCss); 36881 } 36882 }; 36883 // Add editor specific CSS styles 36884 if (editor.contentStyles.length > 0) { 36885 let contentCssText = ''; 36886 Tools.each(editor.contentStyles, (style) => { 36887 contentCssText += style + '\r\n'; 36888 }); 36889 editor.dom.addStyle(contentCssText); 36890 } 36891 // Load all stylesheets 36892 const allStylesheets = Promise.all(makeStylesheetLoadingPromises(editor, css, fontCss)).then(loaded).catch(loaded); 36893 // Append specified content CSS last 36894 const contentStyle = getContentStyle(editor); 36895 if (contentStyle) { 36896 appendStyle(editor, contentStyle); 36897 } 36898 return allStylesheets; 36899 }; 36900 const preInit = (editor) => { 36901 const doc = editor.getDoc(), body = editor.getBody(); 36902 firePreInit(editor); 36903 if (!shouldBrowserSpellcheck(editor)) { 36904 doc.body.spellcheck = false; // Gecko 36905 DOM$6.setAttrib(body, 'spellcheck', 'false'); 36906 } 36907 editor.quirks = Quirks(editor); 36908 firePostRender(editor); 36909 const directionality = getDirectionality(editor); 36910 if (directionality !== undefined) { 36911 body.dir = directionality; 36912 } 36913 const protect = getProtect(editor); 36914 if (protect) { 36915 editor.on('BeforeSetContent', (e) => { 36916 Tools.each(protect, (pattern) => { 36917 e.content = e.content.replace(pattern, (str) => { 36918 return '<!--mce:protected ' + escape(str) + '-->'; 36919 }); 36920 }); 36921 }); 36922 } 36923 editor.on('SetContent', () => { 36924 editor.addVisual(editor.getBody()); 36925 }); 36926 editor.on('compositionstart compositionend', (e) => { 36927 editor.composing = e.type === 'compositionstart'; 36928 }); 36929 }; 36930 const loadInitialContent = (editor) => { 36931 if (!isRtc(editor)) { 36932 editor.load({ initial: true, format: 'html' }); 36933 } 36934 editor.startContent = editor.getContent({ format: 'raw' }); 36935 }; 36936 const initEditorWithInitialContent = (editor) => { 36937 if (editor.removed !== true) { 36938 loadInitialContent(editor); 36939 initEditor(editor); 36940 } 36941 }; 36942 const startProgress = (editor) => { 36943 let canceled = false; 36944 const progressTimeout = setTimeout(() => { 36945 if (!canceled) { 36946 editor.setProgressState(true); 36947 } 36948 }, 500); 36949 return () => { 36950 clearTimeout(progressTimeout); 36951 canceled = true; 36952 editor.setProgressState(false); 36953 }; 36954 }; 36955 const contentBodyLoaded = (editor) => { 36956 const targetElm = editor.getElement(); 36957 let doc = editor.getDoc(); 36958 if (editor.inline) { 36959 DOM$6.addClass(targetElm, 'mce-content-body'); 36960 editor.contentDocument = doc = document; 36961 editor.contentWindow = window; 36962 editor.bodyElement = targetElm; 36963 editor.contentAreaContainer = targetElm; 36964 } 36965 // It will not steal focus while setting contentEditable 36966 const body = editor.getBody(); 36967 // disabled isn't valid on all body elements, so need to cast here 36968 // TODO: See if we actually need to disable/re-enable here 36969 body.disabled = true; 36970 editor.readonly = isReadOnly$1(editor); 36971 editor._editableRoot = hasEditableRoot$1(editor); 36972 if (!isDisabled$1(editor) && editor.hasEditableRoot()) { 36973 if (editor.inline && DOM$6.getStyle(body, 'position', true) === 'static') { 36974 body.style.position = 'relative'; 36975 } 36976 body.contentEditable = 'true'; 36977 } 36978 body.disabled = false; 36979 editor.editorUpload = EditorUpload(editor); 36980 editor.schema = Schema(mkSchemaSettings(editor)); 36981 editor.dom = DOMUtils(doc, { 36982 keep_values: true, 36983 // Note: Don't bind here, as the binding is handled via the `url_converter_scope` 36984 // eslint-disable-next-line @typescript-eslint/unbound-method 36985 url_converter: editor.convertURL, 36986 url_converter_scope: editor, 36987 update_styles: true, 36988 root_element: editor.inline ? editor.getBody() : null, 36989 collect: editor.inline, 36990 schema: editor.schema, 36991 contentCssCors: shouldUseContentCssCors(editor), 36992 referrerPolicy: getReferrerPolicy(editor), 36993 crossOrigin: getCrossOrigin(editor), 36994 onSetAttrib: (e) => { 36995 editor.dispatch('SetAttrib', e); 36996 }, 36997 }); 36998 editor.parser = createParser(editor); 36999 editor.serializer = DomSerializer(mkSerializerSettings(editor), editor); 37000 editor.selection = EditorSelection(editor.dom, editor.getWin(), editor.serializer, editor); 37001 editor.annotator = Annotator(editor); 37002 editor.formatter = Formatter(editor); 37003 editor.undoManager = UndoManager(editor); 37004 editor._nodeChangeDispatcher = new NodeChange(editor); 37005 editor._selectionOverrides = SelectionOverrides(editor); 37006 setup$b(editor); 37007 setup$u(editor); 37008 setup$6(editor); 37009 if (shouldAllowNonEditable(editor)) { 37010 setup$s(editor); 37011 } 37012 if (!isRtc(editor)) { 37013 setup$5(editor); 37014 setup$1(editor); 37015 } 37016 const caret = setup$g(editor); 37017 setup$v(editor, caret); 37018 setup$t(editor); 37019 setup$w(editor); 37020 setup$7(editor, caret); 37021 const setupRtcThunk = setup$z(editor); 37022 preInit(editor); 37023 loadComponents(editor); 37024 setupRtcThunk.fold(() => { 37025 const cancelProgress = startProgress(editor); 37026 // eslint-disable-next-line @typescript-eslint/no-floating-promises 37027 loadContentCss(editor).then(() => { 37028 initEditorWithInitialContent(editor); 37029 cancelProgress(); 37030 }); 37031 }, (setupRtc) => { 37032 editor.setProgressState(true); 37033 // eslint-disable-next-line @typescript-eslint/no-floating-promises 37034 loadContentCss(editor).then(() => { 37035 setupRtc().then((_rtcMode) => { 37036 editor.setProgressState(false); 37037 initEditorWithInitialContent(editor); 37038 bindEvents(editor); 37039 }, (err) => { 37040 editor.notificationManager.open({ type: 'error', text: String(err) }); 37041 initEditorWithInitialContent(editor); 37042 bindEvents(editor); 37043 }); 37044 }); 37045 }); 37046 }; 37047 37048 const DOM$5 = DOMUtils.DOM; 37049 const createIframeElement = (id, title, customAttrs, tabindex) => { 37050 const iframe = SugarElement.fromTag('iframe'); 37051 // This can also be explicitly set by customAttrs, so do this first 37052 tabindex.each((t) => set$4(iframe, 'tabindex', t)); 37053 setAll$1(iframe, customAttrs); 37054 setAll$1(iframe, { 37055 id: id + '_ifr', 37056 frameBorder: '0', 37057 allowTransparency: 'true', 37058 ...Env.browser.isFirefox() ? { title } : {} 37059 }); 37060 add$2(iframe, 'tox-edit-area__iframe'); 37061 return iframe; 37062 }; 37063 const getIframeHtml = (editor) => { 37064 let iframeHTML = getDocType(editor) + '<html><head>'; 37065 // We only need to override paths if we have to 37066 // IE has a bug where it remove site absolute urls to relative ones if this is specified 37067 if (getDocumentBaseUrl(editor) !== editor.editorManager.documentBaseURL) { 37068 iframeHTML += '<base href="' + editor.documentBaseURI.getURI() + '" />'; 37069 } 37070 iframeHTML += '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />'; 37071 const bodyId = getBodyId(editor); 37072 const bodyClass = getBodyClass(editor); 37073 const translatedAriaText = editor.translate(getIframeAriaText(editor)); 37074 const iframeBodyAriaLabel = Env.browser.isFirefox() ? '' : `aria-label="${translatedAriaText}"`; 37075 if (getContentSecurityPolicy(editor)) { 37076 iframeHTML += '<meta http-equiv="Content-Security-Policy" content="' + getContentSecurityPolicy(editor) + '" />'; 37077 } 37078 iframeHTML += '</head>' + 37079 `<body id="${bodyId}" class="mce-content-body ${bodyClass}" data-id="${editor.id}" ${iframeBodyAriaLabel}>` + 37080 '<br>' + 37081 '</body></html>'; 37082 return iframeHTML; 37083 }; 37084 const createIframe = (editor, boxInfo) => { 37085 const iframeTitle = getIframeAriaText(editor); 37086 const translatedTitle = editor.translate(iframeTitle); 37087 const tabindex = getOpt(SugarElement.fromDom(editor.getElement()), 'tabindex').bind(toInt); 37088 const ifr = createIframeElement(editor.id, translatedTitle, getIframeAttrs(editor), tabindex).dom; 37089 ifr.onload = () => { 37090 ifr.onload = null; 37091 editor.dispatch('load'); 37092 }; 37093 editor.contentAreaContainer = boxInfo.iframeContainer; 37094 editor.iframeElement = ifr; 37095 editor.iframeHTML = getIframeHtml(editor); 37096 DOM$5.add(boxInfo.iframeContainer, ifr); 37097 }; 37098 const setupIframeBody = (editor) => { 37099 // Setup iframe body 37100 const iframe = editor.iframeElement; 37101 const ready = () => { 37102 // Set the content document, now that it is available 37103 editor.contentDocument = iframe.contentDocument; 37104 // Continue to init the editor 37105 contentBodyLoaded(editor); 37106 }; 37107 // TINY-8916: Firefox has a bug in its srcdoc implementation that prevents cookies being sent so unfortunately we need 37108 // to fallback to legacy APIs to load the iframe content. See https://bugzilla.mozilla.org/show_bug.cgi?id=1741489 37109 if (shouldUseDocumentWrite(editor) || Env.browser.isFirefox()) { 37110 const doc = editor.getDoc(); 37111 doc.open(); 37112 doc.write(editor.iframeHTML); 37113 doc.close(); 37114 ready(); 37115 } 37116 else { 37117 const binder = bind$1(SugarElement.fromDom(iframe), 'load', () => { 37118 binder.unbind(); 37119 ready(); 37120 }); 37121 iframe.srcdoc = editor.iframeHTML; 37122 } 37123 }; 37124 const init$1 = (editor, boxInfo) => { 37125 createIframe(editor, boxInfo); 37126 if (boxInfo.editorContainer) { 37127 boxInfo.editorContainer.style.display = editor.orgDisplay; 37128 editor.hidden = DOM$5.isHidden(boxInfo.editorContainer); 37129 } 37130 editor.getElement().style.display = 'none'; 37131 DOM$5.setAttrib(editor.id, 'aria-hidden', 'true'); 37132 // Restore visibility on target element 37133 editor.getElement().style.visibility = editor.orgVisibility; 37134 setupIframeBody(editor); 37135 }; 37136 37137 const DOM$4 = DOMUtils.DOM; 37138 const initPlugin = (editor, initializedPlugins, plugin) => { 37139 const Plugin = PluginManager.get(plugin); 37140 const pluginUrl = PluginManager.urls[plugin] || editor.editorManager.documentBaseURL.replace(/\/$/, ''); 37141 plugin = Tools.trim(plugin); 37142 if (Plugin && Tools.inArray(initializedPlugins, plugin) === -1) { 37143 if (editor.plugins[plugin]) { 37144 return; 37145 } 37146 try { 37147 const pluginInstance = Plugin(editor, pluginUrl) || {}; 37148 editor.plugins[plugin] = pluginInstance; 37149 if (isFunction(pluginInstance.init)) { 37150 pluginInstance.init(editor, pluginUrl); 37151 initializedPlugins.push(plugin); 37152 } 37153 } 37154 catch (e) { 37155 pluginInitError(editor, plugin, e); 37156 } 37157 } 37158 }; 37159 const initTooltipClosing = (editor) => { 37160 const closeTooltipsListener = (event) => { 37161 if (event.keyCode === VK.ESC && !event.defaultPrevented) { 37162 if (fireCloseTooltips(editor).isDefaultPrevented()) { 37163 event.preventDefault(); 37164 }
vendor: 23,922 bytes, lines 37164-37742
37164 37165 } 37166 }; 37167 document.addEventListener('keyup', closeTooltipsListener); 37168 if (!editor.inline) { 37169 editor.on('keyup', closeTooltipsListener); 37170 } 37171 editor.on('remove', () => { 37172 document.removeEventListener('keyup', closeTooltipsListener); 37173 if (!editor.inline) { 37174 editor.off('keyup', closeTooltipsListener); 37175 } 37176 }); 37177 }; 37178 const trimLegacyPrefix = (name) => { 37179 // Themes and plugins can be prefixed with - to prevent them from being lazy loaded 37180 return name.replace(/^\-/, ''); 37181 }; 37182 const initPlugins = (editor) => { 37183 const initializedPlugins = []; 37184 each$e(getPlugins(editor), (name) => { 37185 initPlugin(editor, initializedPlugins, trimLegacyPrefix(name)); 37186 }); 37187 }; 37188 const initIcons = (editor) => { 37189 const iconPackName = Tools.trim(getIconPackName(editor)); 37190 const currentIcons = editor.ui.registry.getAll().icons; 37191 const loadIcons = { 37192 ...IconManager.get('default').icons, 37193 ...IconManager.get(iconPackName).icons 37194 }; 37195 each$d(loadIcons, (svgData, icon) => { 37196 // Don't override an icon registered manually 37197 if (!has$2(currentIcons, icon)) { 37198 editor.ui.registry.addIcon(icon, svgData); 37199 } 37200 }); 37201 }; 37202 const initTheme = (editor) => { 37203 const theme = getTheme(editor); 37204 if (isString(theme)) { 37205 const Theme = ThemeManager.get(theme); 37206 editor.theme = Theme(editor, ThemeManager.urls[theme]) || {}; 37207 if (isFunction(editor.theme.init)) { 37208 editor.theme.init(editor, ThemeManager.urls[theme] || editor.editorManager.documentBaseURL.replace(/\/$/, '')); 37209 } 37210 } 37211 else { 37212 // Theme set to false or null doesn't produce a theme api 37213 editor.theme = {}; 37214 } 37215 }; 37216 const initModel = (editor) => { 37217 const model = getModel(editor); 37218 const Model = ModelManager.get(model); 37219 editor.model = Model(editor, ModelManager.urls[model]); 37220 }; 37221 const initLicenseKeyManager = (editor) => { 37222 LicenseKeyManagerLoader.init(editor); 37223 }; 37224 const renderFromLoadedTheme = (editor) => { 37225 // Render UI 37226 const render = editor.theme.renderUI; 37227 return render ? render() : renderThemeFalse(editor); 37228 }; 37229 const renderFromThemeFunc = (editor) => { 37230 const elm = editor.getElement(); 37231 const theme = getTheme(editor); 37232 const info = theme(editor, elm); 37233 if (info.editorContainer.nodeType) { 37234 info.editorContainer.id = info.editorContainer.id || editor.id + '_parent'; 37235 } 37236 if (info.iframeContainer && info.iframeContainer.nodeType) { 37237 info.iframeContainer.id = info.iframeContainer.id || editor.id + '_iframecontainer'; 37238 } 37239 info.height = info.iframeHeight ? info.iframeHeight : elm.offsetHeight; 37240 return info; 37241 }; 37242 const createThemeFalseResult = (element, iframe) => { 37243 return { 37244 editorContainer: element, 37245 iframeContainer: iframe, 37246 api: {} 37247 }; 37248 }; 37249 const renderThemeFalseIframe = (targetElement) => { 37250 const iframeContainer = DOM$4.create('div'); 37251 DOM$4.insertAfter(iframeContainer, targetElement); 37252 return createThemeFalseResult(iframeContainer, iframeContainer); 37253 }; 37254 const renderThemeFalse = (editor) => { 37255 const targetElement = editor.getElement(); 37256 return editor.inline ? createThemeFalseResult(null) : renderThemeFalseIframe(targetElement); 37257 }; 37258 const renderThemeUi = (editor) => { 37259 const elm = editor.getElement(); 37260 editor.orgDisplay = elm.style.display; 37261 if (isString(getTheme(editor))) { 37262 return renderFromLoadedTheme(editor); 37263 } 37264 else if (isFunction(getTheme(editor))) { 37265 return renderFromThemeFunc(editor); 37266 } 37267 else { 37268 return renderThemeFalse(editor); 37269 } 37270 }; 37271 const augmentEditorUiApi = (editor, api) => { 37272 const uiApiFacade = { 37273 show: Optional.from(api.show).getOr(noop), 37274 hide: Optional.from(api.hide).getOr(noop), 37275 isEnabled: Optional.from(api.isEnabled).getOr(always), 37276 setEnabled: (state) => { 37277 const shouldSkip = state && (editor.mode.get() === 'readonly' || isDisabled(editor)); 37278 if (!shouldSkip) { 37279 Optional.from(api.setEnabled).each((f) => f(state)); 37280 } 37281 } 37282 }; 37283 editor.ui = { ...editor.ui, ...uiApiFacade }; 37284 }; 37285 const init = async (editor) => { 37286 editor.dispatch('ScriptsLoaded'); 37287 initIcons(editor); 37288 initTooltipClosing(editor); 37289 initTheme(editor); 37290 initModel(editor); 37291 initLicenseKeyManager(editor); 37292 initPlugins(editor); 37293 const renderInfo = await renderThemeUi(editor); 37294 augmentEditorUiApi(editor, Optional.from(renderInfo.api).getOr({})); 37295 editor.editorContainer = renderInfo.editorContainer; 37296 appendContentCssFromSettings(editor); 37297 if (editor.inline) { 37298 contentBodyLoaded(editor); 37299 } 37300 else { 37301 init$1(editor, { 37302 editorContainer: renderInfo.editorContainer, 37303 iframeContainer: renderInfo.iframeContainer 37304 }); 37305 } 37306 }; 37307 37308 const DOM$3 = DOMUtils.DOM; 37309 const hasSkipLoadPrefix = (name) => name.charAt(0) === '-'; 37310 const loadLanguage = (scriptLoader, editor) => { 37311 const languageCode = getLanguageCode(editor); 37312 const languageUrl = getLanguageUrl(editor); 37313 if (!I18n.hasCode(languageCode) && languageCode !== 'en') { 37314 const url = isNotEmpty(languageUrl) ? languageUrl : `${editor.editorManager.baseURL}/langs/${languageCode}.js`; 37315 scriptLoader.add(url).catch(() => { 37316 languageLoadError(editor, url, languageCode); 37317 }); 37318 } 37319 }; 37320 const loadTheme = (editor, suffix) => { 37321 const theme = getTheme(editor); 37322 if (isString(theme) && !hasSkipLoadPrefix(theme) && !has$2(ThemeManager.urls, theme)) { 37323 const themeUrl = getThemeUrl(editor); 37324 const url = themeUrl ? editor.documentBaseURI.toAbsolute(themeUrl) : `themes/${theme}/theme${suffix}.js`; 37325 ThemeManager.load(theme, url).catch(() => { 37326 themeLoadError(editor, url, theme); 37327 }); 37328 } 37329 }; 37330 const loadModel = (editor, suffix) => { 37331 // Special case the 'wait for model' code if a plugin is responsible for it 37332 // as the plugin will provide the instance instead 37333 const model = getModel(editor); 37334 if (model !== 'plugin' && !has$2(ModelManager.urls, model)) { 37335 const modelUrl = getModelUrl(editor); 37336 const url = isString(modelUrl) ? editor.documentBaseURI.toAbsolute(modelUrl) : `models/${model}/model${suffix}.js`; 37337 ModelManager.load(model, url).catch(() => { 37338 modelLoadError(editor, url, model); 37339 }); 37340 } 37341 }; 37342 const loadLicenseKeyManager = (editor, suffix) => { 37343 LicenseKeyManagerLoader.load(editor, suffix); 37344 }; 37345 const getIconsUrlMetaFromUrl = (editor) => Optional.from(getIconsUrl(editor)) 37346 .filter(isNotEmpty) 37347 .map((url) => ({ 37348 url, 37349 name: Optional.none() 37350 })); 37351 const getIconsUrlMetaFromName = (editor, name, suffix) => Optional.from(name) 37352 .filter((name) => isNotEmpty(name) && !IconManager.has(name)) 37353 .map((name) => ({ 37354 url: `${editor.editorManager.baseURL}/icons/${name}/icons${suffix}.js`, 37355 name: Optional.some(name) 37356 })); 37357 const loadIcons = (scriptLoader, editor, suffix) => { 37358 const defaultIconsUrl = getIconsUrlMetaFromName(editor, 'default', suffix); 37359 const customIconsUrl = getIconsUrlMetaFromUrl(editor).orThunk(() => getIconsUrlMetaFromName(editor, getIconPackName(editor), '')); 37360 each$e(cat([defaultIconsUrl, customIconsUrl]), (urlMeta) => { 37361 scriptLoader.add(urlMeta.url).catch(() => { 37362 iconsLoadError(editor, urlMeta.url, urlMeta.name.getOrUndefined()); 37363 }); 37364 }); 37365 }; 37366 const loadPlugins = (editor, suffix) => { 37367 const loadPlugin = (name, url) => { 37368 // If licensekeymanager is included in the plugins list 37369 // or through external_plugins, skip it 37370 if (name === 'licensekeymanager') { 37371 return; 37372 } 37373 PluginManager.load(name, url).catch(() => { 37374 pluginLoadError(editor, url, name); 37375 }); 37376 }; 37377 each$d(getExternalPlugins$1(editor), (url, name) => { 37378 loadPlugin(name, url); 37379 editor.options.set('plugins', getPlugins(editor).concat(name)); 37380 }); 37381 each$e(getPlugins(editor), (plugin) => { 37382 plugin = Tools.trim(plugin); 37383 if (plugin && !PluginManager.urls[plugin] && !hasSkipLoadPrefix(plugin)) { 37384 loadPlugin(plugin, `plugins/${plugin}/plugin${suffix}.js`); 37385 } 37386 }); 37387 }; 37388 const isThemeLoaded = (editor) => { 37389 const theme = getTheme(editor); 37390 return !isString(theme) || isNonNullable(ThemeManager.get(theme)); 37391 }; 37392 const isModelLoaded = (editor) => { 37393 const model = getModel(editor); 37394 return isNonNullable(ModelManager.get(model)); 37395 }; 37396 const loadScripts = (editor, suffix) => { 37397 const scriptLoader = ScriptLoader.ScriptLoader; 37398 const initEditor = () => { 37399 // If the editor has been destroyed or the theme, model haven't loaded then 37400 // don't continue to load the editor 37401 if (!editor.removed && 37402 isThemeLoaded(editor) && 37403 isModelLoaded(editor)) { 37404 // eslint-disable-next-line @typescript-eslint/no-floating-promises 37405 init(editor); 37406 } 37407 }; 37408 loadTheme(editor, suffix); 37409 loadModel(editor, suffix); 37410 loadLicenseKeyManager(editor, suffix); 37411 loadLanguage(scriptLoader, editor); 37412 loadIcons(scriptLoader, editor, suffix); 37413 loadPlugins(editor, suffix); 37414 scriptLoader.loadQueue().then(initEditor, initEditor); 37415 }; 37416 const getStyleSheetLoader = (element, editor) => instance.forElement(element, { 37417 contentCssCors: hasContentCssCors(editor), 37418 referrerPolicy: getReferrerPolicy(editor) 37419 }); 37420 const render = (editor) => { 37421 const id = editor.id; 37422 // The user might have bundled multiple language packs so we need to switch the active code to the user specified language 37423 I18n.setCode(getLanguageCode(editor)); 37424 const readyHandler = () => { 37425 DOM$3.unbind(window, 'ready', readyHandler); 37426 editor.render(); 37427 }; 37428 // Page is not loaded yet, wait for it 37429 if (!EventUtils.Event.domLoaded) { 37430 DOM$3.bind(window, 'ready', readyHandler); 37431 return; 37432 } 37433 // Element not found, then skip initialization 37434 if (!editor.getElement()) { 37435 return; 37436 } 37437 // snapshot the element we're going to render to 37438 const element = SugarElement.fromDom(editor.getElement()); 37439 const snapshot = clone$4(element); 37440 editor.on('remove', () => { 37441 eachr(element.dom.attributes, (attr) => remove$9(element, attr.name)); 37442 setAll$1(element, snapshot); 37443 }); 37444 editor.ui.styleSheetLoader = getStyleSheetLoader(element, editor); 37445 // Hide target element early to prevent content flashing 37446 if (!isInline$2(editor)) { 37447 editor.orgVisibility = editor.getElement().style.visibility; 37448 editor.getElement().style.visibility = 'hidden'; 37449 } 37450 else { 37451 editor.inline = true; 37452 } 37453 // TODO: Investigate the types here 37454 const form = editor.getElement().form || DOM$3.getParent(id, 'form'); 37455 if (form) { 37456 editor.formElement = form; 37457 // Add hidden input for non input elements inside form elements 37458 if (hasHiddenInput(editor) && !isTextareaOrInput(editor.getElement())) { 37459 DOM$3.insertAfter(DOM$3.create('input', { type: 'hidden', name: id }), id); 37460 editor.hasHiddenInput = true; 37461 } 37462 // Pass submit/reset from form to editor instance 37463 editor.formEventDelegate = (e) => { 37464 editor.dispatch(e.type, e); 37465 }; 37466 DOM$3.bind(form, 'submit reset', editor.formEventDelegate); 37467 // Reset contents in editor when the form is reset 37468 editor.on('reset', () => { 37469 editor.resetContent(); 37470 }); 37471 // Check page uses id="submit" or name="submit" for it's submit button 37472 if (shouldPatchSubmit(editor) && !form.submit.nodeType && !form.submit.length && !form._mceOldSubmit) { 37473 form._mceOldSubmit = form.submit; 37474 form.submit = () => { 37475 editor.editorManager.triggerSave(); 37476 editor.setDirty(false); 37477 return form._mceOldSubmit(form); 37478 }; 37479 } 37480 } 37481 editor.windowManager = WindowManager(editor); 37482 editor.notificationManager = NotificationManager(editor); 37483 if (isEncodingXml(editor)) { 37484 editor.on('GetContent', (e) => { 37485 if (e.save) { 37486 e.content = DOM$3.encode(e.content); 37487 } 37488 }); 37489 } 37490 if (shouldAddFormSubmitTrigger(editor)) { 37491 editor.on('submit', () => { 37492 if (editor.initialized) { 37493 editor.save(); 37494 } 37495 }); 37496 } 37497 if (shouldAddUnloadTrigger(editor)) { 37498 editor._beforeUnload = () => { 37499 if (editor.initialized && !editor.destroyed && !editor.isHidden()) { 37500 editor.save({ format: 'raw', no_events: true, set_dirty: false }); 37501 } 37502 }; 37503 editor.editorManager.on('BeforeUnload', editor._beforeUnload); 37504 } 37505 editor.editorManager.add(editor); 37506 loadScripts(editor, editor.suffix); 37507 }; 37508 37509 const AvatarColors = [ 37510 '#E41B60', // Pink 37511 '#AD1457', // Dark Pink 37512 '#1939EC', // Indigo 37513 '#001CB5', // Dark Indigo 37514 '#648000', // Lime 37515 '#465B00', // Dark Lime 37516 '#006CE7', // Blue 37517 '#0054B4', // Dark Blue 37518 '#00838F', // Cyan 37519 '#006064', // Dark Cyan 37520 '#00866F', // Turquoise 37521 '#004D40', // Dark Turquoise 37522 '#51742F', // Green 37523 '#385021', // Dark Green 37524 '#CF4900', // Orange 37525 '#A84600', // Dark Orange 37526 '#CC0000', // Red 37527 '#6A1B9A', // Dark Red 37528 '#9C27B0', // Purple 37529 '#6A00AB', // Dark Purple 37530 '#3041BA', // Navy Blue 37531 '#0A1877', // Dark Navy Blue 37532 '#774433', // Brown 37533 '#452B24', // Dark Brown 37534 '#607D8B', // Blue Gray 37535 '#455A64', // Dark Blue Gray 37536 ]; 37537 const getFirstChar = (name) => { 37538 if (Intl.Segmenter) { 37539 const segmenter = new Intl.Segmenter(); 37540 const iterator = segmenter.segment(name)[Symbol.iterator](); 37541 return `${iterator.next().value?.segment}`; 37542 } 37543 else { 37544 return name.trim()[0]; 37545 } 37546 }; 37547 /* For a given string returns integer between 0 and maxValue (inclusive). 37548 This function is based on the djb2 hash algorithm reported by Dan Bernstein. 37549 You can find more informations here: http://www.cse.yorku.ca/~oz/hash.html 37550 The hashing algorithm is using bitwise operators to multiply by 32, and later to ensure a positive integer. 37551 The result is then reduced to the range of 0 to maxValue using modulo operation. 37552 */ 37553 const djb2Hash = (key, maxValue) => { 37554 let hash = 5381; 37555 for (let i = 0; i < key.length; i++) { 37556 // eslint-disable-next-line no-bitwise 37557 hash = ((hash << 5) + hash) + key.charCodeAt(i); 37558 } 37559 // eslint-disable-next-line no-bitwise 37560 return (hash >>> 0) % (maxValue + 1); 37561 }; 37562 const getColor = (id) => { 37563 const colorIdx = djb2Hash(id ?? '', AvatarColors.length - 1); 37564 return AvatarColors[colorIdx]; 37565 }; 37566 const generateAvatarSvg = (content, color, size) => { 37567 const halfSize = size / 2; 37568 return `<svg height="${size}" width="${size}" xmlns="http://www.w3.org/2000/svg">` + 37569 `<circle cx="${halfSize}" cy="${halfSize}" r="${halfSize}" fill="${color}"/>` + 37570 `<text x="50%" y="50%" text-anchor="middle" dominant-baseline="central" fill="#FFF" font-family="sans-serif" font-size="${halfSize}">` + 37571 content + 37572 `</text>` + 37573 '</svg>'; 37574 }; 37575 /** 37576 * Generates a data URL for an SVG avatar with the specified content, color, and size. 37577 * 37578 * @param content The text content to display in the avatar (typically a single character) 37579 * @param color The background color of the avatar (hex color string) 37580 * @param size The size of the avatar in pixels (width and height) 37581 * @returns A data URL string containing the encoded SVG avatar 37582 */ 37583 const generateAvatar = (content, color, size) => 'data:image/svg+xml,' + encodeURIComponent(generateAvatarSvg(content, color, size)); 37584 /** 37585 * Generates a user avatar based on the user's name and ID. 37586 * 37587 * @param user User object containing id and name properties 37588 * @param user.id Unique identifier used to determine the avatar color 37589 * @param user.name User's name, first character will be displayed in the avatar 37590 * @param config Configuration object for the avatar 37591 * @param config.size The size of the avatar in pixels (defaults to 36) 37592 * @returns A data URL string containing the encoded SVG avatar 37593 */ 37594 const generateUserAvatar = (user, config = { size: 36 }) => generateAvatar(getFirstChar(user.name), getColor(user.id), config.size); 37595 37596 const userSchema = objOf([ 37597 required('id'), 37598 optionString('name'), 37599 optionString('avatar'), 37600 option$1('custom') 37601 ]); 37602 const objectCat = (obj) => { 37603 const result = {}; 37604 each$d(obj, (value, key) => { 37605 value.each((v) => { 37606 result[key] = v; 37607 }); 37608 }); 37609 return result; 37610 }; 37611 const validateResponse = (items) => { 37612 if (!Array.isArray(items)) { 37613 throw new Error('fetch_users must return an array'); 37614 } 37615 const results = map$3(items, (item) => asRaw('Invalid user object', userSchema, item)); 37616 const { errors, values } = partition$1(results); 37617 if (errors.length > 0) { 37618 const formattedErrors = map$3(errors, (error, idx) => `User at index ${idx}: ${formatError(error)}`); 37619 // eslint-disable-next-line no-console 37620 console.warn('User validation errors:\n' + formattedErrors.join('\n')); 37621 } 37622 return map$3(values, (user) => { 37623 const { id, name, avatar, ...rest } = user; 37624 return { 37625 id, 37626 name: name.getOr(id), 37627 avatar: avatar.getOr(generateUserAvatar({ id, name: name.getOr(id) })), 37628 ...objectCat(rest), 37629 }; 37630 }); 37631 }; 37632 const UserLookup = (editor) => { 37633 const userCache = new Map(); 37634 const pendingResolvers = new Map(); 37635 const lookup = (userId) => Optional.from(userCache.get(userId)); 37636 const store = (user, userId) => { 37637 userCache.set(userId, user); 37638 }; 37639 const finallyReject = (userId, error) => Optional 37640 .from(pendingResolvers.get(userId)) 37641 .each(({ reject }) => { 37642 reject(error); 37643 pendingResolvers.delete(userId); 37644 }); 37645 const finallyResolve = (userId, user) => Optional 37646 .from(pendingResolvers.get(userId)) 37647 .each(({ resolve }) => { 37648 resolve(user); 37649 pendingResolvers.delete(userId); 37650 }); 37651 const fetchUsers = (userIds) => { 37652 const fetchUsersFn = getFetchUsers(editor); 37653 if (!Array.isArray(userIds)) { 37654 return {}; 37655 } 37656 else if (!fetchUsersFn) { 37657 return mapToObject(userIds, (userId) => Promise.resolve({ 37658 id: userId, 37659 name: userId, 37660 avatar: generateUserAvatar({ id: userId, name: userId }) 37661 })); 37662 } 37663 const uncachedIds = unique$1(filter$5((userIds), (userId) => !lookup(userId).isSome())); 37664 each$e(uncachedIds, (userId) => { 37665 const newPromise = new Promise((resolve, reject) => { 37666 pendingResolvers.set(userId, { resolve, reject }); 37667 }); 37668 store(newPromise, userId); 37669 }); 37670 if (uncachedIds.length > 0) { 37671 fetchUsersFn(uncachedIds) 37672 .then(validateResponse) 37673 .then((users) => { 37674 const foundUserIds = new Set(map$3(users, (user) => user.id)); 37675 // Resolve found users 37676 each$e(users, (user) => finallyResolve(user.id, user)); 37677 // Reject promises for users not found in the response 37678 each$e(uncachedIds, (userId) => { 37679 if (!foundUserIds.has(userId)) { 37680 finallyReject(userId, new Error(`User ${userId} not found`)); 37681 } 37682 }); 37683 }) 37684 .catch((error) => { 37685 each$e(uncachedIds, (userId) => finallyReject(userId, error instanceof Error ? error : new Error('Network error'))); 37686 }); 37687 } 37688 return foldl(userIds, (acc, userId) => { 37689 acc[userId] = lookup(userId).getOr(Promise.resolve({ 37690 id: userId, 37691 name: userId, 37692 avatar: generateUserAvatar({ id: userId, name: userId }) 37693 })); 37694 return acc; 37695 }, {}); 37696 }; 37697 const userId = getUserId(editor); 37698 return Object.freeze({ 37699 userId, 37700 fetchUsers, 37701 }); 37702 }; 37703 const createUserLookup = (editor) => UserLookup(editor); 37704 37705 const setEditableRoot = (editor, state) => { 37706 if (editor._editableRoot !== state) { 37707 editor._editableRoot = state; 37708 if (!isDisabled(editor)) { 37709 editor.getBody().contentEditable = String(editor.hasEditableRoot()); 37710 editor.nodeChanged(); 37711 } 37712 fireEditableRootStateChange(editor, state); 37713 } 37714 }; 37715 const hasEditableRoot = (editor) => editor._editableRoot; 37716 37717 const sectionResult = (sections, settings) => ({ 37718 sections: constant(sections), 37719 options: constant(settings) 37720 }); 37721 const deviceDetection = detect$1().deviceType; 37722 const isPhone = deviceDetection.isPhone(); 37723 const isTablet = deviceDetection.isTablet(); 37724 const normalizePlugins = (plugins) => { 37725 if (isNullable(plugins)) { 37726 return []; 37727 } 37728 else { 37729 const pluginNames = isArray$1(plugins) ? plugins : plugins.split(/[ ,]/); 37730 const trimmedPlugins = map$3(pluginNames, trim$4); 37731 return filter$5(trimmedPlugins, isNotEmpty); 37732 } 37733 }; 37734 const extractSections = (keys, options) => { 37735 const result = bifilter(options, (value, key) => { 37736 return contains$2(keys, key); 37737 }); 37738 return sectionResult(result.t, result.f); 37739 }; 37740 const getSection = (sectionResult, name, defaults = {}) => { 37741 const sections = sectionResult.sections(); 37742 const sectionOptions = get$
37742a(sections, name).getOr({}); 37743 return Tools.extend({}, defaults, sectionOptions); 37744 }; 37745 const hasSection = (sectionResult, name) => { 37746 return has$2(sectionResult.sections(), name); 37747 }; 37748 const getSectionConfig = (sectionResult, name) => { 37749 return hasSection(sectionResult, name) ? sectionResult.sections()[name] : {}; 37750 };
vendor: 15,316 bytes, lines 37751-38097
37751 // Get a list of options to override any desktop options 37752 const getMobileOverrideOptions = (mobileOptions, isPhone) => { 37753 const defaultMobileOptions = { 37754 table_grid: false, // Table grid relies on hover, which isn't available for touch devices so use the dialog instead 37755 object_resizing: false, // No nice way to do object resizing at this stage 37756 resize: false, // Editor resize doesn't make sense on mobile 37757 toolbar_mode: get$a(mobileOptions, 'toolbar_mode').getOr('scrolling'), // Use the default side-scrolling toolbar for tablets/phones 37758 toolbar_sticky: false // Only enable sticky toolbar on desktop by default 37759 }; 37760 const defaultPhoneOptions = { 37761 menubar: false // Phones don't have a lot of screen space, so disable the menubar 37762 }; 37763 return { 37764 ...defaultMobileOptions, 37765 ...isPhone ? defaultPhoneOptions : {} 37766 }; 37767 }; 37768 const getExternalPlugins = (overrideOptions, options) => { 37769 const userDefinedExternalPlugins = options.external_plugins ?? {}; 37770 if (overrideOptions && overrideOptions.external_plugins) { 37771 return Tools.extend({}, overrideOptions.external_plugins, userDefinedExternalPlugins); 37772 } 37773 else { 37774 return userDefinedExternalPlugins; 37775 } 37776 }; 37777 const combinePlugins = (forcedPlugins, plugins) => [ 37778 ...normalizePlugins(forcedPlugins), 37779 ...normalizePlugins(plugins) 37780 ]; 37781 const getPlatformPlugins = (isMobileDevice, sectionResult, desktopPlugins, mobilePlugins) => { 37782 // is a mobile device with any mobile options 37783 if (isMobileDevice && hasSection(sectionResult, 'mobile')) { 37784 return mobilePlugins; 37785 // is desktop 37786 } 37787 else { 37788 return desktopPlugins; 37789 } 37790 }; 37791 const processPlugins = (isMobileDevice, sectionResult, defaultOverrideOptions, options) => { 37792 const forcedPlugins = normalizePlugins(defaultOverrideOptions.forced_plugins); 37793 const desktopPlugins = normalizePlugins(options.plugins); 37794 const mobileConfig = getSectionConfig(sectionResult, 'mobile'); 37795 const mobilePlugins = mobileConfig.plugins ? normalizePlugins(mobileConfig.plugins) : desktopPlugins; 37796 const platformPlugins = getPlatformPlugins(isMobileDevice, sectionResult, desktopPlugins, mobilePlugins); 37797 const combinedPlugins = combinePlugins(forcedPlugins, platformPlugins); 37798 return Tools.extend(options, { 37799 forced_plugins: forcedPlugins, 37800 plugins: combinedPlugins 37801 }); 37802 }; 37803 const isOnMobile = (isMobileDevice, sectionResult) => { 37804 return isMobileDevice && hasSection(sectionResult, 'mobile'); 37805 }; 37806 const combineOptions = (isMobileDevice, isPhone, defaultOptions, defaultOverrideOptions, options) => { 37807 // Use mobile mode by default on phones, so patch in the mobile override options 37808 const deviceOverrideOptions = isMobileDevice ? { mobile: getMobileOverrideOptions(options.mobile ?? {}, isPhone) } : {}; 37809 const sectionResult = extractSections(['mobile'], deepMerge(deviceOverrideOptions, options)); 37810 const extendedOptions = Tools.extend( 37811 // Default options 37812 defaultOptions, 37813 // tinymce.overrideOptions options 37814 defaultOverrideOptions, 37815 // User options 37816 sectionResult.options(), 37817 // Sections 37818 isOnMobile(isMobileDevice, sectionResult) ? getSection(sectionResult, 'mobile') : {}, 37819 // Forced options 37820 { 37821 external_plugins: getExternalPlugins(defaultOverrideOptions, sectionResult.options()) 37822 }); 37823 return processPlugins(isMobileDevice, sectionResult, defaultOverrideOptions, extendedOptions); 37824 }; 37825 const normalizeOptions = (defaultOverrideOptions, options) => { 37826 const copiedOptions = merge$1(options); 37827 return combineOptions(isPhone || isTablet, isPhone, copiedOptions, defaultOverrideOptions, copiedOptions); 37828 }; 37829 37830 const addVisual = (editor, elm) => addVisual$1(editor, elm); 37831 37832 const registerExecCommands$2 = (editor) => { 37833 const toggleFormat = (name, value) => { 37834 editor.formatter.toggle(name, value); 37835 editor.nodeChanged(); 37836 }; 37837 const toggleAlign = (align) => () => { 37838 // Remove all other alignments first 37839 each$e('left,center,right,justify'.split(','), (name) => { 37840 if (align !== name) { 37841 editor.formatter.remove('align' + name); 37842 } 37843 }); 37844 if (align !== 'none') { 37845 toggleFormat('align' + align); 37846 } 37847 }; 37848 editor.editorCommands.addCommands({ 37849 JustifyLeft: toggleAlign('left'), 37850 JustifyCenter: toggleAlign('center'), 37851 JustifyRight: toggleAlign('right'), 37852 JustifyFull: toggleAlign('justify'), 37853 JustifyNone: toggleAlign('none') 37854 }); 37855 }; 37856 const registerQueryStateCommands = (editor) => { 37857 const alignStates = (name) => () => { 37858 const selection = editor.selection; 37859 const nodes = selection.isCollapsed() ? [editor.dom.getParent(selection.getNode(), editor.dom.isBlock)] : selection.getSelectedBlocks(); 37860 return exists(nodes, (node) => isNonNullable(editor.formatter.matchNode(node, name))); 37861 }; 37862 editor.editorCommands.addCommands({ 37863 JustifyLeft: alignStates('alignleft'), 37864 JustifyCenter: alignStates('aligncenter'), 37865 JustifyRight: alignStates('alignright'), 37866 JustifyFull: alignStates('alignjustify') 37867 }, 'state'); 37868 }; 37869 const registerCommands$a = (editor) => { 37870 registerExecCommands$2(editor); 37871 registerQueryStateCommands(editor); 37872 }; 37873 37874 const registerCommands$9 = (editor) => { 37875 editor.editorCommands.addCommands({ 37876 'Cut,Copy,Paste': (command) => { 37877 const doc = editor.getDoc(); 37878 let failed; 37879 // Try executing the native command 37880 try { 37881 doc.execCommand(command); 37882 } 37883 catch { 37884 // Command failed 37885 failed = true; 37886 } 37887 // Chrome reports the paste command as supported however older IE:s will return false for cut/paste 37888 if (command === 'paste' && !doc.queryCommandEnabled(command)) { 37889 failed = true; 37890 } 37891 // Present alert message about clipboard access not being available 37892 if (failed || !doc.queryCommandSupported(command)) { 37893 let msg = editor.translate(`Your browser doesn't support direct access to the clipboard. ` + 37894 'Please use the Ctrl+X/C/V keyboard shortcuts instead.'); 37895 if (Env.os.isMacOS() || Env.os.isiOS()) { 37896 msg = msg.replace(/Ctrl\+/g, '\u2318+'); 37897 } 37898 editor.notificationManager.open({ text: msg, type: 'error' }); 37899 } 37900 } 37901 }); 37902 }; 37903 37904 const trimOrPadLeftRight = (dom, rng, html, schema) => { 37905 const root = SugarElement.fromDom(dom.getRoot()); 37906 // Adjust the start if it needs to be an nbsp 37907 if (needsToBeNbspLeft(root, CaretPosition.fromRangeStart(rng), schema)) { 37908 html = html.replace(/^ /, ' '); 37909 } 37910 else { 37911 html = html.replace(/^ /, ' '); 37912 } 37913 // Adjust the end if it needs to be an nbsp 37914 if (needsToBeNbspRight(root, CaretPosition.fromRangeEnd(rng), schema)) { 37915 html = html.replace(/( | )(<br( \/)>)?$/, ' '); 37916 } 37917 else { 37918 html = html.replace(/ (<br( \/)?>)?$/, ' '); 37919 } 37920 return html; 37921 }; 37922 37923 const processValue$1 = (value) => { 37924 if (typeof value !== 'string') { 37925 const details = Tools.extend({ 37926 paste: value.paste, 37927 data: { 37928 paste: value.paste 37929 } 37930 }, value); 37931 return { 37932 content: value.content, 37933 details 37934 }; 37935 } 37936 return { 37937 content: value, 37938 details: {} 37939 }; 37940 }; 37941 const trimOrPad = (editor, value) => { 37942 const selection = editor.selection; 37943 const dom = editor.dom; 37944 // Check for whitespace before/after value 37945 if (/^ | $/.test(value)) { 37946 return trimOrPadLeftRight(dom, selection.getRng(), value, editor.schema); 37947 } 37948 else { 37949 return value; 37950 } 37951 }; 37952 const insertAtCaret = (editor, value) => { 37953 if (editor.selection.isEditable()) { 37954 const { content, details } = processValue$1(value); 37955 preProcessSetContent(editor, { ...details, content: trimOrPad(editor, content), format: 'html', set: false, selection: true }).each((args) => { 37956 const insertedContent = insertContent$1(editor, args.content, details); 37957 postProcessSetContent(editor, insertedContent, args); 37958 editor.addVisual(); 37959 }); 37960 } 37961 }; 37962 37963 const registerCommands$8 = (editor) => { 37964 editor.editorCommands.addCommands({ 37965 mceCleanup: () => { 37966 const bm = editor.selection.getBookmark(); 37967 editor.setContent(editor.getContent()); 37968 editor.selection.moveToBookmark(bm); 37969 }, 37970 insertImage: (_command, _ui, value) => { 37971 insertAtCaret(editor, editor.dom.createHTML('img', { src: value })); 37972 }, 37973 insertHorizontalRule: () => { 37974 editor.execCommand('mceInsertContent', false, '<hr>'); 37975 }, 37976 insertText: (_command, _ui, value) => { 37977 insertAtCaret(editor, editor.dom.encode(value)); 37978 }, 37979 insertHTML: (_command, _ui, value) => { 37980 insertAtCaret(editor, value); 37981 }, 37982 mceInsertContent: (_command, _ui, value) => { 37983 insertAtCaret(editor, value); 37984 }, 37985 mceSetContent: (_command, _ui, value) => { 37986 editor.setContent(value); 37987 }, 37988 mceReplaceContent: (_command, _ui, value) => { 37989 editor.execCommand('mceInsertContent', false, value.replace(/\{\$selection\}/g, editor.selection.getContent({ format: 'text' }))); 37990 }, 37991 mceNewDocument: () => { 37992 editor.setContent(getNewDocumentContent(editor)); 37993 } 37994 }); 37995 }; 37996 37997 const legacyPropNames = { 37998 'font-size': 'size', 37999 'font-family': 'face' 38000 }; 38001 const isFont = isTag('font'); 38002 const getSpecifiedFontProp = (propName, rootElm, elm) => { 38003 const getProperty = (elm) => getRaw$1(elm, propName).orThunk(() => { 38004 if (isFont(elm)) { 38005 return get$a(legacyPropNames, propName).bind((legacyPropName) => getOpt(elm, legacyPropName)); 38006 } 38007 else { 38008 return Optional.none(); 38009 } 38010 }); 38011 const isRoot = (elm) => eq(SugarElement.fromDom(rootElm), elm); 38012 return closest$1(SugarElement.fromDom(elm), (elm) => getProperty(elm), isRoot); 38013 }; 38014 const normalizeFontFamily = (fontFamily) => 38015 // 'Font name', Font -> Font name,Font 38016 fontFamily.replace(/[\'\"\\]/g, '').replace(/,\s+/g, ','); 38017 const getComputedFontProp = (propName, elm) => Optional.from(DOMUtils.DOM.getStyle(elm, propName, true)); 38018 const getFontProp = (propName) => (rootElm, elm) => Optional.from(elm) 38019 .map(SugarElement.fromDom) 38020 .filter(isElement$8) 38021 .bind((element) => getSpecifiedFontProp(propName, rootElm, element.dom) 38022 .or(getComputedFontProp(propName, element.dom))) 38023 .getOr(''); 38024 const getFontSize = getFontProp('font-size'); 38025 const getFontFamily = compose(normalizeFontFamily, getFontProp('font-family')); 38026 38027 const findFirstCaretElement = (editor) => firstPositionIn(editor.getBody()) 38028 .bind((caret) => { 38029 const container = caret.container(); 38030 return Optional.from(isText$b(container) ? container.parentNode : container); 38031 }); 38032 const getCaretElement = (editor) => Optional.from(editor.selection.getRng()) 38033 .bind((rng) => { 38034 const root = editor.getBody(); 38035 const atStartOfNode = rng.startContainer === root && rng.startOffset === 0; 38036 return atStartOfNode ? Optional.none() : Optional.from(editor.selection.getStart(true)); 38037 }); 38038 const bindRange = (editor, binder) => getCaretElement(editor) 38039 .orThunk(curry(findFirstCaretElement, editor)) 38040 .map(SugarElement.fromDom) 38041 .filter(isElement$8) 38042 .bind(binder); 38043 const mapRange = (editor, mapper) => bindRange(editor, compose1(Optional.some, mapper)); 38044 38045 const fromFontSizeNumber = (editor, value) => { 38046 if (/^[0-9.]+$/.test(value)) { 38047 const fontSizeNumber = parseInt(value, 10); 38048 // Convert font size 1-7 to styles 38049 if (fontSizeNumber >= 1 && fontSizeNumber <= 7) { 38050 const fontSizes = getFontStyleValues(editor); 38051 const fontClasses = getFontSizeClasses(editor); 38052 if (fontClasses.length > 0) { 38053 return fontClasses[fontSizeNumber - 1] || value; 38054 } 38055 else { 38056 return fontSizes[fontSizeNumber - 1] || value; 38057 } 38058 } 38059 else { 38060 return value; 38061 } 38062 } 38063 else { 38064 return value; 38065 } 38066 }; 38067 const normalizeFontNames = (font) => { 38068 const fonts = font.split(/\s*,\s*/); 38069 return map$3(fonts, (font) => { 38070 if (font.indexOf(' ') !== -1 && !(startsWith(font, '"') || startsWith(font, `'`))) { 38071 // TINY-3801: The font has spaces, so need to wrap with quotes as the browser sometimes automatically handles this, but not always 38072 return `'${font}'`; 38073 } 38074 else { 38075 return font; 38076 } 38077 }).join(','); 38078 }; 38079 const fontNameAction = (editor, value) => { 38080 const font = fromFontSizeNumber(editor, value); 38081 editor.formatter.toggle('fontname', { value: normalizeFontNames(font) }); 38082 editor.nodeChanged(); 38083 }; 38084 const fontNameQuery = (editor) => mapRange(editor, (elm) => getFontFamily(editor.getBody(), elm.dom)).getOr(''); 38085 const fontSizeAction = (editor, value) => { 38086 editor.formatter.toggle('fontsize', { value: fromFontSizeNumber(editor, value) }); 38087 editor.nodeChanged(); 38088 }; 38089 const fontSizeQuery = (editor) => mapRange(editor, (elm) => getFontSize(editor.getBody(), elm.dom)).getOr(''); 38090 38091 const lineHeightQuery = (editor) => mapRange(editor, (elm) => { 38092 const root = SugarElement.fromDom(editor.getBody()); 38093 const specifiedStyle = closest$1(elm, (elm) => getRaw$1(elm, 'line-height'), curry(eq, root)); 38094 const computedStyle = () => { 38095 // Css.get returns computed values (in px), and parseFloat will strip any non-number suffix 38096 const lineHeight = parseFloat(get$7(elm, 'line-height')); 38097 const fontSize = parseFloat(get$7
vendor: 9,363 bytes, lines 38097-38332
38097(elm, 'font-size')); 38098 return String(lineHeight / fontSize); 38099 }; 38100 return specifiedStyle.getOrThunk(computedStyle); 38101 }).getOr(''); 38102 const lineHeightAction = (editor, lineHeight) => { 38103 editor.formatter.toggle('lineheight', { value: String(lineHeight) }); 38104 editor.nodeChanged(); 38105 }; 38106 38107 const registerExecCommands$1 = (editor) => { 38108 const toggleFormat = (name, value) => { 38109 editor.formatter.toggle(name, value); 38110 editor.nodeChanged(); 38111 }; 38112 editor.editorCommands.addCommands({ 38113 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript': (command) => { 38114 toggleFormat(command); 38115 }, 38116 'ForeColor,HiliteColor': (command, _ui, value) => { 38117 toggleFormat(command, { value }); 38118 }, 38119 'BackColor': (_command, _ui, value) => { 38120 toggleFormat('hilitecolor', { value }); 38121 }, 38122 'FontName': (_command, _ui, value) => { 38123 fontNameAction(editor, value); 38124 }, 38125 'FontSize': (_command, _ui, value) => { 38126 fontSizeAction(editor, value); 38127 }, 38128 'LineHeight': (_command, _ui, value) => { 38129 lineHeightAction(editor, value); 38130 }, 38131 'Lang': (command, _ui, lang) => { 38132 toggleFormat(command, { value: lang.code, customValue: lang.customCode ?? null }); 38133 }, 38134 'RemoveFormat': (command) => { 38135 editor.formatter.remove(command); 38136 }, 38137 'mceBlockQuote': () => { 38138 toggleFormat('blockquote'); 38139 }, 38140 'FormatBlock': (_command, _ui, value) => { 38141 toggleFormat(isString(value) ? value : 'p'); 38142 }, 38143 'mceToggleFormat': (_command, _ui, value) => { 38144 toggleFormat(value); 38145 } 38146 }); 38147 }; 38148 const registerQueryValueCommands = (editor) => { 38149 const isFormatMatch = (name) => editor.formatter.match(name); 38150 editor.editorCommands.addCommands({ 38151 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript': (command) => isFormatMatch(command), 38152 'mceBlockQuote': () => isFormatMatch('blockquote') 38153 }, 'state'); 38154 editor.editorCommands.addQueryValueHandler('FontName', () => fontNameQuery(editor)); 38155 editor.editorCommands.addQueryValueHandler('FontSize', () => fontSizeQuery(editor)); 38156 editor.editorCommands.addQueryValueHandler('LineHeight', () => lineHeightQuery(editor)); 38157 }; 38158 const registerCommands$7 = (editor) => { 38159 registerExecCommands$1(editor); 38160 registerQueryValueCommands(editor); 38161 }; 38162 38163 const registerCommands$6 = (editor) => { 38164 editor.editorCommands.addCommands({ 38165 mceAddUndoLevel: () => { 38166 editor.undoManager.add(); 38167 }, 38168 mceEndUndoLevel: () => { 38169 editor.undoManager.add(); 38170 }, 38171 Undo: () => { 38172 editor.undoManager.undo(); 38173 }, 38174 Redo: () => { 38175 editor.undoManager.redo(); 38176 } 38177 }); 38178 }; 38179 38180 const registerCommands$5 = (editor) => { 38181 editor.editorCommands.addCommands({ 38182 Indent: () => { 38183 indent(editor); 38184 }, 38185 Outdent: () => { 38186 outdent(editor); 38187 }, 38188 }); 38189 editor.editorCommands.addCommands({ 38190 Outdent: () => canOutdent(editor), 38191 Indent: () => canIndent(editor), 38192 }, 'state'); 38193 }; 38194 38195 const registerCommands$4 = (editor) => { 38196 const applyLinkToSelection = (_command, _ui, value) => { 38197 if (editor.mode.isReadOnly()) { 38198 return; 38199 } 38200 const linkDetails = isString(value) ? { href: value } : value; 38201 const anchor = editor.dom.getParent(editor.selection.getNode(), 'a'); 38202 if (isObject(linkDetails) && isString(linkDetails.href)) { 38203 // Spaces are never valid in URLs and it's a very common mistake for people to make so we fix it here. 38204 linkDetails.href = linkDetails.href.replace(/ /g, '%20').replace(/&/g, '&'); 38205 // Remove existing links if there could be child links or that the href isn't specified 38206 if (!anchor || !linkDetails.href) { 38207 editor.formatter.remove('link'); 38208 } 38209 // Apply new link to selection 38210 if (linkDetails.href) { 38211 editor.formatter.apply('link', linkDetails, anchor); 38212 } 38213 } 38214 }; 38215 editor.editorCommands.addCommands({ 38216 unlink: () => { 38217 if (editor.selection.isEditable()) { 38218 if (editor.selection.isCollapsed()) { 38219 const elm = editor.dom.getParent(editor.selection.getStart(), 'a'); 38220 if (elm) { 38221 editor.dom.remove(elm, true); 38222 } 38223 return; 38224 } 38225 editor.formatter.remove('link'); 38226 } 38227 }, 38228 mceInsertLink: applyLinkToSelection, 38229 createLink: applyLinkToSelection 38230 }); 38231 }; 38232 38233 const getTopParentBlock = (editor, node, root, container) => { 38234 const dom = editor.dom; 38235 const selector = (node) => dom.isBlock(node) && node.parentElement === root; 38236 const topParentBlock = selector(node) ? node : dom.getParent(container, selector, root); 38237 return Optional.from(topParentBlock).map(SugarElement.fromDom); 38238 }; 38239 const insert = (editor, before) => { 38240 if (editor.mode.isReadOnly()) { 38241 return; 38242 } 38243 const dom = editor.dom; 38244 const rng = editor.selection.getRng(); 38245 const node = before ? editor.selection.getStart() : editor.selection.getEnd(); 38246 const container = before ? rng.startContainer : rng.endContainer; 38247 const root = getEditableRoot(dom, container); 38248 if (!root || !root.isContentEditable) { 38249 return; 38250 } 38251 const insertFn = before ? before$4 : after$4; 38252 const newBlockName = getForcedRootBlock(editor); 38253 getTopParentBlock(editor, node, root, container).each((parentBlock) => { 38254 const newBlock = createNewBlock(editor, container, parentBlock.dom, root, false, newBlockName); 38255 insertFn(parentBlock, SugarElement.fromDom(newBlock)); 38256 editor.selection.setCursorLocation(newBlock, 0); 38257 editor.dispatch('NewBlock', { newBlock }); 38258 fireInputEvent(editor, 'insertParagraph'); 38259 }); 38260 }; 38261 const insertBefore = (editor) => insert(editor, true); 38262 const insertAfter = (editor) => insert(editor, false); 38263 38264 const registerCommands$3 = (editor) => { 38265 editor.editorCommands.addCommands({ 38266 InsertNewBlockBefore: () => { 38267 insertBefore(editor); 38268 }, 38269 InsertNewBlockAfter: () => { 38270 insertAfter(editor); 38271 }, 38272 }); 38273 }; 38274 38275 const registerCommands$2 = (editor) => { 38276 editor.editorCommands.addCommands({ 38277 insertParagraph: () => { 38278 insertBreak(blockbreak, editor); 38279 }, 38280 mceInsertNewLine: (_command, _ui, value) => { 38281 insert$1(editor, value); 38282 }, 38283 InsertLineBreak: (_command, _ui, _value) => { 38284 insertBreak(linebreak, editor); 38285 } 38286 }); 38287 }; 38288 38289 const registerCommands$1 = (editor) => { 38290 editor.editorCommands.addCommands({ 38291 mceSelectNodeDepth: (_command, _ui, value) => { 38292 let counter = 0; 38293 editor.dom.getParent(editor.selection.getNode(), (node) => { 38294 if (isElement$7(node) && counter++ === value) { 38295 editor.selection.select(node); 38296 return false; 38297 } 38298 else { 38299 return true; 38300 } 38301 }, editor.getBody()); 38302 }, 38303 mceSelectNode: (_command, _ui, value) => { 38304 editor.selection.select(value); 38305 }, 38306 selectAll: () => { 38307 const editingHost = editor.dom.getParent(editor.selection.getStart(), isContentEditableTrue$3); 38308 if (editingHost) { 38309 const rng = editor.dom.createRng(); 38310 rng.selectNodeContents(editingHost); 38311 editor.selection.setRng(rng); 38312 } 38313 } 38314 }); 38315 }; 38316 38317 const registerExecCommands = (editor) => { 38318 editor.editorCommands.addCommands({ 38319 mceRemoveNode: (_command, _ui, value) => { 38320 const node = value ?? editor.selection.getNode(); 38321 // Make sure that the body node isn't removed 38322 if (node !== editor.getBody()) { 38323 const bm = editor.selection.getBookmark(); 38324 editor.dom.remove(node, true); 38325 editor.selection.moveToBookmark(bm); 38326 } 38327 }, 38328 mcePrint: () => { 38329 editor.getWin().print(); 38330 }, 38331 mceFocus: (_command, _ui, value) => { 38332 focus(editor, value
38332 === tr
vendor: 32,332 bytes, lines 38332-39125
38332ue); 38333 }, 38334 mceToggleVisualAid: () => { 38335 editor.hasVisual = !editor.hasVisual; 38336 editor.addVisual(); 38337 } 38338 }); 38339 }; 38340 const registerCommands = (editor) => { 38341 registerCommands$a(editor); 38342 registerCommands$9(editor); 38343 registerCommands$6(editor); 38344 registerCommands$1(editor); 38345 registerCommands$8(editor); 38346 registerCommands$4(editor); 38347 registerCommands$5(editor); 38348 registerCommands$3(editor); 38349 registerCommands$2(editor); 38350 registerCommands$7(editor); 38351 registerExecCommands(editor); 38352 }; 38353 38354 // List of commands that are considered safe even if the editor has no selection when the iframe is hidden in Firefox. See TINY-9210 for details. 38355 const selectionSafeCommands = ['toggleview']; 38356 const isSelectionSafeCommand = (command) => contains$2(selectionSafeCommands, command.toLowerCase()); 38357 class EditorCommands { 38358 editor; 38359 commands = { state: {}, exec: {}, value: {} }; 38360 constructor(editor) { 38361 this.editor = editor; 38362 } 38363 /** 38364 * Executes a registered command on the current instance. A list of available commands can be found in 38365 * the tinymce command identifiers documentation. 38366 * 38367 * @method execCommand 38368 * @param {String} command Command name to execute, for example mceLink or Bold. 38369 * @param {Boolean} ui Specifies if a UI (dialog) should be presented or not. 38370 * @param {Object/Array/String/Number/Boolean} value Optional command value, this can be anything. 38371 * @param {Object} args Optional arguments object. 38372 * @return {Boolean} true or false if the command was supported or not. 38373 */ 38374 execCommand(command, ui = false, value, args) { 38375 const editor = this.editor; 38376 const lowerCaseCommand = command.toLowerCase(); 38377 const skipFocus = args?.skip_focus; 38378 if (editor.removed) { 38379 return false; 38380 } 38381 if (lowerCaseCommand !== 'mcefocus') { 38382 if (!/^(mceAddUndoLevel|mceEndUndoLevel)$/i.test(lowerCaseCommand) && !skipFocus) { 38383 editor.focus(); 38384 } 38385 else { 38386 restore(editor); 38387 } 38388 } 38389 const eventArgs = editor.dispatch('BeforeExecCommand', { command, ui, value }); 38390 if (eventArgs.isDefaultPrevented()) { 38391 return false; 38392 } 38393 const func = this.commands.exec[lowerCaseCommand]; 38394 if (isFunction(func)) { 38395 func(lowerCaseCommand, ui, value, args); 38396 editor.dispatch('ExecCommand', { command, ui, value, args }); 38397 return true; 38398 } 38399 return false; 38400 } 38401 /** 38402 * Queries the current state for a command. For example: If the current selection is "bold". 38403 * 38404 * @method queryCommandState 38405 * @param {String} command Command to check the state of. 38406 * @return {Boolean} true/false - For example: If the selected contents is bold or not. 38407 */ 38408 queryCommandState(command) { 38409 if ((!isSelectionSafeCommand(command) && this.editor.quirks.isHidden()) || this.editor.removed) { 38410 return false; 38411 } 38412 const lowerCaseCommand = command.toLowerCase(); 38413 const func = this.commands.state[lowerCaseCommand]; 38414 if (isFunction(func)) { 38415 return func(lowerCaseCommand); 38416 } 38417 return false; 38418 } 38419 /** 38420 * Returns a command specific value, for example the current font size. 38421 * 38422 * @method queryCommandValue 38423 * @param {String} command Command to query value from. 38424 * @return {String} Command value, for example the current font size or an empty string (`""`) if the query command is not found. 38425 */ 38426 queryCommandValue(command) { 38427 if ((!isSelectionSafeCommand(command) && this.editor.quirks.isHidden()) || this.editor.removed) { 38428 return ''; 38429 } 38430 const lowerCaseCommand = command.toLowerCase(); 38431 const func = this.commands.value[lowerCaseCommand]; 38432 if (isFunction(func)) { 38433 return func(lowerCaseCommand); 38434 } 38435 return ''; 38436 } 38437 addCommands(commandList, type = 'exec') { 38438 const commands = this.commands; 38439 each$d(commandList, (callback, command) => { 38440 each$e(command.toLowerCase().split(','), (command) => { 38441 commands[type][command] = callback; 38442 }); 38443 }); 38444 } 38445 addCommand(command, callback, scope) { 38446 const lowerCaseCommand = command.toLowerCase(); 38447 this.commands.exec[lowerCaseCommand] = (_command, ui, value, args) => callback.call(scope ?? this.editor, ui, value, args); 38448 } 38449 /** 38450 * Removes a command from the command collection. 38451 * 38452 * @method removeCommand 38453 * @param {String} command Command name to remove. 38454 * @param {String} type Optional type to remove, defaults to removing all types. Can be 'exec', 'state', 'value', or omitted for all. 38455 */ 38456 removeCommand(command, type) { 38457 const lowerCaseCommand = command.toLowerCase(); 38458 if (type) { 38459 delete this.commands[type][lowerCaseCommand]; 38460 } 38461 else { 38462 delete this.commands.exec[lowerCaseCommand]; 38463 delete this.commands.state[lowerCaseCommand]; 38464 delete this.commands.value[lowerCaseCommand]; 38465 } 38466 } 38467 /** 38468 * Returns true/false if the command is supported or not. 38469 * 38470 * @method queryCommandSupported 38471 * @param {String} command Command that we check support for. 38472 * @return {Boolean} true/false if the command is supported or not. 38473 */ 38474 queryCommandSupported(command) { 38475 const lowerCaseCommand = command.toLowerCase(); 38476 if (this.commands.exec[lowerCaseCommand]) { 38477 return true; 38478 } 38479 else { 38480 return false; 38481 } 38482 } 38483 addQueryStateHandler(command, callback, scope) { 38484 this.commands.state[command.toLowerCase()] = () => callback.call(scope ?? this.editor); 38485 } 38486 addQueryValueHandler(command, callback, scope) { 38487 this.commands.value[command.toLowerCase()] = () => callback.call(scope ?? this.editor); 38488 } 38489 } 38490 38491 /** 38492 * This class lets you add/remove and dispatch events by name on the specified scope. This makes 38493 * it easy to add event listener logic to any class. 38494 * 38495 * @class tinymce.util.EventDispatcher 38496 * @example 38497 * const eventDispatcher = new EventDispatcher(); 38498 * 38499 * eventDispatcher.on('click', () => console.log('data')); 38500 * eventDispatcher.dispatch('click', { data: 123 }); 38501 */ 38502 const nativeEvents = Tools.makeMap('focus blur focusin focusout click dblclick mousedown mouseup mousemove mouseover beforepaste paste cut copy selectionchange ' + 38503 'mouseout mouseenter mouseleave wheel keydown keypress keyup input beforeinput contextmenu dragstart dragend dragover ' + 38504 'draggesture dragdrop drop drag submit ' + 38505 'compositionstart compositionend compositionupdate touchstart touchmove touchend touchcancel', ' '); 38506 class EventDispatcher { 38507 /** 38508 * Returns true/false if the specified event name is a native browser event or not. 38509 * 38510 * @method isNative 38511 * @param {String} name Name to check if it's native. 38512 * @return {Boolean} true/false if the event is native or not. 38513 * @static 38514 */ 38515 static isNative(name) { 38516 return !!nativeEvents[name.toLowerCase()]; 38517 } 38518 settings; 38519 scope; 38520 toggleEvent; 38521 bindings = {}; 38522 constructor(settings) { 38523 this.settings = settings || {}; 38524 this.scope = this.settings.scope || this; 38525 this.toggleEvent = this.settings.toggleEvent || never; 38526 } 38527 /** 38528 * Fires the specified event by name. 38529 * <br> 38530 * <em>Marked for removal in TinyMCE 8.0. Use <code>dispatch</code> instead.</em> 38531 * 38532 * @method fire 38533 * @param {String} name Name of the event to fire. 38534 * @param {Object?} args Event arguments. 38535 * @return {Object} Event args instance passed in. 38536 * @deprecated Use dispatch() instead 38537 * @example 38538 * instance.fire('event', {...}); 38539 */ 38540 fire(name, args) { 38541 return this.dispatch(name, args); 38542 } 38543 /** 38544 * Dispatches the specified event by name. 38545 * 38546 * @method dispatch 38547 * @param {String} name Name of the event to dispatch 38548 * @param {Object?} args Event arguments. 38549 * @return {Object} Event args instance passed in. 38550 * @example 38551 * instance.dispatch('event', {...}); 38552 */ 38553 dispatch(name, args) { 38554 const lcName = name.toLowerCase(); 38555 const event = normalize$3(lcName, args ?? {}, this.scope); 38556 if (this.settings.beforeFire) { 38557 this.settings.beforeFire(event); 38558 } 38559 // Don't clone the array here as this is a hot code path, so instead the handlers 38560 // array is recreated and the this.bindings[name] reference is updated in the `on` 38561 // and `off` functions. This is done to avoid the handlers array being mutated while 38562 // we're iterating over it below. 38563 const handlers = this.bindings[lcName]; 38564 if (handlers) { 38565 for (let i = 0, l = handlers.length; i < l; i++) { 38566 const callback = handlers[i]; 38567 // The handler was removed by an earlier handler in this loop so skip it. 38568 if (callback.removed) { 38569 continue; 38570 } 38571 // Unbind handlers marked with "once" 38572 if (callback.once) { 38573 this.off(lcName, callback.func); 38574 } 38575 // Stop immediate propagation if needed 38576 if (event.isImmediatePropagationStopped()) { 38577 return event; 38578 } 38579 // If callback returns false then prevent default and stop all propagation 38580 if (callback.func.call(this.scope, event) === false) { 38581 event.preventDefault(); 38582 return event; 38583 } 38584 } 38585 } 38586 return event; 38587 } 38588 /** 38589 * Binds an event listener to a specific event by name. 38590 * 38591 * @method on 38592 * @param {String} name Event name or space separated list of events to bind. 38593 * @param {Function} callback Callback to be executed when the event occurs. 38594 * @param {Boolean} prepend Optional flag if the event should be prepended. Use this with care. 38595 * @return {Object} Current class instance. 38596 * @example 38597 * instance.on('event', (e) => { 38598 * // Callback logic 38599 * }); 38600 */ 38601 on(name, callback, prepend, extra) { 38602 if (callback === false) { 38603 callback = never; 38604 } 38605 if (callback) { 38606 const wrappedCallback = { 38607 func: callback, 38608 removed: false 38609 }; 38610 if (extra) { 38611 Tools.extend(wrappedCallback, extra); 38612 } 38613 const names = name.toLowerCase().split(' '); 38614 let i = names.length; 38615 while (i--) { 38616 const currentName = names[i]; 38617 let handlers = this.bindings[currentName]; 38618 if (!handlers) { 38619 handlers = []; 38620 this.toggleEvent(currentName, true); 38621 } 38622 if (prepend) { 38623 handlers = [wrappedCallback, ...handlers]; 38624 } 38625 else { 38626 handlers = [...handlers, wrappedCallback]; 38627 } 38628 this.bindings[currentName] = handlers; 38629 } 38630 } 38631 return this; 38632 } 38633 /** 38634 * Unbinds an event listener to a specific event by name. 38635 * 38636 * @method off 38637 * @param {String?} name Name of the event to unbind. 38638 * @param {Function?} callback Callback to unbind. 38639 * @return {Object} Current class instance. 38640 * @example 38641 * // Unbind specific callback 38642 * instance.off('event', handler); 38643 * 38644 * // Unbind all listeners by name 38645 * instance.off('event'); 38646 * 38647 * // Unbind all events 38648 * instance.off(); 38649 */ 38650 off(name, callback) { 38651 if (name) { 38652 const names = name.toLowerCase().split(' '); 38653 let i = names.length; 38654 while (i--) { 38655 const currentName = names[i]; 38656 let handlers = this.bindings[currentName]; 38657 // Unbind all handlers 38658 if (!currentName) { 38659 each$d(this.bindings, (_value, bindingName) => { 38660 this.toggleEvent(bindingName, false); 38661 delete this.bindings[bindingName]; 38662 }); 38663 return this; 38664 } 38665 if (handlers) { 38666 // Unbind all by name 38667 if (!callback) { 38668 handlers.length = 0; 38669 } 38670 else { 38671 // Unbind specific handlers 38672 const filteredHandlers = partition$2(handlers, (handler) => handler.func === callback); 38673 handlers = filteredHandlers.fail; 38674 this.bindings[currentName] = handlers; 38675 // Mark the removed handlers in case this event is already being processed in `fire` 38676 each$e(filteredHandlers.pass, (handler) => { 38677 handler.removed = true; 38678 }); 38679 } 38680 if (!handlers.length) { 38681 this.toggleEvent(name, false); 38682 delete this.bindings[currentName]; 38683 } 38684 } 38685 } 38686 } 38687 else { 38688 each$d(this.bindings, (_value, name) => { 38689 this.toggleEvent(name, false); 38690 }); 38691 this.bindings = {}; 38692 } 38693 return this; 38694 } 38695 /** 38696 * Binds an event listener to a specific event by name 38697 * and automatically unbind the event once the callback fires. 38698 * 38699 * @method once 38700 * @param {String} name Event name or space separated list of events to bind. 38701 * @param {Function} callback Callback to be executed when the event occurs. 38702 * @param {Boolean} prepend Optional flag if the event should be prepended. Use this with care. 38703 * @return {Object} Current class instance. 38704 * @example 38705 * instance.once('event', (e) => { 38706 * // Callback logic 38707 * }); 38708 */ 38709 once(name, callback, prepend) { 38710 return this.on(name, callback, prepend, { once: true }); 38711 } 38712 /** 38713 * Returns true/false if the dispatcher has a event of the specified name. 38714 * 38715 * @method has 38716 * @param {String} name Name of the event to check for. 38717 * @return {Boolean} true/false if the event exists or not. 38718 */ 38719 has(name) { 38720 name = name.toLowerCase(); 38721 const binding = this.bindings[name]; 38722 return !(!binding || binding.length === 0); 38723 } 38724 } 38725 38726 /** 38727 * This mixin adds event binding logic to classes. Adapts the EventDispatcher class. 38728 * 38729 * @mixin tinymce.util.Observable 38730 */ 38731 const getEventDispatcher = (obj) => { 38732 if (!obj._eventDispatcher) { 38733 obj._eventDispatcher = new EventDispatcher({ 38734 scope: obj, 38735 toggleEvent: (name, state) => { 38736 if (EventDispatcher.isNative(name) && obj.toggleNativeEvent) { 38737 obj.toggleNativeEvent(name, state); 38738 } 38739 } 38740 }); 38741 } 38742 return obj._eventDispatcher; 38743 }; 38744 const Observable = { 38745 /** 38746 * Fires the specified event by name. Consult the 38747 * <a href="https://www.tiny.cloud/docs/tinymce/8/events/">event reference</a> for more details on each event. 38748 * <br> 38749 * <em>Deprecated in TinyMCE 6.0 and has been marked for removal in TinyMCE 9.0. Use <code>dispatch</code> instead.</em> 38750 * 38751 * @method fire 38752 * @param {String} name Name of the event to fire. 38753 * @param {Object?} args Event arguments. 38754 * @param {Boolean?} bubble True/false if the event is to be bubbled. 38755 * @return {Object} Event args instance passed in. 38756 * @deprecated Use dispatch() instead 38757 * @example 38758 * instance.fire('event', {...}); 38759 */ 38760 fire(name, args, bubble) { 38761 logFeatureDeprecationWarning('fire'); 38762 return this.dispatch(name, args, bubble); 38763 }, 38764 /** 38765 * Dispatches the specified event by name. Consult the 38766 * <a href="https://www.tiny.cloud/docs/tinymce/8/events/">event reference</a> for more details on each event. 38767 * 38768 * @method dispatch 38769 * @param {String} name Name of the event to dispatch. 38770 * @param {Object?} args Event arguments. 38771 * @param {Boolean?} bubble True/false if the event is to be bubbled. 38772 * @return {Object} Event args instance passed in. 38773 * @example 38774 * instance.dispatch('event', {...}); 38775 */ 38776 dispatch(name, args, bubble) { 38777 const self = this; 38778 // Prevent all events except the remove/detach event after the instance has been removed 38779 if (self.removed && name !== 'remove' && name !== 'detach') { 38780 return normalize$3(name.toLowerCase(), args ?? {}, self); 38781 } 38782 const dispatcherArgs = getEventDispatcher(self).dispatch(name, args); 38783 // Bubble event up to parents 38784 if (bubble !== false && self.parent) { 38785 let parent = self.parent(); 38786 while (parent && !dispatcherArgs.isPropagationStopped()) { 38787 parent.dispatch(name, dispatcherArgs, false); 38788 parent = parent.parent ? parent.parent() : undefined; 38789 } 38790 } 38791 return dispatcherArgs; 38792 }, 38793 /** 38794 * Binds an event listener to a specific event by name. Consult the 38795 * <a href="https://www.tiny.cloud/docs/tinymce/8/events/">event reference</a> for more details on each event. 38796 * 38797 * @method on 38798 * @param {String} name Event name or space separated list of events to bind. 38799 * @param {Function} callback Callback to be executed when the event occurs. 38800 * @param {Boolean} prepend Optional flag if the event should be prepended. Use this with care. 38801 * @return {Object} Current class instance. 38802 * @example 38803 * instance.on('event', (e) => { 38804 * // Callback logic 38805 * }); 38806 */ 38807 on(name, callback, prepend) { 38808 return getEventDispatcher(this).on(name, callback, prepend); 38809 }, 38810 /** 38811 * Unbinds an event listener to a specific event by name. Consult the 38812 * <a href="https://www.tiny.cloud/docs/tinymce/8/events/">event reference</a> for more details on each event. 38813 * 38814 * @method off 38815 * @param {String?} name Name of the event to unbind. 38816 * @param {Function?} callback Callback to unbind. 38817 * @return {Object} Current class instance. 38818 * @example 38819 * // Unbind specific callback 38820 * instance.off('event', handler); 38821 * 38822 * // Unbind all listeners by name 38823 * instance.off('event'); 38824 * 38825 * // Unbind all events 38826 * instance.off(); 38827 */ 38828 off(name, callback) { 38829 return getEventDispatcher(this).off(name, callback); 38830 }, 38831 /** 38832 * Bind the event callback and once it fires the callback is removed. Consult the 38833 * <a href="https://www.tiny.cloud/docs/tinymce/8/events/">event reference</a> for more details on each event. 38834 * 38835 * @method once 38836 * @param {String} name Name of the event to bind. 38837 * @param {Function} callback Callback to bind only once. 38838 * @return {Object} Current class instance. 38839 */ 38840 once(name, callback) { 38841 return getEventDispatcher(this).once(name, callback); 38842 }, 38843 /** 38844 * Returns true/false if the object has a event of the specified name. 38845 * 38846 * @method hasEventListeners 38847 * @param {String} name Name of the event to check for. 38848 * @return {Boolean} true/false if the event exists or not. 38849 */ 38850 hasEventListeners(name) { 38851 return getEventDispatcher(this).has(name); 38852 } 38853 }; 38854 38855 /** 38856 * This mixin contains the event logic for the tinymce.Editor class. 38857 * 38858 * @mixin tinymce.EditorObservable 38859 * @extends tinymce.util.Observable 38860 * @private 38861 */ 38862 const DOM$2 = DOMUtils.DOM; 38863 let customEventRootDelegates; 38864 /** 38865 * Returns the event target for the specified event. Some events fire 38866 * only on document, some fire on documentElement etc. This also handles the 38867 * custom event root setting where it returns that element instead of the body. 38868 * 38869 * @private 38870 * @param {tinymce.Editor} editor Editor instance to get event target from. 38871 * @param {String} eventName Name of the event for example "click". 38872 * @return {Element/Document} HTML Element or document target to bind on. 38873 */ 38874 const getEventTarget = (editor, eventName) => { 38875 if (eventName === 'selectionchange') { 38876 return editor.getDoc(); 38877 } 38878 // Need to bind mousedown/mouseup etc to document not body in iframe mode 38879 // Since the user might click on the HTML element not the BODY 38880 if (!editor.inline && /^(?:mouse|touch|click|contextmenu|drop|dragover|dragend)/.test(eventName)) { 38881 return editor.getDoc().documentElement; 38882 } 38883 // Bind to event root instead of body if it's defined 38884 const eventRoot = getEventRoot(editor); 38885 if (eventRoot) { 38886 if (!editor.eventRoot) { 38887 editor.eventRoot = DOM$2.select(eventRoot)[0]; 38888 } 38889 return editor.eventRoot; 38890 } 38891 return editor.getBody(); 38892 }; 38893 const isListening = (editor) => !editor.hidden && !isDisabled(editor); 38894 const fireEvent = (editor, eventName, e) => { 38895 if (isListening(editor)) { 38896 editor.dispatch(eventName, e); 38897 } 38898 else if (isDisabled(editor)) { 38899 processDisabledEvents(editor, e); 38900 } 38901 }; 38902 /** 38903 * Binds a event delegate for the specified name this delegate will fire 38904 * the event to the editor dispatcher. 38905 * 38906 * @private 38907 * @param {tinymce.Editor} editor Editor instance to get event target from. 38908 * @param {String} eventName Name of the event for example "click". 38909 */ 38910 const bindEventDelegate = (editor, eventName) => { 38911 if (!editor.delegates) { 38912 editor.delegates = {}; 38913 } 38914 if (editor.delegates[eventName] || editor.removed) { 38915 return; 38916 } 38917 const eventRootElm = getEventTarget(editor, eventName); 38918 if (getEventRoot(editor)) { 38919 if (!customEventRootDelegates) { 38920 customEventRootDelegates = {}; 38921 editor.editorManager.on('removeEditor', () => { 38922 if (!editor.editorManager.activeEditor) { 38923 if (customEventRootDelegates) { 38924 each$d(customEventRootDelegates, (_value, name) => { 38925 editor.dom.unbind(getEventTarget(editor, name)); 38926 }); 38927 customEventRootDelegates = null; 38928 } 38929 } 38930 }); 38931 } 38932 if (customEventRootDelegates[eventName]) { 38933 return; 38934 } 38935 const delegate = (e) => { 38936 const target = e.target; 38937 const editors = editor.editorManager.get(); 38938 let i = editors.length; 38939 while (i--) { 38940 const body = editors[i].getBody(); 38941 if (body === target || DOM$2.isChildOf(target, body)) { 38942 fireEvent(editors[i], eventName, e); 38943 } 38944 } 38945 }; 38946 customEventRootDelegates[eventName] = delegate; 38947 DOM$2.bind(eventRootElm, eventName, delegate); 38948 } 38949 else { 38950 const delegate = (e) => { 38951 fireEvent(editor, eventName, e); 38952 }; 38953 DOM$2.bind(eventRootElm, eventName, delegate); 38954 editor.delegates[eventName] = delegate; 38955 } 38956 }; 38957 const EditorObservable = { 38958 ...Observable, 38959 /** 38960 * Bind any pending event delegates. This gets executed after the target body/document is created. 38961 * 38962 * @private 38963 */ 38964 bindPendingEventDelegates() { 38965 const self = this; 38966 Tools.each(self._pendingNativeEvents, (name) => { 38967 bindEventDelegate(self, name); 38968 }); 38969 }, 38970 /** 38971 * Toggles a native event on/off this is called by the EventDispatcher when 38972 * the first native event handler is added and when the last native event handler is removed. 38973 * 38974 * @private 38975 */ 38976 toggleNativeEvent(name, state) { 38977 const self = this; 38978 // Never bind focus/blur since the FocusManager fakes those 38979 if (name === 'focus' || name === 'blur') { 38980 return; 38981 } 38982 // If the editor has been removed, `unbindAllNativeEvents` has already deleted all native event delegates 38983 if (self.removed) { 38984 return; 38985 } 38986 if (state) { 38987 if (self.initialized) { 38988 bindEventDelegate(self, name); 38989 } 38990 else { 38991 if (!self._pendingNativeEvents) { 38992 self._pendingNativeEvents = [name]; 38993 } 38994 else { 38995 self._pendingNativeEvents.push(name); 38996 } 38997 } 38998 } 38999 else if (self.initialized && self.delegates) { 39000 self.dom.unbind(getEventTarget(self, name), name, self.delegates[name]); 39001 delete self.delegates[name]; 39002 } 39003 }, 39004 /** 39005 * Unbinds all native event handlers that means delegates, custom events bound using the Events API etc. 39006 * 39007 * @private 39008 */ 39009 unbindAllNativeEvents() { 39010 const self = this; 39011 const body = self.getBody(); 39012 const dom = self.dom; 39013 if (self.delegates) { 39014 each$d(self.delegates, (value, name) => { 39015 self.dom.unbind(getEventTarget(self, name), name, value); 39016 }); 39017 delete self.delegates; 39018 } 39019 if (!self.inline && body && dom) { 39020 body.onload = null; 39021 dom.unbind(self.getWin()); 39022 dom.unbind(self.getDoc()); 39023 } 39024 if (dom) { 39025 dom.unbind(body); 39026 dom.unbind(self.getContainer()); 39027 } 39028 } 39029 }; 39030 39031 // A string array allows comma/space separated values as well for ease of use 39032 const stringListProcessor = (value) => { 39033 if (isString(value)) { 39034 return { value: value.split(/[ ,]/), valid: true }; 39035 } 39036 else if (isArrayOf(value, isString)) { 39037 return { value, valid: true }; 39038 } 39039 else { 39040 return { valid: false, message: `The value must be a string[] or a comma/space separated string.` }; 39041 } 39042 }; 39043 const getBuiltInProcessor = (type) => { 39044 const validator = (() => { 39045 switch (type) { 39046 case 'array': 39047 return isArray$1; 39048 case 'boolean': 39049 return isBoolean; 39050 case 'function': 39051 return isFunction; 39052 case 'number': 39053 return isNumber; 39054 case 'object': 39055 return isObject; 39056 case 'string': 39057 return isString; 39058 case 'string[]': 39059 return stringListProcessor; 39060 case 'object[]': 39061 return (val) => isArrayOf(val, isObject); 39062 case 'regexp': 39063 return (val) => is$5(val, RegExp); 39064 default: 39065 return always; 39066 } 39067 })(); 39068 return (value) => processValue(value, validator, `The value must be a ${type}.`); 39069 }; 39070 const isBuiltInSpec = (spec) => isString(spec.processor); 39071 const getErrorMessage = (message, result) => { 39072 const additionalText = isEmpty$5(result.message) ? '' : `. ${result.message}`; 39073 return message + additionalText; 39074 }; 39075 const isValidResult = (result) => result.valid; 39076 const processValue = (value, processor, message = '') => { 39077 const result = processor(value); 39078 if (isBoolean(result)) { 39079 // Note: Need to cast here as if a boolean is returned then we're guaranteed to be returning the same value 39080 return result ? { value: value, valid: true } : { valid: false, message }; 39081 } 39082 else { 39083 return result; 39084 } 39085 }; 39086 const processDefaultValue = (name, defaultValue, processor) => { 39087 if (!isUndefined(defaultValue)) { 39088 const result = processValue(defaultValue, processor); 39089 if (isValidResult(result)) { 39090 return result.value; 39091 } 39092 else { 39093 // eslint-disable-next-line no-console 39094 console.error(getErrorMessage(`Invalid default value passed for the "${name}" option`, result)); 39095 } 39096 } 39097 return undefined; 39098 }; 39099 const create$4 = (editor, initialOptions, rawInitialOptions = initialOptions) => { 39100 const registry = {}; 39101 const values = {}; 39102 const setValue = (name, value, processor) => { 39103 const result = processValue(value, processor); 39104 if (isValidResult(result)) { 39105 values[name] = result.value; 39106 return true; 39107 } 39108 else { 39109 // eslint-disable-next-line no-console 39110 console.warn(getErrorMessage(`Invalid value passed for the ${name} option`, result)); 39111 return false; 39112 } 39113 }; 39114 const register = (name, spec) => { 39115 const processor = isBuiltInSpec(spec) ? getBuiltInProcessor(spec.processor) : spec.processor; 39116 // Process and validate the default value 39117 const defaultValue = processDefaultValue(name, spec.default, processor); 39118 // Register the spec with the validated default and normalized processor 39119 registry[name] = { 39120 ...spec, 39121 default: defaultValue, 39122 processor 39123 }; 39124 // Setup the initial values 39125 const initValue = get$
39125a(values, name).orThunk(() => get$a(initialOptions, name)); 39126 initValue.each((value) => setValue(name, value, processor)); 39127 }; 39128 const isRegistered = (name) => has$2(registry, name); 39129 const get = (name) => get$a(values, name) 39130 .orThunk(() => get$a
vendor: 37,194 bytes, lines 39130-40058
39130(registry, name).map((spec) => spec.default)) 39131 .getOrUndefined(); 39132 const set = (name, value) => { 39133 if (!isRegistered(name)) { 39134 // eslint-disable-next-line no-console 39135 console.warn(`"${name}" is not a registered option. Ensure the option has been registered before setting a value.`); 39136 return false; 39137 } 39138 else { 39139 const spec = registry[name]; 39140 if (spec.immutable) { 39141 // eslint-disable-next-line no-console 39142 console.error(`"${name}" is an immutable option and cannot be updated`); 39143 return false; 39144 } 39145 else { 39146 return setValue(name, value, spec.processor); 39147 } 39148 } 39149 }; 39150 const unset = (name) => { 39151 const registered = isRegistered(name); 39152 if (registered) { 39153 delete values[name]; 39154 } 39155 return registered; 39156 }; 39157 const isSet = (name) => has$2(values, name); 39158 const debug = () => { 39159 try { 39160 // eslint-disable-next-line no-console 39161 console.log(JSON.parse(JSON.stringify(rawInitialOptions, (_key, value) => { 39162 if (isBoolean(value) || 39163 isNumber(value) || 39164 isString(value) || 39165 isNull(value) || 39166 isArray$1(value) || 39167 isPlainObject(value)) { 39168 return value; 39169 } 39170 return Object.prototype.toString.call(value); 39171 }))); 39172 } 39173 catch (error) { 39174 // eslint-disable-next-line no-console 39175 console.error(error); 39176 } 39177 }; 39178 return { 39179 register, 39180 isRegistered, 39181 get, 39182 set, 39183 unset, 39184 isSet, 39185 debug, 39186 }; 39187 }; 39188 39189 const setContentEditable = (elm, state) => { 39190 elm.dom.contentEditable = state ? 'true' : 'false'; 39191 }; 39192 const toggleReadOnly = (editor, state) => { 39193 const body = SugarElement.fromDom(editor.getBody()); 39194 if (state) { 39195 editor.readonly = true; 39196 if (editor.hasEditableRoot()) { 39197 setContentEditable(body, true); 39198 } 39199 disableEditor(editor); 39200 } 39201 else { 39202 editor.readonly = false; 39203 enableEditor(editor); 39204 } 39205 }; 39206 const isReadOnly = (editor) => editor.readonly; 39207 const rollbackChange = (editor) => { 39208 const undoLevel = editor.undoManager.add(); 39209 if (isNonNullable(undoLevel)) { 39210 editor.undoManager.undo(); 39211 editor.undoManager.reset(); 39212 } 39213 }; 39214 const hasContentMutations = (mutations) => exists(mutations, (mutation) => mutation.type === 'characterData' || mutation.type === 'childList'); 39215 const registerReadOnlyInputBlockers = (editor) => { 39216 const handleMutations = (mutations) => { 39217 if (isReadOnly(editor) && hasContentMutations(mutations)) { 39218 rollbackChange(editor); 39219 } 39220 }; 39221 // Set up mutation observer to detect and revert unintended changes 39222 const observer = new MutationObserver(handleMutations); 39223 editor.on('beforeinput paste cut dragend dragover draggesture dragdrop drop drag', (e) => { 39224 if (isReadOnly(editor)) { 39225 e.preventDefault(); 39226 } 39227 }); 39228 editor.on('BeforeExecCommand', (e) => { 39229 if ((e.command === 'Undo' || e.command === 'Redo') && isReadOnly(editor)) { 39230 e.preventDefault(); 39231 } 39232 }); 39233 editor.on('compositionstart', () => { 39234 if (isReadOnly(editor)) { 39235 observer.observe(editor.getBody(), { 39236 characterData: true, 39237 childList: true, 39238 subtree: true 39239 }); 39240 } 39241 }); 39242 editor.on('compositionend', () => { 39243 if (isReadOnly(editor)) { 39244 const records = observer.takeRecords(); 39245 handleMutations(records); 39246 } 39247 observer.disconnect(); 39248 }); 39249 }; 39250 39251 const defaultModes = ['design', 'readonly']; 39252 const switchToMode = (editor, activeMode, availableModes, mode) => { 39253 const oldMode = availableModes[activeMode.get()]; 39254 const newMode = availableModes[mode]; 39255 // if activate fails, hope nothing bad happened and abort 39256 try { 39257 newMode.activate(); 39258 } 39259 catch (e) { 39260 // eslint-disable-next-line no-console 39261 console.error(`problem while activating editor mode ${mode}:`, e); 39262 return; 39263 } 39264 oldMode.deactivate(); 39265 if (oldMode.editorReadOnly !== newMode.editorReadOnly) { 39266 toggleReadOnly(editor, newMode.editorReadOnly); 39267 } 39268 activeMode.set(mode); 39269 fireSwitchMode(editor, mode); 39270 }; 39271 const setMode = (editor, availableModes, activeMode, mode) => { 39272 if (mode === activeMode.get() || (editor.initialized && isDisabled(editor))) { 39273 return; 39274 } 39275 else if (!has$2(availableModes, mode)) { 39276 throw new Error(`Editor mode '${mode}' is invalid`); 39277 } 39278 if (editor.initialized) { 39279 switchToMode(editor, activeMode, availableModes, mode); 39280 } 39281 else { 39282 editor.on('init', () => switchToMode(editor, activeMode, availableModes, mode)); 39283 } 39284 }; 39285 const registerMode = (availableModes, mode, api) => { 39286 if (contains$2(defaultModes, mode)) { 39287 throw new Error(`Cannot override default mode ${mode}`); 39288 } 39289 return { 39290 ...availableModes, 39291 [mode]: { 39292 ...api, 39293 deactivate: () => { 39294 // wrap custom deactivate APIs so they can't break the editor 39295 try { 39296 api.deactivate(); 39297 } 39298 catch (e) { 39299 // eslint-disable-next-line no-console 39300 console.error(`problem while deactivating editor mode ${mode}:`, e); 39301 } 39302 } 39303 } 39304 }; 39305 }; 39306 39307 const create$3 = (editor) => { 39308 const activeMode = Cell('design'); 39309 const availableModes = Cell({ 39310 design: { 39311 activate: noop, 39312 deactivate: noop, 39313 editorReadOnly: false 39314 }, 39315 readonly: { 39316 activate: noop, 39317 deactivate: noop, 39318 editorReadOnly: true 39319 } 39320 }); 39321 registerReadOnlyInputBlockers(editor); 39322 registerEventsAndFilters$1(editor); 39323 return { 39324 isReadOnly: () => isReadOnly(editor), 39325 set: (mode) => setMode(editor, availableModes.get(), activeMode, mode), 39326 get: () => activeMode.get(), 39327 register: (mode, api) => { 39328 availableModes.set(registerMode(availableModes.get(), mode, api)); 39329 } 39330 }; 39331 }; 39332 39333 /** 39334 * Contains logic for handling keyboard shortcuts. 39335 * 39336 * @class tinymce.Shortcuts 39337 * @example 39338 * editor.shortcuts.add('ctrl+a', 'description of the shortcut', () => {}); 39339 * editor.shortcuts.add('ctrl+alt+a', 'description of the shortcut', () => {}); 39340 * // "meta" maps to Command on Mac and Ctrl on PC 39341 * editor.shortcuts.add('meta+a', 'description of the shortcut', () => {}); 39342 * // "access" maps to Control+Option on Mac and shift+alt on PC 39343 * editor.shortcuts.add('access+a', 'description of the shortcut', () => {}); 39344 * 39345 * editor.shortcuts.add('meta+access+c', 'Opens the code editor dialog.', () => { 39346 * editor.execCommand('mceCodeEditor'); 39347 * }); 39348 * 39349 * editor.shortcuts.add('meta+shift+32', 'Inserts "Hello, World!" for meta+shift+space', () => { 39350 * editor.execCommand('mceInsertContent', false, 'Hello, World!'); 39351 * }); 39352 */ 39353 const each$2 = Tools.each, explode = Tools.explode; 39354 const keyCodeLookup = { 39355 f1: 112, 39356 f2: 113, 39357 f3: 114, 39358 f4: 115, 39359 f5: 116, 39360 f6: 117, 39361 f7: 118, 39362 f8: 119, 39363 f9: 120, 39364 f10: 121, 39365 f11: 122, 39366 f12: 123 39367 }; 39368 const modifierNames = Tools.makeMap('alt,ctrl,shift,meta,access'); 39369 const isModifier = (key) => key in modifierNames; 39370 const parseShortcut = (pattern) => { 39371 const shortcut = {}; 39372 const isMac = Env.os.isMacOS() || Env.os.isiOS(); 39373 // Parse modifiers and keys ctrl+alt+b for example 39374 each$2(explode(pattern.toLowerCase(), '+'), (value) => { 39375 if (isModifier(value)) { 39376 shortcut[value] = true; 39377 } 39378 else { 39379 // Allow numeric keycodes like ctrl+219 for ctrl+[ 39380 if (/^[0-9]{2,}$/.test(value)) { 39381 shortcut.keyCode = parseInt(value, 10); 39382 } 39383 else { 39384 shortcut.charCode = value.charCodeAt(0); 39385 shortcut.keyCode = keyCodeLookup[value] || value.toUpperCase().charCodeAt(0); 39386 } 39387 } 39388 }); 39389 // Generate unique id for modifier combination and set default state for unused modifiers 39390 const id = [shortcut.keyCode]; 39391 let key; 39392 for (key in modifierNames) { 39393 if (shortcut[key]) { 39394 id.push(key); 39395 } 39396 else { 39397 shortcut[key] = false; 39398 } 39399 } 39400 shortcut.id = id.join(','); 39401 // Handle special access modifier differently depending on Mac/Win 39402 if (shortcut.access) { 39403 shortcut.alt = true; 39404 if (isMac) { 39405 shortcut.ctrl = true; 39406 } 39407 else { 39408 shortcut.shift = true; 39409 } 39410 } 39411 // Handle special meta modifier differently depending on Mac/Win 39412 if (shortcut.meta) { 39413 if (isMac) { 39414 shortcut.meta = true; 39415 } 39416 else { 39417 shortcut.ctrl = true; 39418 shortcut.meta = false; 39419 } 39420 } 39421 return shortcut; 39422 }; 39423 class Shortcuts { 39424 editor; 39425 shortcuts = {}; 39426 pendingPatterns = []; 39427 constructor(editor) { 39428 this.editor = editor; 39429 const self = this; 39430 editor.on('keyup keypress keydown', (e) => { 39431 if ((self.hasModifier(e) || self.isFunctionKey(e)) && !e.isDefaultPrevented()) { 39432 each$2(self.shortcuts, (shortcut) => { 39433 if (self.matchShortcut(e, shortcut)) { 39434 self.pendingPatterns = shortcut.subpatterns.slice(0); 39435 if (e.type === 'keydown') { 39436 self.executeShortcutAction(shortcut); 39437 } 39438 } 39439 }); 39440 if (self.matchShortcut(e, self.pendingPatterns[0])) { 39441 if (self.pendingPatterns.length === 1) { 39442 if (e.type === 'keydown') { 39443 self.executeShortcutAction(self.pendingPatterns[0]); 39444 } 39445 } 39446 self.pendingPatterns.shift(); 39447 } 39448 } 39449 }); 39450 } 39451 /** 39452 * Adds a keyboard shortcut for some command or function. 39453 * 39454 * @method add 39455 * @param {String} pattern Shortcut pattern. Like for example: ctrl+alt+o. 39456 * @param {String} desc Text description for the command. 39457 * @param {String/Function} cmdFunc Command name string or function to execute when the key is pressed. 39458 * @param {Object} scope Optional scope to execute the function in. 39459 * @return {Boolean} true/false state if the shortcut was added or not. 39460 */ 39461 add(pattern, desc, cmdFunc, scope) { 39462 const self = this; 39463 const func = self.normalizeCommandFunc(cmdFunc); 39464 each$2(explode(Tools.trim(pattern)), (pattern) => { 39465 const shortcut = self.createShortcut(pattern, desc, func, scope); 39466 self.shortcuts[shortcut.id] = shortcut; 39467 }); 39468 return true; 39469 } 39470 /** 39471 * Remove a keyboard shortcut by pattern. 39472 * 39473 * @method remove 39474 * @param {String} pattern Shortcut pattern. Like for example: ctrl+alt+o. 39475 * @return {Boolean} true/false state if the shortcut was removed or not. 39476 */ 39477 remove(pattern) { 39478 const shortcut = this.createShortcut(pattern); 39479 if (this.shortcuts[shortcut.id]) { 39480 delete this.shortcuts[shortcut.id]; 39481 return true; 39482 } 39483 return false; 39484 } 39485 normalizeCommandFunc(cmdFunc) { 39486 const self = this; 39487 const cmd = cmdFunc; 39488 if (typeof cmd === 'string') { 39489 return () => { 39490 self.editor.execCommand(cmd, false, null); 39491 }; 39492 } 39493 else if (Tools.isArray(cmd)) { 39494 return () => { 39495 self.editor.execCommand(cmd[0], cmd[1], cmd[2]); 39496 }; 39497 } 39498 else { 39499 return cmd; 39500 } 39501 } 39502 createShortcut(pattern, desc, cmdFunc, scope) { 39503 const shortcuts = Tools.map(explode(pattern, '>'), parseShortcut); 39504 shortcuts[shortcuts.length - 1] = Tools.extend(shortcuts[shortcuts.length - 1], { 39505 func: cmdFunc, 39506 scope: scope || this.editor 39507 }); 39508 return Tools.extend(shortcuts[0], { 39509 desc: this.editor.translate(desc), 39510 subpatterns: shortcuts.slice(1) 39511 }); 39512 } 39513 hasModifier(e) { 39514 return e.altKey || e.ctrlKey || e.metaKey; 39515 } 39516 isFunctionKey(e) { 39517 return e.type === 'keydown' && e.keyCode >= 112 && e.keyCode <= 123; 39518 } 39519 matchShortcut(e, shortcut) { 39520 if (!shortcut) { 39521 return false; 39522 } 39523 if (shortcut.ctrl !== e.ctrlKey || shortcut.meta !== e.metaKey) { 39524 return false; 39525 } 39526 if (shortcut.alt !== e.altKey || shortcut.shift !== e.shiftKey) { 39527 return false; 39528 } 39529 if (e.keyCode === shortcut.keyCode || (e.charCode && e.charCode === shortcut.charCode)) { 39530 e.preventDefault(); 39531 return true; 39532 } 39533 return false; 39534 } 39535 executeShortcutAction(shortcut) { 39536 return shortcut.func ? shortcut.func.call(shortcut.scope) : null; 39537 } 39538 } 39539 39540 /** 39541 * TinyMCE UI registration API. 39542 * 39543 * @class tinymce.editor.ui.Registry 39544 */ 39545 const registry = () => { 39546 const bridge = create$6(); 39547 return { 39548 /** 39549 * Registers a new auto completer component. When a configured string pattern 39550 * is matched in the content while typing, the autocompleter will be triggered. 39551 * Emoticons and Charmap use an autocompleter. 39552 * <br> 39553 * For information on creating an autocompleter, see: 39554 * <a href="https://www.tiny.cloud/docs/tinymce/8/autocompleter/"> 39555 * UI Components - Autocompleter</a>. 39556 * 39557 * @method addAutocompleter 39558 * @param {String} name Unique name identifying this autocomplete configuration. 39559 * @param {InlineContent.AutocompleterSpec} obj The autocomplete configuration object. 39560 */ 39561 addAutocompleter: bridge.addAutocompleter, 39562 /** 39563 * Registers a new toolbar button that executes a command when clicked or activated 39564 * via keyboard navigation controls. 39565 * <br> 39566 * For information on creating a basic toolbar button, see: 39567 * <a href="https://www.tiny.cloud/docs/tinymce/8/custom-basic-toolbar-button/"> 39568 * UI Components - Types of toolbar buttons: Basic button</a>. 39569 * 39570 * @method addButton 39571 * @param {String} name Unique name identifying the button, this button name will be used in the toolbar configuration to reference the button. 39572 * @param {Toolbar.ToolbarButtonSpec} obj the button configuration object. 39573 */ 39574 addButton: bridge.addButton, 39575 /** 39576 * Registers a new contextual form item. 39577 * Similar to a context menu item, a contextual form is an item with an input 39578 * form element appearing when a content predicate is matched. An example 39579 * of a contextual form is the link plugin when the configuration 39580 * { link_context_toolbar: true } is used. When the cursor is on a link, a 39581 * contextual input form appears allowing for quick changes to the url field. 39582 * <br> 39583 * For information on creating context forms, see: 39584 * <a href="https://www.tiny.cloud/docs/tinymce/8/contextform/"> 39585 * UI Components - Context forms</a>. 39586 * 39587 * @method addContextForm 39588 * @param {String} name Unique name identifying the new contextual form item. 39589 * @param {Toolbar.ContextFormSpec} obj the context form configuration object. 39590 */ 39591 addContextForm: bridge.addContextForm, 39592 /** 39593 * Registers a new context menu section that only appears when a content predicate is matched, 39594 * for example, the cursor is inside a table. 39595 * <br> 39596 * For information on creating context menus, see: 39597 * <a href="https://www.tiny.cloud/docs/tinymce/8/contextmenu/"> 39598 * UI Components - Context Menu</a>. 39599 * 39600 * @method addContextMenu 39601 * @param {String} name Unique name identifying the new context menu. 39602 * @param {Menu.ContextMenuSpec} obj The context menu configuration object. 39603 */ 39604 addContextMenu: bridge.addContextMenu, 39605 /** 39606 * Registers a new context toolbar that only appears when a content predicate is matched for example 39607 * the cursor is on an image element. 39608 * <br> 39609 * For information on creating context toolbars, see: 39610 * <a href="https://www.tiny.cloud/docs/tinymce/8/contexttoolbar/"> 39611 * UI Components - Context Toolbar</a>. 39612 * 39613 * @method addContextToolbar 39614 * @param {String} name Unique name identifying the new context toolbar. 39615 * @param {Toolbar.ContextToolbarSpec} obj The context menu configuration object. 39616 */ 39617 addContextToolbar: bridge.addContextToolbar, 39618 /** 39619 * Registers a new SVG icon. The icon name reference can be configured by any 39620 * TinyMCE UI components that can display an icon. The icon is only available 39621 * to the editor instance it was configured for. 39622 * 39623 * @method addIcon 39624 * @param {String} name Unique name identifying the new icon. 39625 * @param {String} svgData The SVG data string the browser will use to render the SVG icon. 39626 * @example 39627 * //To add a simple triangle icon: 39628 * editor.ui.registry.addIcon('triangleUp', '<svg height="24" width="24"><path d="M12 0 L24 24 L0 24 Z" /></svg>'); 39629 */ 39630 addIcon: bridge.addIcon, 39631 /** 39632 * Registers a new menu button. Adds a toolbar button that opens a menu when 39633 * clicked. The menu can be populated by items created by addMenuItem, 39634 * addNestedMenuItem or addToggleMenuItem. 39635 * <br> 39636 * For information on creating a toolbar menu button, see: 39637 * <a href="https://www.tiny.cloud/docs/tinymce/8/custom-menu-toolbar-button/"> 39638 * UI Components - Types of toolbar buttons: Menu button</a>. 39639 * 39640 * @method addMenuButton 39641 * @param {String} name Unique name identifying the new menu button. 39642 * @param {Toolbar.ToolbarMenuButtonSpec} obj The menu button configuration object. 39643 */ 39644 addMenuButton: bridge.addMenuButton, 39645 /** 39646 * Registers a new menu item that executes a command when clicked or activated 39647 * via keyboard navigation controls. 39648 * <br> 39649 * For information on creating a basic menu item, see: 39650 * <a href="https://www.tiny.cloud/docs/tinymce/8/creating-custom-menu-items/"> 39651 * UI Components - Custom menu items: Basic menu items</a>. 39652 * 39653 * @method addMenuItem 39654 * @param {String} name Unique name identifying the new menu item. 39655 * @param {Menu.MenuItemSpec} obj The menu item configuration object. 39656 */ 39657 addMenuItem: bridge.addMenuItem, 39658 /** 39659 * Registers a new menu item that reveals a submenu when clicked or activated 39660 * by keyboard navigation controls.The submenu can be populated by items 39661 * created by addMenuItem, addNestedMenuItem or addToggleMenuItem. 39662 * <br> 39663 * For information on creating a nested menu item, see: 39664 * <a href="https://www.tiny.cloud/docs/tinymce/8/custom-nested-menu-items/"> 39665 * UI Components - Custom menu items: Nested menu items</a>. 39666 * 39667 * @method addNestedMenuItem 39668 * @param {String} name Unique name identifying the new nested menu item. 39669 * @param {Menu.NestedMenuItemSpec} obj The nested menu item configuration object. 39670 */ 39671 addNestedMenuItem: bridge.addNestedMenuItem, 39672 /** 39673 * Registers a new sidebar container. 39674 * This sidebar container is attached to the right side of the editor and 39675 * can be toggled open or closed. When registered, a new toolbar toggle 39676 * button with the same sidebar name is created. Additionally there is a 39677 * ToggleSidebar command and a 'ToggleSidebar' event that can used to 39678 * manage the sidebar open/closed state. The tinycomments plugin uses a 39679 * sidebar for its Ui components. 39680 * <br> 39681 * For information on creating a custom sidebar, see: 39682 * <a href="https://www.tiny.cloud/docs/tinymce/8/customsidebar/"> 39683 * UI Components - Custom sidebar</a>. 39684 * 39685 * @method addSidebar 39686 * @param {String} name Unique name identifying the new sidebar. 39687 * @param {Sidebar.SidebarSpec} obj The sidebar configuration object. 39688 */ 39689 addSidebar: bridge.addSidebar, 39690 /** 39691 * Registers a new split button for the toolbar. The list styles plugin uses 39692 * a split button to simplify its functionality. 39693 * <br> 39694 * For information on creating a split toolbar button, see: 39695 * <a href="https://www.tiny.cloud/docs/tinymce/8/custom-split-toolbar-button/"> 39696 * UI Components - Types of toolbar buttons: Split button</a>. 39697 * 39698 * @method addSplitButton 39699 * @param {String} name Unique name identifying the new split button. 39700 * @param {Toolbar.ToolbarSplitButtonSpec} obj The split button configuration object. 39701 */ 39702 addSplitButton: bridge.addSplitButton, 39703 /** 39704 * Registers a new toggle button for the toolbar. A toggle buttons state can 39705 * be set in the configuration. 39706 * <br> 39707 * For information on creating a toggle toolbar button, see: 39708 * <a href="https://www.tiny.cloud/docs/tinymce/8/custom-toggle-toolbar-button/"> 39709 * UI Components - Types of toolbar buttons: Toggle button</a>. 39710 * 39711 * @method addToggleButton 39712 * @param {String} name Unique name identifying the new split button. 39713 * @param {Toolbar.ToolbarToggleButtonSpec} obj The toggle button configuration object. 39714 */ 39715 addToggleButton: bridge.addToggleButton, 39716 /** 39717 * Registers a new group toolbar button for the toolbar. Renders a toolbar button that opens a floating toolbar when 39718 * clicked. 39719 * <br> 39720 * <strong>Note:</strong> Group toolbar buttons can only be used when using the floating toolbar mode. 39721 * <br> 39722 * For information on creating a group toolbar button, see: 39723 * <a href="https://www.tiny.cloud/docs/tinymce/8/custom-group-toolbar-button/"> 39724 * UI Components - Types of toolbar buttons: Group toolbar button</a>. 39725 * 39726 * @method addGroupToolbarButton 39727 * @param {String} name Unique name identifying the new group toolbar button. 39728 * @param {Toolbar.GroupToolbarButtonSpec} obj The group toolbar button configuration object. 39729 */ 39730 addGroupToolbarButton: bridge.addGroupToolbarButton, 39731 /** 39732 * Registers a new menu item that will act like a toggle button, 39733 * showing a tick in the menu item to represent state. 39734 * <br> 39735 * For information on creating a toggle menu item, see: 39736 * <a href="https://www.tiny.cloud/docs/tinymce/8/custom-toggle-menu-items/"> 39737 * UI Components - Custom menu items: Toggle menu items</a>. 39738 * 39739 * @method addToggleMenuItem 39740 * @param {String} name Unique name identifying the new menu item. 39741 * @param {Menu.ToggleMenuItemSpec} obj The menu item configuration object. 39742 */ 39743 addToggleMenuItem: bridge.addToggleMenuItem, 39744 /** 39745 * Registers a new view container. 39746 * This view container is hidden (off) by default and attached next to the main view. 39747 * It can be toggled on or off. 39748 * When it is on, the main editor view is hidden and the specific view is shown. 39749 * When it is off, the specific view is hidden and the main view is shown. 39750 * There is also a ToggleView command. 39751 * The ToggleView command can toggle the view visibility. 39752 * The ToggleView command can be queried for its current state. 39753 * <br> 39754 * For information on creating a custom view, see: 39755 * <a href="https://www.tiny.cloud/docs/tinymce/8/custom-view/"> 39756 * UI Components - Custom view</a>. 39757 * 39758 * @method addView 39759 * @param {String} name Unique name identifying the new view. 39760 * @param {View.ViewSpec} obj The view configuration object. 39761 */ 39762 addView: bridge.addView, 39763 /** 39764 * Registers a new context configuration in the registry. 39765 * The registry stores all context configurations. 39766 * The buttons in editor configuration object can contain a context property. 39767 * These button specifications can use the registered contexts to determine 39768 * whether to enable or disable the buttons based on the current context. 39769 * 39770 * @method addContext 39771 * @param {String} name Unique name identifying the new context configuration. 39772 * @param {Function} pred A predicate function that determines if the context is active 39773 */ 39774 addContext: bridge.addContext, 39775 /* note getAll is an internal method and may not be supported in future revisions */ 39776 getAll: bridge.getAll 39777 }; 39778 }; 39779 39780 // Shorten these names 39781 const DOM$1 = DOMUtils.DOM; 39782 const extend = Tools.extend, each$1 = Tools.each; 39783 /** 39784 * Include Editor API docs. 39785 * 39786 * @include ../../../../../tools/docs/tinymce.Editor.js 39787 */ 39788 class Editor { 39789 baseUri; 39790 /** 39791 * Editor instance id, normally the same as the div/textarea that was replaced. 39792 * 39793 * @property id 39794 * @type String 39795 */ 39796 id; 39797 /** 39798 * A uuid string to uniquely identify an editor across any page. 39799 * 39800 * @property editorUid 39801 * @type String 39802 */ 39803 editorUid; 39804 /** 39805 * Name/Value object containing plugin instances. 39806 * 39807 * @property plugins 39808 * @type Object 39809 * @example 39810 * // Execute a method inside a plugin directly 39811 * tinymce.activeEditor.plugins.someplugin.someMethod(); 39812 */ 39813 plugins = {}; 39814 /** 39815 * URI object to document configured for the TinyMCE instance. 39816 * 39817 * @property documentBaseURI 39818 * @type tinymce.util.URI 39819 * @example 39820 * // Get relative URL from the location of document_base_url 39821 * tinymce.activeEditor.documentBaseURI.toRelative('/somedir/somefile.htm'); 39822 * 39823 * // Get absolute URL from the location of document_base_url 39824 * tinymce.activeEditor.documentBaseURI.toAbsolute('somefile.htm'); 39825 */ 39826 documentBaseURI; 39827 /** 39828 * URI object to current document that holds the TinyMCE editor instance. 39829 * 39830 * @property baseURI 39831 * @type tinymce.util.URI 39832 * @example 39833 * // Get relative URL from the location of the API 39834 * tinymce.activeEditor.baseURI.toRelative('/somedir/somefile.htm'); 39835 * 39836 * // Get absolute URL from the location of the API 39837 * tinymce.activeEditor.baseURI.toAbsolute('somefile.htm'); 39838 */ 39839 baseURI; 39840 /** 39841 * Array with CSS files to load into the iframe. 39842 * 39843 * @property contentCSS 39844 * @type Array 39845 */ 39846 contentCSS = []; 39847 /** 39848 * Array of CSS styles to add to head of document when the editor loads. 39849 * 39850 * @property contentStyles 39851 * @type Array 39852 */ 39853 contentStyles = []; 39854 /** 39855 * Editor ui components 39856 * 39857 * @property ui 39858 * @type tinymce.editor.ui.Ui 39859 */ 39860 ui; 39861 /** 39862 * Editor mode API 39863 * 39864 * @property mode 39865 * @type tinymce.EditorMode 39866 */ 39867 mode; 39868 /** 39869 * Editor options API 39870 * 39871 * @property options 39872 * @type tinymce.EditorOptions 39873 */ 39874 options; 39875 /** 39876 * Editor upload API 39877 * 39878 * @property editorUpload 39879 * @type tinymce.EditorUpload 39880 */ 39881 editorUpload; 39882 /** 39883 * Editor user lookup API 39884 * 39885 * @property userLookup 39886 * @type tinymce.UserLookup 39887 */ 39888 userLookup; 39889 shortcuts; 39890 loadedCSS = {}; 39891 editorCommands; 39892 suffix; 39893 editorManager; 39894 hidden; 39895 inline; 39896 hasVisual; 39897 isNotDirty = false; 39898 // Arguments set later, for example by InitContentBody.ts 39899 // Note that these may technically be undefined up until PreInit (or similar) has fired, 39900 // however the types are aimed at an initialised editor for ease of use. 39901 annotator; 39902 bodyElement; 39903 bookmark; // Note: Intentionally any so as to not expose Optional 39904 composing = false; 39905 container; 39906 contentAreaContainer; 39907 contentDocument; 39908 contentWindow; 39909 delegates; 39910 destroyed = false; 39911 dom; 39912 editorContainer; 39913 eventRoot; 39914 formatter; 39915 formElement; 39916 formEventDelegate; 39917 hasHiddenInput = false; 39918 iframeElement = null; 39919 iframeHTML; 39920 initialized = false; 39921 notificationManager; 39922 orgDisplay; 39923 orgVisibility; 39924 parser; 39925 quirks; 39926 readonly = false; 39927 removed = false; 39928 schema; 39929 selection; 39930 serializer; 39931 startContent = ''; 39932 targetElm; 39933 theme; 39934 model; 39935 undoManager; 39936 windowManager; 39937 licenseKeyManager; 39938 _beforeUnload; 39939 _eventDispatcher; 39940 _nodeChangeDispatcher; 39941 _pendingNativeEvents = []; 39942 _selectionOverrides; 39943 _skinLoaded = false; 39944 _editableRoot = true; 39945 // EditorObservable patches 39946 bindPendingEventDelegates; 39947 toggleNativeEvent; 39948 unbindAllNativeEvents; 39949 fire; 39950 dispatch; 39951 on; 39952 off; 39953 once; 39954 hasEventListeners; 39955 /** 39956 * Constructs a editor instance by id. 39957 * 39958 * @constructor 39959 * @method Editor 39960 * @param {String} id Unique id for the editor. 39961 * @param {Object} options Options for the editor. 39962 * @param {tinymce.EditorManager} editorManager EditorManager instance. 39963 */ 39964 constructor(id, options, editorManager) { 39965 this.editorManager = editorManager; 39966 // Patch in the EditorObservable functions 39967 extend(this, EditorObservable); 39968 const self = this; 39969 this.id = id; 39970 this.editorUid = uuidV4(); 39971 this.hidden = false; 39972 const normalizedOptions = normalizeOptions(editorManager.defaultOptions, options); 39973 this.options = create$4(self, normalizedOptions, options); 39974 register$7(self); 39975 this.userLookup = createUserLookup(this); 39976 const getOption = this.options.get; 39977 if (getOption('deprecation_warnings')) { 39978 logWarnings(options, normalizedOptions); 39979 } 39980 const suffix = getOption('suffix'); 39981 if (suffix) { 39982 editorManager.suffix = suffix; 39983 } 39984 this.suffix = editorManager.suffix; 39985 const baseUrl = getOption('base_url'); 39986 if (baseUrl) { 39987 editorManager._setBaseUrl(baseUrl); 39988 } 39989 this.baseUri = editorManager.baseURI; 39990 const referrerPolicy = getReferrerPolicy(self); 39991 if (referrerPolicy) { 39992 ScriptLoader.ScriptLoader._setReferrerPolicy(referrerPolicy); 39993 DOMUtils.DOM.styleSheetLoader._setReferrerPolicy(referrerPolicy); 39994 } 39995 ScriptLoader.ScriptLoader._setCrossOrigin((url) => { 39996 const crossOrigin = getCrossOrigin(self); 39997 return crossOrigin(url, 'script'); 39998 }); 39999 DOMUtils.DOM.styleSheetLoader._setCrossOrigin((url) => { 40000 const crossOrigin = getCrossOrigin(self); 40001 return crossOrigin(url, 'stylesheet'); 40002 }); 40003 const contentCssCors = hasContentCssCors(self); 40004 if (isNonNullable(contentCssCors)) { 40005 DOMUtils.DOM.styleSheetLoader._setContentCssCors(contentCssCors); 40006 } 40007 AddOnManager.languageLoad = getOption('language_load'); 40008 AddOnManager.baseURL = editorManager.baseURL; 40009 this.setDirty(false); 40010 this.documentBaseURI = new URI(getDocumentBaseUrl(self), { 40011 base_uri: this.baseUri 40012 }); 40013 this.baseURI = this.baseUri; 40014 this.inline = isInline$2(self); 40015 this.hasVisual = isVisualAidsEnabled(self); 40016 this.shortcuts = new Shortcuts(this); 40017 this.editorCommands = new EditorCommands(this); 40018 registerCommands(this); 40019 const cacheSuffix = getOption('cache_suffix'); 40020 if (cacheSuffix) { 40021 Env.cacheSuffix = cacheSuffix.replace(/^[\?\&]+/, ''); 40022 } 40023 this.ui = { 40024 registry: registry(), 40025 styleSheetLoader: undefined, 40026 show: noop, 40027 hide: noop, 40028 setEnabled: noop, 40029 isEnabled: always 40030 }; 40031 this.mode = create$3(self); 40032 // Lock certain properties to reduce misuse 40033 Object.defineProperty(this, 'editorUid', { 40034 writable: false, 40035 configurable: false, 40036 enumerable: true, 40037 }); 40038 // Call setup 40039 editorManager.dispatch('SetupEditor', { editor: this }); 40040 const setupCallback = getSetupCallback(self); 40041 if (isFunction(setupCallback)) { 40042 setupCallback.call(self, self); 40043 } 40044 } 40045 /** 40046 * Renders the editor/adds it to the page. 40047 * 40048 * @method render 40049 */ 40050 render() { 40051 render(this); 40052 } 40053 /** 40054 * Focuses/activates the editor. This will set this editor as the activeEditor in the tinymce collection 40055 * it will also place DOM focus inside the editor. 40056 * 40057 * @method focus 40058 * @param {Boolean
40058} skipFocus Skip DOM focus. Just set is as the active editor.
vendor: 22,938 bytes, lines 40058-40616
40058 40059 */ 40060 focus(skipFocus) { 40061 this.execCommand('mceFocus', false, skipFocus); 40062 } 40063 /** 40064 * Returns true/false if the editor has real keyboard focus. 40065 * 40066 * @method hasFocus 40067 * @return {Boolean} Current focus state of the editor. 40068 */ 40069 hasFocus() { 40070 return hasFocus(this); 40071 } 40072 /** 40073 * Translates the specified string by replacing variables with language pack items it will also check if there is 40074 * a key matching the input. 40075 * 40076 * @method translate 40077 * @param {String} text String to translate by the language pack data. 40078 * @return {String} Translated string. 40079 */ 40080 translate(text) { 40081 return I18n.translate(text); 40082 } 40083 getParam(name, defaultVal, type) { 40084 const options = this.options; 40085 // To keep the legacy API we need to register the option if it's not already been registered 40086 if (!options.isRegistered(name)) { 40087 if (isNonNullable(type)) { 40088 options.register(name, { processor: type, default: defaultVal }); 40089 } 40090 else { 40091 options.register(name, { processor: always, default: defaultVal }); 40092 } 40093 } 40094 // Attempt to use the passed default value if nothing has been set already 40095 return !options.isSet(name) && !isUndefined(defaultVal) ? defaultVal : options.get(name); 40096 } 40097 /** 40098 * Checks that the plugin is in the editor configuration and can optionally check if the plugin has been loaded. 40099 * 40100 * @method hasPlugin 40101 * @param {String} name The name of the plugin, as specified for the TinyMCE `plugins` option. 40102 * @param {Boolean} loaded If `true`, will also check that the plugin has been loaded. 40103 * @return {Boolean} If `loaded` is `true`, returns `true` if the plugin is in the configuration and has been loaded. If `loaded` is `false`, returns `true` if the plugin is in the configuration, regardless of plugin load status. 40104 * @example 40105 * // Returns `true` if the Comments plugin is in the editor configuration and has loaded successfully: 40106 * tinymce.activeEditor.hasPlugin('tinycomments', true); 40107 * // Returns `true` if the Table plugin is in the editor configuration, regardless of whether or not it loads: 40108 * tinymce.activeEditor.hasPlugin('table'); 40109 */ 40110 hasPlugin(name, loaded) { 40111 const hasPlugin = contains$2(getPlugins(this), name); 40112 if (hasPlugin) { 40113 return loaded ? PluginManager.get(name) !== undefined : true; 40114 } 40115 else { 40116 return false; 40117 } 40118 } 40119 /** 40120 * Dispatches out a onNodeChange event to all observers. This method should be called when you 40121 * need to update the UI states or element path etc. 40122 * 40123 * @method nodeChanged 40124 * @param {Object} args Optional args to pass to NodeChange event handlers. 40125 */ 40126 nodeChanged(args) { 40127 this._nodeChangeDispatcher.nodeChanged(args); 40128 } 40129 addCommand(name, callback, scope) { 40130 /** 40131 * Callback function that gets called when a command is executed. 40132 * 40133 * @callback addCommandCallback 40134 * @param {Boolean} ui Display UI state true/false. 40135 * @param {Object} value Optional value for command. 40136 * @return {Boolean} True/false state if the command was handled or not. 40137 */ 40138 this.editorCommands.addCommand(name, callback, scope); 40139 } 40140 addQueryStateHandler(name, callback, scope) { 40141 /** 40142 * Callback function that gets called when a queryCommandState is executed. 40143 * 40144 * @callback addQueryStateHandlerCallback 40145 * @return {Boolean} True/false state if the command is enabled or not like is it bold. 40146 */ 40147 this.editorCommands.addQueryStateHandler(name, callback, scope); 40148 } 40149 addQueryValueHandler(name, callback, scope) { 40150 /** 40151 * Callback function that gets called when a queryCommandValue is executed. 40152 * 40153 * @callback addQueryValueHandlerCallback 40154 * @return {Object} Value of the command or undefined. 40155 */ 40156 this.editorCommands.addQueryValueHandler(name, callback, scope); 40157 } 40158 /** 40159 * Adds a keyboard shortcut for some command or function. 40160 * 40161 * @method addShortcut 40162 * @param {String} pattern Shortcut pattern. Like for example: ctrl+alt+o. 40163 * @param {String} desc Text description for the command. 40164 * @param {String/Function} cmdFunc Command name string or function to execute when the key is pressed. 40165 * @param {Object} scope Optional scope to execute the function in. 40166 * @return {Boolean} true/false state if the shortcut was added or not. 40167 * @example 40168 * editor.addShortcut('ctrl+a', 'description of the shortcut', () => {}); 40169 * editor.addShortcut('ctrl+alt+a', 'description of the shortcut', () => {}); 40170 * // "meta" maps to Command on Mac and Ctrl on PC 40171 * editor.addShortcut('meta+a', 'description of the shortcut', () => {}); 40172 * // "access" maps to Control+Option on Mac and shift+alt on PC 40173 * editor.addShortcut('access+a', 'description of the shortcut', () => {}); 40174 * 40175 * editor.addShortcut('meta+access+c', 'Opens the code editor dialog.', () => { 40176 * editor.execCommand('mceCodeEditor'); 40177 * }); 40178 * 40179 * editor.addShortcut('meta+shift+32', 'Inserts "Hello, World!" for meta+shift+space', () => { 40180 * editor.execCommand('mceInsertContent', false, 'Hello, World!'); 40181 * }); 40182 */ 40183 addShortcut(pattern, desc, cmdFunc, scope) { 40184 this.shortcuts.add(pattern, desc, cmdFunc, scope); 40185 } 40186 /** 40187 * Executes a registered command on the current instance. A list of available commands can be found in 40188 * the tinymce command identifiers documentation. 40189 * 40190 * @method execCommand 40191 * @param {String} cmd Command name to execute, for example mceLink or Bold. 40192 * @param {Boolean} ui Specifies if a UI (dialog) should be presented or not. 40193 * @param {Object/Array/String/Number/Boolean} value Optional command value, this can be anything. 40194 * @param {Object} args Optional arguments object. 40195 * @return {Boolean} true or false if the command was supported or not. 40196 */ 40197 execCommand(cmd, ui, value, args) { 40198 return this.editorCommands.execCommand(cmd, ui, value, args); 40199 } 40200 /** 40201 * Returns a command specific state, for example if bold is enabled or not. 40202 * 40203 * @method queryCommandState 40204 * @param {String} cmd Command to query state from. 40205 * @return {Boolean} Command specific state, for example if bold is enabled or not. 40206 */ 40207 queryCommandState(cmd) { 40208 return this.editorCommands.queryCommandState(cmd); 40209 } 40210 /** 40211 * Returns a command specific value, for example the current font size. 40212 * 40213 * @method queryCommandValue 40214 * @param {String} cmd Command to query value from. 40215 * @return {String} Command value, for example the current font size or an empty string (`""`) if the query command is not found. 40216 */ 40217 queryCommandValue(cmd) { 40218 return this.editorCommands.queryCommandValue(cmd); 40219 } 40220 /** 40221 * Returns true/false if the command is supported or not. 40222 * 40223 * @method queryCommandSupported 40224 * @param {String} cmd Command that we check support for. 40225 * @return {Boolean} true/false if the command is supported or not. 40226 */ 40227 queryCommandSupported(cmd) { 40228 return this.editorCommands.queryCommandSupported(cmd); 40229 } 40230 /** 40231 * Shows the editor and hides any textarea/div that the editor is supposed to replace. 40232 * 40233 * @method show 40234 */ 40235 show() { 40236 const self = this; 40237 if (self.hidden) { 40238 self.hidden = false; 40239 if (self.inline) { 40240 self.getBody().contentEditable = 'true'; 40241 } 40242 else { 40243 DOM$1.show(self.getContainer()); 40244 DOM$1.hide(self.id); 40245 } 40246 self.load(); 40247 self.dispatch('show'); 40248 } 40249 } 40250 /** 40251 * Hides the editor and shows any textarea/div that the editor is supposed to replace. 40252 * 40253 * @method hide 40254 */ 40255 hide() { 40256 const self = this; 40257 if (!self.hidden) { 40258 // We must save before we hide so Safari doesn't crash 40259 self.save(); 40260 if (self.inline) { 40261 self.getBody().contentEditable = 'false'; 40262 // Make sure the editor gets blurred 40263 if (self === self.editorManager.focusedEditor) { 40264 self.editorManager.focusedEditor = null; 40265 } 40266 } 40267 else { 40268 DOM$1.hide(self.getContainer()); 40269 DOM$1.setStyle(self.id, 'display', self.orgDisplay); 40270 } 40271 self.hidden = true; 40272 self.dispatch('hide'); 40273 } 40274 } 40275 /** 40276 * Returns true/false if the editor is hidden or not. 40277 * 40278 * @method isHidden 40279 * @return {Boolean} True/false if the editor is hidden or not. 40280 */ 40281 isHidden() { 40282 return this.hidden; 40283 } 40284 /** 40285 * Sets the progress state, this will display a throbber/progess for the editor. 40286 * This is ideal for asynchronous operations like an AJAX save call. 40287 * 40288 * @method setProgressState 40289 * @param {Boolean} state Boolean state if the progress should be shown or hidden. 40290 * @param {Number} time Optional time to wait before the progress gets shown. 40291 * @return {Boolean} Same as the input state. 40292 * @example 40293 * // Show progress for the active editor 40294 * tinymce.activeEditor.setProgressState(true); 40295 * 40296 * // Hide progress for the active editor 40297 * tinymce.activeEditor.setProgressState(false); 40298 * 40299 * // Show progress after 3 seconds 40300 * tinymce.activeEditor.setProgressState(true, 3000); 40301 */ 40302 setProgressState(state, time) { 40303 this.dispatch('ProgressState', { state, time }); 40304 } 40305 /** 40306 * Loads contents from the textarea, input or other element that got converted into an editor instance. 40307 * This method will move the contents from that textarea, input or other element into the editor by using setContent 40308 * so all events etc that method has will get dispatched as well. 40309 * 40310 * @method load 40311 * @param {Object} args Optional content object, this gets passed around through the whole load process. 40312 */ 40313 load(args = {}) { 40314 const self = this; 40315 const elm = self.getElement(); 40316 if (self.removed) { 40317 return; 40318 } 40319 if (elm) { 40320 const loadArgs = { 40321 ...args, 40322 load: true 40323 }; 40324 const value = isTextareaOrInput(elm) ? elm.value : elm.innerHTML; 40325 self.setContent(value, loadArgs); 40326 if (!loadArgs.no_events) { 40327 self.dispatch('LoadContent', { 40328 ...loadArgs, 40329 element: elm 40330 }); 40331 } 40332 } 40333 } 40334 /** 40335 * Saves the contents from an editor out to the textarea or div element that got converted into an editor instance. 40336 * This method will move the HTML contents from the editor into that textarea or div by getContent 40337 * so all events etc that method has will get dispatched as well. 40338 * 40339 * @method save 40340 * @param {Object} args Optional content object, this gets passed around through the whole save process. 40341 * @return {String} HTML string that got set into the textarea/div. 40342 */ 40343 save(args = {}) { 40344 const self = this; 40345 let elm = self.getElement(); 40346 if (!elm || !self.initialized || self.removed) { 40347 return ''; 40348 } 40349 const getArgs = { 40350 ...args, 40351 save: true, 40352 element: elm 40353 }; 40354 let html = self.getContent(getArgs); 40355 const saveArgs = { ...getArgs, content: html }; 40356 if (!saveArgs.no_events) { 40357 self.dispatch('SaveContent', saveArgs); 40358 } 40359 // Always run this internal event 40360 if (saveArgs.format === 'raw') { 40361 self.dispatch('RawSaveContent', saveArgs); 40362 } 40363 html = saveArgs.content; 40364 if (!isTextareaOrInput(elm)) { 40365 if (args.is_removing || !self.inline) { 40366 elm.innerHTML = html; 40367 } 40368 // Update hidden form element 40369 const form = DOM$1.getParent(self.id, 'form'); 40370 if (form) { 40371 each$1(form.elements, (elm) => { 40372 if (elm.name === self.id) { 40373 elm.value = html; 40374 return false; 40375 } 40376 else { 40377 return true; 40378 } 40379 }); 40380 } 40381 } 40382 else { 40383 elm.value = html; 40384 } 40385 saveArgs.element = getArgs.element = elm = null; 40386 if (saveArgs.set_dirty !== false) { 40387 self.setDirty(false); 40388 } 40389 return html; 40390 } 40391 setContent(content, args) { 40392 setContent(this, content, args); 40393 } 40394 getContent(args) { 40395 return getContent(this, args); 40396 } 40397 /** 40398 * Inserts content at caret position. 40399 * 40400 * @method insertContent 40401 * @param {String} content Content to insert. 40402 * @param {Object} args Optional args to pass to insert call. 40403 */ 40404 insertContent(content, args) { 40405 if (args) { 40406 content = extend({ content }, args); 40407 } 40408 this.execCommand('mceInsertContent', false, content); 40409 } 40410 /** 40411 * Resets the editors content, undo/redo history and dirty state. If `initialContent` isn't specified, then 40412 * the editor is reset back to the initial start content. 40413 * 40414 * @method resetContent 40415 * @param {String} initialContent An optional string to use as the initial content of the editor. 40416 */ 40417 resetContent(initialContent) { 40418 // Set the editor content 40419 if (initialContent === undefined) { 40420 // editor.startContent is generated by using the `raw` format, so we should set it the same way 40421 setContent(this, this.startContent, { initial: true, format: 'raw' }); 40422 } 40423 else { 40424 setContent(this, initialContent, { initial: true }); 40425 } 40426 // Update the editor/undo manager state 40427 this.undoManager.reset(); 40428 this.setDirty(false); 40429 // Fire a node change event 40430 this.nodeChanged(); 40431 } 40432 /** 40433 * Returns true/false if the editor is dirty or not. It will get dirty if the user has made modifications to the contents. 40434 * 40435 * The dirty state is automatically set to `true` when the user modifies editor content after initialization or the 40436 * last `editor.save()` call. This includes changes made using undo or redo. 40437 * 40438 * @method isDirty 40439 * @return {Boolean} True/false if the editor is dirty or not. It will get dirty if the user has made modifications to the contents. 40440 * @example 40441 * if (tinymce.activeEditor.isDirty()) { 40442 * alert("You must save your contents."); 40443 * } 40444 */ 40445 isDirty() { 40446 return !this.isNotDirty; 40447 } 40448 /** 40449 * Explicitly sets the dirty state. This will fire the dirty event if the editor dirty state is changed from false to true 40450 * by invoking this method. 40451 * 40452 * @method setDirty 40453 * @param {Boolean} state True/false if the editor is considered dirty. 40454 * @example 40455 * const ajaxSave = () => { 40456 * const editor = tinymce.get('elm1'); 40457 * 40458 * // Save contents using some XHR call 40459 * alert(editor.getContent()); 40460 * 40461 * editor.setDirty(false); // Force not dirty state 40462 * } 40463 */ 40464 setDirty(state) { 40465 const oldState = !this.isNotDirty; 40466 this.isNotDirty = !state; 40467 if (state && state !== oldState) { 40468 this.dispatch('dirty'); 40469 } 40470 } 40471 /** 40472 * Returns the container element of the editor. The container element includes 40473 * all the elements added to the page for the editor. Such as UI, iframe, etc. 40474 * 40475 * @method getContainer 40476 * @return {Element} HTML DOM element for the editor container. 40477 */ 40478 getContainer() { 40479 const self = this; 40480 if (!self.container) { 40481 self.container = self.editorContainer || DOM$1.get(self.id + '_parent'); 40482 } 40483 return self.container; 40484 } 40485 /** 40486 * Returns the content area container element of the editor. This element 40487 * holds the iframe or the editable element. 40488 * 40489 * @method getContentAreaContainer 40490 * @return {Element} HTML DOM element for the editor area container. 40491 */ 40492 getContentAreaContainer() { 40493 return this.contentAreaContainer; 40494 } 40495 /** 40496 * Returns the target element/textarea that got replaced with a TinyMCE editor instance. 40497 * 40498 * @method getElement 40499 * @return {Element} HTML DOM element for the replaced element. 40500 */ 40501 getElement() { 40502 if (!this.targetElm) { 40503 this.targetElm = DOM$1.get(this.id); 40504 } 40505 return this.targetElm; 40506 } 40507 /** 40508 * Returns the iframes window object. 40509 * 40510 * @method getWin 40511 * @return {Window} Iframe DOM window object. 40512 */ 40513 getWin() { 40514 const self = this; 40515 if (!self.contentWindow) { 40516 const elm = self.iframeElement; 40517 if (elm) { 40518 self.contentWindow = elm.contentWindow; 40519 } 40520 } 40521 return self.contentWindow; 40522 } 40523 /** 40524 * Returns the iframes document object. 40525 * 40526 * @method getDoc 40527 * @return {Document} Iframe DOM document object. 40528 */ 40529 getDoc() { 40530 const self = this; 40531 if (!self.contentDocument) { 40532 const win = self.getWin(); 40533 if (win) { 40534 self.contentDocument = win.document; 40535 } 40536 } 40537 return self.contentDocument; 40538 } 40539 /** 40540 * Returns the root element of the editable area. 40541 * For a non-inline iframe-based editor, returns the iframe's body element. 40542 * 40543 * @method getBody 40544 * @return {Element} The root element of the editable area. 40545 */ 40546 getBody() { 40547 const doc = this.getDoc(); 40548 return this.bodyElement ?? doc?.body ?? null; 40549 } 40550 /** 40551 * URL converter function this gets executed each time a user adds an img, a or 40552 * any other element that has a URL in it. This will be called both by the DOM and HTML 40553 * manipulation functions. 40554 * 40555 * @method convertURL 40556 * @param {String} url URL to convert. 40557 * @param {String} name Attribute name src, href etc. 40558 * @param {String/HTMLElement} elm Tag name or HTML DOM element depending on HTML or DOM insert. 40559 * @return {String} Converted URL string. 40560 */ 40561 convertURL(url, name, elm) { 40562 const self = this, getOption = self.options.get; 40563 // Use callback instead 40564 const urlConverterCallback = getUrlConverterCallback(self); 40565 if (isFunction(urlConverterCallback)) { 40566 return urlConverterCallback.call(self, url, elm, true, name); 40567 } 40568 // Don't convert link href since that's the CSS files that gets loaded into the editor also skip local file URLs 40569 if (!getOption('convert_urls') || 40570 elm === 'link' || 40571 (isObject(elm) && elm.nodeName === 'LINK') || 40572 url.indexOf('file:') === 0 || 40573 url.length === 0) { 40574 return url; 40575 } 40576 const urlObject = new URI(url); 40577 if (urlObject.protocol !== 'http' && urlObject.protocol !== 'https' && urlObject.protocol !== '') { 40578 return url; 40579 } 40580 // Convert to relative 40581 if (getOption('relative_urls')) { 40582 return self.documentBaseURI.toRelative(url); 40583 } 40584 // Convert to absolute 40585 url = self.documentBaseURI.toAbsolute(url, getOption('remove_script_host')); 40586 return url; 40587 } 40588 /** 40589 * Adds visual aids for tables, anchors, etc so they can be more easily edited inside the editor. 40590 * 40591 * @method addVisual 40592 * @param {Element} elm Optional root element to loop though to find tables, etc that needs the visual aid. 40593 */ 40594 addVisual(elm) { 40595 addVisual(this, elm); 40596 } 40597 /** 40598 * Changes the editable state of the editor's root element. 40599 * 40600 * @method setEditableRoot 40601 * @param {Boolean} state State to set true for editable and false for non-editable. 40602 */ 40603 setEditableRoot(state) { 40604 setEditableRoot(this, state); 40605 } 40606 /** 40607 * Returns the current editable state of the editor's root element. 40608 * 40609 * @method hasEditableRoot 40610 * @return {Boolean} True if the root element is editable, false if it is not editable. 40611 */ 40612 hasEditableRoot() { 40613 return hasEditableRoot(this); 40614 } 40615 /** 40616 *
40616Removes the editor from the dom and tinymce collection. 40617 * 40618 * @method remove 40619 */ 40620 remove() { 40621 remove$1(this); 40622 } 40623 /** 40624 * Destroys the editor instance by removing all events, element references or other resources 40625 * that could leak memory. This method will be called automatically when the page is unloaded
vendor: 3,611 bytes, lines 40626-40722
40626 * but you can also call it directly if you know what you are doing. 40627 * 40628 * @method destroy 40629 * @param {Boolean} automatic Optional state if the destroy is an automatic destroy or user called one. 40630 */ 40631 destroy(automatic) { 40632 destroy(this, automatic); 40633 } 40634 /** 40635 * Uploads all data uri/blob uri images in the editor contents to server. 40636 * 40637 * @method uploadImages 40638 * @return {Promise} Promise instance with images and status for each image. 40639 */ 40640 uploadImages() { 40641 return this.editorUpload.uploadImages(); 40642 } 40643 // Internal functions 40644 _scanForImages() { 40645 return this.editorUpload.scanForImages(); 40646 } 40647 } 40648 40649 const DOM = DOMUtils.DOM; 40650 const each = Tools.each; 40651 let boundGlobalEvents = false; 40652 let beforeUnloadDelegate; 40653 let editors = []; 40654 const globalEventDelegate = (e) => { 40655 const type = e.type; 40656 each(EditorManager.get(), (editor) => { 40657 switch (type) { 40658 case 'scroll': 40659 editor.dispatch('ScrollWindow', e); 40660 break; 40661 case 'resize': 40662 editor.dispatch('ResizeWindow', e); 40663 break; 40664 } 40665 }); 40666 }; 40667 const toggleGlobalEvents = (state) => { 40668 if (state !== boundGlobalEvents) { 40669 const DOM = DOMUtils.DOM; 40670 if (state) { 40671 DOM.bind(window, 'resize', globalEventDelegate); 40672 DOM.bind(window, 'scroll', globalEventDelegate); 40673 } 40674 else { 40675 DOM.unbind(window, 'resize', globalEventDelegate); 40676 DOM.unbind(window, 'scroll', globalEventDelegate); 40677 } 40678 boundGlobalEvents = state; 40679 } 40680 }; 40681 const removeEditorFromList = (targetEditor) => { 40682 const oldEditors = editors; 40683 editors = filter$5(editors, (editor) => { 40684 return targetEditor !== editor; 40685 }); 40686 // Select another editor since the active one was removed 40687 if (EditorManager.activeEditor === targetEditor) { 40688 EditorManager.activeEditor = editors.length > 0 ? editors[0] : null; 40689 } 40690 // Clear focusedEditor if necessary, so that we don't try to blur the destroyed editor 40691 if (EditorManager.focusedEditor === targetEditor) { 40692 EditorManager.focusedEditor = null; 40693 } 40694 return oldEditors.length !== editors.length; 40695 }; 40696 const purgeDestroyedEditor = (editor) => { 40697 // User has manually destroyed the editor lets clean up the mess 40698 if (editor && editor.initialized && !(editor.getContainer() || editor.getBody()).parentNode) { 40699 removeEditorFromList(editor); 40700 editor.unbindAllNativeEvents(); 40701 editor.destroy(true); 40702 editor.removed = true; 40703 } 40704 }; 40705 const isQuirksMode = document.compatMode !== 'CSS1Compat'; 40706 const EditorManager = { 40707 ...Observable, 40708 baseURI: null, 40709 baseURL: null, 40710 /** 40711 * Object containing the options that will be passed by default to the <code>init</code> method upon each initialization of an editor. These defaults will be shallow merged with other options passed to <code>init</code>. 40712 * 40713 * @property defaultOptions 40714 * @type Object 40715 */ 40716 defaultOptions: {}, 40717 documentBaseURL: null, 40718 suffix: null, 40719 /** 40720 * A uuid string to anonymously identify the page tinymce is loaded in 40721 * 40722 * @property pageUid
40723 * @type String 40724 */ 40725 pageUid: uuidV4(), 40726 /** 40727 * Major version of TinyMCE build. 40728 * 40729 * @property majorVersion 40730 * @type String 40731 */ 40732 majorVersion: '8', 40733 /** 40734 * Minor version of TinyMCE build. 40735 * 40736 * @property minorVersion 40737 * @type String 40738 */ 40739 minorVersion: '4.0', 40740 /** 40741 * Release date of TinyMCE build. 40742 * 40743 * @property releaseDate 40744 * @type String 40745 */ 40746 releaseDate: '2026-03-31
vendor: 7,621 bytes, lines 40746-40918
40746', 40747 /** 40748 * Collection of language pack data. 40749 * 40750 * @property i18n 40751 * @type Object 40752 */ 40753 i18n: I18n, 40754 /** 40755 * Currently active editor instance. 40756 * 40757 * @property activeEditor 40758 * @type tinymce.Editor 40759 * @example 40760 * tinyMCE.activeEditor.selection.getContent(); 40761 * tinymce.EditorManager.activeEditor.selection.getContent(); 40762 */ 40763 activeEditor: null, 40764 focusedEditor: null, 40765 setup() { 40766 const self = this; 40767 let baseURL = ''; 40768 let suffix = ''; 40769 // Get base URL for the current document 40770 let documentBaseURL = URI.getDocumentBaseUrl(document.location); 40771 // Check if the URL is a document based format like: http://site/dir/file and file:/// 40772 // leave other formats like applewebdata://... intact 40773 if (/^[^:]+:\/\/\/?[^\/]+\//.test(documentBaseURL)) { 40774 documentBaseURL = documentBaseURL.replace(/[\?#].*$/, '').replace(/[\/\\][^\/]+$/, ''); 40775 if (!/[\/\\]$/.test(documentBaseURL)) { 40776 documentBaseURL += '/'; 40777 } 40778 } 40779 // If tinymce is defined and has a base use that or use the old tinyMCEPreInit 40780 const preInit = window.tinymce || window.tinyMCEPreInit; 40781 if (preInit) { 40782 baseURL = preInit.base || preInit.baseURL; 40783 suffix = preInit.suffix; 40784 } 40785 else { 40786 // Get base where the tinymce script is located 40787 const scripts = document.getElementsByTagName('script'); 40788 for (let i = 0; i < scripts.length; i++) { 40789 const src = scripts[i].src || ''; 40790 if (src === '') { 40791 continue; 40792 } 40793 // Script types supported: 40794 // tinymce.js tinymce.min.js tinymce.dev.js 40795 // tinymce.jquery.js tinymce.jquery.min.js tinymce.jquery.dev.js 40796 // tinymce.full.js tinymce.full.min.js tinymce.full.dev.js 40797 const srcScript = src.substring(src.lastIndexOf('/')); 40798 if (/tinymce(\.full|\.jquery|)(\.min|\.dev|)\.js/.test(src)) { 40799 if (srcScript.indexOf('.min') !== -1) { 40800 suffix = '.min'; 40801 } 40802 baseURL = src.substring(0, src.lastIndexOf('/')); 40803 break; 40804 } 40805 } 40806 // We didn't find any baseURL by looking at the script elements 40807 // Try to use the document.currentScript as a fallback 40808 if (!baseURL && document.currentScript) { 40809 const src = document.currentScript.src; 40810 if (src.indexOf('.min') !== -1) { 40811 suffix = '.min'; 40812 } 40813 baseURL = src.substring(0, src.lastIndexOf('/')); 40814 } 40815 } 40816 /** 40817 * Base URL where the root directory if TinyMCE is located. 40818 * 40819 * @property baseURL 40820 * @type String 40821 */ 40822 self.baseURL = new URI(documentBaseURL).toAbsolute(baseURL); 40823 /** 40824 * Document base URL where the current document is located. 40825 * 40826 * @property documentBaseURL 40827 * @type String 40828 */ 40829 self.documentBaseURL = documentBaseURL; 40830 /** 40831 * Absolute baseURI for the installation path of TinyMCE. 40832 * 40833 * @property baseURI 40834 * @type tinymce.util.URI 40835 */ 40836 self.baseURI = new URI(self.baseURL); 40837 /** 40838 * Current suffix to add to each plugin/theme that gets loaded for example ".min". 40839 * 40840 * @property suffix 40841 * @type String 40842 */ 40843 self.suffix = suffix; 40844 setup$C(self); 40845 // Lock certain properties to reduce misuse 40846 each$e(['majorVersion', 'minorVersion', 'releaseDate', 'pageUid', '_addLicenseKeyManager'], (property) => Object.defineProperty(self, property, { 40847 writable: false, 40848 configurable: false, 40849 enumerable: true, 40850 })); 40851 }, 40852 /** 40853 * Overrides the default options for editor instances. The <code>overrideDefaults</code> method replaces the <code>defaultOptions</code>, including any set by a previous call to the <code>overrideDefaults</code> method. 40854 * <br /><br /> 40855 * When using the Tiny Cloud, some of these defaults are required for the cloud-based editor to function. 40856 * <br /><br /> 40857 * Therefore, when using <code>overrideDefaults</code> with the cloud-based editor, any newly integrated options must be combined with the options in <code>tinymce.defaultOptions</code>. 40858 * 40859 * @method overrideDefaults 40860 * @param {Object} defaultOptions Default options object. 40861 * @example 40862 * const customOptions = { 40863 * toolbar_sticky: true 40864 * }; 40865 * 40866 * tinymce.overrideDefaults({ 40867 * ...tinymce.defaultOptions, 40868 * ...customOptions 40869 * }); 40870 */ 40871 overrideDefaults(defaultOptions) { 40872 const baseUrl = defaultOptions.base_url; 40873 if (baseUrl) { 40874 this._setBaseUrl(baseUrl); 40875 } 40876 const suffix = defaultOptions.suffix; 40877 if (suffix) { 40878 this.suffix = suffix; 40879 } 40880 this.defaultOptions = defaultOptions; 40881 const pluginBaseUrls = defaultOptions.plugin_base_urls; 40882 if (pluginBaseUrls !== undefined) { 40883 each$d(pluginBaseUrls, (pluginBaseUrl, pluginName) => { 40884 AddOnManager.PluginManager.urls[pluginName] = pluginBaseUrl; 40885 }); 40886 } 40887 }, 40888 /** 40889 * Initializes a set of editors. This method will create editors based on various settings. 40890 * <br /><br /> 40891 * For information on basic usage of <code>init</code>, see: <a href="https://www.tiny.cloud/docs/tinymce/8/basic-setup/">Basic setup</a>. 40892 * 40893 * @method init 40894 * @param {Object} options Options object to be passed to each editor instance. 40895 * @return {Promise} Promise that gets resolved with an array of editors when all editor instances are initialized. 40896 * @example 40897 * // Initializes a editor using the longer method 40898 * tinymce.EditorManager.init({ 40899 * some_settings : 'some value' 40900 * }); 40901 * 40902 * // Initializes a editor instance using the shorter version and with a promise 40903 * tinymce.init({ 40904 * some_settings : 'some value' 40905 * }).then((editors) => { 40906 * ... 40907 * }); 40908 */ 40909 init(options) { 40910 const self = this; 40911 let result; 40912 const invalidInlineTargets = Tools.makeMap('area base basefont br col frame hr img input isindex link meta param embed source wbr track ' + 40913 'colgroup option table tbody tfoot thead tr th td script noscript style textarea video audio iframe object menu', ' '); 40914 const isInvalidInlineTarget = (options, elm) => options.inline && elm.tagName.toLowerCase() in invalidInlineTargets; 40915 const createId = (elm) => { 40916 let id = elm.id; 40917 if (!id) { 40918 id = get$
40918a
vendor: 13,282 bytes, lines 40918-41258
40918(elm, 'name').filter((name) => !DOM.get(name)).getOrThunk(DOM.uniqueId); 40919 elm.setAttribute('id', id); 40920 } 40921 return id; 40922 }; 40923 const execCallback = (name) => { 40924 const callback = options[name]; 40925 if (!callback) { 40926 return; 40927 } 40928 return callback.apply(self, []); 40929 }; 40930 const findTargets = (options) => { 40931 if (Env.browser.isIE() || Env.browser.isEdge()) { 40932 initError('TinyMCE does not support the browser you are using. For a list of supported' + 40933 ' browsers please see: https://www.tiny.cloud/docs/tinymce/8/support/#supportedwebbrowsers'); 40934 return []; 40935 } 40936 else if (isQuirksMode) { 40937 initError('Failed to initialize the editor as the document is not in standards mode. ' + 40938 'TinyMCE requires standards mode.'); 40939 return []; 40940 } 40941 else if (isString(options.selector)) { 40942 return DOM.select(options.selector); 40943 } 40944 else if (isNonNullable(options.target)) { 40945 return [options.target]; 40946 } 40947 else { 40948 return []; 40949 } 40950 }; 40951 let provideResults = (editors) => { 40952 result = editors; 40953 }; 40954 const initEditors = () => { 40955 let initCount = 0; 40956 const editors = []; 40957 let targets; 40958 const createEditor = (id, options, targetElm) => { 40959 const editor = new Editor(id, options, self); 40960 editors.push(editor); 40961 editor.on('init', () => { 40962 if (++initCount === targets.length) { 40963 provideResults(editors); 40964 } 40965 }); 40966 editor.targetElm = editor.targetElm || targetElm; 40967 editor.render(); 40968 }; 40969 DOM.unbind(window, 'ready', initEditors); 40970 execCallback('onpageload'); 40971 targets = unique$1(findTargets(options)); 40972 Tools.each(targets, (elm) => { 40973 purgeDestroyedEditor(self.get(elm.id)); 40974 }); 40975 targets = Tools.grep(targets, (elm) => { 40976 return !self.get(elm.id); 40977 }); 40978 if (targets.length === 0) { 40979 provideResults([]); 40980 } 40981 else { 40982 each(targets, (elm) => { 40983 if (isInvalidInlineTarget(options, elm)) { 40984 initError('Could not initialize inline editor on invalid inline target element', elm); 40985 } 40986 else { 40987 createEditor(createId(elm), options, elm); 40988 } 40989 }); 40990 } 40991 }; 40992 DOM.bind(window, 'ready', initEditors); 40993 return new Promise((resolve) => { 40994 if (result) { 40995 resolve(result); 40996 } 40997 else { 40998 provideResults = (editors) => { 40999 resolve(editors); 41000 }; 41001 } 41002 }); 41003 }, 41004 /** 41005 * Returns an editor instance for a given id. 41006 * 41007 * @method get 41008 * @param {String/Number} id The id or index of the editor instance to return. 41009 * @return {tinymce.Editor/Array} Editor instance or an array of editor instances. 41010 * @example 41011 * // Adds an onclick event to an editor by id 41012 * tinymce.get('mytextbox').on('click', (e) => { 41013 * ed.windowManager.alert('Hello world!'); 41014 * }); 41015 * 41016 * // Adds an onclick event to an editor by index 41017 * tinymce.get(0).on('click', (e) => { 41018 * ed.windowManager.alert('Hello world!'); 41019 * }); 41020 * 41021 * // Adds an onclick event to an editor by id (longer version) 41022 * tinymce.EditorManager.get('mytextbox').on('click', (e) => { 41023 * ed.windowManager.alert('Hello world!'); 41024 * }); 41025 */ 41026 get(id) { 41027 if (arguments.length === 0) { 41028 return editors.slice(0); 41029 } 41030 else if (isString(id)) { 41031 return find$2(editors, (editor) => { 41032 return editor.id === id; 41033 }).getOr(null); 41034 } 41035 else if (isNumber(id)) { 41036 return editors[id] ? editors[id] : null; 41037 } 41038 else { 41039 return null; 41040 } 41041 }, 41042 /** 41043 * Adds an editor instance to the editor collection. This will also set it as the active editor. 41044 * 41045 * @method add 41046 * @param {tinymce.Editor} editor Editor instance to add to the collection. 41047 * @return {tinymce.Editor} The same instance that got passed in. 41048 */ 41049 add(editor) { 41050 const self = this; 41051 // Prevent existing editors from being added again this could happen 41052 // if a user calls createEditor then render or add multiple times. 41053 const existingEditor = self.get(editor.id); 41054 if (existingEditor === editor) { 41055 return editor; 41056 } 41057 if (existingEditor === null) { 41058 editors.push(editor); 41059 } 41060 toggleGlobalEvents(true); 41061 // Doesn't call setActive method since we don't want 41062 // to fire a bunch of activate/deactivate calls while initializing 41063 self.activeEditor = editor; 41064 self.dispatch('AddEditor', { editor }); 41065 if (!beforeUnloadDelegate) { 41066 beforeUnloadDelegate = (e) => { 41067 const event = self.dispatch('BeforeUnload'); 41068 if (event.returnValue) { 41069 // browsers are all a little bit special about this: https://developer.mozilla.org/en-US/docs/Web/API/BeforeUnloadEvent 41070 e.preventDefault(); 41071 e.returnValue = event.returnValue; 41072 return event.returnValue; 41073 } 41074 }; 41075 window.addEventListener('beforeunload', beforeUnloadDelegate); 41076 } 41077 return editor; 41078 }, 41079 /** 41080 * Creates an editor instance and adds it to the EditorManager collection. 41081 * 41082 * @method createEditor 41083 * @param {String} id Instance id to use for editor. 41084 * @param {Object} options Editor instance options. 41085 * @return {tinymce.Editor} Editor instance that got created. 41086 */ 41087 createEditor(id, options) { 41088 return this.add(new Editor(id, options, this)); 41089 }, 41090 /** 41091 * Removes a editor or editors form page. 41092 * 41093 * @example 41094 * // Remove all editors bound to divs 41095 * tinymce.remove('div'); 41096 * 41097 * // Remove all editors bound to textareas 41098 * tinymce.remove('textarea'); 41099 * 41100 * // Remove all editors 41101 * tinymce.remove(); 41102 * 41103 * // Remove specific instance by id 41104 * tinymce.remove('#id'); 41105 * 41106 * @method remove 41107 * @param {tinymce.Editor/String/Object} [selector] CSS selector or editor instance to remove. 41108 * @return {tinymce.Editor} The editor that got passed in will be return if it was found otherwise null. 41109 */ 41110 remove(selector) { 41111 const self = this; 41112 let editor; 41113 // Remove all editors 41114 if (!selector) { 41115 for (let i = editors.length - 1; i >= 0; i--) { 41116 self.remove(editors[i]); 41117 } 41118 return; 41119 } 41120 // Remove editors by selector 41121 if (isString(selector)) { 41122 each(DOM.select(selector), (elm) => { 41123 editor = self.get(elm.id); 41124 if (editor) { 41125 self.remove(editor); 41126 } 41127 }); 41128 return; 41129 } 41130 // Remove specific editor 41131 editor = selector; 41132 // Not in the collection 41133 if (isNull(self.get(editor.id))) { 41134 return null; 41135 } 41136 if (removeEditorFromList(editor)) { 41137 self.dispatch('RemoveEditor', { editor }); 41138 } 41139 if (editors.length === 0) { 41140 window.removeEventListener('beforeunload', beforeUnloadDelegate); 41141 } 41142 editor.remove(); 41143 toggleGlobalEvents(editors.length > 0); 41144 return editor; 41145 }, 41146 /** 41147 * Executes a specific command on the currently active editor. 41148 * 41149 * @method execCommand 41150 * @param {String} cmd Command to perform for example Bold. 41151 * @param {Boolean} ui Optional boolean state if a UI should be presented for the command or not. 41152 * @param {Object/String/Number/Boolean} value Optional value parameter like for example an URL to a link. 41153 * @return {Boolean} true/false if the command was executed or not. 41154 */ 41155 execCommand(cmd, ui, value) { 41156 const self = this; 41157 const editorId = isObject(value) ? value.id ?? value.index : value; 41158 // Manager commands 41159 switch (cmd) { 41160 case 'mceAddEditor': { 41161 if (!self.get(editorId)) { 41162 const editorOptions = value.options; 41163 new Editor(editorId, editorOptions, self).render(); 41164 } 41165 return true; 41166 } 41167 case 'mceRemoveEditor': { 41168 const editor = self.get(editorId); 41169 if (editor) { 41170 editor.remove(); 41171 } 41172 return true; 41173 } 41174 case 'mceToggleEditor': { 41175 const editor = self.get(editorId); 41176 if (!editor) { 41177 self.execCommand('mceAddEditor', false, value); 41178 return true; 41179 } 41180 if (editor.isHidden()) { 41181 editor.show(); 41182 } 41183 else { 41184 editor.hide(); 41185 } 41186 return true; 41187 } 41188 } 41189 // Run command on active editor 41190 if (self.activeEditor) { 41191 return self.activeEditor.execCommand(cmd, ui, value); 41192 } 41193 return false; 41194 }, 41195 /** 41196 * Calls the save method on all editor instances in the collection. This can be useful when a form is to be submitted. 41197 * 41198 * @method triggerSave 41199 * @example 41200 * // Saves all contents 41201 * tinyMCE.triggerSave(); 41202 */ 41203 triggerSave: () => { 41204 each(editors, (editor) => { 41205 editor.save(); 41206 }); 41207 }, 41208 /** 41209 * Adds a language pack, this gets called by the loaded language files like en.js. 41210 * 41211 * @method addI18n 41212 * @param {String} code Optional language code. 41213 * @param {Object} items Name/value object with translations. 41214 */ 41215 addI18n: (code, items) => { 41216 I18n.add(code, items); 41217 }, 41218 /** 41219 * Translates the specified string using the language pack items. 41220 * 41221 * @method translate 41222 * @param {String/Array/Object} text String to translate 41223 * @return {String} Translated string. 41224 */ 41225 translate: (text) => { 41226 return I18n.translate(text); 41227 }, 41228 /** 41229 * Sets the active editor instance and fires the deactivate/activate events. 41230 * 41231 * @method setActive 41232 * @param {tinymce.Editor} editor Editor instance to set as the active instance. 41233 */ 41234 setActive(editor) { 41235 const activeEditor = this.activeEditor; 41236 if (this.activeEditor !== editor) { 41237 if (activeEditor) { 41238 activeEditor.dispatch('deactivate', { relatedTarget: editor }); 41239 } 41240 editor.dispatch('activate', { relatedTarget: activeEditor }); 41241 } 41242 this.activeEditor = editor; 41243 }, 41244 _setBaseUrl(baseUrl) { 41245 this.baseURL = new URI(this.documentBaseURL).toAbsolute(baseUrl.replace(/\/+$/, '')); 41246 this.baseURI = new URI(this.baseURL); 41247 }, 41248 _addLicenseKeyManager: (addOn) => LicenseKeyManagerLoader.add(addOn), 41249 }; 41250 EditorManager.setup(); 41251 41252 // The FakeClipboard has been designed to match the native Clipboard API as closely as possible 41253 const setup = () => { 41254 const dataValue = value$1(); 41255 const FakeClipboardItem = (items) => ({ 41256 items, 41257 types: keys(items), 41258 getType: (type) => get$
41258a
vendor: 10,911 bytes, lines 41258-41604
41258(items, type).getOrUndefined() 41259 }); 41260 const write = (data) => { 41261 dataValue.set(data); 41262 }; 41263 const read = () => dataValue.get().getOrUndefined(); 41264 const clear = dataValue.clear; 41265 return { 41266 FakeClipboardItem, 41267 write, 41268 read, 41269 clear 41270 }; 41271 }; 41272 const FakeClipboard = setup(); 41273 41274 /** 41275 * Contains various tools for rect/position calculation. 41276 * 41277 * @class tinymce.geom.Rect 41278 */ 41279 const min = Math.min, max = Math.max, round = Math.round; 41280 /** 41281 * Returns the rect positioned based on the relative position name 41282 * to the target rect. 41283 * 41284 * @method relativePosition 41285 * @param {Rect} rect Source rect to modify into a new rect. 41286 * @param {Rect} targetRect Rect to move relative to based on the rel option. 41287 * @param {String} rel Relative position. For example: tr-bl. 41288 */ 41289 const relativePosition = (rect, targetRect, rel) => { 41290 let x = targetRect.x; 41291 let y = targetRect.y; 41292 const w = rect.w; 41293 const h = rect.h; 41294 const targetW = targetRect.w; 41295 const targetH = targetRect.h; 41296 const relChars = (rel || '').split(''); 41297 if (relChars[0] === 'b') { 41298 y += targetH; 41299 } 41300 if (relChars[1] === 'r') { 41301 x += targetW; 41302 } 41303 if (relChars[0] === 'c') { 41304 y += round(targetH / 2); 41305 } 41306 if (relChars[1] === 'c') { 41307 x += round(targetW / 2); 41308 } 41309 if (relChars[3] === 'b') { 41310 y -= h; 41311 } 41312 if (relChars[4] === 'r') { 41313 x -= w; 41314 } 41315 if (relChars[3] === 'c') { 41316 y -= round(h / 2); 41317 } 41318 if (relChars[4] === 'c') { 41319 x -= round(w / 2); 41320 } 41321 return create$2(x, y, w, h); 41322 }; 41323 /** 41324 * Tests various positions to get the most suitable one. 41325 * 41326 * @method findBestRelativePosition 41327 * @param {Rect} rect Rect to use as source. 41328 * @param {Rect} targetRect Rect to move relative to. 41329 * @param {Rect} constrainRect Rect to constrain within. 41330 * @param {Array} rels Array of relative positions to test against. 41331 */ 41332 const findBestRelativePosition = (rect, targetRect, constrainRect, rels) => { 41333 for (let i = 0; i < rels.length; i++) { 41334 const pos = relativePosition(rect, targetRect, rels[i]); 41335 if (pos.x >= constrainRect.x && pos.x + pos.w <= constrainRect.w + constrainRect.x && 41336 pos.y >= constrainRect.y && pos.y + pos.h <= constrainRect.h + constrainRect.y) { 41337 return rels[i]; 41338 } 41339 } 41340 return null; 41341 }; 41342 /** 41343 * Inflates the rect in all directions. 41344 * 41345 * @method inflate 41346 * @param {Rect} rect Rect to expand. 41347 * @param {Number} w Relative width to expand by. 41348 * @param {Number} h Relative height to expand by. 41349 * @return {Rect} New expanded rect. 41350 */ 41351 const inflate = (rect, w, h) => { 41352 return create$2(rect.x - w, rect.y - h, rect.w + w * 2, rect.h + h * 2); 41353 }; 41354 /** 41355 * Returns the intersection of the specified rectangles. 41356 * 41357 * @method intersect 41358 * @param {Rect} rect The first rectangle to compare. 41359 * @param {Rect} cropRect The second rectangle to compare. 41360 * @return {Rect} The intersection of the two rectangles or null if they don't intersect. 41361 */ 41362 const intersect = (rect, cropRect) => { 41363 const x1 = max(rect.x, cropRect.x); 41364 const y1 = max(rect.y, cropRect.y); 41365 const x2 = min(rect.x + rect.w, cropRect.x + cropRect.w); 41366 const y2 = min(rect.y + rect.h, cropRect.y + cropRect.h); 41367 if (x2 - x1 < 0 || y2 - y1 < 0) { 41368 return null; 41369 } 41370 return create$2(x1, y1, x2 - x1, y2 - y1); 41371 }; 41372 /** 41373 * Returns a rect clamped within the specified clamp rect. This forces the 41374 * rect to be inside the clamp rect. 41375 * 41376 * @method clamp 41377 * @param {Rect} rect Rectangle to force within clamp rect. 41378 * @param {Rect} clampRect Rectable to force within. 41379 * @param {Boolean} fixedSize True/false if size should be fixed. 41380 * @return {Rect} Clamped rect. 41381 */ 41382 const clamp = (rect, clampRect, fixedSize) => { 41383 let x1 = rect.x; 41384 let y1 = rect.y; 41385 let x2 = rect.x + rect.w; 41386 let y2 = rect.y + rect.h; 41387 const cx2 = clampRect.x + clampRect.w; 41388 const cy2 = clampRect.y + clampRect.h; 41389 const underflowX1 = max(0, clampRect.x - x1); 41390 const underflowY1 = max(0, clampRect.y - y1); 41391 const overflowX2 = max(0, x2 - cx2); 41392 const overflowY2 = max(0, y2 - cy2); 41393 x1 += underflowX1; 41394 y1 += underflowY1; 41395 if (fixedSize) { 41396 x2 += underflowX1; 41397 y2 += underflowY1; 41398 x1 -= overflowX2; 41399 y1 -= overflowY2; 41400 } 41401 x2 -= overflowX2; 41402 y2 -= overflowY2; 41403 return create$2(x1, y1, x2 - x1, y2 - y1); 41404 }; 41405 /** 41406 * Creates a new rectangle object. 41407 * 41408 * @method create 41409 * @param {Number} x Rectangle x location. 41410 * @param {Number} y Rectangle y location. 41411 * @param {Number} w Rectangle width. 41412 * @param {Number} h Rectangle height. 41413 * @return {Rect} New rectangle object. 41414 */ 41415 const create$2 = (x, y, w, h) => { 41416 return { x, y, w, h }; 41417 }; 41418 /** 41419 * Creates a new rectangle object form a clientRects object. 41420 * 41421 * @method fromClientRect 41422 * @param {ClientRect} clientRect DOM ClientRect object. 41423 * @return {Rect} New rectangle object. 41424 */ 41425 const fromClientRect = (clientRect) => { 41426 return create$2(clientRect.left, clientRect.top, clientRect.width, clientRect.height); 41427 }; 41428 const Rect = { 41429 inflate, 41430 relativePosition, 41431 findBestRelativePosition, 41432 intersect, 41433 clamp, 41434 create: create$2, 41435 fromClientRect 41436 }; 41437 41438 const awaiter = (resolveCb, rejectCb, timeout = 1000) => { 41439 let done = false; 41440 let timer = null; 41441 const complete = (completer) => (...args) => { 41442 if (!done) { 41443 done = true; 41444 if (timer !== null) { 41445 window.clearTimeout(timer); 41446 timer = null; 41447 } 41448 completer.apply(null, args); 41449 } 41450 }; 41451 const resolve = complete(resolveCb); 41452 const reject = complete(rejectCb); 41453 const start = (...args) => { 41454 if (!done && timer === null) { 41455 timer = window.setTimeout(() => reject.apply(null, args), timeout); 41456 } 41457 }; 41458 return { 41459 start, 41460 resolve, 41461 reject 41462 }; 41463 }; 41464 const create$1 = () => { 41465 const tasks = {}; 41466 const resultFns = {}; 41467 const resources = {}; 41468 const load = (id, url) => { 41469 const loadErrMsg = `Script at URL "${url}" failed to load`; 41470 const runErrMsg = `Script at URL "${url}" did not call \`tinymce.Resource.add('${id}', data)\` within 1 second`; 41471 if (tasks[id] !== undefined) { 41472 return tasks[id]; 41473 } 41474 else { 41475 const task = new Promise((resolve, reject) => { 41476 const waiter = awaiter(resolve, reject); 41477 resultFns[id] = waiter.resolve; 41478 ScriptLoader.ScriptLoader.loadScript(url).then(() => waiter.start(runErrMsg), () => waiter.reject(loadErrMsg)); 41479 }); 41480 tasks[id] = task; 41481 return task; 41482 } 41483 }; 41484 const add = (id, data) => { 41485 if (resultFns[id] !== undefined) { 41486 resultFns[id](data); 41487 delete resultFns[id]; 41488 } 41489 tasks[id] = Promise.resolve(data); 41490 resources[id] = data; 41491 }; 41492 const has = (id) => { 41493 return id in resources; 41494 }; 41495 const unload = (id) => { 41496 delete tasks[id]; 41497 delete resources[id]; 41498 }; 41499 const get = (id) => resources[id]; 41500 return { 41501 load, 41502 add, 41503 has, 41504 get, 41505 unload 41506 }; 41507 }; 41508 const Resource = create$1(); 41509 41510 // Simple stub of localstorage with strict security settings #TINY-1782 41511 const create = () => (() => { 41512 let data = {}; 41513 let keys = []; 41514 const storage = { 41515 getItem: (key) => { 41516 const item = data[key]; 41517 return item ? item : null; 41518 }, 41519 setItem: (key, value) => { 41520 keys.push(key); 41521 data[key] = String(value); 41522 }, 41523 key: (index) => { 41524 return keys[index]; 41525 }, 41526 removeItem: (key) => { 41527 keys = keys.filter((k) => k === key); 41528 delete data[key]; 41529 }, 41530 clear: () => { 41531 keys = []; 41532 data = {}; 41533 }, 41534 length: 0 41535 }; 41536 Object.defineProperty(storage, 'length', { 41537 get: () => keys.length, 41538 configurable: false, 41539 enumerable: false 41540 }); 41541 return storage; 41542 })(); 41543 41544 /** 41545 * @class tinymce.util.LocalStorage 41546 * @static 41547 * @version 4.0 41548 * @private 41549 * @example 41550 * tinymce.util.LocalStorage.setItem('key', 'value'); 41551 * const value = tinymce.util.LocalStorage.getItem('key'); 41552 */ 41553 let localStorage; 41554 // Browsers with certain strict security settings will explode when trying to access localStorage 41555 // so we need to do a try/catch and a simple stub here. #TINY-1782 & #TINY-5935 41556 try { 41557 const test = '__storage_test__'; 41558 localStorage = window.localStorage; 41559 // Make sure we can set a value, as storage may also be full 41560 localStorage.setItem(test, test); 41561 localStorage.removeItem(test); 41562 } 41563 catch { 41564 localStorage = create(); 41565 } 41566 var LocalStorage = localStorage; 41567 41568 /** 41569 * @include ../../../../../tools/docs/tinymce.js 41570 */ 41571 /** 41572 * @include ../../../../../tools/docs/tinymce.Event.js 41573 */ 41574 /** 41575 * @include ../../../../../tools/docs/tinymce.editor.ui.Ui.js 41576 */ 41577 const publicApi = { 41578 geom: { 41579 Rect 41580 }, 41581 util: { 41582 Delay, 41583 Tools, 41584 VK, 41585 URI, 41586 EventDispatcher, 41587 Observable, 41588 I18n, 41589 LocalStorage, 41590 ImageUploader 41591 }, 41592 dom: { 41593 EventUtils, 41594 TreeWalker: DomTreeWalker, 41595 TextSeeker, 41596 DOMUtils, 41597 ScriptLoader, 41598 RangeUtils, 41599 Serializer: DomSerializer, 41600 StyleSheetLoader, 41601 ControlSelection, 41602 BookmarkManager, 41603 Selection: EditorSelection, 41604
vendor: 2,037 bytes, lines 41604-41682
41604Event: EventUtils.Event 41605 }, 41606 html: { 41607 Styles, 41608 Entities, 41609 Node: AstNode, 41610 Schema, 41611 DomParser, 41612 Writer, 41613 Serializer: HtmlSerializer 41614 }, 41615 Env, 41616 AddOnManager, 41617 Annotator, 41618 Formatter, 41619 UndoManager, 41620 EditorCommands, 41621 WindowManager, 41622 NotificationManager, 41623 EditorObservable, 41624 Shortcuts, 41625 Editor, 41626 FocusManager, 41627 EditorManager, 41628 // Global instances 41629 DOM: DOMUtils.DOM, 41630 ScriptLoader: ScriptLoader.ScriptLoader, 41631 PluginManager, 41632 ThemeManager, 41633 ModelManager, 41634 IconManager, 41635 Resource, 41636 FakeClipboard, 41637 // Global utility functions 41638 trim: Tools.trim, 41639 isArray: Tools.isArray, 41640 is: Tools.is, 41641 toArray: Tools.toArray, 41642 makeMap: Tools.makeMap, 41643 each: Tools.each, 41644 map: Tools.map, 41645 grep: Tools.grep, 41646 inArray: Tools.inArray, 41647 extend: Tools.extend, 41648 walk: Tools.walk, 41649 resolve: Tools.resolve, 41650 explode: Tools.explode, 41651 _addCacheSuffix: Tools._addCacheSuffix 41652 }; 41653 const tinymce$1 = Tools.extend(EditorManager, publicApi); 41654 41655 const exportToModuleLoaders = (tinymce) => { 41656 if (typeof module === 'object') { 41657 try { 41658 module.exports = tinymce; 41659 } 41660 catch { 41661 // It will thrown an error when running this module 41662 // within webpack where the module.exports object is sealed 41663 } 41664 } 41665 }; 41666 const exportToWindowGlobal = (tinymce) => { 41667 window.tinymce = tinymce; 41668 window.tinyMCE = tinymce; 41669 }; 41670 exportToWindowGlobal(tinymce$1); 41671 exportToModuleLoaders(tinymce$1); 41672 41673})(); 41674 41675 41676 41677// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails 41678 41679import "controllers"; 41680import "trix"; 41681import "@rails/actiontext"; 41682import Alpine from "alpinejs";
41683import collapse from '@alpinejs/collapse' 41684import "@hotwired/turbo-rails" 41685 41686window.Alpine = Alpine; 41687Alpine.plugin(collapse) 41688Alpine.start();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.