vendor: 3,077 bytes, lines 1-98
1/** 2 * TinyMCE version 8.8.2 (2026-07-27) 3 */ 4 5(function () { 6 'use strict'; 7 8 /* eslint-disable @typescript-eslint/no-wrapper-object-types */ 9 const getPrototypeOf$2 = Object.getPrototypeOf; 10 const hasProto = (v, constructor, predicate) => { 11 if (predicate(v, constructor.prototype)) { 12 return true; 13 } 14 else { 15 // String-based fallback time 16 return v.constructor?.name === constructor.name; 17 } 18 }; 19 const typeOf = (x) => { 20 const t = typeof x; 21 if (x === null) { 22 return 'null'; 23 } 24 else if (t === 'object' && Array.isArray(x)) { 25 return 'array'; 26 } 27 else if (t === 'object' && hasProto(x, String, (o, proto) => proto.isPrototypeOf(o))) { 28 return 'string'; 29 } 30 else { 31 return t; 32 } 33 }; 34 const isType$1 = (type) => (value) => typeOf(value) === type; 35 const isSimpleType = (type) => (value) => typeof value === type; 36 const eq$1 = (t) => (a) => t === a; 37 const is$2 = (value, constructor) => isObject(value) && hasProto(value, constructor, (o, proto) => getPrototypeOf$2(o) === proto); 38 const isString = isType$1('string'); 39 const isObject = isType$1('object'); 40 const isPlainObject = (value) => is$2(value, Object); 41 const isArray = isType$1('array'); 42 const isNull = eq$1(null); 43 const isBoolean = isSimpleType('boolean'); 44 const isUndefined = eq$1(undefined); 45 const isNullable = (a) => a === null || a === undefined; 46 const isNonNullable = (a) => !isNullable(a); 47 const isFunction = isSimpleType('function'); 48 const isNumber = isSimpleType('number'); 49 const isArrayOf = (value, pred) => { 50 if (isArray(value)) { 51 for (let i = 0, len = value.length; i < len; ++i) { 52 if (!(pred(value[i]))) { 53 return false; 54 } 55 } 56 return true; 57 } 58 return false; 59 }; 60 61 const noop = () => { }; 62 const noarg = (f) => () => f(); 63 /** Compose a unary function with an n-ary function */ 64 const compose = (fa, fb) => { 65 return (...args) => { 66 return fa(fb.apply(null, args)); 67 }; 68 }; 69 /** Compose two unary functions. Similar to compose, but avoids using Function.prototype.apply. */ 70 const compose1 = (fbc, fab) => (a) => fbc(fab(a)); 71 const constant$1 = (value) => { 72 return () => { 73 return value; 74 }; 75 }; 76 const identity = (x) => { 77 return x; 78 }; 79 const tripleEquals = (a, b) => { 80 return a === b; 81 }; 82 function curry(fn, ...initialArgs) { 83 return (...restArgs) => { 84 const all = initialArgs.concat(restArgs); 85 return fn.apply(null, all); 86 }; 87 } 88 const not = (f) => (t) => !f(t); 89 const die = (msg) => { 90 return () => { 91 throw new Error(msg); 92 }; 93 }; 94 const apply$1 = (f) => { 95 return f(); 96 }; 97 const never = constant$1(false); 98 const always = constant$1
vendor: 10,618 bytes, lines 98-357
98(true); 99 100 /** 101 * The `Optional` type represents a value (of any type) that potentially does 102 * not exist. Any `Optional<T>` can either be a `Some<T>` (in which case the 103 * value does exist) or a `None` (in which case the value does not exist). This 104 * module defines a whole lot of FP-inspired utility functions for dealing with 105 * `Optional` objects. 106 * 107 * Comparison with null or undefined: 108 * - We don't get fancy null coalescing operators with `Optional` 109 * - We do get fancy helper functions with `Optional` 110 * - `Optional` support nesting, and allow for the type to still be nullable (or 111 * another `Optional`) 112 * - There is no option to turn off strict-optional-checks like there is for 113 * strict-null-checks 114 */ 115 class Optional { 116 tag; 117 value; 118 // Sneaky optimisation: every instance of Optional.none is identical, so just 119 // reuse the same object 120 static singletonNone = new Optional(false); 121 // The internal representation has a `tag` and a `value`, but both are 122 // private: able to be console.logged, but not able to be accessed by code 123 constructor(tag, value) { 124 this.tag = tag; 125 this.value = value; 126 } 127 // --- Identities --- 128 /** 129 * Creates a new `Optional<T>` that **does** contain a value. 130 */ 131 static some(value) { 132 return new Optional(true, value); 133 } 134 /** 135 * Create a new `Optional<T>` that **does not** contain a value. `T` can be 136 * any type because we don't actually have a `T`. 137 */ 138 static none() { 139 return Optional.singletonNone; 140 } 141 /** 142 * Perform a transform on an `Optional` type. Regardless of whether this 143 * `Optional` contains a value or not, `fold` will return a value of type `U`. 144 * If this `Optional` does not contain a value, the `U` will be created by 145 * calling `onNone`. If this `Optional` does contain a value, the `U` will be 146 * created by calling `onSome`. 147 * 148 * For the FP enthusiasts in the room, this function: 149 * 1. Could be used to implement all of the functions below 150 * 2. Forms a catamorphism 151 */ 152 fold(onNone, onSome) { 153 if (this.tag) { 154 return onSome(this.value); 155 } 156 else { 157 return onNone(); 158 } 159 } 160 /** 161 * Determine if this `Optional` object contains a value. 162 */ 163 isSome() { 164 return this.tag; 165 } 166 /** 167 * Determine if this `Optional` object **does not** contain a value. 168 */ 169 isNone() { 170 return !this.tag; 171 } 172 // --- Functor (name stolen from Haskell / maths) --- 173 /** 174 * Perform a transform on an `Optional` object, **if** there is a value. If 175 * you provide a function to turn a T into a U, this is the function you use 176 * to turn an `Optional<T>` into an `Optional<U>`. If this **does** contain 177 * a value then the output will also contain a value (that value being the 178 * output of `mapper(this.value)`), and if this **does not** contain a value 179 * then neither will the output. 180 */ 181 map(mapper) { 182 if (this.tag) { 183 return Optional.some(mapper(this.value)); 184 } 185 else { 186 return Optional.none(); 187 } 188 } 189 // --- Monad (name stolen from Haskell / maths) --- 190 /** 191 * Perform a transform on an `Optional` object, **if** there is a value. 192 * Unlike `map`, here the transform itself also returns an `Optional`. 193 */ 194 bind(binder) { 195 if (this.tag) { 196 return binder(this.value); 197 } 198 else { 199 return Optional.none(); 200 } 201 } 202 // --- Traversable (name stolen from Haskell / maths) --- 203 /** 204 * For a given predicate, this function finds out if there **exists** a value 205 * inside this `Optional` object that meets the predicate. In practice, this 206 * means that for `Optional`s that do not contain a value it returns false (as 207 * no predicate-meeting value exists). 208 */ 209 exists(predicate) { 210 return this.tag && predicate(this.value); 211 } 212 /** 213 * For a given predicate, this function finds out if **all** the values inside 214 * this `Optional` object meet the predicate. In practice, this means that 215 * for `Optional`s that do not contain a value it returns true (as all 0 216 * objects do meet the predicate). 217 */ 218 forall(predicate) { 219 return !this.tag || predicate(this.value); 220 } 221 filter(predicate) { 222 if (!this.tag || predicate(this.value)) { 223 return this; 224 } 225 else { 226 return Optional.none(); 227 } 228 } 229 // --- Getters --- 230 /** 231 * Get the value out of the inside of the `Optional` object, using a default 232 * `replacement` value if the provided `Optional` object does not contain a 233 * value. 234 */ 235 getOr(replacement) { 236 return this.tag ? this.value : replacement; 237 } 238 /** 239 * Get the value out of the inside of the `Optional` object, using a default 240 * `replacement` value if the provided `Optional` object does not contain a 241 * value. Unlike `getOr`, in this method the `replacement` object is also 242 * `Optional` - meaning that this method will always return an `Optional`. 243 */ 244 or(replacement) { 245 return this.tag ? this : replacement; 246 } 247 /** 248 * Get the value out of the inside of the `Optional` object, using a default 249 * `replacement` value if the provided `Optional` object does not contain a 250 * value. Unlike `getOr`, in this method the `replacement` value is 251 * "thunked" - that is to say that you don't pass a value to `getOrThunk`, you 252 * pass a function which (if called) will **return** the `value` you want to 253 * use. 254 */ 255 getOrThunk(thunk) { 256 return this.tag ? this.value : thunk(); 257 } 258 /** 259 * Get the value out of the inside of the `Optional` object, using a default 260 * `replacement` value if the provided Optional object does not contain a 261 * value. 262 * 263 * Unlike `or`, in this method the `replacement` value is "thunked" - that is 264 * to say that you don't pass a value to `orThunk`, you pass a function which 265 * (if called) will **return** the `value` you want to use. 266 * 267 * Unlike `getOrThunk`, in this method the `replacement` value is also 268 * `Optional`, meaning that this method will always return an `Optional`. 269 */ 270 orThunk(thunk) { 271 return this.tag ? this : thunk(); 272 } 273 /** 274 * Get the value out of the inside of the `Optional` object, throwing an 275 * exception if the provided `Optional` object does not contain a value. 276 * 277 * WARNING: 278 * You should only be using this function if you know that the `Optional` 279 * object **is not** empty (otherwise you're throwing exceptions in production 280 * code, which is bad). 281 * 282 * In tests this is more acceptable. 283 * 284 * Prefer other methods to this, such as `.each`. 285 */ 286 getOrDie(message) { 287 if (!this.tag) { 288 throw new Error(message ?? 'Called getOrDie on None'); 289 } 290 else { 291 return this.value; 292 } 293 } 294 // --- Interop with null and undefined --- 295 /** 296 * Creates an `Optional` value from a nullable (or undefined-able) input. 297 * Null, or undefined, is converted to `None`, and anything else is converted 298 * to `Some`. 299 */ 300 static from(value) { 301 return isNonNullable(value) ? Optional.some(value) : Optional.none(); 302 } 303 /** 304 * Converts an `Optional` to a nullable type, by getting the value if it 305 * exists, or returning `null` if it does not. 306 */ 307 getOrNull() { 308 return this.tag ? this.value : null; 309 } 310 /** 311 * Converts an `Optional` to an undefined-able type, by getting the value if 312 * it exists, or returning `undefined` if it does not. 313 */ 314 getOrUndefined() { 315 return this.value; 316 } 317 // --- Utilities --- 318 /** 319 * If the `Optional` contains a value, perform an action on that value. 320 * Unlike the rest of the methods on this type, `.each` has side-effects. If 321 * you want to transform an `Optional<T>` **into** something, then this is not 322 * the method for you. If you want to use an `Optional<T>` to **do** 323 * something, then this is the method for you - provided you're okay with not 324 * doing anything in the case where the `Optional` doesn't have a value inside 325 * it. If you're not sure whether your use-case fits into transforming 326 * **into** something or **doing** something, check whether it has a return 327 * value. If it does, you should be performing a transform. 328 */ 329 each(worker) { 330 if (this.tag) { 331 worker(this.value); 332 } 333 } 334 /** 335 * Turn the `Optional` object into an array that contains all of the values 336 * stored inside the `Optional`. In practice, this means the output will have 337 * either 0 or 1 elements. 338 */ 339 toArray() { 340 return this.tag ? [this.value] : []; 341 } 342 /** 343 * Turn the `Optional` object into a string for debugging or printing. Not 344 * recommended for production code, but good for debugging. Also note that 345 * these days an `Optional` object can be logged to the console directly, and 346 * its inner value (if it exists) will be visible. 347 */ 348 toString() { 349 return this.tag ? `some(${this.value})` : 'none()'; 350 } 351 } 352 353 const nativeSlice = Array.prototype.slice; 354 const nativeIndexOf = Array.prototype.indexOf; 355 const nativePush = Array.prototype.push; 356 const rawIndexOf = (ts, t) => nativeIndexOf.call(ts, t); 357 const indexOf
vendor: 3,266 bytes, lines 357-462
357 = (xs, x) => { 358 // The rawIndexOf method does not wrap up in an option. This is for performance reasons. 359 const r = rawIndexOf(xs, x); 360 return r === -1 ? Optional.none() : Optional.some(r); 361 }; 362 const contains$2 = (xs, x) => rawIndexOf(xs, x) > -1; 363 const exists = (xs, pred) => { 364 for (let i = 0, len = xs.length; i < len; i++) { 365 const x = xs[i]; 366 if (pred(x, i)) { 367 return true; 368 } 369 } 370 return false; 371 }; 372 const range$2 = (num, f) => { 373 const r = []; 374 for (let i = 0; i < num; i++) { 375 r.push(f(i)); 376 } 377 return r; 378 }; 379 // It's a total micro optimisation, but these do make some difference. 380 // Particularly for browsers other than Chrome. 381 // - length caching 382 // http://jsperf.com/browser-diet-jquery-each-vs-for-loop/69 383 // - not using push 384 // http://jsperf.com/array-direct-assignment-vs-push/2 385 const chunk$1 = (array, size) => { 386 const r = []; 387 for (let i = 0; i < array.length; i += size) { 388 const s = nativeSlice.call(array, i, i + size); 389 r.push(s); 390 } 391 return r; 392 }; 393 const map$2 = (xs, f) => { 394 // pre-allocating array size when it's guaranteed to be known 395 // http://jsperf.com/push-allocated-vs-dynamic/22 396 const len = xs.length; 397 const r = new Array(len); 398 for (let i = 0; i < len; i++) { 399 const x = xs[i]; 400 r[i] = f(x, i); 401 } 402 return r; 403 }; 404 // Unwound implementing other functions in terms of each. 405 // The code size is roughly the same, and it should allow for better optimisation. 406 // const each = function<T, U>(xs: T[], f: (x: T, i?: number, xs?: T[]) => void): void { 407 const each$1 = (xs, f) => { 408 for (let i = 0, len = xs.length; i < len; i++) { 409 const x = xs[i]; 410 f(x, i); 411 } 412 }; 413 const eachr = (xs, f) => { 414 for (let i = xs.length - 1; i >= 0; i--) { 415 const x = xs[i]; 416 f(x, i); 417 } 418 }; 419 const partition$3 = (xs, pred) => { 420 const pass = []; 421 const fail = []; 422 for (let i = 0, len = xs.length; i < len; i++) { 423 const x = xs[i]; 424 const arr = pred(x, i) ? pass : fail; 425 arr.push(x); 426 } 427 return { pass, fail }; 428 }; 429 const filter$2 = (xs, pred) => { 430 const r = []; 431 for (let i = 0, len = xs.length; i < len; i++) { 432 const x = xs[i]; 433 if (pred(x, i)) { 434 r.push(x); 435 } 436 } 437 return r; 438 }; 439 const foldr = (xs, f, acc) => { 440 eachr(xs, (x, i) => { 441 acc = f(acc, x, i); 442 }); 443 return acc; 444 }; 445 const foldl = (xs, f, acc) => { 446 each$1(xs, (x, i) => { 447 acc = f(acc, x, i); 448 }); 449 return acc; 450 }; 451 const findUntil = (xs, pred, until) => { 452 for (let i = 0, len = xs.length; i < len; i++) { 453 const x = xs[i]; 454 if (pred(x, i)) { 455 return Optional.some(x); 456 } 457 else if (until(x, i)) { 458 break; 459 } 460 } 461 return Optional.none(); 462 };
vendor: 5,749 bytes, lines 463-636
463 const find$5 = (xs, pred) => { 464 return findUntil(xs, pred, never); 465 }; 466 const findIndex$1 = (xs, pred) => { 467 for (let i = 0, len = xs.length; i < len; i++) { 468 const x = xs[i]; 469 if (pred(x, i)) { 470 return Optional.some(i); 471 } 472 } 473 return Optional.none(); 474 }; 475 const flatten = (xs) => { 476 // Note, this is possible because push supports multiple arguments: 477 // http://jsperf.com/concat-push/6 478 // Note that in the past, concat() would silently work (very slowly) for array-like objects. 479 // With this change it will throw an error. 480 const r = []; 481 for (let i = 0, len = xs.length; i < len; ++i) { 482 // Ensure that each value is an array itself 483 if (!isArray(xs[i])) { 484 throw new Error('Arr.flatten item ' + i + ' was not an array, input: ' + xs); 485 } 486 nativePush.apply(r, xs[i]); 487 } 488 return r; 489 }; 490 const bind$3 = (xs, f) => flatten(map$2(xs, f)); 491 const forall = (xs, pred) => { 492 for (let i = 0, len = xs.length; i < len; ++i) { 493 const x = xs[i]; 494 if (pred(x, i) !== true) { 495 return false; 496 } 497 } 498 return true; 499 }; 500 const reverse = (xs) => { 501 const r = nativeSlice.call(xs, 0); 502 r.reverse(); 503 return r; 504 }; 505 const difference = (a1, a2) => filter$2(a1, (x) => !contains$2(a2, x)); 506 const mapToObject = (xs, f) => { 507 const r = {}; 508 for (let i = 0, len = xs.length; i < len; i++) { 509 const x = xs[i]; 510 r[String(x)] = f(x, i); 511 } 512 return r; 513 }; 514 const pure$2 = (x) => [x]; 515 const sort = (xs, comparator) => { 516 const copy = nativeSlice.call(xs, 0); 517 copy.sort(comparator); 518 return copy; 519 }; 520 const get$i = (xs, i) => i >= 0 && i < xs.length ? Optional.some(xs[i]) : Optional.none(); 521 const head = (xs) => get$i(xs, 0); 522 const last$1 = (xs) => get$i(xs, xs.length - 1); 523 const from = isFunction(Array.from) ? Array.from : (x) => nativeSlice.call(x); 524 const findMap = (arr, f) => { 525 for (let i = 0; i < arr.length; i++) { 526 const r = f(arr[i], i); 527 if (r.isSome()) { 528 return r; 529 } 530 } 531 return Optional.none(); 532 }; 533 534 // There are many variations of Object iteration that are faster than the 'for-in' style: 535 // http://jsperf.com/object-keys-iteration/107 536 // 537 // Use the native keys if it is available (IE9+), otherwise fall back to manually filtering 538 const keys = Object.keys; 539 const hasOwnProperty = Object.hasOwnProperty; 540 const each = (obj, f) => { 541 const props = keys(obj); 542 for (let k = 0, len = props.length; k < len; k++) { 543 const i = props[k]; 544 const x = obj[i]; 545 f(x, i); 546 } 547 }; 548 const map$1 = (obj, f) => { 549 return tupleMap(obj, (x, i) => ({ 550 k: i, 551 v: f(x, i) 552 })); 553 }; 554 const tupleMap = (obj, f) => { 555 const r = {}; 556 each(obj, (x, i) => { 557 const tuple = f(x, i); 558 r[tuple.k] = tuple.v; 559 }); 560 return r; 561 }; 562 const objAcc = (r) => (x, i) => { 563 r[i] = x; 564 }; 565 const internalFilter = (obj, pred, onTrue, onFalse) => { 566 each(obj, (x, i) => { 567 (pred(x, i) ? onTrue : onFalse)(x, i); 568 }); 569 }; 570 const bifilter = (obj, pred) => { 571 const t = {}; 572 const f = {}; 573 internalFilter(obj, pred, objAcc(t), objAcc(f)); 574 return { t, f }; 575 }; 576 const filter$1 = (obj, pred) => { 577 const t = {}; 578 internalFilter(obj, pred, objAcc(t), noop); 579 return t; 580 }; 581 const mapToArray = (obj, f) => { 582 const r = []; 583 each(obj, (value, name) => { 584 r.push(f(value, name)); 585 }); 586 return r; 587 }; 588 const find$4 = (obj, pred) => { 589 const props = keys(obj); 590 for (let k = 0, len = props.length; k < len; k++) { 591 const i = props[k]; 592 const x = obj[i]; 593 if (pred(x, i, obj)) { 594 return Optional.some(x); 595 } 596 } 597 return Optional.none(); 598 }; 599 const values = (obj) => { 600 return mapToArray(obj, identity); 601 }; 602 const get$h = (obj, key) => { 603 return has$2(obj, key) ? Optional.from(obj[key]) : Optional.none(); 604 }; 605 const has$2 = (obj, key) => hasOwnProperty.call(obj, key); 606 const hasNonNullableKey = (obj, key) => has$2(obj, key) && obj[key] !== undefined && obj[key] !== null; 607 608 /* 609 * Generates a church encoded ADT (https://en.wikipedia.org/wiki/Church_encoding) 610 * For syntax and use, look at the test code. 611 */ 612 const generate$7 = (cases) => { 613 // validation 614 if (!isArray(cases)) { 615 throw new Error('cases must be an array'); 616 } 617 if (cases.length === 0) { 618 throw new Error('there must be at least one case'); 619 } 620 const constructors = []; 621 // adt is mutated to add the individual cases 622 const adt = {}; 623 each$1(cases, (acase, count) => { 624 const keys$1 = keys(acase); 625 // validation 626 if (keys$1.length !== 1) { 627 throw new Error('one and only one name per case'); 628 } 629 const key = keys$1[0]; 630 const value = acase[key]; 631 // validation 632 if (adt[key] !== undefined) { 633 throw new Error('duplicate key detected:' + key); 634 } 635 else if (key === 'cata') { 636 throw new Error('cannot have a case named cata (sorry
636)'); 637 } 638 else if (!isArray(value)) { 639 // this implicitly checks if acase is an object 640 throw new Error('case arguments must be an array'); 641 } 642 constructors.push(key); 643 // 644 // constructor for key 645 // 646 adt[key] = (...args) => { 647 const argLength = args.length; 648 // validation 649 if (argLength !== value.length) { 650 throw new Error('Wrong number of arguments to case ' + key + '. Expected ' + value.length + ' (' + value + '), got ' + argLength); 651 } 652 const match = (branches) => { 653 const branchKeys = keys(branches); 654 if (constructors.length !== branchKeys.length) { 655 throw new Error('Wrong number of arguments to match. Expected: ' + constructors.join(',') + '\nActual: ' + branchKeys.join(',')); 656 } 657 const allReqd = forall(constructors, (reqKey) => { 658 return contains$2(branchKeys, reqKey); 659 }); 660 if (!allReqd) { 661 throw new Error('Not all branches were specified when using match. Specified: ' + branchKeys.join(', ') + '\nRequired: ' + constructors.join(', ')); 662 } 663 return branches[key].apply(null, args); 664 }; 665 // 666 // the fold function for key 667 // 668 return { 669 fold: (...foldArgs) => { 670 // runtime validation 671 if (foldArgs.length !== cases.length) { 672 throw new Error('Wrong number of arguments to fold. Expected ' + cases.length + ', got ' + foldArgs.length); 673 } 674 const target = foldArgs[count]; 675 return target.apply(null, args); 676 }, 677 match, 678 // NOTE: Only for debugging. 679 log: (label) => { 680 // eslint-disable-next-line no-console 681 console.log(label, { 682 constructors, 683 constructor: key, 684 params: args 685 }); 686 } 687 }; 688 }; 689 }); 690 return adt; 691 }; 692 const Adt = { 693 generate: generate$7 694 }; 695 696 const Cell = (initial) => { 697 let value = initial; 698 const get = () => { 699 return value; 700 }; 701 const set = (v) => { 702 value = v; 703 }; 704 return { 705 get, 706 set 707 }; 708 }; 709 710 const nu$d = (baseFn) => { 711 let data = Optional.none(); 712 let callbacks = []; 713 /** map :: this LazyValue a -> (a -> b) -> LazyValue b */ 714 const map = (f) => nu$d((nCallback) => { 715 get((data) => { 716 nCallback(f(data)); 717 }); 718 }); 719 const get = (nCallback) => { 720 if (isReady()) { 721 call(nCallback); 722 } 723 else { 724 callbacks.push(nCallback); 725 } 726 }; 727 const set = (x) => { 728 if (!isReady()) { 729 data = Optional.some(x); 730 run(callbacks); 731 callbacks = []; 732 } 733 }; 734 const isReady = () => data.isSome(); 735 const run = (cbs) => { 736 each$1(cbs, call); 737 }; 738 const call = (cb) => { 739 data.each((x) => { 740 setTimeout(() => { 741 cb(x); 742 }, 0); 743 }); 744 }; 745 // Lazy values cache the value and kick off immediately 746 baseFn(set); 747 return { 748 get, 749 map, 750 isReady 751 }; 752 }; 753 const pure$1 = (a) => nu$d((callback) => { 754 callback(a); 755 }); 756 const LazyValue = { 757 nu: nu$d, 758 pure: pure$1 759 }; 760 761 const errorReporter = (err) => { 762 // we can not throw the error in the reporter as it will just be black-holed 763 // by the Promise so we use a setTimeout to escape the Promise. 764 setTimeout(() => { 765 throw err; 766 }, 0); 767 }; 768 const make$8 = (run) => { 769 const get = (callback) => { 770 run().then(callback, errorReporter); 771 }; 772 /** map :: this Future a -> (a -> b) -> Future b */ 773 const map = (fab) => { 774 return make$8(() => run().then(fab)); 775 }; 776 /** bind :: this Future a -> (a -> Future b) -> Future b */ 777 const bind = (aFutureB) => { 778 return make$8(() => run().then((v) =>
778 aFutureB(v).toPromise())); 779 }; 780 /** anonBind :: this Future a -> Future b -> Future b 781 * Returns a future, which evaluates the first future, ignores the result, then evaluates the second. 782 */ 783 const anonBind = (futureB) => { 784 return make$8(() => run().then(() => futureB.toPromise())); 785 }; 786 const toLazy = () => { 787 return LazyValue.nu(get); 788 }; 789 const toCached = () => { 790 let cache = null; 791 return make$8(() => { 792 if (cache === null) { 793 cache = run(); 794 } 795 return cache; 796 }); 797 }; 798 const toPromise = run; 799 return { 800 map, 801 bind, 802 anonBind, 803 toLazy, 804 toCached, 805 toPromise, 806 get 807 }; 808 }; 809 const nu$c = (baseFn) => { 810 return make$8(() => new Promise(baseFn)); 811 }; 812 /** a -> Future a */ 813 const pure = (a) => { 814 return make$8(() => Promise.resolve(a)); 815 }; 816 const Future = { 817 nu: nu$c, 818 pure 819 }; 820 821 /** 822 * Creates a new `Result<T, E>` that **does** contain a value. 823 */ 824 const value$4 = (value) => { 825 const applyHelper = (fn) => fn(value); 826 const constHelper = constant$1(value); 827 const outputHelper = () => output; 828 const output = { 829 // Debug info 830 tag: true, 831 inner: value, 832 // Actual Result methods 833 fold: (_onError, onValue) => onValue(value), 834 isValue: always, 835 isError: never, 836 map: (mapper) => Result.value(mapper(value)), 837 mapError: outputHelper, 838 bind: applyHelper, 839 exists: applyHelper, 840 forall: applyHelper, 841 getOr: constHelper, 842 or: outputHelper, 843 getOrThunk: constHelper, 844 orThunk: outputHelper, 845 getOrDie: constHelper, 846 each: (fn) => { 847 // Can't write the function inline because we don't want to return something by mistake 848 fn(value); 849 }, 850 toOptional: () => Optional.some(value), 851 }; 852 return output; 853 }; 854 /** 855 * Creates a new `Result<T, E>` that **does not** contain a value, and therefore 856 * contains an error. 857 */ 858 const error$1 = (error) => { 859 const outputHelper = () => output; 860 const output = { 861 // Debug info 862 tag: false, 863 inner: error, 864 // Actual Result methods 865 fold: (onError, _onValue) => onError(error), 866 isValue: never, 867 isError: always, 868 map: outputHelper, 869 mapError: (mapper) => Result.error(mapper(error)), 870 bind: outputHelper, 871 exists: never, 872 forall: always, 873 getOr: identity, 874 or: identity, 875 getOrThunk: apply$1, 876 orThunk: apply$1, 877 getOrDie: die(String(error)), 878 each: noop, 879 toOptional: Optional.none, 880 }; 881 return output; 882 }; 883 /** 884 * Creates a new `Result<T, E>` from an `Optional<T>` and an `E`. If the 885 * `Optional` contains a value, so will the outputted `Result`. If it does not, 886 * the outputted `Result` will contain an error (and that error will be the 887 * error passed in). 888 */ 889 const fromOption = (optional, err) => optional.fold(() => error$1(err), value$4); 890 const Result = { 891 value: value$4, 892 error: error$1, 893 fromOption 894 }; 895 896 const wrap$2 = (delegate) => { 897 const toCached = () => { 898 return wrap$2(delegate.toCached()); 899 }; 900 const bindFuture = (f) => { 901 return wrap$2(delegate.bind((resA) => resA.fold((err) => (Future.pure(Result.error(err))), (a) => f(a)))); 902 }; 903 const bindResult = (f) => { 904 return wrap$2(delegate.map((resA) => resA.bind(f))); 905 }; 906 const mapResult = (f) => { 907 return wrap$2(delegate.map((resA) => resA.map(f))); 908 }; 909 const mapError = (f) => { 910 return wrap$2(delegate.map((resA) => resA.mapError(f))); 911 }; 912 const foldResult = (whenError, whenValue) => { 913 return delegate.map((res) => res.fold(whenError, whenValue)); 914 }; 915 const withTimeout = (timeout, errorThunk) => { 916 return wrap$2(Future.nu((callback) => { 917 let timedOut = false;
918 const timer = setTimeout(() => { 919 timedOut = true; 920 callback(Result.error(errorThunk())); 921 }, timeout); 922 delegate.get((result) => { 923 if (!timedOut) { 924 clearTimeout(timer); 925 callback(result); 926 } 927 }); 928 })); 929 }; 930 return { 931 ...delegate, 932 toCached, 933 bindFuture, 934 bindResult, 935 mapResult, 936 mapError, 937 foldResult, 938 withTimeout 939 }; 940 }; 941 const nu$b = (worker) => { 942 return wrap$2(Future.nu(worker)); 943 }; 944 const value$3 = (value) => { 945 return wrap$2(Future.pure(Result.value(value))); 946 }; 947 const error = (error) => { 948 return wrap$2(Future.pure(Result.error(error))); 949 }; 950 const fromResult$1 = (result) => { 951 return wrap$2(Future.pure(result)); 952 }; 953 const fromFuture = (future) => { 954 return wrap$2(future.map(Result.value)); 955 }; 956 const fromPromise = (promise) => { 957 return nu$b((completer) => { 958 promise.then((value) => { 959 completer(Result.value(value)); 960 }, (error) => { 961 completer(Result.error(error)); 962 }); 963 }); 964 }; 965 const FutureResult = { 966 nu: nu$b, 967 wrap: wrap$2, 968 pure: value$3, 969 value: value$3, 970 error, 971 fromResult: fromResult$1, 972 fromFuture, 973 fromPromise 974 }; 975 976 // Use window object as the global if it's available since CSP will block script evals 977 // eslint-disable-next-line @typescript-eslint/no-implied-eval 978 const Global = typeof window !== 'undefined' ? window : Function('return this;')(); 979 980 /** 981 * Adds two numbers, and wrap to a range. 982 * If the result overflows to the right, snap to the left. 983 * If the result overflows to the left, snap to the right. 984 */ 985 const cycleBy = (value, delta, min, max) => { 986 const r = value + delta; 987 if (r > max) { 988 return min; 989 } 990 else if (r < min) { 991 return max; 992 } 993 else { 994 return r; 995 } 996 }; 997 // ASSUMPTION: Max will always be larger than min 998 const clamp = (value, min, max) => Math.min(Math.max(value, min), max); 999 // 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 1000 const random = () => window.crypto.getRandomValues(new Uint32Array(1))[0] / 4294967295; 1001 1002 /** 1003 * Generate a unique identifier. 1004 * 1005 * The unique portion of the identifier only contains an underscore 1006 * and digits, so that it may safely be used within HTML attributes. 1007 * 1008 * The chance of generating a non-unique identifier has been minimized 1009 * by combining the current time, a random number and a one-up counter. 1010 * 1011 * generate :: String -> String 1012 */ 1013 let unique = 0; 1014 const generate$6 = (prefix) => { 1015 const date = new Date(); 1016 const time = date.getTime(); 1017 const random$1 = Math.floor(random() * 1000000000); 1018 unique++; 1019 return prefix + '_' + random$1 + unique + String(time); 1020 }; 1021 1022 const shallow$1 = (old, nu) => { 1023 return nu; 1024 }; 1025 const deep$1 = (old, nu) => { 1026 const bothObjects = isPlainObject(old) && isPlainObject(nu); 1027 return bothObjects ? deepMerge(old, nu) : nu; 1028 }; 1029 const baseMerge = (merger) => { 1030 return (...objects) => { 1031 if (objects.length === 0) { 1032 throw new Error(`Can't merge zero objects`); 1033 } 1034 const ret = {}; 1035 for (let j = 0; j < objects.length; j++) { 1036 const curObject = objects[j]; 1037 for (const key in curObject) { 1038 if (has$2(curObject, key)) { 1039 ret[key] = merger(ret[key], curObject[key]); 1040 } 1041 } 1042 } 1043 return ret; 1044 }; 1045 }; 1046 const deepMerge = baseMerge(deep$1); 1047 const merge$1 = baseMerge(shallow$1); 1048 1049 /** 1050 * **Is** the value stored inside this Optional object equal to `rhs`? 1051 */ 1052 const is$1 = (lhs, rhs, comparator = tripleEquals) => lhs.exists((left) => comparator(left, rhs)); 1053 /** 1054 * Are these two Optional objects equal? Equality here means either they're both 1055 * `Some` (and the values are equal under the comparator) or they're both `None`. 1056 */ 1057 const equals = (lhs, rhs, comparator = tripleEquals) => lift2(lhs, rhs, comparator).getOr(lhs.isNone() && rhs.isNone());
vendor: 71,133 bytes, lines 1058-2905
1058 const cat = (arr) => { 1059 const r = []; 1060 const push = (x) => { 1061 r.push(x); 1062 }; 1063 for (let i = 0; i < arr.length; i++) { 1064 arr[i].each(push); 1065 } 1066 return r; 1067 }; 1068 const sequence = (arr) => { 1069 const r = []; 1070 for (let i = 0; i < arr.length; i++) { 1071 const x = arr[i]; 1072 if (x.isSome()) { 1073 r.push(x.getOrDie()); 1074 } 1075 else { 1076 return Optional.none(); 1077 } 1078 } 1079 return Optional.some(r); 1080 }; 1081 /* 1082 Notes on the lift functions: 1083 - We used to have a generic liftN, but we were concerned about its type-safety, and the below variants were faster in microbenchmarks. 1084 - The getOrDie calls are partial functions, but are checked beforehand. This is faster and more convenient (but less safe) than folds. 1085 - && is used instead of a loop for simplicity and performance. 1086 */ 1087 const lift2 = (oa, ob, f) => oa.isSome() && ob.isSome() ? Optional.some(f(oa.getOrDie(), ob.getOrDie())) : Optional.none(); 1088 const lift3 = (oa, ob, oc, f) => oa.isSome() && ob.isSome() && oc.isSome() ? Optional.some(f(oa.getOrDie(), ob.getOrDie(), oc.getOrDie())) : Optional.none(); 1089 const mapFrom = (a, f) => (a !== undefined && a !== null) ? Optional.some(f(a)) : Optional.none(); 1090 // This can help with type inference, by specifying the type param on the none case, so the caller doesn't have to. 1091 const someIf = (b, a) => b ? Optional.some(a) : Optional.none(); 1092 1093 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping 1094 const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); 1095 1096 /** path :: ([String], JsObj?) -> JsObj */ 1097 const path$1 = (parts, scope) => { 1098 let o = scope !== undefined && scope !== null ? scope : Global; 1099 for (let i = 0; i < parts.length && o !== undefined && o !== null; ++i) { 1100 o = o[parts[i]]; 1101 } 1102 return o; 1103 }; 1104 /** resolve :: (String, JsObj?) -> JsObj */ 1105 const resolve = (p, scope) => { 1106 const parts = p.split('.'); 1107 return path$1(parts, scope); 1108 }; 1109 1110 Adt.generate([ 1111 { bothErrors: ['error1', 'error2'] }, 1112 { firstError: ['error1', 'value2'] }, 1113 { secondError: ['value1', 'error2'] }, 1114 { bothValues: ['value1', 'value2'] } 1115 ]); 1116 /** partition :: [Result a] -> { errors: [String], values: [a] } */ 1117 const partition$2 = (results) => { 1118 const errors = []; 1119 const values = []; 1120 each$1(results, (result) => { 1121 result.fold((err) => { 1122 errors.push(err); 1123 }, (value) => { 1124 values.push(value); 1125 }); 1126 }); 1127 return { errors, values }; 1128 }; 1129 1130 const singleton$1 = (doRevoke) => { 1131 const subject = Cell(Optional.none()); 1132 const revoke = () => subject.get().each(doRevoke); 1133 const clear = () => { 1134 revoke(); 1135 subject.set(Optional.none()); 1136 }; 1137 const isSet = () => subject.get().isSome(); 1138 const get = () => subject.get(); 1139 const set = (s) => { 1140 revoke(); 1141 subject.set(Optional.some(s)); 1142 }; 1143 return { 1144 clear, 1145 isSet, 1146 get, 1147 set 1148 }; 1149 }; 1150 const destroyable = () => singleton$1((s) => s.destroy()); 1151 const unbindable = () => singleton$1((s) => s.unbind()); 1152 const value$2 = () => { 1153 const subject = singleton$1(noop); 1154 const on = (f) => subject.get().each(f); 1155 return { 1156 ...subject, 1157 on 1158 }; 1159 }; 1160 1161 const addToEnd = (str, suffix) => { 1162 return str + suffix; 1163 }; 1164 const removeFromStart = (str, numChars) => { 1165 return str.substring(numChars); 1166 }; 1167 1168 const checkRange = (str, substr, start) => substr === '' || str.length >= substr.length && str.substr(start, start + substr.length) === substr; 1169 const removeLeading = (str, prefix) => { 1170 return startsWith(str, prefix) ? removeFromStart(str, prefix.length) : str; 1171 }; 1172 const ensureTrailing = (str, suffix) => { 1173 return endsWith(str, suffix) ? str : addToEnd(str, suffix); 1174 }; 1175 const contains$1 = (str, substr, start = 0, end) => { 1176 const idx = str.indexOf(substr, start); 1177 if (idx !== -1) { 1178 return isUndefined(end) ? true : idx + substr.length <= end; 1179 } 1180 else { 1181 return false; 1182 } 1183 }; 1184 /** Does 'str' start with 'prefix'? 1185 * Note: all strings start with the empty string. 1186 * More formally, for all strings x, startsWith(x, ""). 1187 * This is so that for all strings x and y, startsWith(y + x, y) 1188 */ 1189 const startsWith = (str, prefix) => { 1190 return checkRange(str, prefix, 0); 1191 }; 1192 /** Does 'str' end with 'suffix'? 1193 * Note: all strings end with the empty string. 1194 * More formally, for all strings x, endsWith(x, ""). 1195 * This is so that for all strings x and y, endsWith(x + y, y) 1196 */ 1197 const endsWith = (str, suffix) => { 1198 return checkRange(str, suffix, str.length - suffix.length); 1199 }; 1200 const blank = (r) => (s) => s.replace(r, ''); 1201 /** removes all leading and trailing spaces */ 1202 const trim$1 = blank(/^\s+|\s+$/g); 1203 const isNotEmpty = (s) => s.length > 0; 1204 const isEmpty = (s) => !isNotEmpty(s); 1205 const toFloat = (value) => { 1206 const num = parseFloat(value); 1207 return isNaN(num) ? Optional.none() : Optional.some(num); 1208 }; 1209 1210 // Run a function fn after rate ms. If another invocation occurs 1211 // during the time it is waiting, update the arguments f will run 1212 // with (but keep the current schedule) 1213 const adaptable = (fn, rate) => { 1214 let timer = null; 1215 let args = null; 1216 const cancel = () => { 1217 if (!isNull(timer)) { 1218 clearTimeout(timer); 1219 timer = null; 1220 args = null; 1221 } 1222 }; 1223 const throttle = (...newArgs) => { 1224 args = newArgs; 1225 if (isNull(timer)) { 1226 timer = setTimeout(() => { 1227 const tempArgs = args; 1228 timer = null; 1229 args = null; 1230 fn.apply(null, tempArgs); 1231 }, rate); 1232 } 1233 }; 1234 return { 1235 cancel, 1236 throttle 1237 }; 1238 }; 1239 // Run a function fn after rate ms. If another invocation occurs 1240 // during the time it is waiting, ignore it completely. 1241 const first$1 = (fn, rate) => { 1242 let timer = null; 1243 const cancel = () => { 1244 if (!isNull(timer)) { 1245 clearTimeout(timer); 1246 timer = null; 1247 } 1248 }; 1249 const throttle = (...args) => { 1250 if (isNull(timer)) { 1251 timer = setTimeout(() => { 1252 timer = null; 1253 fn.apply(null, args); 1254 }, rate); 1255 } 1256 }; 1257 return { 1258 cancel, 1259 throttle 1260 }; 1261 }; 1262 // Run a function fn after rate ms. If another invocation occurs 1263 // during the time it is waiting, reschedule the function again 1264 // with the new arguments. 1265 const last = (fn, rate) => { 1266 let timer = null; 1267 const cancel = () => { 1268 if (!isNull(timer)) { 1269 clearTimeout(timer); 1270 timer = null; 1271 } 1272 }; 1273 const throttle = (...args) => { 1274 cancel(); 1275 timer = setTimeout(() => { 1276 timer = null; 1277 fn.apply(null, args); 1278 }, rate); 1279 }; 1280 return { 1281 cancel, 1282 throttle 1283 }; 1284 }; 1285 1286 const cached = (f) => { 1287 let called = false; 1288 let r; 1289 return (...args) => { 1290 if (!called) { 1291 called = true; 1292 r = f.apply(null, args); 1293 } 1294 return r; 1295 }; 1296 }; 1297 1298 const zeroWidth = '\uFEFF'; 1299 const nbsp = '\u00A0'; 1300 1301 const fromHtml$2 = (html, scope) => { 1302 const doc = scope || document; 1303 const div = doc.createElement('div'); 1304 div.innerHTML = html; 1305 if (!div.hasChildNodes() || div.childNodes.length > 1) { 1306 const message = 'HTML does not have a single root node'; 1307 // eslint-disable-next-line no-console 1308 console.error(message, html); 1309 throw new Error(message); 1310 } 1311 return fromDom(div.childNodes[0]); 1312 }; 1313 const fromTag = (tag, scope) => { 1314 const doc = scope || document; 1315 const node = doc.createElement(tag); 1316 return fromDom(node); 1317 }; 1318 const fromText = (text, scope) => { 1319 const doc = scope || document; 1320 const node = doc.createTextNode(text); 1321 return fromDom(node); 1322 }; 1323 const fromDom = (node) => { 1324 // TODO: Consider removing this check, but left atm for safety 1325 if (node === null || node === undefined) { 1326 throw new Error('Node cannot be null or undefined'); 1327 } 1328 return { 1329 dom: node 1330 }; 1331 }; 1332 const fromPoint = (docElm, x, y) => Optional.from(docElm.dom.elementFromPoint(x, y)).map(fromDom); 1333 // tslint:disable-next-line:variable-name 1334 const SugarElement = { 1335 fromHtml: fromHtml$2, 1336 fromTag, 1337 fromText, 1338 fromDom, 1339 fromPoint 1340 }; 1341 1342 // NOTE: Mutates the range. 1343 const setStart = (rng, situ) => { 1344 situ.fold((e) => { 1345 rng.setStartBefore(e.dom); 1346 }, (e, o) => { 1347 rng.setStart(e.dom, o); 1348 }, (e) => { 1349 rng.setStartAfter(e.dom); 1350 }); 1351 }; 1352 const setFinish = (rng, situ) => { 1353 situ.fold((e) => { 1354 rng.setEndBefore(e.dom); 1355 }, (e, o) => { 1356 rng.setEnd(e.dom, o); 1357 }, (e) => { 1358 rng.setEndAfter(e.dom); 1359 }); 1360 }; 1361 const relativeToNative = (win, startSitu, finishSitu) => { 1362 const range = win.document.createRange(); 1363 setStart(range, startSitu); 1364 setFinish(range, finishSitu); 1365 return range; 1366 }; 1367 const exactToNative = (win, start, soffset, finish, foffset) => { 1368 const rng = win.document.createRange(); 1369 rng.setStart(start.dom, soffset); 1370 rng.setEnd(finish.dom, foffset); 1371 return rng; 1372 }; 1373 const toRect = (rect) => ({ 1374 left: rect.left, 1375 top: rect.top, 1376 right: rect.right, 1377 bottom: rect.bottom, 1378 width: rect.width, 1379 height: rect.height 1380 }); 1381 const getFirstRect$1 = (rng) => { 1382 const rects = rng.getClientRects(); 1383 // ASSUMPTION: The first rectangle is the start of the selection 1384 const rect = rects.length > 0 ? rects[0] : rng.getBoundingClientRect(); 1385 return rect.width > 0 || rect.height > 0 ? Optional.some(rect).map(toRect) : Optional.none(); 1386 }; 1387 const getBounds$3 = (rng) => { 1388 const rect = rng.getBoundingClientRect(); 1389 return rect.width > 0 || rect.height > 0 ? Optional.some(rect).map(toRect) : Optional.none(); 1390 }; 1391 1392 const adt$a = Adt.generate([ 1393 { ltr: ['start', 'soffset', 'finish', 'foffset'] }, 1394 { rtl: ['start', 'soffset', 'finish', 'foffset'] } 1395 ]); 1396 const fromRange = (win, type, range) => type(SugarElement.fromDom(range.startContainer), range.startOffset, SugarElement.fromDom(range.endContainer), range.endOffset); 1397 const getRanges = (win, selection) => selection.match({ 1398 domRange: (rng) => { 1399 return { 1400 ltr: constant$1(rng), 1401 rtl: Optional.none 1402 }; 1403 }, 1404 relative: (startSitu, finishSitu) => { 1405 return { 1406 ltr: cached(() => relativeToNative(win, startSitu, finishSitu)), 1407 rtl: cached(() => Optional.some(relativeToNative(win, finishSitu, startSitu))) 1408 }; 1409 }, 1410 exact: (start, soffset, finish, foffset) => { 1411 return { 1412 ltr: cached(() => exactToNative(win, start, soffset, finish, foffset)), 1413 rtl: cached(() => Optional.some(exactToNative(win, finish, foffset, start, soffset))) 1414 }; 1415 } 1416 }); 1417 const doDiagnose = (win, ranges) => { 1418 // If we cannot create a ranged selection from start > finish, it could be RTL 1419 const rng = ranges.ltr(); 1420 if (rng.collapsed) { 1421 // Let's check if it's RTL ... if it is, then reversing the direction will not be collapsed 1422 const reversed = ranges.rtl().filter((rev) => rev.collapsed === false); 1423 return reversed.map((rev) => 1424 // We need to use "reversed" here, because the original only has one point (collapsed) 1425 adt$a.rtl(SugarElement.fromDom(rev.endContainer), rev.endOffset, SugarElement.fromDom(rev.startContainer), rev.startOffset)).getOrThunk(() => fromRange(win, adt$a.ltr, rng)); 1426 } 1427 else { 1428 return fromRange(win, adt$a.ltr, rng); 1429 } 1430 }; 1431 const diagnose = (win, selection) => { 1432 const ranges = getRanges(win, selection); 1433 return doDiagnose(win, ranges); 1434 }; 1435 const asLtrRange = (win, selection) => { 1436 const diagnosis = diagnose(win, selection); 1437 return diagnosis.match({ 1438 ltr: (start, soffset, finish, foffset) => { 1439 const rng = win.document.createRange(); 1440 rng.setStart(start.dom, soffset); 1441 rng.setEnd(finish.dom, foffset); 1442 return rng; 1443 }, 1444 rtl: (start, soffset, finish, foffset) => { 1445 // NOTE: Reversing start and finish 1446 const rng = win.document.createRange(); 1447 rng.setStart(finish.dom, foffset); 1448 rng.setEnd(start.dom, soffset); 1449 return rng; 1450 } 1451 }); 1452 }; 1453 adt$a.ltr; 1454 adt$a.rtl; 1455 1456 const DOCUMENT = 9; 1457 const DOCUMENT_FRAGMENT = 11; 1458 const ELEMENT = 1; 1459 const TEXT = 3; 1460 1461 const is = (element, selector) => { 1462 const dom = element.dom; 1463 if (dom.nodeType !== ELEMENT) { 1464 return false; 1465 } 1466 else { 1467 const elem = dom; 1468 if (elem.matches !== undefined) { 1469 return elem.matches(selector); 1470 } 1471 else if (elem.msMatchesSelector !== undefined) { 1472 return elem.msMatchesSelector(selector); 1473 } 1474 else if (elem.webkitMatchesSelector !== undefined) { 1475 return elem.webkitMatchesSelector(selector); 1476 } 1477 else if (elem.mozMatchesSelector !== undefined) { 1478 // cast to any as mozMatchesSelector doesn't exist in TS DOM lib 1479 return elem.mozMatchesSelector(selector); 1480 } 1481 else { 1482 throw new Error('Browser lacks native selectors'); 1483 } // unfortunately we can't throw this on startup :( 1484 } 1485 }; 1486 const bypassSelector = (dom) => 1487 // Only elements, documents and shadow roots support querySelector 1488 // shadow root element type is DOCUMENT_FRAGMENT 1489 dom.nodeType !== ELEMENT && dom.nodeType !== DOCUMENT && dom.nodeType !== DOCUMENT_FRAGMENT || 1490 // IE fix for complex queries on empty nodes: http://jsfiddle.net/spyder/fv9ptr5L/ 1491 dom.childElementCount === 0; 1492 const all$3 = (selector, scope) => { 1493 const base = scope === undefined ? document : scope.dom; 1494 return bypassSelector(base) ? [] : map$2(base.querySelectorAll(selector), SugarElement.fromDom); 1495 }; 1496 const one = (selector, scope) => { 1497 const base = scope === undefined ? document : scope.dom; 1498 return bypassSelector(base) ? Optional.none() : Optional.from(base.querySelector(selector)).map(SugarElement.fromDom); 1499 }; 1500 1501 const eq = (e1, e2) => e1.dom === e2.dom; 1502 // Returns: true if node e1 contains e2, otherwise false. 1503 // (returns false if e1===e2: A node does not contain itself). 1504 const contains = (e1, e2) => { 1505 const d1 = e1.dom; 1506 const d2 = e2.dom; 1507 return d1 === d2 ? false : d1.contains(d2); 1508 }; 1509 1510 const DeviceType = (os, browser, userAgent, mediaMatch) => { 1511 const isiPad = os.isiOS() && /ipad/i.test(userAgent) === true; 1512 const isiPhone = os.isiOS() && !isiPad; 1513 const isMobile = os.isiOS() || os.isAndroid(); 1514 const isTouch = isMobile || mediaMatch('(pointer:coarse)'); 1515 const isTablet = isiPad || !isiPhone && isMobile && mediaMatch('(min-device-width:768px)'); 1516 const isPhone = isiPhone || isMobile && !isTablet; 1517 const iOSwebview = browser.isSafari() && os.isiOS() && /safari/i.test(userAgent) === false; 1518 const isDesktop = !isPhone && !isTablet && !iOSwebview; 1519 return { 1520 isiPad: constant$1(isiPad), 1521 isiPhone: constant$1(isiPhone), 1522 isTablet: constant$1(isTablet), 1523 isPhone: constant$1(isPhone), 1524 isTouch: constant$1(isTouch), 1525 isAndroid: os.isAndroid, 1526 isiOS: os.isiOS, 1527 isWebView: constant$1(iOSwebview), 1528 isDesktop: constant$1(isDesktop) 1529 }; 1530 }; 1531 1532 const firstMatch = (regexes, s) => { 1533 for (let i = 0; i < regexes.length; i++) { 1534 const x = regexes[i]; 1535 if (x.test(s)) { 1536 return x; 1537 } 1538 } 1539 return undefined; 1540 }; 1541 const find$3 = (regexes, agent) => { 1542 const r = firstMatch(regexes, agent); 1543 if (!r) { 1544 return { major: 0, minor: 0 }; 1545 } 1546 const group = (i) => { 1547 return Number(agent.replace(r, '$' + i)); 1548 }; 1549 return nu$a(group(1), group(2)); 1550 }; 1551 const detect$4 = (versionRegexes, agent) => { 1552 const cleanedAgent = String(agent).toLowerCase(); 1553 if (versionRegexes.length === 0) { 1554 return unknown$3(); 1555 } 1556 return find$3(versionRegexes, cleanedAgent); 1557 }; 1558 const unknown$3 = () => { 1559 return nu$a(0, 0); 1560 }; 1561 const nu$a = (major, minor) => { 1562 return { major, minor }; 1563 }; 1564 const Version = { 1565 nu: nu$a, 1566 detect: detect$4, 1567 unknown: unknown$3 1568 }; 1569 1570 const detectBrowser$1 = (browsers, userAgentData) => { 1571 return findMap(userAgentData.brands, (uaBrand) => { 1572 const lcBrand = uaBrand.brand.toLowerCase(); 1573 return find$5(browsers, (browser) => lcBrand === browser.brand?.toLowerCase()) 1574 .map((info) => ({ 1575 current: info.name, 1576 version: Version.nu(parseInt(uaBrand.version, 10), 0) 1577 })); 1578 }); 1579 }; 1580 1581 const detect$3 = (candidates, userAgent) => { 1582 const agent = String(userAgent).toLowerCase(); 1583 return find$5(candidates, (candidate) => { 1584 return candidate.search(agent); 1585 }); 1586 }; 1587 // They (browser and os) are the same at the moment, but they might 1588 // not stay that way. 1589 const detectBrowser = (browsers, userAgent) => { 1590 return detect$3(browsers, userAgent).map((browser) => { 1591 const version = Version.detect(browser.versionRegexes, userAgent); 1592 return { 1593 current: browser.name, 1594 version 1595 }; 1596 }); 1597 }; 1598 const detectOs = (oses, userAgent) => { 1599 return detect$3(oses, userAgent).map((os) => { 1600 const version = Version.detect(os.versionRegexes, userAgent); 1601 return { 1602 current: os.name, 1603 version 1604 }; 1605 }); 1606 }; 1607 1608 const normalVersionRegex = /.*?version\/\ ?([0-9]+)\.([0-9]+).*/; 1609 const checkContains = (target) => { 1610 return (uastring) => { 1611 return contains$1(uastring, target); 1612 }; 1613 }; 1614 const browsers = [ 1615 // This is legacy Edge 1616 { 1617 name: 'Edge', 1618 versionRegexes: [/.*?edge\/ ?([0-9]+)\.([0-9]+)$/], 1619 search: (uastring) => { 1620 return contains$1(uastring, 'edge/') && contains$1(uastring, 'chrome') && contains$1(uastring, 'safari') && contains$1(uastring, 'applewebkit'); 1621 } 1622 }, 1623 // This is Google Chrome and Chromium Edge 1624 { 1625 name: 'Chromium', 1626 brand: 'Chromium', 1627 versionRegexes: [/.*?chrome\/([0-9]+)\.([0-9]+).*/, normalVersionRegex], 1628 search: (uastring) => { 1629 return contains$1(uastring, 'chrome') && !contains$1(uastring, 'chromeframe'); 1630 } 1631 }, 1632 { 1633 name: 'IE', 1634 versionRegexes: [/.*?msie\ ?([0-9]+)\.([0-9]+).*/, /.*?rv:([0-9]+)\.([0-9]+).*/], 1635 search: (uastring) => { 1636 return contains$1(uastring, 'msie') || contains$1(uastring, 'trident'); 1637 } 1638 }, 1639 // INVESTIGATE: Is this still the Opera user agent? 1640 { 1641 name: 'Opera', 1642 versionRegexes: [normalVersionRegex, /.*?opera\/([0-9]+)\.([0-9]+).*/], 1643 search: checkContains('opera') 1644 }, 1645 { 1646 name: 'Firefox', 1647 versionRegexes: [/.*?firefox\/\ ?([0-9]+)\.([0-9]+).*/], 1648 search: checkContains('firefox') 1649 }, 1650 { 1651 name: 'Safari', 1652 versionRegexes: [normalVersionRegex, /.*?cpu os ([0-9]+)_([0-9]+).*/], 1653 search: (uastring) => { 1654 return (contains$1(uastring, 'safari') || contains$1(uastring, 'mobile/')) && contains$1(uastring, 'applewebkit'); 1655 } 1656 } 1657 ]; 1658 const oses = [ 1659 { 1660 name: 'Windows', 1661 search: checkContains('win'), 1662 versionRegexes: [/.*?windows\ nt\ ?([0-9]+)\.([0-9]+).*/] 1663 }, 1664 { 1665 name: 'iOS', 1666 search: (uastring) => { 1667 return contains$1(uastring, 'iphone') || contains$1(uastring, 'ipad'); 1668 }, 1669 versionRegexes: [/.*?version\/\ ?([0-9]+)\.([0-9]+).*/, /.*cpu os ([0-9]+)_([0-9]+).*/, /.*cpu iphone os ([0-9]+)_([0-9]+).*/] 1670 }, 1671 { 1672 name: 'Android', 1673 search: checkContains('android'), 1674 versionRegexes: [/.*?android\ ?([0-9]+)\.([0-9]+).*/] 1675 }, 1676 { 1677 name: 'macOS', 1678 search: checkContains('mac os x'), 1679 versionRegexes: [/.*?mac\ os\ x\ ?([0-9]+)_([0-9]+).*/] 1680 }, 1681 { 1682 name: 'Linux', 1683 search: checkContains('linux'), 1684 versionRegexes: [] 1685 }, 1686 { name: 'Solaris', 1687 search: checkContains('sunos'), 1688 versionRegexes: [] 1689 }, 1690 { 1691 name: 'FreeBSD', 1692 search: checkContains('freebsd'), 1693 versionRegexes: [] 1694 }, 1695 { 1696 name: 'ChromeOS', 1697 search: checkContains('cros'), 1698 versionRegexes: [/.*?chrome\/([0-9]+)\.([0-9]+).*/] 1699 } 1700 ]; 1701 const PlatformInfo = { 1702 browsers: constant$1(browsers), 1703 oses: constant$1(oses) 1704 }; 1705 1706 const edge = 'Edge'; 1707 const chromium = 'Chromium'; 1708 const ie = 'IE'; 1709 const opera = 'Opera'; 1710 const firefox = 'Firefox'; 1711 const safari = 'Safari'; 1712 const unknown$2 = () => { 1713 return nu$9({ 1714 current: undefined, 1715 version: Version.unknown() 1716 }); 1717 }; 1718 const nu$9 = (info) => { 1719 const current = info.current; 1720 const version = info.version; 1721 const isBrowser = (name) => () => current === name; 1722 return { 1723 current, 1724 version, 1725 isEdge: isBrowser(edge), 1726 isChromium: isBrowser(chromium), 1727 // NOTE: isIe just looks too weird 1728 isIE: isBrowser(ie), 1729 isOpera: isBrowser(opera), 1730 isFirefox: isBrowser(firefox), 1731 isSafari: isBrowser(safari) 1732 }; 1733 }; 1734 const Browser = { 1735 unknown: unknown$2, 1736 nu: nu$9, 1737 edge: constant$1(edge), 1738 chromium: constant$1(chromium), 1739 ie: constant$1(ie), 1740 opera: constant$1(opera), 1741 firefox: constant$1(firefox), 1742 safari: constant$1(safari) 1743 }; 1744 1745 const windows = 'Windows'; 1746 const ios = 'iOS'; 1747 const android = 'Android'; 1748 const linux = 'Linux'; 1749 const macos = 'macOS'; 1750 const solaris = 'Solaris'; 1751 const freebsd = 'FreeBSD'; 1752 const chromeos = 'ChromeOS'; 1753 // Though there is a bit of dupe with this and Browser, trying to 1754 // reuse code makes it much harder to follow and change. 1755 const unknown$1 = () => { 1756 return nu$8({ 1757 current: undefined, 1758 version: Version.unknown() 1759 }); 1760 }; 1761 const nu$8 = (info) => { 1762 const current = info.current; 1763 const version = info.version; 1764 const isOS = (name) => () => current === name; 1765 return { 1766 current, 1767 version, 1768 isWindows: isOS(windows), 1769 // TODO: Fix capitalisation 1770 isiOS: isOS(ios), 1771 isAndroid: isOS(android), 1772 isMacOS: isOS(macos), 1773 isLinux: isOS(linux), 1774 isSolaris: isOS(solaris), 1775 isFreeBSD: isOS(freebsd), 1776 isChromeOS: isOS(chromeos) 1777 }; 1778 }; 1779 const OperatingSystem = { 1780 unknown: unknown$1, 1781 nu: nu$8, 1782 windows: constant$1(windows), 1783 ios: constant$1(ios), 1784 android: constant$1(android), 1785 linux: constant$1(linux), 1786 macos: constant$1(macos), 1787 solaris: constant$1(solaris), 1788 freebsd: constant$1(freebsd), 1789 chromeos: constant$1(chromeos) 1790 }; 1791 1792 const detect$2 = (userAgent, userAgentDataOpt, mediaMatch) => { 1793 const browsers = PlatformInfo.browsers(); 1794 const oses = PlatformInfo.oses(); 1795 const browser = userAgentDataOpt.bind((userAgentData) => detectBrowser$1(browsers, userAgentData)) 1796 .orThunk(() => detectBrowser(browsers, userAgent)) 1797 .fold(Browser.unknown, Browser.nu); 1798 const os = detectOs(oses, userAgent).fold(OperatingSystem.unknown, OperatingSystem.nu); 1799 const deviceType = DeviceType(os, browser, userAgent, mediaMatch); 1800 return { 1801 browser, 1802 os, 1803 deviceType 1804 }; 1805 }; 1806 const PlatformDetection = { 1807 detect: detect$2 1808 }; 1809 1810 const mediaMatch = (query) => window.matchMedia(query).matches; 1811 // IMPORTANT: Must be in a thunk, otherwise rollup thinks calling this immediately 1812 // causes side effects and won't tree shake this away 1813 // Note: navigator.userAgentData is not part of the native typescript types yet 1814 let platform = cached(() => PlatformDetection.detect(window.navigator.userAgent, Optional.from((window.navigator.userAgentData)), mediaMatch)); 1815 const detect$1 = () => platform(); 1816 1817 const unsafe = (name, scope) => { 1818 return resolve(name, scope); 1819 }; 1820 const getOrDie$1 = (name, scope) => { 1821 const actual = unsafe(name, scope); 1822 if (actual === undefined || actual === null) { 1823 throw new Error(name + ' not available on this browser'); 1824 } 1825 return actual; 1826 }; 1827 1828 const getPrototypeOf$1 = Object.getPrototypeOf; 1829 /* 1830 * IE9 and above 1831 * 1832 * MDN no use on this one, but here's the link anyway: 1833 * https://developer.mozilla.org/en/docs/Web/API/HTMLElement 1834 */ 1835 const sandHTMLElement = (scope) => { 1836 return getOrDie$1('HTMLElement', scope); 1837 }; 1838 const isPrototypeOf = (x) => { 1839 // use Resolve to get the window object for x and just return undefined if it can't find it. 1840 // undefined scope later triggers using the global window. 1841 const scope = resolve('ownerDocument.defaultView', x); 1842 // TINY-7374: We can't rely on looking at the owner window HTMLElement as the element may have 1843 // been constructed in a different window and then appended to the current window document. 1844 return isObject(x) && (sandHTMLElement(scope).prototype.isPrototypeOf(x) || /^HTML\w*Element$/.test(getPrototypeOf$1(x).constructor.name)); 1845 }; 1846 1847 const name$3 = (element) => { 1848 const r = element.dom.nodeName; 1849 return r.toLowerCase(); 1850 }; 1851 const type$1 = (element) => element.dom.nodeType; 1852 const isType = (t) => (element) => type$1(element) === t; 1853 const isHTMLElement = (element) => isElement$1(element) && isPrototypeOf(element.dom); 1854 const isElement$1 = isType(ELEMENT); 1855 const isText = isType(TEXT); 1856 const isDocument = isType(DOCUMENT); 1857 const isDocumentFragment = isType(DOCUMENT_FRAGMENT); 1858 const isTag = (tag) => (e) => isElement$1(e) && name$3(e) === tag; 1859 1860 /** 1861 * The document associated with the current element 1862 * NOTE: this will throw if the owner is null. 1863 */ 1864 const owner$4 = (element) => SugarElement.fromDom(element.dom.ownerDocument); 1865 /** 1866 * If the element is a document, return it. Otherwise, return its ownerDocument. 1867 * @param dos 1868 */ 1869 const documentOrOwner = (dos) => isDocument(dos) ? dos : owner$4(dos); 1870 const documentElement = (element) => SugarElement.fromDom(documentOrOwner(element).dom.documentElement); 1871 /** 1872 * The window element associated with the element 1873 * NOTE: this will throw if the defaultView is null. 1874 */ 1875 const defaultView = (element) => SugarElement.fromDom(documentOrOwner(element).dom.defaultView); 1876 const parent = (element) => Optional.from(element.dom.parentNode).map(SugarElement.fromDom); 1877 // Cast down to just be SugarElement<Node> 1878 const parentNode = (element) => parent(element); 1879 const parentElement = (element) => Optional.from(element.dom.parentElement).map(SugarElement.fromDom); 1880 const parents = (element, isRoot) => { 1881 const stop = isFunction(isRoot) ? isRoot : never; 1882 // This is used a *lot* so it needs to be performant, not recursive 1883 let dom = element.dom; 1884 const ret = []; 1885 while (dom.parentNode !== null && dom.parentNode !== undefined) { 1886 const rawParent = dom.parentNode; 1887 const p = SugarElement.fromDom(rawParent); 1888 ret.push(p); 1889 if (stop(p) === true) { 1890 break; 1891 } 1892 else { 1893 dom = rawParent; 1894 } 1895 } 1896 return ret; 1897 }; 1898 const offsetParent = (element) => Optional.from(element.dom.offsetParent).map(SugarElement.fromDom); 1899 const prevSibling = (element) => Optional.from(element.dom.previousSibling).map(SugarElement.fromDom); 1900 const nextSibling = (element) => Optional.from(element.dom.nextSibling).map(SugarElement.fromDom); 1901 const children = (element) => map$2(element.dom.childNodes, SugarElement.fromDom); 1902 const child$2 = (element, index) => { 1903 const cs = element.dom.childNodes; 1904 return Optional.from(cs[index]).map(SugarElement.fromDom); 1905 }; 1906 const firstChild = (element) => child$2(element, 0); 1907 const spot = (element, offset) => ({ 1908 element, 1909 offset 1910 }); 1911 const leaf = (element, offset) => { 1912 const cs = children(element); 1913 return cs.length > 0 && offset < cs.length ? spot(cs[offset], 0) : spot(element, offset); 1914 }; 1915 1916 const makeRange = (start, soffset, finish, foffset) => { 1917 const doc = owner$4(start); 1918 // TODO: We need to think about a better place to put native range creation code. Does it even belong in sugar? 1919 // Could the `Compare` checks (node.compareDocumentPosition) handle these situations better? 1920 const rng = doc.dom.createRange(); 1921 rng.setStart(start.dom, soffset); 1922 rng.setEnd(finish.dom, foffset); 1923 return rng; 1924 }; 1925 const after$2 = (start, soffset, finish, foffset) => { 1926 const r = makeRange(start, soffset, finish, foffset); 1927 const same = eq(start, finish) && soffset === foffset; 1928 return r.collapsed && !same; 1929 }; 1930 1931 /** 1932 * Is the element a ShadowRoot? 1933 * 1934 * Note: this is insufficient to test if any element is a shadow root, but it is sufficient to differentiate between 1935 * a Document and a ShadowRoot. 1936 */ 1937 const isShadowRoot = (dos) => isDocumentFragment(dos) && isNonNullable(dos.dom.host); 1938 const getRootNode = (e) => SugarElement.fromDom(e.dom.getRootNode()); 1939 /** Where content needs to go. ShadowRoot or document body */ 1940 const getContentContainer = (dos) => 1941 // Can't use SugarBody.body without causing a circular module reference (since SugarBody.inBody uses SugarShadowDom) 1942 isShadowRoot(dos) ? dos : SugarElement.fromDom(documentOrOwner(dos).dom.body); 1943 /** Is this element either a ShadowRoot or a descendent of a ShadowRoot. */ 1944 const isInShadowRoot = (e) => getShadowRoot(e).isSome(); 1945 /** If this element is in a ShadowRoot, return it. */ 1946 const getShadowRoot = (e) => { 1947 const r = getRootNode(e); 1948 return isShadowRoot(r) ? Optional.some(r) : Optional.none(); 1949 }; 1950 /** Return the host of a ShadowRoot. 1951 * 1952 * This function will throw if Shadow DOM is unsupported in the browser, or if the host is null. 1953 * If you actually have a ShadowRoot, this shouldn't happen. 1954 */ 1955 const getShadowHost = (e) => SugarElement.fromDom(e.dom.host); 1956 /** 1957 * When Events bubble up through a ShadowRoot, the browser changes the target to be the shadow host. 1958 * This function gets the "original" event target if possible. 1959 * This only works if the shadow tree is open - if the shadow tree is closed, event.target is returned. 1960 * See: https://developers.google.com/web/fundamentals/web-components/shadowdom#events 1961 */ 1962 const getOriginalEventTarget = (event) => { 1963 if (isNonNullable(event.target)) { 1964 const el = SugarElement.fromDom(event.target); 1965 if (isElement$1(el) && isOpenShadowHost(el)) { 1966 // When target element is inside Shadow DOM we need to take first element from composedPath 1967 // otherwise we'll get Shadow Root parent, not actual target element. 1968 if (event.composed && event.composedPath) { 1969 const composedPath = event.composedPath(); 1970 if (composedPath) { 1971 return head(composedPath); 1972 } 1973 } 1974 } 1975 } 1976 return Optional.from(event.target); 1977 }; 1978 /** Return true if the element is a host of an open shadow root. 1979 * Return false if the element is a host of a closed shadow root, or if the element is not a host. 1980 */ 1981 const isOpenShadowHost = (element) => isNonNullable(element.dom.shadowRoot); 1982 1983 const mkEvent = (target, x, y, stop, prevent, kill, raw) => ({ 1984 target, 1985 x, 1986 y, 1987 stop, 1988 prevent, 1989 kill, 1990 raw 1991 }); 1992 /** Wraps an Event in an EventArgs structure. 1993 * The returned EventArgs structure has its target set to the "original" target if possible. 1994 * See SugarShadowDom.getOriginalEventTarget 1995 */ 1996 const fromRawEvent$1 = (rawEvent) => { 1997 const target = SugarElement.fromDom(getOriginalEventTarget(rawEvent).getOr(rawEvent.target)); 1998 const stop = () => rawEvent.stopPropagation(); 1999 const prevent = () => rawEvent.preventDefault(); 2000 const kill = compose(prevent, stop); // more of a sequence than a compose, but same effect 2001 // FIX: Don't just expose the raw event. Need to identify what needs standardisation. 2002 return mkEvent(target, rawEvent.clientX, rawEvent.clientY, stop, prevent, kill, rawEvent); 2003 }; 2004 const handle = (filter, handler) => (rawEvent) => { 2005 if (filter(rawEvent)) { 2006 handler(fromRawEvent$1(rawEvent)); 2007 } 2008 }; 2009 const binder = (element, event, filter, handler, useCapture) => { 2010 const wrapped = handle(filter, handler); 2011 // IE9 minimum 2012 element.dom.addEventListener(event, wrapped, useCapture); 2013 return { 2014 unbind: curry(unbind, element, event, wrapped, useCapture) 2015 }; 2016 }; 2017 const bind$2 = (element, event, filter, handler) => binder(element, event, filter, handler, false); 2018 const capture$1 = (element, event, filter, handler) => binder(element, event, filter, handler, true); 2019 const unbind = (element, event, handler, useCapture) => { 2020 // IE9 minimum 2021 element.dom.removeEventListener(event, handler, useCapture); 2022 }; 2023 2024 const filter = always; // no filter on plain DomEvents 2025 const bind$1 = (element, event, handler) => bind$2(element, event, filter, handler); 2026 const capture = (element, event, handler) => capture$1(element, event, filter, handler); 2027 const fromRawEvent = fromRawEvent$1; 2028 2029 const getDocument = () => SugarElement.fromDom(document); 2030 2031 const focus$4 = (element, preventScroll = false) => element.dom.focus({ preventScroll }); 2032 const blur$1 = (element) => element.dom.blur(); 2033 const hasFocus = (element) => { 2034 const root = getRootNode(element).dom; 2035 return element.dom === root.activeElement; 2036 }; 2037 // Note: assuming that activeElement will always be a HTMLElement (maybe we should add a runtime check?) 2038 const active$1 = (root = getDocument()) => Optional.from(root.dom.activeElement).map(SugarElement.fromDom); 2039 /** 2040 * Return the descendant element that has focus. 2041 * Use instead of SelectorFind.descendant(container, ':focus') 2042 * because the :focus selector relies on keyboard focus. 2043 */ 2044 const search = (element) => active$1(getRootNode(element)) 2045 .filter((e) => element.dom.contains(e.dom)); 2046 2047 const before$1 = (marker, element) => { 2048 const parent$1 = parent(marker); 2049 parent$1.each((v) => { 2050 v.dom.insertBefore(element.dom, marker.dom); 2051 }); 2052 }; 2053 const after$1 = (marker, element) => { 2054 const sibling = nextSibling(marker); 2055 sibling.fold(() => { 2056 const parent$1 = parent(marker); 2057 parent$1.each((v) => { 2058 append$2(v, element); 2059 }); 2060 }, (v) => { 2061 before$1(v, element); 2062 }); 2063 }; 2064 const prepend$1 = (parent, element) => { 2065 const firstChild$1 = firstChild(parent); 2066 firstChild$1.fold(() => { 2067 append$2(parent, element); 2068 }, (v) => { 2069 parent.dom.insertBefore(element.dom, v.dom); 2070 }); 2071 }; 2072 const append$2 = (parent, element) => { 2073 parent.dom.appendChild(element.dom); 2074 }; 2075 const appendAt = (parent, element, index) => { 2076 child$2(parent, index).fold(() => { 2077 append$2(parent, element); 2078 }, (v) => { 2079 before$1(v, element); 2080 }); 2081 }; 2082 2083 const append$1 = (parent, elements) => { 2084 each$1(elements, (x) => { 2085 append$2(parent, x); 2086 }); 2087 }; 2088 2089 const rawSet = (dom, key, value) => { 2090 /* 2091 * JQuery coerced everything to a string, and silently did nothing on text node/null/undefined. 2092 * 2093 * We fail on those invalid cases, only allowing numbers and booleans. 2094 */ 2095 if (isString(value) || isBoolean(value) || isNumber(value)) { 2096 dom.setAttribute(key, value + ''); 2097 } 2098 else { 2099 // eslint-disable-next-line no-console 2100 console.error('Invalid call to Attribute.set. Key ', key, ':: Value ', value, ':: Element ', dom); 2101 throw new Error('Attribute value was not simple'); 2102 } 2103 }; 2104 const set$9 = (element, key, value) => { 2105 rawSet(element.dom, key, value); 2106 }; 2107 const setAll$1 = (element, attrs) => { 2108 const dom = element.dom; 2109 each(attrs, (v, k) => { 2110 rawSet(dom, k, v); 2111 }); 2112 }; 2113 const get$g = (element, key) => { 2114 const v = element.dom.getAttribute(key); 2115 // undefined is the more appropriate value for JS, and this matches JQuery 2116 return v === null ? undefined : v; 2117 }; 2118 const getOpt = (element, key) => Optional.from(get$g(element, key)); 2119 const has$1 = (element, key) => { 2120 const dom = element.dom; 2121 // return false for non-element nodes, no point in throwing an error 2122 return dom && dom.hasAttribute ? dom.hasAttribute(key) : false; 2123 }; 2124 const remove$8 = (element, key) => { 2125 element.dom.removeAttribute(key); 2126 }; 2127 const clone$2 = (element) => foldl(element.dom.attributes, (acc, attr) => { 2128 acc[attr.name] = attr.value; 2129 return acc; 2130 }, {}); 2131 2132 const empty = (element) => { 2133 // shortcut "empty node" trick. Requires IE 9. 2134 element.dom.textContent = ''; 2135 // If the contents was a single empty text node, the above doesn't remove it. But, it's still faster in general 2136 // than removing every child node manually. 2137 // The following is (probably) safe for performance as 99.9% of the time the trick works and 2138 // Traverse.children will return an empty array. 2139 each$1(children(element), (rogue) => { 2140 remove$7(rogue); 2141 }); 2142 }; 2143 const remove$7 = (element) => { 2144 const dom = element.dom; 2145 if (dom.parentNode !== null) { 2146 dom.parentNode.removeChild(dom); 2147 } 2148 }; 2149 2150 const clone$1 = (original, isDeep) => SugarElement.fromDom(original.dom.cloneNode(isDeep)); 2151 /** Shallow clone - just the tag, no children */ 2152 const shallow = (original) => clone$1(original, false); 2153 /** Deep clone - everything copied including children */ 2154 const deep = (original) => clone$1(original, true); 2155 2156 const fromHtml$1 = (html, scope) => { 2157 const doc = scope || document; 2158 const div = doc.createElement('div'); 2159 div.innerHTML = html; 2160 return children(SugarElement.fromDom(div)); 2161 }; 2162 2163 const get$f = (element) => element.dom.innerHTML; 2164 const set$8 = (element, content) => { 2165 const owner = owner$4(element); 2166 const docDom = owner.dom; 2167 // FireFox has *terrible* performance when using innerHTML = x 2168 const fragment = SugarElement.fromDom(docDom.createDocumentFragment()); 2169 const contentElements = fromHtml$1(content, docDom); 2170 append$1(fragment, contentElements); 2171 empty(element); 2172 append$2(element, fragment); 2173 }; 2174 const getOuter$2 = (element) => { 2175 const container = SugarElement.fromTag('div'); 2176 const clone = SugarElement.fromDom(element.dom.cloneNode(true)); 2177 append$2(container, clone); 2178 return get$f(container); 2179 }; 2180 2181 const getHtml = (element) => { 2182 if (isShadowRoot(element)) { 2183 return '#shadow-root'; 2184 } 2185 else { 2186 const clone = shallow(element); 2187 return getOuter$2(clone); 2188 } 2189 }; 2190 2191 const image = (image) => new Promise((resolve, reject) => { 2192 const loaded = () => { 2193 destroy(); 2194 resolve(image); 2195 }; 2196 const listeners = [ 2197 bind$1(image, 'load', loaded), 2198 bind$1(image, 'error', () => { 2199 destroy(); 2200 reject('Unable to load data from image: ' + image.dom.src); 2201 }), 2202 ]; 2203 const destroy = () => each$1(listeners, (l) => l.unbind()); 2204 if (image.dom.complete) { 2205 loaded(); 2206 } 2207 }); 2208 2209 // some elements, such as mathml, don't have style attributes 2210 // others, such as angular elements, have style attributes that aren't a CSSStyleDeclaration 2211 const isSupported = (dom) => dom.style !== undefined && isFunction(dom.style.getPropertyValue); 2212 2213 // Node.contains() is very, very, very good performance 2214 // http://jsperf.com/closest-vs-contains/5 2215 const inBody = (element) => { 2216 // Technically this is only required on IE, where contains() returns false for text nodes. 2217 // But it's cheap enough to run everywhere and Sugar doesn't have platform detection (yet). 2218 const dom = isText(element) ? element.dom.parentNode : element.dom; 2219 // use ownerDocument.body to ensure this works inside iframes. 2220 // Normally contains is bad because an element "contains" itself, but here we want that. 2221 if (dom === undefined || dom === null || dom.ownerDocument === null) { 2222 return false; 2223 } 2224 const doc = dom.ownerDocument; 2225 return getShadowRoot(SugarElement.fromDom(dom)).fold(() => doc.body.contains(dom), compose1(inBody, getShadowHost)); 2226 }; 2227 const body = () => getBody(SugarElement.fromDom(document)); 2228 const getBody = (doc) => { 2229 const b = doc.dom.body; 2230 if (b === null || b === undefined) { 2231 throw new Error('Body is not available yet'); 2232 } 2233 return SugarElement.fromDom(b); 2234 }; 2235 2236 const internalSet = (dom, property, value) => { 2237 // This is going to hurt. Apologies. 2238 // JQuery coerces numbers to pixels for certain property names, and other times lets numbers through. 2239 // we're going to be explicit; strings only. 2240 if (!isString(value)) { 2241 // eslint-disable-next-line no-console 2242 console.error('Invalid call to CSS.set. Property ', property, ':: Value ', value, ':: Element ', dom); 2243 throw new Error('CSS value must be a string: ' + value); 2244 } 2245 // removed: support for dom().style[property] where prop is camel case instead of normal property name 2246 if (isSupported(dom)) { 2247 dom.style.setProperty(property, value); 2248 } 2249 }; 2250 const internalRemove = (dom, property) => { 2251 /* 2252 * IE9 and above - MDN doesn't have details, but here's a couple of random internet claims 2253 * 2254 * http://help.dottoro.com/ljopsjck.php 2255 * http://stackoverflow.com/a/7901886/7546 2256 */ 2257 if (isSupported(dom)) { 2258 dom.style.removeProperty(property); 2259 } 2260 }; 2261 const set$7 = (element, property, value) => { 2262 const dom = element.dom; 2263 internalSet(dom, property, value); 2264 }; 2265 const setAll = (element, css) => { 2266 const dom = element.dom; 2267 each(css, (v, k) => { 2268 internalSet(dom, k, v); 2269 }); 2270 }; 2271 const setOptions = (element, css) => { 2272 const dom = element.dom; 2273 each(css, (v, k) => { 2274 v.fold(() => { 2275 internalRemove(dom, k); 2276 }, (value) => { 2277 internalSet(dom, k, value); 2278 }); 2279 }); 2280 }; 2281 /* 2282 * NOTE: For certain properties, this returns the "used value" which is subtly different to the "computed value" (despite calling getComputedStyle). 2283 * Blame CSS 2.0. 2284 * 2285 * https://developer.mozilla.org/en-US/docs/Web/CSS/used_value 2286 */ 2287 const get$e = (element, property) => { 2288 const dom = element.dom; 2289 /* 2290 * IE9 and above per 2291 * https://developer.mozilla.org/en/docs/Web/API/window.getComputedStyle 2292 * 2293 * Not in numerosity, because it doesn't memoize and looking this up dynamically in performance critical code would be horrendous. 2294 * 2295 * JQuery has some magic here for IE popups, but we don't really need that. 2296 * It also uses element.ownerDocument.defaultView to handle iframes but that hasn't been required since FF 3.6. 2297 */ 2298 const styles = window.getComputedStyle(dom); 2299 const r = styles.getPropertyValue(property); 2300 // 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. 2301 // Turns out we do this a lot. 2302 return (r === '' && !inBody(element)) ? getUnsafeProperty(dom, property) : r; 2303 }; 2304 // removed: support for dom().style[property] where prop is camel case instead of normal property name 2305 // empty string is what the browsers (IE11 and Chrome) return when the propertyValue doesn't exists. 2306 const getUnsafeProperty = (dom, property) => isSupported(dom) ? dom.style.getPropertyValue(property) : ''; 2307 /* 2308 * Gets the raw value from the style attribute. Useful for retrieving "used values" from the DOM: 2309 * https://developer.mozilla.org/en-US/docs/Web/CSS/used_value 2310 * 2311 * Returns NONE if the property isn't set, or the value is an empty string. 2312 */ 2313 const getRaw = (element, property) => { 2314 const dom = element.dom; 2315 const raw = getUnsafeProperty(dom, property); 2316 return Optional.from(raw).filter((r) => r.length > 0); 2317 }; 2318 const getAllRaw = (element) => { 2319 const css = {}; 2320 const dom = element.dom; 2321 if (isSupported(dom)) { 2322 for (let i = 0; i < dom.style.length; i++) { 2323 const ruleName = dom.style.item(i); 2324 css[ruleName] = dom.style[ruleName]; 2325 } 2326 } 2327 return css; 2328 }; 2329 const isValidValue$1 = (tag, property, value) => { 2330 const element = SugarElement.fromTag(tag); 2331 set$7(element, property, value); 2332 const style = getRaw(element, property); 2333 return style.isSome(); 2334 }; 2335 const remove$6 = (element, property) => { 2336 const dom = element.dom; 2337 internalRemove(dom, property); 2338 if (is$1(getOpt(element, 'style').map(trim$1), '')) { 2339 // No more styles left, remove the style attribute as well 2340 remove$8(element, 'style'); 2341 } 2342 }; 2343 /* NOTE: This function is here for the side effect it triggers. 2344 The value itself is not used. 2345 Be sure to not use the return value, and that it is not removed by a minifier. 2346 */ 2347 const reflow = (e) => e.dom.offsetWidth; 2348 2349 const Dimension = (name, getOffset) => { 2350 const set = (element, h) => { 2351 if (!isNumber(h) && !h.match(/^[0-9]+$/)) { 2352 throw new Error(name + '.set accepts only positive integer values. Value was ' + h); 2353 } 2354 const dom = element.dom; 2355 if (isSupported(dom)) { 2356 dom.style[name] = h + 'px'; 2357 } 2358 }; 2359 /* 2360 * jQuery supports querying width and height on the document and window objects. 2361 * 2362 * TBIO doesn't do this, so the code is removed to save space, but left here just in case. 2363 */ 2364 /* 2365 var getDocumentWidth = (element) => { 2366 var dom = element.dom; 2367 if (Node.isDocument(element)) { 2368 var body = dom.body; 2369 var doc = dom.documentElement; 2370 return Math.max( 2371 body.scrollHeight, 2372 doc.scrollHeight, 2373 body.offsetHeight, 2374 doc.offsetHeight, 2375 doc.clientHeight 2376 ); 2377 } 2378 }; 2379 2380 var getWindowWidth = (element) => { 2381 var dom = element.dom; 2382 if (dom.window === dom) { 2383 // There is no offsetHeight on a window, so use the clientHeight of the document 2384 return dom.document.documentElement.clientHeight; 2385 } 2386 }; 2387 */ 2388 const get = (element) => { 2389 const r = getOffset(element); 2390 // zero or null means non-standard or disconnected, fall back to CSS 2391 if (r <= 0 || r === null) { 2392 const css = get$e(element, name); 2393 // ugh this feels dirty, but it saves cycles 2394 return parseFloat(css) || 0; 2395 } 2396 return r; 2397 }; 2398 // in jQuery, getOuter replicates (or uses) box-sizing: border-box calculations 2399 // although these calculations only seem relevant for quirks mode, and edge cases TBIO doesn't rely on 2400 const getOuter = get; 2401 const aggregate = (element, properties) => foldl(properties, (acc, property) => { 2402 const val = get$e(element, property); 2403 const value = val === undefined ? 0 : parseInt(val, 10); 2404 return isNaN(value) ? acc : acc + value; 2405 }, 0); 2406 const max = (element, value, properties) => { 2407 const cumulativeInclusions = aggregate(element, properties); 2408 // if max-height is 100px and your cumulativeInclusions is 150px, there is no way max-height can be 100px, so we return 0. 2409 const absoluteMax = value > cumulativeInclusions ? value - cumulativeInclusions : 0; 2410 return absoluteMax; 2411 }; 2412 return { 2413 set, 2414 get, 2415 getOuter, 2416 aggregate, 2417 max 2418 }; 2419 }; 2420 2421 const api$2 = Dimension('height', (element) => { 2422 // getBoundingClientRect gives better results than offsetHeight for tables with captions on Firefox 2423 const dom = element.dom; 2424 return inBody(element) ? dom.getBoundingClientRect().height : dom.offsetHeight; 2425 }); 2426 const get$d = (element) => api$2.get(element); 2427 const getOuter$1 = (element) => api$2.getOuter(element); 2428 const setMax$1 = (element, value) => { 2429 // These properties affect the absolute max-height, they are not counted natively, we want to include these properties. 2430 const inclusions = ['margin-top', 'border-top-width', 'padding-top', 'padding-bottom', 'border-bottom-width', 'margin-bottom']; 2431 const absMax = api$2.max(element, value, inclusions); 2432 set$7(element, 'max-height', absMax + 'px'); 2433 }; 2434 2435 const isHidden$1 = (dom) => dom.offsetWidth <= 0 && dom.offsetHeight <= 0; 2436 const isVisible = (element) => !isHidden$1(element.dom); 2437 2438 const api$1 = Dimension('width', (element) => { 2439 const dom = element.dom; 2440 return inBody(element) ? dom.getBoundingClientRect().width : dom.offsetWidth; 2441 }); 2442 const set$6 = (element, h) => api$1.set(element, h); 2443 const get$c = (element) => api$1.get(element); 2444 const getOuter = (element) => api$1.getOuter(element); 2445 const setMax = (element, value) => { 2446 // These properties affect the absolute max-height, they are not counted natively, we want to include these properties. 2447 const inclusions = ['margin-left', 'border-left-width', 'padding-left', 'padding-right', 'border-right-width', 'margin-right']; 2448 const absMax = api$1.max(element, value, inclusions); 2449 set$7(element, 'max-width', absMax + 'px'); 2450 }; 2451 2452 const r$1 = (left, top) => { 2453 const translate = (x, y) => r$1(left + x, top + y); 2454 return { 2455 left, 2456 top, 2457 translate 2458 }; 2459 }; 2460 // tslint:disable-next-line:variable-name 2461 const SugarPosition = r$1; 2462 2463 const boxPosition = (dom) => { 2464 const box = dom.getBoundingClientRect(); 2465 return SugarPosition(box.left, box.top); 2466 }; 2467 // Avoids falsy false fallthrough 2468 const firstDefinedOrZero = (a, b) => { 2469 if (a !== undefined) { 2470 return a; 2471 } 2472 else { 2473 return b !== undefined ? b : 0; 2474 } 2475 }; 2476 const absolute$3 = (element) => { 2477 const doc = element.dom.ownerDocument; 2478 const body = doc.body; 2479 const win = doc.defaultView; 2480 const html = doc.documentElement; 2481 if (body === element.dom) { 2482 return SugarPosition(body.offsetLeft, body.offsetTop); 2483 } 2484 const scrollTop = firstDefinedOrZero(win?.pageYOffset, html.scrollTop); 2485 const scrollLeft = firstDefinedOrZero(win?.pageXOffset, html.scrollLeft); 2486 const clientTop = firstDefinedOrZero(html.clientTop, body.clientTop); 2487 const clientLeft = firstDefinedOrZero(html.clientLeft, body.clientLeft); 2488 return viewport$1(element).translate(scrollLeft - clientLeft, scrollTop - clientTop); 2489 }; 2490 const viewport$1 = (element) => { 2491 const dom = element.dom; 2492 const doc = dom.ownerDocument; 2493 const body = doc.body; 2494 if (body === dom) { 2495 return SugarPosition(body.offsetLeft, body.offsetTop); 2496 } 2497 if (!inBody(element)) { 2498 return SugarPosition(0, 0); 2499 } 2500 return boxPosition(dom); 2501 }; 2502 2503 // get scroll position (x,y) relative to document _doc (or global if not supplied) 2504 const get$b = (_DOC) => { 2505 const doc = _DOC !== undefined ? _DOC.dom : document; 2506 // ASSUMPTION: This is for cross-browser support, body works for Safari & EDGE, and when we have an iframe body scroller 2507 const x = doc.body.scrollLeft || doc.documentElement.scrollLeft; 2508 const y = doc.body.scrollTop || doc.documentElement.scrollTop; 2509 return SugarPosition(x, y); 2510 }; 2511 // Scroll content to (x,y) relative to document _doc (or global if not supplied) 2512 const to = (x, y, _DOC) => { 2513 const doc = _DOC !== undefined ? _DOC.dom : document; 2514 const win = doc.defaultView; 2515 if (win) { 2516 win.scrollTo(x, y); 2517 } 2518 }; 2519 2520 const NodeValue = (is, name) => { 2521 const get = (element) => { 2522 if (!is(element)) { 2523 throw new Error('Can only get ' + name + ' value of a ' + name + ' node'); 2524 } 2525 return getOption(element).getOr(''); 2526 }; 2527 const getOption = (element) => is(element) ? Optional.from(element.dom.nodeValue) : Optional.none(); 2528 const set = (element, value) => { 2529 if (!is(element)) { 2530 throw new Error('Can only set raw ' + name + ' value of a ' + name + ' node'); 2531 } 2532 element.dom.nodeValue = value; 2533 }; 2534 return { 2535 get, 2536 getOption, 2537 set 2538 }; 2539 }; 2540 2541 const api = NodeValue(isText, 'text'); 2542 const get$a = (element) => api.get(element); 2543 2544 const onDirection = (isLtr, isRtl) => (element) => getDirection(element) === 'rtl' ? isRtl : isLtr; 2545 const getDirection = (element) => get$e(element, 'direction') === 'rtl' ? 'rtl' : 'ltr'; 2546 2547 // Methods for handling attributes that contain a list of values <div foo="alpha beta theta"> 2548 const read$2 = (element, attr) => { 2549 const value = get$g(element, attr); 2550 return value === undefined || value === '' ? [] : value.split(' '); 2551 }; 2552 const add$4 = (element, attr, id) => { 2553 const old = read$2(element, attr); 2554 const nu = old.concat([id]); 2555 set$9(element, attr, nu.join(' ')); 2556 return true; 2557 }; 2558 const remove$5 = (element, attr, id) => { 2559 const nu = filter$2(read$2(element, attr), (v) => v !== id); 2560 if (nu.length > 0) { 2561 set$9(element, attr, nu.join(' ')); 2562 } 2563 else { 2564 remove$8(element, attr); 2565 } 2566 return false; 2567 }; 2568 2569 var ClosestOrAncestor = (is, ancestor, scope, a, isRoot) => { 2570 if (is(scope, a)) { 2571 return Optional.some(scope); 2572 } 2573 else if (isFunction(isRoot) && isRoot(scope)) { 2574 return Optional.none(); 2575 } 2576 else { 2577 return ancestor(scope, a, isRoot); 2578 } 2579 }; 2580 2581 const ancestor$2 = (scope, predicate, isRoot) => { 2582 let element = scope.dom; 2583 const stop = isFunction(isRoot) ? isRoot : never; 2584 while (element.parentNode) { 2585 element = element.parentNode; 2586 const el = SugarElement.fromDom(element); 2587 if (predicate(el)) { 2588 return Optional.some(el); 2589 } 2590 else if (stop(el)) { 2591 break; 2592 } 2593 } 2594 return Optional.none(); 2595 }; 2596 const closest$4 = (scope, predicate, isRoot) => { 2597 // This is required to avoid ClosestOrAncestor passing the predicate to itself 2598 const is = (s, test) => test(s); 2599 return ClosestOrAncestor(is, ancestor$2, scope, predicate, isRoot); 2600 }; 2601 const sibling$1 = (scope, predicate) => { 2602 const element = scope.dom; 2603 if (!element.parentNode) { 2604 return Optional.none(); 2605 } 2606 return child$1(SugarElement.fromDom(element.parentNode), (x) => !eq(scope, x) && predicate(x)); 2607 }; 2608 const child$1 = (scope, predicate) => { 2609 const pred = (node) => predicate(SugarElement.fromDom(node)); 2610 const result = find$5(scope.dom.childNodes, pred); 2611 return result.map(SugarElement.fromDom); 2612 }; 2613 const descendant$1 = (scope, predicate) => { 2614 const descend = (node) => { 2615 // tslint:disable-next-line:prefer-for-of 2616 for (let i = 0; i < node.childNodes.length; i++) { 2617 const child = SugarElement.fromDom(node.childNodes[i]); 2618 if (predicate(child)) { 2619 return Optional.some(child); 2620 } 2621 const res = descend(node.childNodes[i]); 2622 if (res.isSome()) { 2623 return res; 2624 } 2625 } 2626 return Optional.none(); 2627 }; 2628 return descend(scope.dom); 2629 }; 2630 2631 // TODO: An internal SelectorFilter module that doesn't SugarElement.fromDom() everything 2632 const first = (selector) => one(selector); 2633 const ancestor$1 = (scope, selector, isRoot) => ancestor$2(scope, (e) => is(e, selector), isRoot); 2634 const sibling = (scope, selector) => sibling$1(scope, (e) => is(e, selector)); 2635 const child = (scope, selector) => child$1(scope, (e) => is(e, selector)); 2636 const descendant = (scope, selector) => one(selector, scope); 2637 // Returns Some(closest ancestor element (sugared)) matching 'selector' up to isRoot, or None() otherwise 2638 const closest$3 = (scope, selector, isRoot) => { 2639 const is$1 = (element, selector) => is(element, selector); 2640 return ClosestOrAncestor(is$1, ancestor$1, scope, selector, isRoot); 2641 }; 2642 2643 const set$5 = (element, status) => { 2644 element.dom.checked = status; 2645 }; 2646 const get$9 = (element) => element.dom.checked; 2647 2648 // 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. 2649 const supports = (element) => element.dom.classList !== undefined; 2650 const get$8 = (element) => read$2(element, 'class'); 2651 const add$3 = (element, clazz) => add$4(element, 'class', clazz); 2652 const remove$4 = (element, clazz) => remove$5(element, 'class', clazz); 2653 const toggle$4 = (element, clazz) => { 2654 if (contains$2(get$8(element), clazz)) { 2655 return remove$4(element, clazz); 2656 } 2657 else { 2658 return add$3(element, clazz); 2659 } 2660 }; 2661 2662 /* 2663 * ClassList is IE10 minimum: 2664 * https://developer.mozilla.org/en-US/docs/Web/API/Element.classList 2665 * 2666 * Note that IE doesn't support the second argument to toggle (at all). 2667 * If it did, the toggler could be better. 2668 */ 2669 const add$2 = (element, clazz) => { 2670 if (supports(element)) { 2671 element.dom.classList.add(clazz); 2672 } 2673 else { 2674 add$3(element, clazz); 2675 } 2676 }; 2677 const cleanClass = (element) => { 2678 const classList = supports(element) ? element.dom.classList : get$8(element); 2679 // classList is a "live list", so this is up to date already 2680 if (classList.length === 0) { 2681 // No more classes left, remove the class attribute as well 2682 remove$8(element, 'class'); 2683 } 2684 }; 2685 const remove$3 = (element, clazz) => { 2686 if (supports(element)) { 2687 const classList = element.dom.classList; 2688 classList.remove(clazz); 2689 } 2690 else { 2691 remove$4(element, clazz); 2692 } 2693 cleanClass(element); 2694 }; 2695 const toggle$3 = (element, clazz) => { 2696 const result = supports(element) ? element.dom.classList.toggle(clazz) : toggle$4(element, clazz); 2697 cleanClass(element); 2698 return result; 2699 }; 2700 const has = (element, clazz) => supports(element) && element.dom.classList.contains(clazz); 2701 2702 /* 2703 * ClassList is IE10 minimum: 2704 * https://developer.mozilla.org/en-US/docs/Web/API/Element.classList 2705 */ 2706 const add$1 = (element, classes) => { 2707 each$1(classes, (x) => { 2708 add$2(element, x); 2709 }); 2710 }; 2711 const remove$2 = (element, classes) => { 2712 each$1(classes, (x) => { 2713 remove$3(element, x); 2714 }); 2715 }; 2716 const toggle$2 = (element, classes) => { 2717 each$1(classes, (x) => { 2718 toggle$3(element, x); 2719 }); 2720 }; 2721 const hasAll = (element, classes) => forall(classes, (clazz) => has(element, clazz)); 2722 const getNative = (element) => { 2723 const classList = element.dom.classList; 2724 const r = new Array(classList.length); 2725 for (let i = 0; i < classList.length; i++) { 2726 const item = classList.item(i); 2727 if (item !== null) { 2728 r[i] = item; 2729 } 2730 } 2731 return r; 2732 }; 2733 const get$7 = (element) => supports(element) ? getNative(element) : get$8(element); 2734 2735 const get$6 = (element) => element.dom.textContent; 2736 2737 const get$5 = (element) => element.dom.value; 2738 const set$4 = (element, value) => { 2739 if (value === undefined) { 2740 throw new Error('Value.set was undefined'); 2741 } 2742 element.dom.value = value; 2743 }; 2744 2745 const ancestors = (scope, predicate, isRoot) => filter$2(parents(scope, isRoot), predicate); 2746 2747 const descendants = (scope, selector) => all$3(selector, scope); 2748 2749 const closest$2 = (scope, predicate, isRoot) => closest$4(scope, predicate, isRoot).isSome(); 2750 2751 const closest$1 = (scope, selector, isRoot) => closest$3(scope, selector, isRoot).isSome(); 2752 2753 const ensureIsRoot = (isRoot) => isFunction(isRoot) ? isRoot : never; 2754 const ancestor = (scope, transform, isRoot) => { 2755 let element = scope.dom; 2756 const stop = ensureIsRoot(isRoot); 2757 while (element.parentNode) { 2758 element = element.parentNode; 2759 const el = SugarElement.fromDom(element); 2760 const transformed = transform(el); 2761 if (transformed.isSome()) { 2762 return transformed; 2763 } 2764 else if (stop(el)) { 2765 break; 2766 } 2767 } 2768 return Optional.none(); 2769 }; 2770 const closest = (scope, transform, isRoot) => { 2771 const current = transform(scope); 2772 const stop = ensureIsRoot(isRoot); 2773 return current.orThunk(() => stop(scope) ? Optional.none() : ancestor(scope, transform, stop)); 2774 }; 2775 2776 const create$5 = (start, soffset, finish, foffset) => ({ 2777 start, 2778 soffset, 2779 finish, 2780 foffset 2781 }); 2782 // tslint:disable-next-line:variable-name 2783 const SimRange = { 2784 create: create$5 2785 }; 2786 2787 const adt$9 = Adt.generate([ 2788 { before: ['element'] }, 2789 { on: ['element', 'offset'] }, 2790 { after: ['element'] } 2791 ]); 2792 // Probably don't need this given that we now have "match" 2793 const cata$2 = (subject, onBefore, onOn, onAfter) => subject.fold(onBefore, onOn, onAfter); 2794 const getStart$1 = (situ) => situ.fold(identity, identity, identity); 2795 const before = adt$9.before; 2796 const on$1 = adt$9.on; 2797 const after = adt$9.after; 2798 // tslint:disable-next-line:variable-name 2799 const Situ = { 2800 before, 2801 on: on$1, 2802 after, 2803 cata: cata$2, 2804 getStart: getStart$1 2805 }; 2806 2807 // Consider adding a type for "element" 2808 const adt$8 = Adt.generate([ 2809 { domRange: ['rng'] }, 2810 { relative: ['startSitu', 'finishSitu'] }, 2811 { exact: ['start', 'soffset', 'finish', 'foffset'] } 2812 ]); 2813 const exactFromRange = (simRange) => adt$8.exact(simRange.start, simRange.soffset, simRange.finish, simRange.foffset); 2814 const getStart = (selection) => selection.match({ 2815 domRange: (rng) => SugarElement.fromDom(rng.startContainer), 2816 relative: (startSitu, _finishSitu) => Situ.getStart(startSitu), 2817 exact: (start, _soffset, _finish, _foffset) => start 2818 }); 2819 const domRange = adt$8.domRange; 2820 const relative$1 = adt$8.relative; 2821 const exact = adt$8.exact; 2822 const getWin = (selection) => { 2823 const start = getStart(selection); 2824 return defaultView(start); 2825 }; 2826 // This is out of place but it's API so I can't remove it 2827 const range$1 = SimRange.create; 2828 // tslint:disable-next-line:variable-name 2829 const SimSelection = { 2830 domRange, 2831 relative: relative$1, 2832 exact, 2833 exactFromRange, 2834 getWin, 2835 range: range$1 2836 }; 2837 2838 const beforeSpecial = (element, offset) => { 2839 // From memory, we don't want to use <br> directly on Firefox because it locks the keyboard input. 2840 // It turns out that <img> directly on IE locks the keyboard as well. 2841 // If the offset is 0, use before. If the offset is 1, use after. 2842 // TBIO-3889: Firefox Situ.on <input> results in a child of the <input>; Situ.before <input> results in platform inconsistencies 2843 const name = name$3(element); 2844 if ('input' === name) { 2845 return Situ.after(element); 2846 } 2847 else if (!contains$2(['br', 'img'], name)) { 2848 return Situ.on(element, offset); 2849 } 2850 else { 2851 return offset === 0 ? Situ.before(element) : Situ.after(element); 2852 } 2853 }; 2854 const preprocessExact = (start, soffset, finish, foffset) => { 2855 const startSitu = beforeSpecial(start, soffset); 2856 const finishSitu = beforeSpecial(finish, foffset); 2857 return SimSelection.relative(startSitu, finishSitu); 2858 }; 2859 2860 const getNativeSelection = (win) => Optional.from(win.getSelection()); 2861 const doSetNativeRange = (win, rng) => { 2862 getNativeSelection(win).each((selection) => { 2863 selection.removeAllRanges(); 2864 selection.addRange(rng); 2865 }); 2866 }; 2867 const doSetRange = (win, start, soffset, finish, foffset) => { 2868 const rng = exactToNative(win, start, soffset, finish, foffset); 2869 doSetNativeRange(win, rng); 2870 }; 2871 const setLegacyRtlRange = (win, selection, start, soffset, finish, foffset) => { 2872 selection.collapse(start.dom, soffset); 2873 selection.extend(finish.dom, foffset); 2874 }; 2875 const setRangeFromRelative = (win, relative) => diagnose(win, relative).match({ 2876 ltr: (start, soffset, finish, foffset) => { 2877 doSetRange(win, start, soffset, finish, foffset); 2878 }, 2879 rtl: (start, soffset, finish, foffset) => { 2880 getNativeSelection(win).each((selection) => { 2881 // If this selection is backwards, then we need to use extend. 2882 if (selection.setBaseAndExtent) { 2883 selection.setBaseAndExtent(start.dom, soffset, finish.dom, foffset); 2884 } 2885 else if (selection.extend) { 2886 // This try catch is for older browsers (Firefox 52) as they're sometimes unable to handle setting backwards selections using selection.extend and error out. 2887 try { 2888 setLegacyRtlRange(win, selection, start, soffset, finish, foffset); 2889 } 2890 catch { 2891 // If it does fail, try again with ltr. 2892 doSetRange(win, finish, foffset, start, soffset); 2893 } 2894 } 2895 else { 2896 doSetRange(win, finish, foffset, start, soffset); 2897 } 2898 }); 2899 } 2900 }); 2901 const setExact = (win, start, soffset, finish, foffset) => { 2902 const relative = preprocessExact(start, soffset, finish, foffset); 2903 setRangeFromRelative(win, relative); 2904 }; 2905 // NOTE: We are still reading the range because it gives subtly different behaviour
2906 // than using the anchorNode and focusNode. I'm not sure if this behaviour is any 2907 // better or worse; it's just different. 2908 const readRange = (selection) => { 2909 if (selection.rangeCount > 0) { 2910 const firstRng = selection.getRangeAt(0); 2911 const lastRng = selection.getRangeAt(selection.rangeCount - 1); 2912 return Optional.some(SimRange.create(SugarElement.fromDom(firstRng.startContainer), firstRng.startOffset, SugarElement.fromDom(lastRng.endContainer), lastRng.endOffset)); 2913 } 2914 else { 2915 return Optional.none(); 2916 } 2917 }; 2918 const doGetExact = (selection) => { 2919 if (selection.anchorNode === null || selection.focusNode === null) { 2920 return readRange(selection); 2921 } 2922 else { 2923 const anchor = SugarElement.fromDom(selection.anchorNode); 2924 const focus = SugarElement.fromDom(selection.focusNode); 2925 // if this returns true anchor is _after_ focus, so we need a custom selection object to maintain the RTL selection 2926 return after$2(anchor, selection.anchorOffset, focus, selection.focusOffset) ? Optional.some(SimRange.create(anchor, selection.anchorOffset, focus, selection.focusOffset)) : readRange(selection); 2927 } 2928 }; 2929 const getExact = (win) => 2930 // We want to retrieve the selection as it is. 2931 getNativeSelection(win) 2932 .filter((sel) => sel.rangeCount > 0) 2933 .bind(doGetExact); 2934 const getFirstRect = (win, selection) => { 2935 const rng = asLtrRange(win, selection); 2936 return getFirstRect$1(rng); 2937 }; 2938 const getBounds$2 = (win, selection) => { 2939 const rng = asLtrRange(win, selection); 2940 return getBounds$3(rng); 2941 }; 2942 2943 const units = { 2944 // we don't really support all of these different ways to express a length 2945 unsupportedLength: [ 2946 'em', 2947 'ex', 2948 'cap', 2949 'ch', 2950 'ic', 2951 'rem', 2952 'lh', 2953 'rlh', 2954 'vw', 2955 'vh', 2956 'vi', 2957 'vb', 2958 'vmin', 2959 'vmax', 2960 'cm', 2961 'mm', 2962 'Q', 2963 'in', 2964 'pc', 2965 'pt', 2966 'px' 2967 ], 2968 // these are the length values we do support 2969 fixed: ['px', 'pt'], 2970 relative: ['%'], 2971 empty: [''] 2972 }; 2973 // Built from https://tc39.es/ecma262/#prod-StrDecimalLiteral 2974 // Matches a float followed by a trailing set of characters 2975 const pattern = (() => { 2976 const decimalDigits = '[0-9]+'; 2977 const signedInteger = '[+-]?' + decimalDigits; 2978 const exponentPart = '[eE]' + signedInteger; 2979 const dot = '\\.'; 2980 const opt = (input) => `(?:${input})?`; 2981 const unsignedDecimalLiteral = [ 2982 'Infinity', 2983 decimalDigits + dot + opt(decimalDigits) + opt(exponentPart), 2984 dot + decimalDigits + opt(exponentPart), 2985 decimalDigits + opt(exponentPart) 2986 ].join('|'); 2987 const float = `[+-]?(?:${unsignedDecimalLiteral})`; 2988 return new RegExp(`^(${float})(.*)$`); 2989 })(); 2990 const isUnit = (unit, accepted) => exists(accepted, (acc) => exists(units[acc], (check) => unit === check)); 2991 const parse = (input, accepted) => { 2992 const match = Optional.from(pattern.exec(input)); 2993 return match.bind((array) => { 2994 const value = Number(array[1]); 2995 const unitRaw = array[2]; 2996 if (isUnit(unitRaw, accepted)) { 2997 return Optional.some({ 2998 value, 2999 unit: unitRaw 3000 }); 3001 } 3002 else { 3003 return Optional.none(); 3004 } 3005 }); 3006 }; 3007 const normalise = (input, accepted) => parse(input, accepted).map(({ value, unit }) => value + unit); 3008 3009 const get$4 = (_win) => { 3010 const win = _win === undefined ? window : _win; 3011 if (detect$1().browser.isFirefox()) { 3012 // TINY-7984: Firefox 91 is returning incorrect values for visualViewport.pageTop, so disable it for now 3013 return Optional.none(); 3014 } 3015 else {
vendor: 17,505 bytes, lines 3016-3428
3016 return Optional.from(win.visualViewport); 3017 } 3018 }; 3019 const bounds$1 = (x, y, width, height) => ({ 3020 x, 3021 y, 3022 width, 3023 height, 3024 right: x + width, 3025 bottom: y + height 3026 }); 3027 const getBounds$1 = (_win) => { 3028 const win = _win === undefined ? window : _win; 3029 const doc = win.document; 3030 const scroll = get$b(SugarElement.fromDom(doc)); 3031 return get$4(win).fold(() => { 3032 const html = win.document.documentElement; 3033 // Don't use window.innerWidth/innerHeight here, as we don't want to include scrollbars 3034 // since the right/bottom position is based on the edge of the scrollbar not the window 3035 const width = html.clientWidth; 3036 const height = html.clientHeight; 3037 return bounds$1(scroll.left, scroll.top, width, height); 3038 }, (visualViewport) => 3039 // iOS doesn't update the pageTop/pageLeft when element.scrollIntoView() is called, so we need to fallback to the 3040 // scroll position which will always be less than the page top/left values when page top/left are accurate/correct. 3041 bounds$1(Math.max(visualViewport.pageLeft, scroll.left), Math.max(visualViewport.pageTop, scroll.top), visualViewport.width, visualViewport.height)); 3042 }; 3043 3044 const walkUp = (navigation, doc) => { 3045 const frame = navigation.view(doc); 3046 return frame.fold(constant$1([]), (f) => { 3047 const parent = navigation.owner(f); 3048 const rest = walkUp(navigation, parent); 3049 return [f].concat(rest); 3050 }); 3051 }; 3052 // TODO: Why is this an option if it is always some? 3053 const pathTo = (element, navigation) => { 3054 const d = navigation.owner(element); 3055 const paths = walkUp(navigation, d); 3056 return Optional.some(paths); 3057 }; 3058 3059 const view = (doc) => { 3060 // Only walk up to the document this script is defined in. 3061 // This prevents walking up to the parent window when the editor is in an iframe. 3062 const element = doc.dom === document ? Optional.none() : Optional.from(doc.dom.defaultView?.frameElement); 3063 return element.map(SugarElement.fromDom); 3064 }; 3065 const owner$3 = (element) => owner$4(element); 3066 3067 var Navigation = /*#__PURE__*/Object.freeze({ 3068 __proto__: null, 3069 view: view, 3070 owner: owner$3 3071 }); 3072 3073 const find$2 = (element) => { 3074 const doc = getDocument(); 3075 const scroll = get$b(doc); 3076 // Get the path of iframe elements to this element. 3077 const path = pathTo(element, Navigation); 3078 return path.fold(curry(absolute$3, element), (frames) => { 3079 const offset = viewport$1(element); 3080 const r = foldr(frames, (b, a) => { 3081 const loc = viewport$1(a); 3082 return { 3083 left: b.left + loc.left, 3084 top: b.top + loc.top 3085 }; 3086 }, { left: 0, top: 0 }); 3087 return SugarPosition(r.left + offset.left + scroll.left, r.top + offset.top + scroll.top); 3088 }); 3089 }; 3090 3091 const pointed = (point, width, height) => ({ 3092 point, 3093 width, 3094 height 3095 }); 3096 const rect = (x, y, width, height) => ({ 3097 x, 3098 y, 3099 width, 3100 height 3101 }); 3102 const bounds = (x, y, width, height) => ({ 3103 x, 3104 y, 3105 width, 3106 height, 3107 right: x + width, 3108 bottom: y + height 3109 }); 3110 const box$1 = (element) => { 3111 const xy = absolute$3(element); 3112 const w = getOuter(element); 3113 const h = getOuter$1(element); 3114 return bounds(xy.left, xy.top, w, h); 3115 }; 3116 // NOTE: We used to use AriaFocus.preserve here, but there is no reason to do that now that 3117 // we are not changing the visibility of the element. Hopefully (2015-09-29). 3118 const absolute$2 = (element) => { 3119 const position = find$2(element); 3120 const width = getOuter(element); 3121 const height = getOuter$1(element); 3122 return bounds(position.left, position.top, width, height); 3123 }; 3124 const constrain = (original, constraint) => { 3125 const left = Math.max(original.x, constraint.x); 3126 const top = Math.max(original.y, constraint.y); 3127 const right = Math.min(original.right, constraint.right); 3128 const bottom = Math.min(original.bottom, constraint.bottom); 3129 const width = right - left; 3130 const height = bottom - top; 3131 return bounds(left, top, width, height); 3132 }; 3133 const constrainByMany = (original, constraints) => { 3134 return foldl(constraints, (acc, c) => constrain(acc, c), original); 3135 }; 3136 const win = () => getBounds$1(window); 3137 3138 const isSource = (component, simulatedEvent) => eq(component.element, simulatedEvent.event.target); 3139 3140 const getOffsetParent = (element) => { 3141 // Firefox sets the offsetParent to the body when fixed instead of null like 3142 // all other browsers. So we need to check if the element is fixed and if so then 3143 // disregard the elements offsetParent. 3144 const isFixed = is$1(getRaw(element, 'position'), 'fixed'); 3145 const offsetParent$1 = isFixed ? Optional.none() : offsetParent(element); 3146 return offsetParent$1.orThunk(() => { 3147 const marker = SugarElement.fromTag('span'); 3148 // PERFORMANCE: Append the marker to the parent element, as adding it before the current element will 3149 // trigger the styles to be recalculated which is a little costly (particularly in scroll/resize events) 3150 return parent(element).bind((parent) => { 3151 append$2(parent, marker); 3152 const offsetParent$1 = offsetParent(marker); 3153 remove$7(marker); 3154 return offsetParent$1; 3155 }); 3156 }); 3157 }; 3158 /* 3159 * This allows the absolute coordinates to be obtained by adding the 3160 * origin to the offset coordinates and not needing to know scroll. 3161 */ 3162 const getOrigin = (element) => getOffsetParent(element).map(absolute$3).getOrThunk(() => SugarPosition(0, 0)); 3163 3164 const describedBy = (describedElement, describeElement) => { 3165 const describeId = Optional.from(get$g(describedElement, 'id')) 3166 .getOrThunk(() => { 3167 const id = generate$6('aria'); 3168 set$9(describeElement, 'id', id); 3169 return id; 3170 }); 3171 set$9(describedElement, 'aria-describedby', describeId); 3172 }; 3173 const remove$1 = (describedElement) => { 3174 remove$8(describedElement, 'aria-describedby'); 3175 }; 3176 3177 var SimpleResultType; 3178 (function (SimpleResultType) { 3179 SimpleResultType[SimpleResultType["Error"] = 0] = "Error"; 3180 SimpleResultType[SimpleResultType["Value"] = 1] = "Value"; 3181 })(SimpleResultType || (SimpleResultType = {})); 3182 const fold$1 = (res, onError, onValue) => res.stype === SimpleResultType.Error ? onError(res.serror) : onValue(res.svalue); 3183 const partition$1 = (results) => { 3184 const values = []; 3185 const errors = []; 3186 each$1(results, (obj) => { 3187 fold$1(obj, (err) => errors.push(err), (val) => values.push(val)); 3188 }); 3189 return { values, errors }; 3190 }; 3191 const mapError = (res, f) => { 3192 if (res.stype === SimpleResultType.Error) { 3193 return { stype: SimpleResultType.Error, serror: f(res.serror) }; 3194 } 3195 else { 3196 return res; 3197 } 3198 }; 3199 const map = (res, f) => { 3200 if (res.stype === SimpleResultType.Value) { 3201 return { stype: SimpleResultType.Value, svalue: f(res.svalue) }; 3202 } 3203 else { 3204 return res; 3205 } 3206 }; 3207 const bind = (res, f) => { 3208 if (res.stype === SimpleResultType.Value) { 3209 return f(res.svalue); 3210 } 3211 else { 3212 return res; 3213 } 3214 }; 3215 const bindError = (res, f) => { 3216 if (res.stype === SimpleResultType.Error) { 3217 return f(res.serror); 3218 } 3219 else { 3220 return res; 3221 } 3222 }; 3223 const svalue = (v) => ({ stype: SimpleResultType.Value, svalue: v }); 3224 const serror = (e) => ({ stype: SimpleResultType.Error, serror: e }); 3225 const toResult$1 = (res) => fold$1(res, Result.error, Result.value); 3226 const fromResult = (res) => res.fold(serror, svalue); 3227 const SimpleResult = { 3228 fromResult, 3229 toResult: toResult$1, 3230 svalue, 3231 partition: partition$1, 3232 serror, 3233 bind, 3234 bindError, 3235 map, 3236 mapError, 3237 fold: fold$1 3238 }; 3239 3240 const formatObj = (input) => { 3241 return isObject(input) && keys(input).length > 100 ? ' removed due to size' : JSON.stringify(input, null, 2); 3242 }; 3243 const formatErrors = (errors) => { 3244 const es = errors.length > 10 ? errors.slice(0, 10).concat([ 3245 { 3246 path: [], 3247 getErrorInfo: constant$1('... (only showing first ten failures)') 3248 } 3249 ]) : errors; 3250 // TODO: Work out a better split between PrettyPrinter and SchemaError 3251 return map$2(es, (e) => { 3252 return 'Failed path: (' + e.path.join(' > ') + ')\n' + e.getErrorInfo(); 3253 }); 3254 }; 3255 3256 const nu$7 = (path, getErrorInfo) => { 3257 return SimpleResult.serror([{ 3258 path, 3259 // This is lazy so that it isn't calculated unnecessarily 3260 getErrorInfo 3261 }]); 3262 }; 3263 const missingRequired = (path, key, obj) => nu$7(path, () => 'Could not find valid *required* value for "' + key + '" in ' + formatObj(obj)); 3264 const missingKey = (path, key) => nu$7(path, () => 'Choice schema did not contain choice key: "' + key + '"'); 3265 const missingBranch = (path, branches, branch) => nu$7(path, () => 'The chosen schema: "' + branch + '" did not exist in branches: ' + formatObj(branches)); 3266 const unsupportedFields = (path, unsupported) => nu$7(path, () => 'There are unsupported fields: [' + unsupported.join(', ') + '] specified'); 3267 const custom = (path, err) => nu$7(path, constant$1(err)); 3268 3269 const value$1 = (validator) => { 3270 const extract = (path, val) => { 3271 return SimpleResult.bindError(validator(val), (err) => custom(path, err)); 3272 }; 3273 const toString = constant$1('val'); 3274 return { 3275 extract, 3276 toString 3277 }; 3278 }; 3279 const anyValue$1 = value$1(SimpleResult.svalue); 3280 3281 const anyValue = constant$1(anyValue$1); 3282 const typedValue = (validator, expectedType) => value$1((a) => { 3283 const actualType = typeof a; 3284 return validator(a) ? SimpleResult.svalue(a) : SimpleResult.serror(`Expected type: ${expectedType} but got: ${actualType}`); 3285 }); 3286 const number = typedValue(isNumber, 'number'); 3287 const string = typedValue(isString, 'string'); 3288 const boolean = typedValue(isBoolean, 'boolean'); 3289 const functionProcessor = typedValue(isFunction, 'function'); 3290 // Test if a value can be copied by the structured clone algorithm and hence sendable via postMessage 3291 // https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm 3292 // from https://stackoverflow.com/a/32673910/7377237 with adjustments for typescript 3293 const isPostMessageable = (val) => { 3294 if (Object(val) !== val) { // Primitive value 3295 return true; 3296 } 3297 switch ({}.toString.call(val).slice(8, -1)) { // Class 3298 case 'Boolean': 3299 case 'Number': 3300 case 'String': 3301 case 'Date': 3302 case 'RegExp': 3303 case 'Blob': 3304 case 'FileList': 3305 case 'ImageData': 3306 case 'ImageBitmap': 3307 case 'ArrayBuffer': 3308 return true; 3309 case 'Array': 3310 case 'Object': 3311 return Object.keys(val).every((prop) => isPostMessageable(val[prop])); 3312 default: 3313 return false; 3314 } 3315 }; 3316 const postMessageable = value$1((a) => { 3317 if (isPostMessageable(a)) { 3318 return SimpleResult.svalue(a); 3319 } 3320 else { 3321 return SimpleResult.serror('Expected value to be acceptable for sending via postMessage'); 3322 } 3323 }); 3324 3325 const required$2 = () => ({ tag: "required" /* FieldPresenceTag.Required */, process: {} }); 3326 const defaultedThunk = (fallbackThunk) => ({ tag: "defaultedThunk" /* FieldPresenceTag.DefaultedThunk */, process: fallbackThunk }); 3327 const defaulted$1 = (fallback) => defaultedThunk(constant$1(fallback)); 3328 const asOption = () => ({ tag: "option" /* FieldPresenceTag.Option */, process: {} }); 3329 const mergeWithThunk = (baseThunk) => ({ tag: "mergeWithThunk" /* FieldPresenceTag.MergeWithThunk */, process: baseThunk }); 3330 const mergeWith = (base) => mergeWithThunk(constant$1(base)); 3331 3332 const field$2 = (key, newKey, presence, prop) => ({ tag: "field" /* FieldTag.Field */, key, newKey, presence, prop }); 3333 const customField$1 = (newKey, instantiator) => ({ tag: "custom" /* FieldTag.CustomField */, newKey, instantiator }); 3334 const fold = (value, ifField, ifCustom) => { 3335 switch (value.tag) { 3336 case "field" /* FieldTag.Field */: 3337 return ifField(value.key, value.newKey, value.presence, value.prop); 3338 case "custom" /* FieldTag.CustomField */: 3339 return ifCustom(value.newKey, value.instantiator); 3340 } 3341 }; 3342 3343 const mergeValues$1 = (values, base) => { 3344 return SimpleResult.svalue(deepMerge(base, merge$1.apply(undefined, values))); 3345 }; 3346 const mergeErrors$1 = (errors) => compose(SimpleResult.serror, flatten)(errors); 3347 const consolidateObj = (objects, base) => { 3348 const partition = SimpleResult.partition(objects); 3349 return partition.errors.length > 0 ? mergeErrors$1(partition.errors) : mergeValues$1(partition.values, base); 3350 }; 3351 const consolidateArr = (objects) => { 3352 const partitions = SimpleResult.partition(objects); 3353 return partitions.errors.length > 0 ? mergeErrors$1(partitions.errors) : SimpleResult.svalue(partitions.values); 3354 }; 3355 const ResultCombine = { 3356 consolidateObj, 3357 consolidateArr 3358 }; 3359 3360 const requiredAccess = (path, obj, key, bundle) => 3361 // In required mode, if it is undefined, it is an error. 3362 get$h(obj, key).fold(() => missingRequired(path, key, obj), bundle); 3363 const fallbackAccess = (obj, key, fallback, bundle) => { 3364 const v = get$h(obj, key).getOrThunk(() => fallback(obj)); 3365 return bundle(v); 3366 }; 3367 const optionAccess = (obj, key, bundle) => bundle(get$h(obj, key)); 3368 const optionDefaultedAccess = (obj, key, fallback, bundle) => { 3369 const opt = get$h(obj, key).map((val) => val === true ? fallback(obj) : val); 3370 return bundle(opt); 3371 }; 3372 const extractField = (field, path, obj, key, prop) => { 3373 const bundle = (av) => prop.extract(path.concat([key]), av); 3374 const bundleAsOption = (optValue) => optValue.fold(() => SimpleResult.svalue(Optional.none()), (ov) => { 3375 const result = prop.extract(path.concat([key]), ov); 3376 return SimpleResult.map(result, Optional.some); 3377 }); 3378 switch (field.tag) { 3379 case "required" /* FieldPresenceTag.Required */: 3380 return requiredAccess(path, obj, key, bundle); 3381 case "defaultedThunk" /* FieldPresenceTag.DefaultedThunk */: 3382 return fallbackAccess(obj, key, field.process, bundle); 3383 case "option" /* FieldPresenceTag.Option */: 3384 return optionAccess(obj, key, bundleAsOption); 3385 case "defaultedOptionThunk" /* FieldPresenceTag.DefaultedOptionThunk */: 3386 return optionDefaultedAccess(obj, key, field.process, bundleAsOption); 3387 case "mergeWithThunk" /* FieldPresenceTag.MergeWithThunk */: { 3388 return fallbackAccess(obj, key, constant$1({}), (v) => { 3389 const result = deepMerge(field.process(obj), v); 3390 return bundle(result); 3391 }); 3392 } 3393 } 3394 }; 3395 const extractFields = (path, obj, fields) => { 3396 const success = {}; 3397 const errors = []; 3398 // PERFORMANCE: We use a for loop here instead of Arr.each as this is a hot code path 3399 for (const field of fields) { 3400 fold(field, (key, newKey, presence, prop) => { 3401 const result = extractField(presence, path, obj, key, prop); 3402 SimpleResult.fold(result, (err) => { 3403 errors.push(...err); 3404 }, (res) => { 3405 success[newKey] = res; 3406 }); 3407 }, (newKey, instantiator) => { 3408 success[newKey] = instantiator(obj); 3409 }); 3410 } 3411 return errors.length > 0 ? SimpleResult.serror(errors) : SimpleResult.svalue(success); 3412 }; 3413 const valueThunk = (getDelegate) => { 3414 const extract = (path, val) => getDelegate().extract(path, val); 3415 const toString = () => getDelegate().toString(); 3416 return { 3417 extract, 3418 toString 3419 }; 3420 }; 3421 // This is because Obj.keys can return things where the key is set to undefined. 3422 const getSetKeys = (obj) => keys(filter$1(obj, isNonNullable)); 3423 const objOfOnly = (fields) => { 3424 const delegate = objOf(fields); 3425 const fieldNames = foldr(fields, (acc, value) => { 3426 return fold(value, (key) => deepMerge(acc, { [key]: true }), constant$1(acc)); 3427 }, {}); 3428 const extract = (path, o) => {
3429 const keys = isBoolean(o) ? [] : getSetKeys(o); 3430 const extra = filter$2(keys, (k) => !hasNonNullableKey(fieldNames, k)); 3431 return extra.length === 0 ? delegate.extract(path, o) : unsupportedFields(path, extra); 3432 }; 3433 return { 3434 extract, 3435 toString: delegate.toString 3436 }; 3437 }; 3438 const objOf = (values) => { 3439 const extract = (path, o) => extractFields(path, o, values); 3440 const toString = () => { 3441 const fieldStrings = map$2(values, (value) => fold(value, (key, _okey, _presence, prop) => key + ' -> ' + prop.toString(), (newKey, _instantiator) => 'state(' + newKey + ')')); 3442 return 'obj{\n' + fieldStrings.join('\n') + '}'; 3443 }; 3444 return { 3445 extract, 3446 toString 3447 }; 3448 }; 3449 const arrOf = (prop) => { 3450 const extract = (path, array) => { 3451 const results = map$2(array, (a, i) => prop.extract(path.concat(['[' + i + ']']), a)); 3452 return ResultCombine.consolidateArr(results); 3453 }; 3454 const toString = () => 'array(' + prop.toString() + ')'; 3455 return { 3456 extract, 3457 toString 3458 }; 3459 }; 3460 const oneOf = (props, rawF) => { 3461 // If f is not supplied, then use identity. 3462 const f = rawF !== undefined ? rawF : identity; 3463 const extract = (path, val) => { 3464 const errors = []; 3465 // Return on first match 3466 for (const prop of props) { 3467 const res = prop.extract(path, val); 3468 if (res.stype === SimpleResultType.Value) { 3469 return { 3470 stype: SimpleResultType.Value, 3471 svalue: f(res.svalue) 3472 }; 3473 } 3474 errors.push(res); 3475 } 3476 // All failed, return errors 3477 return ResultCombine.consolidateArr(errors); 3478 }; 3479 const toString = () => 'oneOf(' + map$2(props, (prop) => prop.toString()).join(', ') + ')'; 3480 return { 3481 extract, 3482 toString 3483 }; 3484 }; 3485 const setOf$1 = (validator, prop) => { 3486 const validateKeys = (path, keys) => arrOf(value$1(validator)).extract(path, keys); 3487 const extract = (path, o) => { 3488 // 3489 const keys$1 = keys(o); 3490 const validatedKeys = validateKeys(path, keys$1); 3491 return SimpleResult.bind(validatedKeys, (validKeys) => { 3492 const schema = map$2(validKeys, (vk) => { 3493 return field$2(vk, vk, required$2(), prop); 3494 }); 3495 return objOf(schema).extract(path, o); 3496 }); 3497 }; 3498 const toString = () => 'setOf(' + prop.toString() + ')'; 3499 return { 3500 extract, 3501 toString 3502 }; 3503 }; 3504 const thunk = (_desc, processor) => { 3505 const getP = cached(processor); 3506 const extract = (path, val) => getP().extract(path, val); 3507 const toString = () => getP().toString(); 3508 return { 3509 extract, 3510 toString 3511 }; 3512 }; 3513 const arrOfObj = compose(arrOf, objOf); 3514 3515 const chooseFrom = (path, input, branches, ch) => { 3516 const fields = get$h(branches, ch); 3517 return fields.fold(() => missingBranch(path, branches, ch), (vp) => vp.extract(path.concat(['branch: ' + ch]), input)); 3518 }; 3519 // The purpose of choose is to have a key which picks which of the schemas to follow. 3520 // The key will index into the object of schemas: branches 3521 const choose$2 = (key, branches) => { 3522 const extract = (path, input) => { 3523 const choice = get$h(input, key); 3524 return choice.fold(() => missingKey(path, key), (chosen) => chooseFrom(path, input, branches, chosen)); 3525 }; 3526 const toString = () => 'chooseOn(' + key + '). Possible values: ' + keys(branches); 3527 return { 3528 extract, 3529 toString 3530 }; 3531 }; 3532 3533 const arrOfVal = () => arrOf(anyValue$1); 3534 const valueOf = (validator) => value$1((v) => validator(v).fold(SimpleResult.serror, SimpleResult.svalue)); 3535 const setOf = (validator, prop) => setOf$1((v) => SimpleResult.fromResult(validator(v)), prop); 3536 const extractValue = (label, prop, obj) => { 3537 const res = prop.extract([label], obj); 3538 return SimpleResult.mapError(res, (errs) => ({ input: obj, errors: errs })); 3539 }; 3540 const asRaw = (label, prop, obj) => SimpleResult.toResult(extractValue(label, prop, obj)); 3541 const getOrDie = (extraction) => { 3542 return extraction.fold((errInfo) => { 3543 // A readable version of the error. 3544 throw new Error(formatError(errInfo)); 3545 }, identity); 3546 }; 3547 const asRawOrDie$1 = (label, prop, obj) => getOrDie(asRaw(label, prop, obj)); 3548 const formatError = (errInfo) => { 3549 return 'Errors: \n' + formatErrors(errInfo.errors).join('\n') + 3550 '\n\nInput object: ' + formatObj(errInfo.input); 3551 };
3552 const choose$1 = (key, branches) => choose$2(key, map$1(branches, objOf)); 3553 const thunkOf = (desc, schema) => thunk(desc, schema); 3554 3555 const field$1 = field$2; 3556 const customField = customField$1; 3557 const validateEnum = (values) => valueOf((value) => contains$2(values, value) ? 3558 Result.value(value) : 3559 Result.error(`Unsupported value: "${value}", choose one of "${values.join(', ')}".`)); 3560 const required$1 = (key) => field$1(key, key, required$2(), anyValue()); 3561 const requiredOf = (key, schema) => field$1(key, key, required$2(), schema); 3562 const requiredNumber = (key) => requiredOf(key, number); 3563 const requiredString = (key) => requiredOf(key, string); 3564 const requiredStringEnum = (key, values) => field$1(key, key, required$2(), validateEnum(values)); 3565 const requiredFunction = (key) => requiredOf(key, functionProcessor); 3566 const forbid = (key, message) => field$1(key, key, asOption(), value$1((_v) => SimpleResult.serror('The field: ' + key + ' is forbidden. ' + message))); 3567 const requiredObjOf = (key, objSchema) => field$1(key, key, required$2(), objOf(objSchema)); 3568 const requiredArrayOfObj = (key, objFields) => field$1(key, key, required$2(), arrOfObj(objFields)); 3569 const requiredArrayOf = (key, schema) => field$1(key, key, required$2(), arrOf(schema)); 3570 const option$3 = (key) => field$1(key, key, asOption(), anyValue()); 3571 const optionOf = (key, schema) => field$1(key, key, asOption(), schema); 3572 const optionNumber = (key) => optionOf(key, number); 3573 const optionString = (key) => optionOf(key, string); 3574 const optionStringEnum = (key, values) => optionOf(key, validateEnum(values)); 3575 const optionBoolean = (key) => optionOf(key, boolean); 3576 const optionFunction = (key) => optionOf(key, functionProcessor); 3577 const optionArrayOf = (key, schema) => optionOf(key, arrOf(schema)); 3578 const optionObjOf = (key, objSchema) => optionOf(key, objOf(objSchema)); 3579 const optionObjOfOnly = (key, objSchema) => optionOf(key, objOfOnly(objSchema)); 3580 const defaulted = (key, fallback) => field$1(key, key, defaulted$1(fallback), anyValue()); 3581 const defaultedOf = (key, fallback, schema) => field$1(key, key, defaulted$1(fallback), schema); 3582 const defaultedNumber = (key, fallback) => defaultedOf(key, fallback, number); 3583 const defaultedString = (key, fallback) => defaultedOf(key, fallback, string); 3584 const defaultedStringEnum = (key, fallback, values) => defaultedOf(key, fallback, validateEnum(values)); 3585 const defaultedBoolean = (key, fallback) => defaultedOf(key, fallback, boolean); 3586 const defaultedFunction = (key, fallback) => defaultedOf(key, fallback, functionProcessor); 3587 const defaultedPostMsg = (key, fallback) => defaultedOf(key, fallback, postMessageable); 3588 const defaultedArrayOf = (key, fallback, schema) => defaultedOf(key, fallback, arrOf(schema)); 3589 const defaultedObjOf = (key, fallback, objSchema) => defaultedOf(key, fallback, objOf(objSchema)); 3590 3591 const exclude$1 = (obj, fields) => { 3592 const r = {}; 3593 each(obj, (v, k) => { 3594 if (!contains$2(fields, k)) { 3595 r[k] = v; 3596 } 3597 }); 3598 return r; 3599 }; 3600 3601 const wrap$1 = (key, value) => ({ [key]: value }); 3602 const wrapAll$1 = (keyvalues) => { 3603 const r = {}; 3604 each$1(keyvalues, (kv) => { 3605 r[kv.key] = kv.value; 3606 }); 3607 return r; 3608 }; 3609 3610 const exclude = (obj, fields) => exclude$1(obj, fields); 3611 const wrap = (key, value) => wrap$1(key, value); 3612 const wrapAll = (keyvalues) => wrapAll$1(keyvalues); 3613 const mergeValues = (values, base) => { 3614 return values.length === 0 ? Result.value(base) : Result.value(deepMerge(base, merge$1.apply(undefined, values)) 3615 // Merger.deepMerge.apply(undefined, [ base ].concat(values)) 3616 ); 3617 }; 3618 const mergeErrors = (errors) => Result.error(flatten(errors)); 3619 const consolidate = (objs, base) => { 3620 const partitions = partition$2(objs); 3621 return partitions.errors.length > 0 ? mergeErrors(partitions.errors) : mergeValues(partitions.values, base); 3622 }; 3623 3624 const constant = constant$1; 3625 const touchstart = constant('touchstart'); 3626 const touchmove = constant('touchmove'); 3627 const touchend = constant('touchend'); 3628 const touchcancel = constant('touchcancel'); 3629 const mousedown = constant('mousedown'); 3630 const mousemove = constant('mousemove'); 3631 const mouseout = constant('mouseout'); 3632 const mouseup = constant('mouseup'); 3633 const mouseover = constant('mouseover'); 3634 // Not really a native event as it has to be simulated 3635 const focusin = constant('focusin'); 3636 const focusout = constant('focusout');
vendor: 11,596 bytes, lines 3637-3886
3637 const keydown = constant('keydown'); 3638 const keyup = constant('keyup'); 3639 const input = constant('input'); 3640 const change = constant('change'); 3641 const click = constant('click'); 3642 const transitioncancel = constant('transitioncancel'); 3643 const transitionend = constant('transitionend'); 3644 const transitionstart = constant('transitionstart'); 3645 const selectstart = constant('selectstart'); 3646 const pointerdown = constant('pointerdown'); 3647 const pointermove = constant('pointermove'); 3648 const pointerup = constant('pointerup'); 3649 const lostpointercapture = constant('lostpointercapture'); 3650 3651 const prefixName = (name) => constant$1('alloy.' + name); 3652 const alloy = { tap: prefixName('tap') }; 3653 // This is used to pass focus to a component. A component might interpret 3654 // this event and pass the DOM focus to one of its children, depending on its 3655 // focus model. 3656 const focus$3 = prefixName('focus'); 3657 // This event is fired a small amount of time after the blur has fired. This 3658 // allows the handler to know what was the focused element, and what is now. 3659 const postBlur = prefixName('blur.post'); 3660 // This event is fired a small amount of time after the paste event has fired. 3661 const postPaste = prefixName('paste.post'); 3662 // This event is fired by gui.broadcast*. It is defined by 'receivers' 3663 const receive = prefixName('receive'); 3664 // This event is for executing buttons and things that have (mostly) enter actions 3665 const execute$5 = prefixName('execute'); 3666 // This event is used by a menu to tell an item to focus itself because it has been 3667 // selected. This might automatically focus inside the item, it might focus the outer 3668 // part of the widget etc. 3669 const focusItem = prefixName('focus.item'); 3670 // This event represents a touchstart and touchend on the same location, and fires on 3671 // the touchend 3672 const tap = alloy.tap; 3673 // This event represents a longpress on the same location 3674 const longpress = prefixName('longpress'); 3675 // Fire by a child element to tell the outer element to close 3676 const sandboxClose = prefixName('sandbox.close'); 3677 // Tell the typeahead to cancel any pending fetches (that haven't already executed) 3678 const typeaheadCancel = prefixName('typeahead.cancel'); 3679 // Fired when adding to a world 3680 const systemInit = prefixName('system.init'); 3681 // Fired when a touchmove on the document happens 3682 const documentTouchmove = prefixName('system.touchmove'); 3683 // Fired when a touchend on the document happens 3684 const documentTouchend = prefixName('system.touchend'); 3685 // Fired when the window scrolls 3686 const windowScroll = prefixName('system.scroll'); 3687 // Fired when the window resizes 3688 const windowResize = prefixName('system.resize'); 3689 const attachedToDom = prefixName('system.attached'); 3690 const detachedFromDom = prefixName('system.detached'); 3691 const dismissRequested = prefixName('system.dismissRequested'); 3692 const repositionRequested = prefixName('system.repositionRequested'); 3693 const focusShifted = prefixName('focusmanager.shifted'); 3694 // Fired when slots are made hidden/shown 3695 const slotVisibility = prefixName('slotcontainer.visibility'); 3696 // Used for containers outside the mothership that scroll. Used by docking. 3697 const externalElementScroll = prefixName('system.external.element.scroll'); 3698 const changeTab = prefixName('change.tab'); 3699 const dismissTab = prefixName('dismiss.tab'); 3700 const highlight$1 = prefixName('highlight'); 3701 const dehighlight$1 = prefixName('dehighlight'); 3702 3703 const element = (elem) => getHtml(elem); 3704 3705 const unknown = 'unknown'; 3706 /* 3707 typescipt qwerk: 3708 const debugging: boolean = true; 3709 if (boolean === false) { -> this throws a type error! // TS2365:Operator '===' cannot be applied to types 'false' and 'true' 3710 https://www.typescriptlang.org/play/#src=const%20foo%3A%20boolean%20%3D%20true%3B%0D%0A%0D%0Aif%20(foo%20%3D%3D%3D%20false)%20%7B%0D%0A%20%20%20%20%0D%0A%7D 3711 } 3712 */ 3713 const debugging = true; 3714 var EventConfiguration; 3715 (function (EventConfiguration) { 3716 EventConfiguration[EventConfiguration["STOP"] = 0] = "STOP"; 3717 EventConfiguration[EventConfiguration["NORMAL"] = 1] = "NORMAL"; 3718 EventConfiguration[EventConfiguration["LOGGING"] = 2] = "LOGGING"; 3719 })(EventConfiguration || (EventConfiguration = {})); 3720 const eventConfig = Cell({}); 3721 const makeEventLogger = (eventName, initialTarget) => { 3722 const sequence = []; 3723 const startTime = new Date().getTime(); 3724 return { 3725 logEventCut: (_name, target, purpose) => { 3726 sequence.push({ outcome: 'cut', target, purpose }); 3727 }, 3728 logEventStopped: (_name, target, purpose) => { 3729 sequence.push({ outcome: 'stopped', target, purpose }); 3730 }, 3731 logNoParent: (_name, target, purpose) => { 3732 sequence.push({ outcome: 'no-parent', target, purpose }); 3733 }, 3734 logEventNoHandlers: (_name, target) => { 3735 sequence.push({ outcome: 'no-handlers-left', target }); 3736 }, 3737 logEventResponse: (_name, target, purpose) => { 3738 sequence.push({ outcome: 'response', purpose, target }); 3739 }, 3740 write: () => { 3741 const finishTime = new Date().getTime(); 3742 if (contains$2(['mousemove', 'mouseover', 'mouseout', systemInit()], eventName)) { 3743 return; 3744 } 3745 // eslint-disable-next-line no-console 3746 console.log(eventName, { 3747 event: eventName, 3748 time: finishTime - startTime, 3749 target: initialTarget.dom, 3750 sequence: map$2(sequence, (s) => { 3751 if (!contains$2(['cut', 'stopped', 'response'], s.outcome)) { 3752 return s.outcome; 3753 } 3754 else { 3755 return '{' + s.purpose + '} ' + s.outcome + ' at (' + element(s.target) + ')'; 3756 } 3757 }) 3758 }); 3759 } 3760 }; 3761 }; 3762 const processEvent = (eventName, initialTarget, f) => { 3763 const status = get$h(eventConfig.get(), eventName).orThunk(() => { 3764 const patterns = keys(eventConfig.get()); 3765 return findMap(patterns, (p) => eventName.indexOf(p) > -1 ? Optional.some(eventConfig.get()[p]) : Optional.none()); 3766 }).getOr(EventConfiguration.NORMAL); 3767 switch (status) { 3768 case EventConfiguration.NORMAL: 3769 return f(noLogger()); 3770 case EventConfiguration.LOGGING: { 3771 const logger = makeEventLogger(eventName, initialTarget); 3772 const output = f(logger); 3773 logger.write(); 3774 return output; 3775 } 3776 case EventConfiguration.STOP: 3777 // Does not even run the function to trigger event and listen to handlers 3778 return true; 3779 } 3780 }; 3781 // Ignore these files in the error stack 3782 const path = [ 3783 'alloy/data/Fields', 3784 'alloy/debugging/Debugging' 3785 ]; 3786 const getTrace = () => { 3787 if (debugging === false) { 3788 return unknown; 3789 } 3790 const err = new Error(); 3791 if (err.stack !== undefined) { 3792 const lines = err.stack.split('\n'); 3793 return find$5(lines, (line) => line.indexOf('alloy') > 0 && !exists(path, (p) => line.indexOf(p) > -1)).getOr(unknown); 3794 } 3795 else { 3796 return unknown; 3797 } 3798 }; 3799 const ignoreEvent = { 3800 logEventCut: noop, 3801 logEventStopped: noop, 3802 logNoParent: noop, 3803 logEventNoHandlers: noop, 3804 logEventResponse: noop, 3805 write: noop 3806 }; 3807 const monitorEvent = (eventName, initialTarget, f) => processEvent(eventName, initialTarget, f); 3808 const noLogger = constant$1(ignoreEvent); 3809 3810 const menuFields = constant$1([ 3811 required$1('menu'), 3812 required$1('selectedMenu') 3813 ]); 3814 const itemFields = constant$1([ 3815 required$1('item'), 3816 required$1('selectedItem') 3817 ]); 3818 constant$1(objOf(itemFields().concat(menuFields()))); 3819 const itemSchema$3 = constant$1(objOf(itemFields())); 3820 3821 const _initSize = requiredObjOf('initSize', [ 3822 required$1('numColumns'), 3823 required$1('numRows') 3824 ]); 3825 const itemMarkers = () => requiredOf('markers', itemSchema$3()); 3826 const tieredMenuMarkers = () => requiredObjOf('markers', [ 3827 required$1('backgroundMenu') 3828 ].concat(menuFields()).concat(itemFields())); 3829 const markers$1 = (required) => requiredObjOf('markers', map$2(required, required$1)); 3830 const onPresenceHandler = (label, fieldName, presence) => { 3831 // We care about where the handler was declared (in terms of which schema) 3832 getTrace(); 3833 return field$1(fieldName, fieldName, presence, 3834 // Apply some wrapping to their supplied function 3835 valueOf((f) => Result.value((...args) => { 3836 return f.apply(undefined, args); 3837 }))); 3838 }; 3839 const onHandler = (fieldName) => onPresenceHandler('onHandler', fieldName, defaulted$1(noop)); 3840 const onKeyboardHandler = (fieldName) => onPresenceHandler('onKeyboardHandler', fieldName, defaulted$1(Optional.none)); 3841 const onStrictHandler = (fieldName) => onPresenceHandler('onHandler', fieldName, required$2()); 3842 const onStrictKeyboardHandler = (fieldName) => onPresenceHandler('onKeyboardHandler', fieldName, required$2()); 3843 const output$1 = (name, value) => customField(name, constant$1(value)); 3844 const snapshot = (name) => customField(name, identity); 3845 const initSize = constant$1(_initSize); 3846 3847 const markAsBehaviourApi = (f, apiName, apiFunction) => { 3848 const delegate = apiFunction.toString(); 3849 const endIndex = delegate.indexOf(')') + 1; 3850 const openBracketIndex = delegate.indexOf('('); 3851 const parameters = delegate.substring(openBracketIndex + 1, endIndex - 1).split(/,\s*/); 3852 f.toFunctionAnnotation = () => ({ 3853 name: apiName, 3854 parameters: cleanParameters(parameters.slice(0, 1).concat(parameters.slice(3))) 3855 }); 3856 return f; 3857 }; 3858 // Remove any comment (/*) at end of parameter names 3859 const cleanParameters = (parameters) => map$2(parameters, (p) => endsWith(p, '/*') ? p.substring(0, p.length - '/*'.length) : p); 3860 const markAsExtraApi = (f, extraName) => { 3861 const delegate = f.toString(); 3862 const endIndex = delegate.indexOf(')') + 1; 3863 const openBracketIndex = delegate.indexOf('('); 3864 const parameters = delegate.substring(openBracketIndex + 1, endIndex - 1).split(/,\s*/); 3865 f.toFunctionAnnotation = () => ({ 3866 name: extraName, 3867 parameters: cleanParameters(parameters) 3868 }); 3869 return f; 3870 }; 3871 const markAsSketchApi = (f, apiFunction) => { 3872 const delegate = apiFunction.toString(); 3873 const endIndex = delegate.indexOf(')') + 1; 3874 const openBracketIndex = delegate.indexOf('('); 3875 const parameters = delegate.substring(openBracketIndex + 1, endIndex - 1).split(/,\s*/); 3876 f.toFunctionAnnotation = () => ({ 3877 name: 'OVERRIDE', 3878 parameters: cleanParameters(parameters.slice(1)) 3879 }); 3880 return f; 3881 }; 3882 3883 const DelayedFunction = (fun, delay) => { 3884 let ref = null; 3885 const schedule = (...args) => { 3886 ref = setTimeout(() => {
3887 fun.apply(null, args); 3888 ref = null; 3889 }, delay); 3890 }; 3891 const cancel = () => { 3892 if (ref !== null) { 3893 clearTimeout(ref); 3894 ref = null; 3895 } 3896 }; 3897 return { 3898 cancel, 3899 schedule 3900 }; 3901 }; 3902 3903 const SIGNIFICANT_MOVE = 5; 3904 const LONGPRESS_DELAY = 400; 3905 const getTouch = (event) => { 3906 const raw = event.raw; 3907 if (raw.touches === undefined || raw.touches.length !== 1) { 3908 return Optional.none(); 3909 } 3910 return Optional.some(raw.touches[0]); 3911 }; 3912 // Check to see if the touch has changed a *significant* amount 3913 const isFarEnough = (touch, data) => { 3914 const distX = Math.abs(touch.clientX - data.x); 3915 const distY = Math.abs(touch.clientY - data.y); 3916 return distX > SIGNIFICANT_MOVE || distY > SIGNIFICANT_MOVE; 3917 }; 3918 const monitor = (settings) => { 3919 /* A tap event is a combination of touchstart and touchend on the same element 3920 * without a *significant* touchmove in between. 3921 */ 3922 const startData = value$2(); 3923 const longpressFired = Cell(false); 3924 const longpress$1 = DelayedFunction((event) => { 3925 settings.triggerEvent(longpress(), event); 3926 longpressFired.set(true); 3927 }, LONGPRESS_DELAY); 3928 const handleTouchstart = (event) => { 3929 getTouch(event).each((touch) => { 3930 longpress$1.cancel(); 3931 const data = { 3932 x: touch.clientX, 3933 y: touch.clientY, 3934 target: event.target 3935 }; 3936 longpress$1.schedule(event); 3937 longpressFired.set(false); 3938 startData.set(data); 3939 }); 3940 return Optional.none(); 3941 }; 3942 const handleTouchmove = (event) => { 3943 longpress$1.cancel(); 3944 getTouch(event).each((touch) => { 3945 startData.on((data) => { 3946 if (isFarEnough(touch, data)) { 3947 startData.clear(); 3948 } 3949 }); 3950 }); 3951 return Optional.none(); 3952 }; 3953 const handleTouchend = (event) => { 3954 longpress$1.cancel(); 3955 const isSame = (data) => eq(data.target, event.target); 3956 return startData.get().filter(isSame).map((_data) => { 3957 if (longpressFired.get()) { 3958 event.prevent(); 3959 return false; 3960 } 3961 else { 3962 return settings.triggerEvent(tap(), event); 3963 } 3964 }); 3965 }; 3966 const handlers = wrapAll([ 3967 { key: touchstart(), value: handleTouchstart }, 3968 { key: touchmove(), value: handleTouchmove }, 3969 { key: touchend(), value: handleTouchend } 3970 ]); 3971 const fireIfReady = (event, type) => get$h(handlers, type).bind((handler) => handler(event)); 3972 return { 3973 fireIfReady 3974 }; 3975 }; 3976 3977 var FocusInsideModes; 3978 (function (FocusInsideModes) { 3979 FocusInsideModes["OnFocusMode"] = "onFocus"; 3980 FocusInsideModes["OnEnterOrSpaceMode"] = "onEnterOrSpace"; 3981 FocusInsideModes["OnApiMode"] = "onApi"; 3982 })(FocusInsideModes || (FocusInsideModes = {})); 3983 3984 const _placeholder = 'placeholder'; 3985 const adt$7 = Adt.generate([ 3986 { single: ['required', 'valueThunk'] }, 3987 { multiple: ['required', 'valueThunks'] } 3988 ]); 3989 const isSubstituted = (spec) => has$2(spec, 'uiType'); 3990 const subPlaceholder = (owner, detail, compSpec, placeholders) => { 3991 if (owner.exists((o) => o !== compSpec.owner)) { 3992 return adt$7.single(true, constant$1(compSpec)); 3993 } 3994 // Ignore having to find something for the time being. 3995 return get$h(placeholders, compSpec.name).fold(() => { 3996 throw new Error('Unknown placeholder component: ' + compSpec.name + '\nKnown: [' + 3997 keys(placeholders) + ']\nNamespace: ' + owner.getOr('none') + '\nSpec: ' + JSON.stringify(compSpec, null, 2)); 3998 }, (newSpec) => 3999 // Must return a single/multiple type 4000 newSpec.replace()); 4001 }; 4002 const scan = (owner, detail, compSpec, placeholders) => {
vendor: 16,137 bytes, lines 4003-4370
4003 if (isSubstituted(compSpec) && compSpec.uiType === _placeholder) { 4004 return subPlaceholder(owner, detail, compSpec, placeholders); 4005 } 4006 else { 4007 return adt$7.single(false, constant$1(compSpec)); 4008 } 4009 }; 4010 const substitute = (owner, detail, compSpec, placeholders) => { 4011 const base = scan(owner, detail, compSpec, placeholders); 4012 return base.fold((req, valueThunk) => { 4013 const value = isSubstituted(compSpec) ? valueThunk(detail, compSpec.config, compSpec.validated) : valueThunk(detail); 4014 const childSpecs = get$h(value, 'components').getOr([]); 4015 const substituted = bind$3(childSpecs, (c) => substitute(owner, detail, c, placeholders)); 4016 return [ 4017 { 4018 ...value, 4019 components: substituted 4020 } 4021 ]; 4022 }, (req, valuesThunk) => { 4023 if (isSubstituted(compSpec)) { 4024 const values = valuesThunk(detail, compSpec.config, compSpec.validated); 4025 // Allow a preprocessing step for groups before returning the components 4026 const preprocessor = compSpec.validated.preprocess.getOr(identity); 4027 return preprocessor(values); 4028 } 4029 else { 4030 return valuesThunk(detail); 4031 } 4032 }); 4033 }; 4034 const substituteAll = (owner, detail, components, placeholders) => bind$3(components, (c) => substitute(owner, detail, c, placeholders)); 4035 const oneReplace = (label, replacements) => { 4036 let called = false; 4037 const used = () => called; 4038 const replace = () => { 4039 if (called) { 4040 throw new Error('Trying to use the same placeholder more than once: ' + label); 4041 } 4042 called = true; 4043 return replacements; 4044 }; 4045 const required = () => replacements.fold((req, _) => req, (req, _) => req); 4046 return { 4047 name: constant$1(label), 4048 required, 4049 used, 4050 replace 4051 }; 4052 }; 4053 const substitutePlaces = (owner, detail, components, placeholders) => { 4054 const ps = map$1(placeholders, (ph, name) => oneReplace(name, ph)); 4055 const outcome = substituteAll(owner, detail, components, ps); 4056 each(ps, (p) => { 4057 if (p.used() === false && p.required()) { 4058 throw new Error('Placeholder: ' + p.name() + ' was not found in components list\nNamespace: ' + owner.getOr('none') + '\nComponents: ' + 4059 JSON.stringify(detail.components, null, 2)); 4060 } 4061 }); 4062 return outcome; 4063 }; 4064 const single$2 = adt$7.single; 4065 const multiple = adt$7.multiple; 4066 const placeholder = constant$1(_placeholder); 4067 4068 const adt$6 = Adt.generate([ 4069 { required: ['data'] }, 4070 { external: ['data'] }, 4071 { optional: ['data'] }, 4072 { group: ['data'] } 4073 ]); 4074 const fFactory = defaulted('factory', { sketch: identity }); 4075 const fSchema = defaulted('schema', []); 4076 const fName = required$1('name'); 4077 const fPname = field$1('pname', 'pname', defaultedThunk((typeSpec) => '<alloy.' + generate$6(typeSpec.name) + '>'), anyValue()); 4078 // Groups cannot choose their schema. 4079 const fGroupSchema = customField('schema', () => [ 4080 option$3('preprocess') 4081 ]); 4082 const fDefaults = defaulted('defaults', constant$1({})); 4083 const fOverrides = defaulted('overrides', constant$1({})); 4084 const requiredSpec = objOf([ 4085 fFactory, fSchema, fName, fPname, fDefaults, fOverrides 4086 ]); 4087 const externalSpec = objOf([ 4088 fFactory, fSchema, fName, fDefaults, fOverrides 4089 ]); 4090 const optionalSpec = objOf([ 4091 fFactory, fSchema, fName, fPname, fDefaults, fOverrides 4092 ]); 4093 const groupSpec = objOf([ 4094 fFactory, fGroupSchema, fName, 4095 required$1('unit'), 4096 fPname, fDefaults, fOverrides 4097 ]); 4098 const asNamedPart = (part) => { 4099 return part.fold(Optional.some, Optional.none, Optional.some, Optional.some); 4100 }; 4101 const name$2 = (part) => { 4102 const get = (data) => data.name; 4103 return part.fold(get, get, get, get); 4104 }; 4105 const asCommon = (part) => { 4106 return part.fold(identity, identity, identity, identity); 4107 }; 4108 const convert = (adtConstructor, partSchema) => (spec) => { 4109 const data = asRawOrDie$1('Converting part type', partSchema, spec); 4110 return adtConstructor(data); 4111 }; 4112 const required = convert(adt$6.required, requiredSpec); 4113 const external$1 = convert(adt$6.external, externalSpec); 4114 const optional = convert(adt$6.optional, optionalSpec); 4115 const group = convert(adt$6.group, groupSpec); 4116 const original = constant$1('entirety'); 4117 4118 var PartType = /*#__PURE__*/Object.freeze({ 4119 __proto__: null, 4120 required: required, 4121 external: external$1, 4122 optional: optional, 4123 group: group, 4124 asNamedPart: asNamedPart, 4125 name: name$2, 4126 asCommon: asCommon, 4127 original: original 4128 }); 4129 4130 const combine$2 = (detail, data, partSpec, partValidated) => 4131 // Extremely confusing names and types :( 4132 deepMerge(data.defaults(detail, partSpec, partValidated), partSpec, { uid: detail.partUids[data.name] }, data.overrides(detail, partSpec, partValidated)); 4133 const subs = (owner, detail, parts) => { 4134 const internals = {}; 4135 const externals = {}; 4136 each$1(parts, (part) => { 4137 part.fold( 4138 // Internal 4139 (data) => { 4140 internals[data.pname] = single$2(true, (detail, partSpec, partValidated) => data.factory.sketch(combine$2(detail, data, partSpec, partValidated))); 4141 }, 4142 // External 4143 (data) => { 4144 const partSpec = detail.parts[data.name]; 4145 externals[data.name] = constant$1(data.factory.sketch(combine$2(detail, data, partSpec[original()]), partSpec) // This is missing partValidated 4146 ); 4147 // no placeholders 4148 }, 4149 // Optional 4150 (data) => { 4151 internals[data.pname] = single$2(false, (detail, partSpec, partValidated) => data.factory.sketch(combine$2(detail, data, partSpec, partValidated))); 4152 }, 4153 // Group 4154 (data) => { 4155 internals[data.pname] = multiple(true, (detail, _partSpec, _partValidated) => { 4156 const units = detail[data.name]; 4157 return map$2(units, (u) => 4158 // Group multiples do not take the uid because there is more than one. 4159 data.factory.sketch(deepMerge(data.defaults(detail, u, _partValidated), u, data.overrides(detail, u)))); 4160 }); 4161 }); 4162 }); 4163 return { 4164 internals: constant$1(internals), 4165 externals: constant$1(externals) 4166 }; 4167 }; 4168 4169 // TODO: Make more functional if performance isn't an issue. 4170 const generate$5 = (owner, parts) => { 4171 const r = {}; 4172 each$1(parts, (part) => { 4173 asNamedPart(part).each((np) => { 4174 const g = doGenerateOne(owner, np.pname); 4175 r[np.name] = (config) => { 4176 const validated = asRawOrDie$1('Part: ' + np.name + ' in ' + owner, objOf(np.schema), config); 4177 return { 4178 ...g, 4179 config, 4180 validated 4181 }; 4182 }; 4183 }); 4184 }); 4185 return r; 4186 }; 4187 // Does not have the config. 4188 const doGenerateOne = (owner, pname) => ({ 4189 uiType: placeholder(), 4190 owner, 4191 name: pname 4192 }); 4193 const generateOne$1 = (owner, pname, config) => ({ 4194 uiType: placeholder(), 4195 owner, 4196 name: pname, 4197 config, 4198 validated: {} 4199 }); 4200 const schemas = (parts) => 4201 // This actually has to change. It needs to return the schemas for things that will 4202 // not appear in the components list, which is only externals 4203 bind$3(parts, (part) => part.fold(Optional.none, Optional.some, Optional.none, Optional.none).map((data) => requiredObjOf(data.name, data.schema.concat([ 4204 snapshot(original()) 4205 ]))).toArray()); 4206 const names = (parts) => map$2(parts, name$2); 4207 const substitutes = (owner, detail, parts) => subs(owner, detail, parts); 4208 const components$1 = (owner, detail, internals) => substitutePlaces(Optional.some(owner), detail, detail.components, internals); 4209 const getPart = (component, detail, partKey) => { 4210 const uid = detail.partUids[partKey]; 4211 return component.getSystem().getByUid(uid).toOptional(); 4212 }; 4213 const getPartOrDie = (component, detail, partKey) => getPart(component, detail, partKey).getOrDie('Could not find part: ' + partKey); 4214 const getParts = (component, detail, partKeys) => { 4215 const r = {}; 4216 const uids = detail.partUids; 4217 const system = component.getSystem(); 4218 each$1(partKeys, (pk) => { 4219 r[pk] = constant$1(system.getByUid(uids[pk])); 4220 }); 4221 return r; 4222 }; 4223 const getAllParts = (component, detail) => { 4224 const system = component.getSystem(); 4225 return map$1(detail.partUids, (pUid, _k) => constant$1(system.getByUid(pUid))); 4226 }; 4227 const getAllPartNames = (detail) => keys(detail.partUids); 4228 const getPartsOrDie = (component, detail, partKeys) => { 4229 const r = {}; 4230 const uids = detail.partUids; 4231 const system = component.getSystem(); 4232 each$1(partKeys, (pk) => { 4233 r[pk] = constant$1(system.getByUid(uids[pk]).getOrDie()); 4234 }); 4235 return r; 4236 }; 4237 const defaultUids = (baseUid, partTypes) => { 4238 const partNames = names(partTypes); 4239 return wrapAll(map$2(partNames, (pn) => ({ key: pn, value: baseUid + '-' + pn }))); 4240 }; 4241 const defaultUidsSchema = (partTypes) => field$1('partUids', 'partUids', mergeWithThunk((spec) => defaultUids(spec.uid, partTypes)), anyValue()); 4242 4243 var AlloyParts = /*#__PURE__*/Object.freeze({ 4244 __proto__: null, 4245 generate: generate$5, 4246 generateOne: generateOne$1, 4247 schemas: schemas, 4248 names: names, 4249 substitutes: substitutes, 4250 components: components$1, 4251 defaultUids: defaultUids, 4252 defaultUidsSchema: defaultUidsSchema, 4253 getAllParts: getAllParts, 4254 getAllPartNames: getAllPartNames, 4255 getPart: getPart, 4256 getPartOrDie: getPartOrDie, 4257 getParts: getParts, 4258 getPartsOrDie: getPartsOrDie 4259 }); 4260 4261 const allAlignments = [ 4262 'valignCentre', 4263 'alignLeft', 4264 'alignRight', 4265 'alignCentre', 4266 'top', 4267 'bottom', 4268 'left', 4269 'right', 4270 'inset' 4271 ]; 4272 const nu$6 = (xOffset, yOffset, classes, insetModifier = 1) => { 4273 const insetXOffset = xOffset * insetModifier; 4274 const insetYOffset = yOffset * insetModifier; 4275 const getClasses = (prop) => get$h(classes, prop).getOr([]); 4276 const make = (xDelta, yDelta, alignmentsOn) => { 4277 const alignmentsOff = difference(allAlignments, alignmentsOn); 4278 return { 4279 offset: SugarPosition(xDelta, yDelta), 4280 classesOn: bind$3(alignmentsOn, getClasses), 4281 classesOff: bind$3(alignmentsOff, getClasses) 4282 }; 4283 }; 4284 return { 4285 southeast: () => make(-xOffset, yOffset, ['top', 'alignLeft']), 4286 southwest: () => make(xOffset, yOffset, ['top', 'alignRight']), 4287 south: () => make(-xOffset / 2, yOffset, ['top', 'alignCentre']), 4288 northeast: () => make(-xOffset, -yOffset, ['bottom', 'alignLeft']), 4289 northwest: () => make(xOffset, -yOffset, ['bottom', 'alignRight']), 4290 north: () => make(-xOffset / 2, -yOffset, ['bottom', 'alignCentre']), 4291 east: () => make(xOffset, -yOffset / 2, ['valignCentre', 'left']), 4292 west: () => make(-xOffset, -yOffset / 2, ['valignCentre', 'right']), 4293 insetNortheast: () => make(insetXOffset, insetYOffset, ['top', 'alignLeft', 'inset']), 4294 insetNorthwest: () => make(-insetXOffset, insetYOffset, ['top', 'alignRight', 'inset']), 4295 insetNorth: () => make(-insetXOffset / 2, insetYOffset, ['top', 'alignCentre', 'inset']), 4296 insetSoutheast: () => make(insetXOffset, -insetYOffset, ['bottom', 'alignLeft', 'inset']), 4297 insetSouthwest: () => make(-insetXOffset, -insetYOffset, ['bottom', 'alignRight', 'inset']), 4298 insetSouth: () => make(-insetXOffset / 2, -insetYOffset, ['bottom', 'alignCentre', 'inset']), 4299 insetEast: () => make(-insetXOffset, -insetYOffset / 2, ['valignCentre', 'right', 'inset']), 4300 insetWest: () => make(insetXOffset, -insetYOffset / 2, ['valignCentre', 'left', 'inset']) 4301 }; 4302 }; 4303 const fallback = () => nu$6(0, 0, {}); 4304 4305 const nu$5 = (x, y, bubble, direction, placement, boundsRestriction, labelPrefix, alwaysFit = false) => ({ 4306 x, 4307 y, 4308 bubble, 4309 direction, 4310 placement, 4311 restriction: boundsRestriction, 4312 label: `${labelPrefix}-${placement}`, 4313 alwaysFit 4314 }); 4315 4316 const adt$5 = Adt.generate([ 4317 { southeast: [] }, 4318 { southwest: [] }, 4319 { northeast: [] }, 4320 { northwest: [] }, 4321 { south: [] }, 4322 { north: [] }, 4323 { east: [] }, 4324 { west: [] } 4325 ]); 4326 const cata$1 = (subject, southeast, southwest, northeast, northwest, south, north, east, west) => subject.fold(southeast, southwest, northeast, northwest, south, north, east, west); 4327 const cataVertical = (subject, south, middle, north) => subject.fold(south, south, north, north, south, north, middle, middle); 4328 const cataHorizontal = (subject, east, middle, west) => subject.fold(east, west, east, west, middle, middle, east, west); 4329 // TODO: Simplify with the typescript approach. 4330 const southeast$3 = adt$5.southeast; 4331 const southwest$3 = adt$5.southwest; 4332 const northeast$3 = adt$5.northeast; 4333 const northwest$3 = adt$5.northwest; 4334 const south$3 = adt$5.south; 4335 const north$3 = adt$5.north; 4336 const east$3 = adt$5.east; 4337 const west$3 = adt$5.west; 4338 4339 const getRestriction = (anchor, restriction) => { 4340 switch (restriction) { 4341 case 1 /* AnchorBoxBounds.LeftEdge */: 4342 return anchor.x; 4343 case 0 /* AnchorBoxBounds.RightEdge */: 4344 return anchor.x + anchor.width; 4345 case 2 /* AnchorBoxBounds.TopEdge */: 4346 return anchor.y; 4347 case 3 /* AnchorBoxBounds.BottomEdge */: 4348 return anchor.y + anchor.height; 4349 } 4350 }; 4351 const boundsRestriction = (anchor, restrictions) => mapToObject(['left', 'right', 'top', 'bottom'], (dir) => get$h(restrictions, dir).map((restriction) => getRestriction(anchor, restriction))); 4352 const adjustBounds = (bounds$1, restriction, bubbleOffset) => { 4353 const applyRestriction = (dir, current) => restriction[dir].map((pos) => { 4354 const isVerticalAxis = dir === 'top' || dir === 'bottom'; 4355 const offset = isVerticalAxis ? bubbleOffset.top : bubbleOffset.left; 4356 const comparator = dir === 'left' || dir === 'top' ? Math.max : Math.min; 4357 const newPos = comparator(pos, current) + offset; 4358 // Ensure the new restricted position is within the current bounds 4359 return isVerticalAxis ? clamp(newPos, bounds$1.y, bounds$1.bottom) : clamp(newPos, bounds$1.x, bounds$1.right); 4360 }).getOr(current); 4361 const adjustedLeft = applyRestriction('left', bounds$1.x); 4362 const adjustedTop = applyRestriction('top', bounds$1.y); 4363 const adjustedRight = applyRestriction('right', bounds$1.right); 4364 const adjustedBottom = applyRestriction('bottom', bounds$1.bottom); 4365 return bounds(adjustedLeft, adjustedTop, adjustedRight - adjustedLeft, adjustedBottom - adjustedTop); 4366 }; 4367 4368 /* 4369 Layout for menus and inline context dialogs; 4370 Either above or below. Never left or right.
4371 Aligned to the left or right of the anchor as appropriate. 4372 */ 4373 const labelPrefix$2 = 'layout'; 4374 // display element to the right, left edge against the anchor 4375 const eastX$1 = (anchor) => anchor.x; 4376 // element centre aligned horizontally with the anchor 4377 const middleX$1 = (anchor, element) => anchor.x + (anchor.width / 2) - (element.width / 2); 4378 // display element to the left, right edge against the right of the anchor 4379 const westX$1 = (anchor, element) => anchor.x + anchor.width - element.width; 4380 // display element above, bottom edge against the top of the anchor 4381 const northY$2 = (anchor, element) => anchor.y - element.height; 4382 // display element below, top edge against the bottom of the anchor 4383 const southY$2 = (anchor) => anchor.y + anchor.height; 4384 // display element below, top edge against the bottom of the anchor 4385 const centreY$1 = (anchor, element) => anchor.y + (anchor.height / 2) - (element.height / 2); 4386 const eastEdgeX$1 = (anchor) => anchor.x + anchor.width; 4387 const westEdgeX$1 = (anchor, element) => anchor.x - element.width; 4388 const southeast$2 = (anchor, element, bubbles) => nu$5(eastX$1(anchor), southY$2(anchor), bubbles.southeast(), southeast$3(), "southeast" /* Placement.Southeast */, boundsRestriction(anchor, { left: 1 /* AnchorBoxBounds.LeftEdge */, top: 3 /* AnchorBoxBounds.BottomEdge */ }), labelPrefix$2); 4389 const southwest$2 = (anchor, element, bubbles) => nu$5(westX$1(anchor, element), southY$2(anchor), bubbles.southwest(), southwest$3(), "southwest" /* Placement.Southwest */, boundsRestriction(anchor, { right: 0 /* AnchorBoxBounds.RightEdge */, top: 3 /* AnchorBoxBounds.BottomEdge */ }), labelPrefix$2); 4390 const northeast$2 = (anchor, element, bubbles) => nu$5(eastX$1(anchor), northY$2(anchor, element), bubbles.northeast(), northeast$3(), "northeast" /* Placement.Northeast */, boundsRestriction(anchor, { left: 1 /* AnchorBoxBounds.LeftEdge */, bottom: 2 /* AnchorBoxBounds.TopEdge */ }), labelPrefix$2); 4391 const northwest$2 = (anchor, element, bubbles) => nu$5(westX$1(anchor, element), northY$2(anchor, element), bubbles.northwest(), northwest$3(), "northwest" /* Placement.Northwest */, boundsRestriction(anchor, { right: 0 /* AnchorBoxBounds.RightEdge */, bottom: 2 /* AnchorBoxBounds.TopEdge */ }), labelPrefix$2); 4392 const north$2 = (anchor, element, bubbles) => nu$5(middleX$1(anchor, element), northY$2(anchor, element), bubbles.north(), north$3(), "north" /* Placement.North */, boundsRestriction(anchor, { bottom: 2 /* AnchorBoxBounds.TopEdge */ }), labelPrefix$2); 4393 const south$2 = (anchor, element, bubbles) => nu$5(middleX$1(anchor, element), southY$2(anchor), bubbles.south(), south$3(), "south" /* Placement.South */, boundsRestriction(anchor, { top: 3 /* AnchorBoxBounds.BottomEdge */ }), labelPrefix$2); 4394 const east$2 = (anchor, element, bubbles) => nu$5(eastEdgeX$1(anchor), centreY$1(anchor, element), bubbles.east(), east$3(), "east" /* Placement.East */, boundsRestriction(anchor, { left: 0 /* AnchorBoxBounds.RightEdge */ }), labelPrefix$2); 4395 const west$2 = (anchor, element, bubbles) => nu$5(westEdgeX$1(anchor, element), centreY$1(anchor, element), bubbles.west(), west$3(), "west" /* Placement.West */, boundsRestriction(anchor, { right: 1 /* AnchorBoxBounds.LeftEdge */ }), labelPrefix$2); 4396 const all$2 = () => [southeast$2, southwest$2, northeast$2, northwest$2, south$2, north$2, east$2, west$2]; 4397 const allRtl$1 = () => [southwest$2, southeast$2, northwest$2, northeast$2, south$2, north$2, east$2, west$2]; 4398 const aboveOrBelow = () => [northeast$2, northwest$2, southeast$2, southwest$2, north$2, south$2]; 4399 const aboveOrBelowRtl = () => [northwest$2, northeast$2, southwest$2, southeast$2, north$2, south$2]; 4400 const belowOrAbove = () => [southeast$2, southwest$2, northeast$2, northwest$2, south$2, north$2]; 4401 const belowOrAboveRtl = () => [southwest$2, southeast$2, northwest$2, northeast$2, south$2, north$2]; 4402 4403 const placementAttribute = 'data-alloy-placement'; 4404 const setPlacement$1 = (element, placement) => { 4405 set$9(element, placementAttribute, placement); 4406 }; 4407 const getPlacement = (element) => getOpt(element, placementAttribute); 4408 const reset$2 = (element) => remove$8(element, placementAttribute); 4409 4410 /*
4411 Layouts for things that overlay over the anchor element/box. These are designed to mirror 4412 the `Layout` logic. 4413 4414 As an example `Layout.north` will appear horizontally centered above the anchor, whereas 4415 `LayoutInset.north` will appear horizontally centered overlapping the top of the anchor. 4416 */ 4417 const labelPrefix$1 = 'layout-inset'; 4418 // returns left edge of anchor - used to display element to the left, left edge against the anchor 4419 const westEdgeX = (anchor) => anchor.x; 4420 // returns middle of anchor minus half the element width - used to horizontally centre element to the anchor 4421 const middleX = (anchor, element) => anchor.x + (anchor.width / 2) - (element.width / 2); 4422 // returns right edge of anchor minus element width - used to display element to the right, right edge against the anchor 4423 const eastEdgeX = (anchor, element) => anchor.x + anchor.width - element.width; 4424 // returns top edge - used to display element to the top, top edge against the anchor 4425 const northY$1 = (anchor) => anchor.y; 4426 // returns bottom edge minus element height - used to display element at the bottom, bottom edge against the anchor 4427 const southY$1 = (anchor, element) => anchor.y + anchor.height - element.height; 4428 // returns centre of anchor minus half the element height - used to vertically centre element to the anchor 4429 const centreY = (anchor, element) => anchor.y + (anchor.height / 2) - (element.height / 2); 4430 // positions element relative to the bottom right of the anchor 4431 const southwest$1 = (anchor, element, bubbles) => nu$5(eastEdgeX(anchor, element), southY$1(anchor, element), bubbles.insetSouthwest(), northwest$3(), "southwest" /* Placement.Southwest */, boundsRestriction(anchor, { right: 0 /* AnchorBoxBounds.RightEdge */, bottom: 3 /* AnchorBoxBounds.BottomEdge */ }), labelPrefix$1); 4432 // positions element relative to the bottom left of the anchor 4433 const southeast$1 = (anchor, element, bubbles) => nu$5(westEdgeX(anchor), southY$1(anchor, element), bubbles.insetSoutheast(), northeast$3(), "southeast" /* Placement.Southeast */, boundsRestriction(anchor, { left: 1 /* AnchorBoxBounds.LeftEdge */, bottom: 3 /* AnchorBoxBounds.BottomEdge */ }), labelPrefix$1); 4434 // positions element relative to the top right of the anchor 4435 const northwest$1 = (anchor, element, bubbles) => nu$5(eastEdgeX(anchor, element), northY$1(anchor), bubbles.insetNorthwest(), southwest$3(), "northwest" /* Placement.Northwest */, boundsRestriction(anchor, { right: 0 /* AnchorBoxBounds.RightEdge */, top: 2 /* AnchorBoxBounds.TopEdge */ }), labelPrefix$1); 4436 // positions element relative to the top left of the anchor 4437 const northeast$1 = (anchor, element, bubbles) => nu$5(westEdgeX(anchor), northY$1(anchor), bubbles.insetNortheast(), southeast$3(), "northeast" /* Placement.Northeast */, boundsRestriction(anchor, { left: 1 /* AnchorBoxBounds.LeftEdge */, top: 2 /* AnchorBoxBounds.TopEdge */ }), labelPrefix$1); 4438 // positions element relative to the top of the anchor, horizontally centered 4439 const north$1 = (anchor, element, bubbles) => nu$5(middleX(anchor, element), northY$1(anchor), bubbles.insetNorth(), south$3(), "north" /* Placement.North */, boundsRestriction(anchor, { top: 2 /* AnchorBoxBounds.TopEdge */ }), labelPrefix$1); 4440 // positions element relative to the bottom of the anchor, horizontally centered 4441 const south$1 = (anchor, element, bubbles) => nu$5(middleX(anchor, element), southY$1(anchor, element), bubbles.insetSouth(), north$3(), "south" /* Placement.South */, boundsRestriction(anchor, { bottom: 3 /* AnchorBoxBounds.BottomEdge */ }), labelPrefix$1); 4442 // positions element with the right edge against the anchor, vertically centered 4443 const east$1 = (anchor, element, bubbles) => nu$5(eastEdgeX(anchor, element), centreY(anchor, element), bubbles.insetEast(), west$3(), "east" /* Placement.East */, boundsRestriction(anchor, { right: 0 /* AnchorBoxBounds.RightEdge */ }), labelPrefix$1); 4444 // positions element with the left each against the anchor, vertically centered 4445 const west$1 = (anchor, element, bubbles) => nu$5(westEdgeX(anchor), centreY(anchor, element), bubbles.insetWest(), east$3(), "west" /* Placement.West */, boundsRestriction(anchor, { left: 1 /* AnchorBoxBounds.LeftEdge */ }), labelPrefix$1); 4446 const lookupPreserveLayout = (lastPlacement) => { 4447 switch (lastPlacement) { 4448 case "north" /* Placement.North */: 4449 return north$1; 4450 case "northeast" /* Placement.Northeast */: 4451 return northeast$1; 4452 case "northwest" /* Placement.Northwest */: 4453 return northwest$1; 4454 case "south" /* Placement.South */: 4455 return south$1; 4456 case "southeast" /* Placement.Southeast */: 4457 return southeast$1; 4458 case "southwest" /* Placement.Southwest */: 4459 return southwest$1; 4460 case "east" /* Placement.East */: 4461 return east$1; 4462 case "west" /* Placement.West */: 4463 return west$1; 4464 } 4465 }; 4466 const preserve$1 = (anchor, element, bubbles, placee, bounds) => { 4467 const layout = getPlacement(placee).map(lookupPreserveLayout).getOr(north$1); 4468 return layout(anchor, element, bubbles, placee, bounds); 4469 }; 4470 const lookupFlippedLayout = (lastPlacement) => { 4471 switch (lastPlacement) { 4472 case "north" /* Placement.North */: 4473 return south$1; 4474 case "northeast" /* Placement.Northeast */: 4475 return southeast$1; 4476 case "northwest" /* Placement.Northwest */: 4477 return southwest$1; 4478 case "south" /* Placement.South */: 4479 return north$1; 4480 case "southeast" /* Placement.Southeast */: 4481 return northeast$1; 4482 case "southwest" /* Placement.Southwest */: 4483 return northwest$1; 4484 case "east" /* Placement.East */: 4485 return west$1; 4486 case "west" /* Placement.West */: 4487 return east$1; 4488 } 4489 }; 4490 const flip = (anchor, element, bubbles, placee, bounds) => { 4491 const layout = getPlacement(placee).map(lookupFlippedLayout).getOr(north$1); 4492 return layout(anchor, element, bubbles, placee, bounds); 4493 }; 4494 4495 // applies the max-height as determined by Bounder
vendor: 19,065 bytes, lines 4496-5011
4496 const setMaxHeight = (element, maxHeight) => { 4497 setMax$1(element, Math.floor(maxHeight)); 4498 }; 4499 // adds both max-height and overflow to constrain it 4500 const anchored = constant$1((element, available) => { 4501 setMaxHeight(element, available); 4502 setAll(element, { 4503 'overflow-x': 'hidden', 4504 'overflow-y': 'auto' 4505 }); 4506 }); 4507 /* 4508 * This adds max height, but not overflow - the effect of this is that elements can grow beyond the max height, 4509 * but if they run off the top they're pushed down. 4510 * 4511 * If the element expands below the screen height it will be cut off, but we were already doing that. 4512 */ 4513 const expandable$1 = constant$1((element, available) => { 4514 setMaxHeight(element, available); 4515 }); 4516 4517 // applies the max-width as determined by Bounder 4518 const expandable = constant$1((element, available) => { 4519 setMax(element, Math.floor(available)); 4520 }); 4521 4522 var AttributeValue; 4523 (function (AttributeValue) { 4524 AttributeValue["TopToBottom"] = "toptobottom"; 4525 AttributeValue["BottomToTop"] = "bottomtotop"; 4526 })(AttributeValue || (AttributeValue = {})); 4527 const Attribute = 'data-alloy-vertical-dir'; 4528 const isBottomToTopDir = (el) => closest$2(el, (current) => isElement$1(current) && get$g(current, 'data-alloy-vertical-dir') === AttributeValue.BottomToTop); 4529 4530 var HighlightOnOpen; 4531 (function (HighlightOnOpen) { 4532 HighlightOnOpen[HighlightOnOpen["HighlightMenuAndItem"] = 0] = "HighlightMenuAndItem"; 4533 HighlightOnOpen[HighlightOnOpen["HighlightJustMenu"] = 1] = "HighlightJustMenu"; 4534 HighlightOnOpen[HighlightOnOpen["HighlightNone"] = 2] = "HighlightNone"; 4535 })(HighlightOnOpen || (HighlightOnOpen = {})); 4536 4537 const NoState = { 4538 init: () => nu$4({ 4539 readState: constant$1('No State required') 4540 }) 4541 }; 4542 const nu$4 = (spec) => spec; 4543 4544 const defaultEventHandler = { 4545 can: always, 4546 abort: never, 4547 run: noop 4548 }; 4549 const nu$3 = (parts) => { 4550 if (!hasNonNullableKey(parts, 'can') && !hasNonNullableKey(parts, 'abort') && !hasNonNullableKey(parts, 'run')) { 4551 throw new Error('EventHandler defined by: ' + JSON.stringify(parts, null, 2) + ' does not have can, abort, or run!'); 4552 } 4553 return { 4554 ...defaultEventHandler, 4555 ...parts 4556 }; 4557 }; 4558 const all$1 = (handlers, f) => (...args) => foldl(handlers, (acc, handler) => acc && f(handler).apply(undefined, args), true); 4559 const any = (handlers, f) => (...args) => foldl(handlers, (acc, handler) => acc || f(handler).apply(undefined, args), false); 4560 const read$1 = (handler) => isFunction(handler) ? { 4561 can: always, 4562 abort: never, 4563 run: handler 4564 } : handler; 4565 const fuse$1 = (handlers) => { 4566 const can = all$1(handlers, (handler) => handler.can); 4567 const abort = any(handlers, (handler) => handler.abort); 4568 const run = (...args) => { 4569 each$1(handlers, (handler) => { 4570 // ASSUMPTION: Return value is unimportant. 4571 handler.run.apply(undefined, args); 4572 }); 4573 }; 4574 return { 4575 can, 4576 abort, 4577 run 4578 }; 4579 }; 4580 4581 const emit = (component, event) => { 4582 dispatchWith(component, component.element, event, {}); 4583 }; 4584 const emitWith = (component, event, properties) => { 4585 dispatchWith(component, component.element, event, properties); 4586 }; 4587 const emitExecute = (component) => { 4588 emit(component, execute$5()); 4589 }; 4590 const dispatch = (component, target, event) => { 4591 dispatchWith(component, target, event, {}); 4592 }; 4593 const dispatchWith = (component, target, event, properties) => { 4594 // NOTE: The order of spreading here means that it will maintain any target that 4595 // exists in the current properties. Because this function has been used for situations where 4596 // properties is either an emulated SugarEvent with no target (see TouchEvent) or 4597 // for emitting custom events that have no target, this likely hasn't been a problem. 4598 // But until we verify that nothing is relying on this ordering, there is an alternate 4599 // function below called retargetAndDispatchWith, which spreads in the other direction. 4600 const data = { 4601 target, 4602 ...properties 4603 }; 4604 component.getSystem().triggerEvent(event, target, data); 4605 }; 4606 const retargetAndDispatchWith = (component, target, eventName, properties) => { 4607 // This is essentially the same as dispatchWith, except the spreading order 4608 // means that it clobbers anything in the nativeEvent with "target". It also 4609 // expects what is being passed in to be a real sugar event, not just a data 4610 // blob 4611 const data = { 4612 ...properties, 4613 target 4614 }; 4615 component.getSystem().triggerEvent(eventName, target, data); 4616 }; 4617 const dispatchEvent = (component, target, event, simulatedEvent) => { 4618 component.getSystem().triggerEvent(event, target, simulatedEvent.event); 4619 }; 4620 4621 const derive$2 = (configs) => wrapAll(configs); 4622 // const combine = (configs...); 4623 const abort = (name, predicate) => { 4624 return { 4625 key: name, 4626 value: nu$3({ 4627 abort: predicate 4628 }) 4629 }; 4630 }; 4631 const can = (name, predicate) => { 4632 return { 4633 key: name, 4634 value: nu$3({ 4635 can: predicate 4636 }) 4637 }; 4638 }; 4639 const preventDefault = (name) => { 4640 return { 4641 key: name, 4642 value: nu$3({ 4643 run: (component, simulatedEvent) => { 4644 simulatedEvent.event.prevent(); 4645 } 4646 }) 4647 }; 4648 }; 4649 const run$1 = (name, handler) => { 4650 return { 4651 key: name, 4652 value: nu$3({ 4653 run: handler 4654 }) 4655 }; 4656 }; 4657 // Extra can be used when your handler needs more context, and is declared in one spot 4658 // It's really just convenient partial application. 4659 const runActionExtra = (name, action, extra) => { 4660 return { 4661 key: name, 4662 value: nu$3({ 4663 run: (component, simulatedEvent) => { 4664 action.apply(undefined, [component, simulatedEvent].concat(extra)); 4665 } 4666 }) 4667 }; 4668 }; 4669 const runOnName = (name) => { 4670 return (handler) => run$1(name, handler); 4671 }; 4672 const runOnSourceName = (name) => { 4673 return (handler) => ({ 4674 key: name, 4675 value: nu$3({ 4676 run: (component, simulatedEvent) => { 4677 if (isSource(component, simulatedEvent)) { 4678 handler(component, simulatedEvent); 4679 } 4680 } 4681 }) 4682 }); 4683 }; 4684 const redirectToUid = (name, uid) => { 4685 return run$1(name, (component, simulatedEvent) => { 4686 component.getSystem().getByUid(uid).each((redirectee) => { 4687 dispatchEvent(redirectee, redirectee.element, name, simulatedEvent); 4688 }); 4689 }); 4690 }; 4691 const redirectToPart = (name, detail, partName) => { 4692 const uid = detail.partUids[partName]; 4693 return redirectToUid(name, uid); 4694 }; 4695 const runWithTarget = (name, f) => { 4696 return run$1(name, (component, simulatedEvent) => { 4697 const ev = simulatedEvent.event; 4698 const target = component.getSystem().getByDom(ev.target).getOrThunk( 4699 // If we don't find an alloy component for the target, I guess we go up the tree 4700 // until we find an alloy component? Performance concern? 4701 // TODO: Write tests for this. 4702 () => { 4703 const closest$1 = closest(ev.target, (el) => component.getSystem().getByDom(el).toOptional(), never); 4704 // If we still found nothing ... fire on component itself; 4705 return closest$1.getOr(component); 4706 }); 4707 f(component, target, simulatedEvent); 4708 }); 4709 }; 4710 const cutter = (name) => { 4711 return run$1(name, (component, simulatedEvent) => { 4712 simulatedEvent.cut(); 4713 }); 4714 }; 4715 const stopper = (name) => { 4716 return run$1(name, (component, simulatedEvent) => { 4717 simulatedEvent.stop(); 4718 }); 4719 }; 4720 const runOnSource = (name, f) => { 4721 return runOnSourceName(name)(f); 4722 }; 4723 const runOnAttached = runOnSourceName(attachedToDom()); 4724 const runOnDetached = runOnSourceName(detachedFromDom()); 4725 const runOnInit = runOnSourceName(systemInit()); 4726 const runOnExecute$1 = runOnName(execute$5()); 4727 4728 // Maybe we'll need to allow add/remove 4729 const nu$2 = (s) => ({ 4730 classes: isUndefined(s.classes) ? [] : s.classes, 4731 attributes: isUndefined(s.attributes) ? {} : s.attributes, 4732 styles: isUndefined(s.styles) ? {} : s.styles 4733 }); 4734 const merge = (defnA, mod) => ({ 4735 ...defnA, 4736 attributes: { ...defnA.attributes, ...mod.attributes }, 4737 styles: { ...defnA.styles, ...mod.styles }, 4738 classes: defnA.classes.concat(mod.classes) 4739 }); 4740 4741 const executeEvent = (bConfig, bState, executor) => runOnExecute$1((component) => { 4742 executor(component, bConfig, bState); 4743 }); 4744 const loadEvent = (bConfig, bState, f) => runOnInit((component, _simulatedEvent) => { 4745 f(component, bConfig, bState); 4746 }); 4747 const create$4 = (schema, name, active, apis, extra, state) => { 4748 const configSchema = objOfOnly(schema); 4749 const schemaSchema = optionObjOf(name, [ 4750 optionObjOfOnly('config', schema) 4751 ]); 4752 return doCreate(configSchema, schemaSchema, name, active, apis, extra, state); 4753 }; 4754 const createModes$1 = (modes, name, active, apis, extra, state) => { 4755 const configSchema = modes; 4756 const schemaSchema = optionObjOf(name, [ 4757 optionOf('config', modes) 4758 ]); 4759 return doCreate(configSchema, schemaSchema, name, active, apis, extra, state); 4760 }; 4761 const wrapApi = (bName, apiFunction, apiName) => { 4762 const f = (component, ...rest) => { 4763 const args = [component].concat(rest); 4764 return component.config({ 4765 name: constant$1(bName) 4766 }).fold(() => { 4767 throw new Error('We could not find any behaviour configuration for: ' + bName + '. Using API: ' + apiName); 4768 }, (info) => { 4769 const rest = Array.prototype.slice.call(args, 1); 4770 return apiFunction.apply(undefined, [component, info.config, info.state].concat(rest)); 4771 }); 4772 }; 4773 return markAsBehaviourApi(f, apiName, apiFunction); 4774 }; 4775 // I think the "revoke" idea is fragile at best. 4776 const revokeBehaviour = (name) => ({ 4777 key: name, 4778 value: undefined 4779 }); 4780 const doCreate = (configSchema, schemaSchema, name, active, apis, extra, state) => { 4781 const getConfig = (info) => hasNonNullableKey(info, name) ? info[name]() : Optional.none(); 4782 const wrappedApis = map$1(apis, (apiF, apiName) => wrapApi(name, apiF, apiName)); 4783 const wrappedExtra = map$1(extra, (extraF, extraName) => markAsExtraApi(extraF, extraName)); 4784 const me = { 4785 ...wrappedExtra, 4786 ...wrappedApis, 4787 revoke: curry(revokeBehaviour, name), 4788 config: (spec) => { 4789 const prepared = asRawOrDie$1(name + '-config', configSchema, spec); 4790 return { 4791 key: name, 4792 value: { 4793 config: prepared, 4794 me, 4795 configAsRaw: cached(() => asRawOrDie$1(name + '-config', configSchema, spec)), 4796 initialConfig: spec, 4797 state 4798 } 4799 }; 4800 }, 4801 schema: constant$1(schemaSchema), 4802 exhibit: (info, base) => { 4803 return lift2(getConfig(info), get$h(active, 'exhibit'), (behaviourInfo, exhibitor) => { 4804 return exhibitor(base, behaviourInfo.config, behaviourInfo.state); 4805 }).getOrThunk(() => nu$2({})); 4806 }, 4807 name: constant$1(name), 4808 handlers: (info) => { 4809 return getConfig(info).map((behaviourInfo) => { 4810 const getEvents = get$h(active, 'events').getOr(() => ({})); 4811 return getEvents(behaviourInfo.config, behaviourInfo.state); 4812 }).getOr({}); 4813 } 4814 }; 4815 return me; 4816 }; 4817 4818 const derive$1 = (capabilities) => wrapAll(capabilities); 4819 const simpleSchema = objOfOnly([ 4820 required$1('fields'), 4821 required$1('name'), 4822 defaulted('active', {}), 4823 defaulted('apis', {}), 4824 defaulted('state', NoState), 4825 defaulted('extra', {}) 4826 ]); 4827 const create$3 = (data) => { 4828 const value = asRawOrDie$1('Creating behaviour: ' + data.name, simpleSchema, data); 4829 return create$4(value.fields, value.name, value.active, value.apis, value.extra, value.state); 4830 }; 4831 const modeSchema = objOfOnly([ 4832 required$1('branchKey'), 4833 required$1('branches'), 4834 required$1('name'), 4835 defaulted('active', {}), 4836 defaulted('apis', {}), 4837 defaulted('state', NoState), 4838 defaulted('extra', {}) 4839 ]); 4840 const createModes = (data) => { 4841 const value = asRawOrDie$1('Creating behaviour: ' + data.name, modeSchema, data); 4842 return createModes$1(choose$1(value.branchKey, value.branches), value.name, value.active, value.apis, value.extra, value.state); 4843 }; 4844 const revoke = constant$1(undefined); 4845 4846 // AlloyEventKeyAndHandler type argument needs to be any here to satisfy an array of handlers 4847 // where each item can be any subtype of EventFormat we can't use <T extends EventFormat> since 4848 // then each item would have to be the same type 4849 const events$j = (name, eventHandlers) => { 4850 const events = derive$2(eventHandlers); 4851 return create$3({ 4852 fields: [ 4853 required$1('enabled') 4854 ], 4855 name, 4856 active: { 4857 events: constant$1(events) 4858 } 4859 }); 4860 }; 4861 const config = (name, eventHandlers) => { 4862 const me = events$j(name, eventHandlers); 4863 return { 4864 key: name, 4865 value: { 4866 config: {}, 4867 me, 4868 configAsRaw: constant$1({}), 4869 initialConfig: {}, 4870 state: NoState 4871 } 4872 }; 4873 }; 4874 4875 const SetupBehaviourCellState = (initialState) => { 4876 const init = () => { 4877 const cell = Cell(initialState); 4878 const get = () => cell.get(); 4879 const set = (newState) => cell.set(newState); 4880 const clear = () => cell.set(initialState); 4881 const readState = () => cell.get(); 4882 return { 4883 get, 4884 set, 4885 clear, 4886 readState 4887 }; 4888 }; 4889 return { 4890 init 4891 }; 4892 }; 4893 4894 const focus$2 = (component, focusConfig) => { 4895 if (!focusConfig.ignore) { 4896 focus$4(component.element); 4897 focusConfig.onFocus(component); 4898 } 4899 }; 4900 const blur = (component, focusConfig) => { 4901 if (!focusConfig.ignore) { 4902 blur$1(component.element); 4903 } 4904 }; 4905 const isFocused = (component) => hasFocus(component.element); 4906 4907 var FocusApis = /*#__PURE__*/Object.freeze({ 4908 __proto__: null, 4909 focus: focus$2, 4910 blur: blur, 4911 isFocused: isFocused 4912 }); 4913 4914 // TODO: DomModification types 4915 const exhibit$6 = (base, focusConfig) => { 4916 const mod = focusConfig.ignore ? {} : { 4917 attributes: { 4918 tabindex: '-1' 4919 } 4920 }; 4921 return nu$2(mod); 4922 }; 4923 const events$i = (focusConfig) => derive$2([ 4924 run$1(focus$3(), (component, simulatedEvent) => { 4925 focus$2(component, focusConfig); 4926 simulatedEvent.stop(); 4927 }) 4928 ].concat(focusConfig.stopMousedown ? [ 4929 run$1(mousedown(), (_, simulatedEvent) => { 4930 // This setting is often used in tandem with ignoreFocus. Basically, if you 4931 // don't prevent default on a menu that has fake focus, then it can transfer 4932 // focus to the outer body when they click on it, which can break things 4933 // which dismiss on blur (e.g. typeahead) 4934 simulatedEvent.event.prevent(); 4935 }) 4936 ] : [])); 4937 4938 var ActiveFocus = /*#__PURE__*/Object.freeze({ 4939 __proto__: null, 4940 exhibit: exhibit$6, 4941 events: events$i 4942 }); 4943 4944 var FocusSchema = [ 4945 // TODO: Work out when we want to call this. Only when it is has changed? 4946 onHandler('onFocus'), 4947 defaulted('stopMousedown', false), 4948 defaulted('ignore', false) 4949 ]; 4950 4951 const Focusing = create$3({ 4952 fields: FocusSchema, 4953 name: 'focusing', 4954 active: ActiveFocus, 4955 apis: FocusApis 4956 // Consider adding isFocused an an extra 4957 }); 4958 4959 const BACKSPACE = [8]; 4960 const TAB = [9]; 4961 const ENTER = [13]; 4962 const ESCAPE = [27]; 4963 const SPACE = [32]; 4964 const LEFT = [37]; 4965 const UP = [38]; 4966 const RIGHT = [39]; 4967 const DOWN = [40]; 4968 4969 const closeTooltips = constant$1('tooltipping.close.all'); 4970 const dismissPopups = constant$1('dismiss.popups'); 4971 const repositionPopups = constant$1('reposition.popups'); 4972 const mouseReleased = constant$1('mouse.released'); 4973 4974 const cyclePrev = (values, index, predicate) => { 4975 const before = reverse(values.slice(0, index)); 4976 const after = reverse(values.slice(index + 1)); 4977 return find$5(before.concat(after), predicate); 4978 }; 4979 const tryPrev = (values, index, predicate) => { 4980 const before = reverse(values.slice(0, index)); 4981 return find$5(before, predicate); 4982 }; 4983 const cycleNext = (values, index, predicate) => { 4984 const before = values.slice(0, index); 4985 const after = values.slice(index + 1); 4986 return find$5(after.concat(before), predicate); 4987 }; 4988 const tryNext = (values, index, predicate) => { 4989 const after = values.slice(index + 1); 4990 return find$5(after, predicate); 4991 }; 4992 4993 const inSet = (keys) => (event) => { 4994 const raw = event.raw; 4995 return contains$2(keys, raw.which); 4996 }; 4997 const and = (preds) => (event) => forall(preds, (pred) => pred(event)); 4998 const isShift$1 = (event) => { 4999 const raw = event.raw; 5000 return raw.shiftKey === true; 5001 }; 5002 const isControl = (event) => { 5003 const raw = event.raw; 5004 return raw.ctrlKey === true; 5005 }; 5006 const isNotShift = not(isShift$1); 5007 5008 const rule = (matches, action) => ({ 5009 matches, 5010 classification: action 5011 });
5012 const choose = (transitions, event) => { 5013 const transition = find$5(transitions, (t) => t.matches(event)); 5014 return transition.map((t) => t.classification); 5015 }; 5016 5017 // THIS IS NOT API YET 5018 const dehighlightAllExcept = (component, hConfig, hState, skip) => { 5019 const highlighted = descendants(component.element, '.' + hConfig.highlightClass); 5020 each$1(highlighted, (h) => { 5021 // We don't want to dehighlight anything that should be skipped. 5022 // Generally, this is because we are about to highlight that thing. 5023 const shouldSkip = exists(skip, (skipComp) => eq(skipComp.element, h)); 5024 if (!shouldSkip) { 5025 remove$3(h, hConfig.highlightClass); 5026 component.getSystem().getByDom(h).each((target) => { 5027 hConfig.onDehighlight(component, target); 5028 emit(target, dehighlight$1()); 5029 }); 5030 } 5031 }); 5032 }; 5033 const dehighlightAll = (component, hConfig, hState) => dehighlightAllExcept(component, hConfig, hState, []); 5034 const dehighlight = (component, hConfig, hState, target) => { 5035 // Only act if it was highlighted. 5036 if (isHighlighted(component, hConfig, hState, target)) { 5037 remove$3(target.element, hConfig.highlightClass); 5038 hConfig.onDehighlight(component, target); 5039 emit(target, dehighlight$1()); 5040 } 5041 }; 5042 const highlight = (component, hConfig, hState, target) => { 5043 // If asked to highlight something, dehighlight everything else first except 5044 // for the new thing we are going to highlight. It's a rare case, but we don't 5045 // want to get an onDehighlight, onHighlight for the same item on a highlight call. 5046 // We also don't want to call onHighlight if it was already highlighted. 5047 // 5048 // Note, that there is an important distinction here: highlight is NOT a no-op 5049 // if target is already highlighted, because it will still dehighlight everything else. 5050 // However, it won't fire any onHighlight or onDehighlight handlers for the already 5051 // highlighted item. I'm not sure if this is behaviour we need to maintain, but it is now 5052 // tested. A simpler approach might just be to not do anything if it's already highlighted, 5053 // but that could leave us in an inconsistent state, where multiple items have highlights 5054 // even after a highlight call. This way, highlight validates the highlights in the 5055 // component, and ensures there is only one thing highlighted. 5056 dehighlightAllExcept(component, hConfig, hState, [target]); 5057 if (!isHighlighted(component, hConfig, hState, target)) { 5058 add$2(target.element, hConfig.highlightClass); 5059 hConfig.onHighlight(component, target); 5060 emit(target, highlight$1()); 5061 } 5062 }; 5063 const highlightFirst = (component, hConfig, hState) => { 5064 getFirst(component, hConfig).each((firstComp) => { 5065 highlight(component, hConfig, hState, firstComp); 5066 }); 5067 }; 5068 const highlightLast = (component, hConfig, hState) => { 5069 getLast(component, hConfig).each((lastComp) => { 5070 highlight(component, hConfig, hState, lastComp); 5071 }); 5072 }; 5073 const highlightAt = (component, hConfig, hState, index) => { 5074 getByIndex(component, hConfig, hState, index).fold((err) => { 5075 throw err; 5076 }, (firstComp) => { 5077 highlight(component, hConfig, hState, firstComp); 5078 }); 5079 }; 5080 const highlightBy = (component, hConfig, hState, predicate) => { 5081 const candidates = getCandidates(component, hConfig); 5082 const targetComp = find$5(candidates, predicate); 5083 targetComp.each((c) => { 5084 highlight(component, hConfig, hState, c); 5085 }); 5086 }; 5087 const isHighlighted = (component, hConfig, hState, queryTarget) => has(queryTarget.element, hConfig.highlightClass); 5088 const getHighlighted = (component, hConfig, _hState) => descendant(component.element, '.' + hConfig.highlightClass).bind((e) => component.getSystem().getByDom(e).toOptional()); 5089 const getByIndex = (component, hConfig, hState, index) => { 5090 const items = descendants(component.element, '.' + hConfig.itemClass); 5091 return Optional.from(items[index]).fold(() => Result.error(new Error('No element found with index ' + index)), component.getSystem().getByDom); 5092 }; 5093 const getFirst = (component, hConfig, _hState) =>
vendor: 5,457 bytes, lines 5093-5211
5093 descendant(component.element, '.' + hConfig.itemClass).bind((e) => component.getSystem().getByDom(e).toOptional()); 5094 const getLast = (component, hConfig, _hState) => { 5095 const items = descendants(component.element, '.' + hConfig.itemClass); 5096 const last = items.length > 0 ? Optional.some(items[items.length - 1]) : Optional.none(); 5097 return last.bind((c) => component.getSystem().getByDom(c).toOptional()); 5098 }; 5099 const getDelta$3 = (component, hConfig, hState, delta) => { 5100 const items = descendants(component.element, '.' + hConfig.itemClass); 5101 const current = findIndex$1(items, (item) => has(item, hConfig.highlightClass)); 5102 return current.bind((selected) => { 5103 const dest = cycleBy(selected, delta, 0, items.length - 1); 5104 return component.getSystem().getByDom(items[dest]).toOptional(); 5105 }); 5106 }; 5107 const getPrevious = (component, hConfig, hState) => getDelta$3(component, hConfig, hState, -1); 5108 const getNext = (component, hConfig, hState) => getDelta$3(component, hConfig, hState, +1); 5109 const getCandidates = (component, hConfig, _hState) => { 5110 const items = descendants(component.element, '.' + hConfig.itemClass); 5111 return cat(map$2(items, (i) => component.getSystem().getByDom(i).toOptional())); 5112 }; 5113 5114 var HighlightApis = /*#__PURE__*/Object.freeze({ 5115 __proto__: null, 5116 dehighlightAll: dehighlightAll, 5117 dehighlight: dehighlight, 5118 highlight: highlight, 5119 highlightFirst: highlightFirst, 5120 highlightLast: highlightLast, 5121 highlightAt: highlightAt, 5122 highlightBy: highlightBy, 5123 isHighlighted: isHighlighted, 5124 getHighlighted: getHighlighted, 5125 getFirst: getFirst, 5126 getLast: getLast, 5127 getPrevious: getPrevious, 5128 getNext: getNext, 5129 getCandidates: getCandidates 5130 }); 5131 5132 var HighlightSchema = [ 5133 required$1('highlightClass'), 5134 required$1('itemClass'), 5135 onHandler('onHighlight'), 5136 onHandler('onDehighlight') 5137 ]; 5138 5139 const Highlighting = create$3({ 5140 fields: HighlightSchema, 5141 name: 'highlighting', 5142 apis: HighlightApis 5143 }); 5144 5145 const reportFocusShifting = (component, prevFocus, newFocus) => { 5146 const noChange = prevFocus.exists((p) => newFocus.exists((n) => eq(n, p))); 5147 if (!noChange) { 5148 emitWith(component, focusShifted(), { 5149 prevFocus, 5150 newFocus 5151 }); 5152 } 5153 }; 5154 const dom$2 = () => { 5155 const get = (component) => search(component.element); 5156 const set = (component, focusee) => { 5157 const prevFocus = get(component); 5158 component.getSystem().triggerFocus(focusee, component.element); 5159 const newFocus = get(component); 5160 reportFocusShifting(component, prevFocus, newFocus); 5161 }; 5162 return { 5163 get, 5164 set 5165 }; 5166 }; 5167 const highlights = () => { 5168 const get = (component) => Highlighting.getHighlighted(component).map((item) => item.element); 5169 const set = (component, element) => { 5170 const prevFocus = get(component); 5171 component.getSystem().getByDom(element).fold(noop, (item) => { 5172 Highlighting.highlight(component, item); 5173 }); 5174 const newFocus = get(component); 5175 reportFocusShifting(component, prevFocus, newFocus); 5176 }; 5177 return { 5178 get, 5179 set 5180 }; 5181 }; 5182 5183 const typical = (infoSchema, stateInit, getKeydownRules, getKeyupRules, optFocusIn) => { 5184 const schema = () => infoSchema.concat([ 5185 defaulted('focusManager', dom$2()), 5186 defaultedOf('focusInside', 'onFocus', valueOf((val) => contains$2(['onFocus', 'onEnterOrSpace', 'onApi'], val) ? Result.value(val) : Result.error('Invalid value for focusInside'))), 5187 output$1('handler', me), 5188 output$1('state', stateInit), 5189 output$1('sendFocusIn', optFocusIn) 5190 ]); 5191 const processKey = (component, simulatedEvent, getRules, keyingConfig, keyingState) => { 5192 const rules = getRules(component, simulatedEvent, keyingConfig, keyingState); 5193 return choose(rules, simulatedEvent.event).bind((rule) => rule(component, simulatedEvent, keyingConfig, keyingState)); 5194 }; 5195 const toEvents = (keyingConfig, keyingState) => { 5196 const onFocusHandler = keyingConfig.focusInside !== FocusInsideModes.OnFocusMode 5197 ? Optional.none() 5198 : optFocusIn(keyingConfig).map((focusIn) => run$1(focus$3(), (component, simulatedEvent) => { 5199 focusIn(component, keyingConfig, keyingState); 5200 simulatedEvent.stop(); 5201 })); 5202 // On enter or space on root element, if using EnterOrSpace focus mode, fire a focusIn on the component 5203 const tryGoInsideComponent = (component, simulatedEvent) => { 5204 const isEnterOrSpace = inSet(SPACE.concat(ENTER))(simulatedEvent.event); 5205 if (keyingConfig.focusInside === FocusInsideModes.OnEnterOrSpaceMode && isEnterOrSpace && isSource(component, simulatedEvent)) { 5206 optFocusIn(keyingConfig).each((focusIn) => { 5207 focusIn(component, keyingConfig, keyingState); 5208 simulatedEvent.stop(); 5209 }); 5210 } 5211 };
5212 const keyboardEvents = [ 5213 run$1(keydown(), (component, simulatedEvent) => { 5214 processKey(component, simulatedEvent, getKeydownRules, keyingConfig, keyingState).fold(() => { 5215 // Key wasn't handled ... so see if we should enter into the component (focusIn) 5216 tryGoInsideComponent(component, simulatedEvent); 5217 }, (_) => { 5218 simulatedEvent.stop(); 5219 }); 5220 }), 5221 run$1(keyup(), (component, simulatedEvent) => { 5222 processKey(component, simulatedEvent, getKeyupRules, keyingConfig, keyingState).each((_) => { 5223 simulatedEvent.stop(); 5224 }); 5225 }) 5226 ]; 5227 return derive$2(onFocusHandler.toArray().concat(keyboardEvents)); 5228 }; 5229 const me = { 5230 schema, 5231 processKey, 5232 toEvents 5233 }; 5234 return me; 5235 }; 5236 5237 const create$2 = (cyclicField) => { 5238 const schema = [ 5239 option$3('onEscape'), 5240 option$3('onEnter'), 5241 defaulted('selector', '[data-alloy-tabstop="true"]:not(:disabled)'), 5242 defaulted('firstTabstop', 0), 5243 defaulted('useTabstopAt', always), 5244 // Maybe later we should just expose isVisible 5245 option$3('visibilitySelector') 5246 ].concat([ 5247 cyclicField 5248 ]); 5249 // TODO: Test this 5250 const isVisible = (tabbingConfig, element) => { 5251 const target = tabbingConfig.visibilitySelector 5252 .bind((sel) => closest$3(element, sel)) 5253 .getOr(element); 5254 // NOTE: We can't use Visibility.isVisible, because the toolbar has width when it has closed, just not height. 5255 return get$d(target) > 0; 5256 }; 5257 const findInitial = (component, tabbingConfig) => { 5258 const tabstops = descendants(component.element, tabbingConfig.selector); 5259 const visibles = filter$2(tabstops, (elem) => isVisible(tabbingConfig, elem)); 5260 return Optional.from(visibles[tabbingConfig.firstTabstop]); 5261 }; 5262 const findCurrent = (component, tabbingConfig) => tabbingConfig.focusManager.get(component) 5263 .bind((elem) => closest$3(elem, tabbingConfig.selector)); 5264 const isTabstop = (tabbingConfig, element) => isVisible(tabbingConfig, element) && tabbingConfig.useTabstopAt(element); 5265 // Fire an alloy focus on the first visible element that matches the selector 5266 const focusIn = (component, tabbingConfig, _tabbingState) => { 5267 findInitial(component, tabbingConfig).each((target) => { 5268 tabbingConfig.focusManager.set(component, target); 5269 }); 5270 }; 5271 const goFromTabstop = (component, tabstops, stopIndex, tabbingConfig, cycle) => cycle(tabstops, stopIndex, (elem) => isTabstop(tabbingConfig, elem)) 5272 .fold( 5273 // Even if there is only one, still capture the event if cycling 5274 () => tabbingConfig.cyclic ? Optional.some(true) : Optional.none(), (target) => { 5275 tabbingConfig.focusManager.set(component, target); 5276 // Kill the event 5277 return Optional.some(true); 5278 }); 5279 const go = (component, _simulatedEvent, tabbingConfig, cycle) => { 5280 // 1. Find our current tabstop 5281 // 2. Find the index of that tabstop 5282 // 3. Cycle the tabstop 5283 // 4. Fire alloy focus on the resultant tabstop 5284 const tabstops = filter$2(descendants(component.element, tabbingConfig.selector), (element) => isVisible(tabbingConfig, element)); 5285 return findCurrent(component, tabbingConfig).bind((tabstop) => { 5286 // focused component 5287 const optStopIndex = findIndex$1(tabstops, curry(eq, tabstop)); 5288 return optStopIndex.bind((stopIndex) => goFromTabstop(component, tabstops, stopIndex, tabbingConfig, cycle)); 5289 }); 5290 }; 5291 const goBackwards = (component, simulatedEvent, tabbingConfig) => { 5292 const navigate = tabbingConfig.cyclic ? cyclePrev : tryPrev; 5293 return go(component, simulatedEvent, tabbingConfig, navigate); 5294 }; 5295 const goForwards = (component, simulatedEvent, tabbingConfig) => { 5296 const navigate = tabbingConfig.cyclic ? cycleNext : tryNext; 5297 return go(component, simulatedEvent, tabbingConfig, navigate); 5298 }; 5299 const isFirstChild = (elem) => parentNode(elem).bind(firstChild).exists((child) => eq(child, elem)); 5300 const goFromPseudoTabstop = (component, simulatedEvent, tabbingConfig) => findCurrent(component, tabbingConfig).filter((elem) => !tabbingConfig.useTabstopAt(elem)) 5301 .bind((elem) => (isFirstChild(elem) ? goBackwards : goForwards)(component, simulatedEvent, tabbingConfig)); 5302 const execute = (component, simulatedEvent, tabbingConfig) => tabbingConfig.onEnter.bind((f) => f(component, simulatedEvent)); 5303 const exit = (component, simulatedEvent, tabbingConfig) => { 5304 component.getSystem().broadcastOn([closeTooltips()], { 5305 closedTooltip: () => { 5306 simulatedEvent.stop(); 5307 } 5308 }); 5309 if (!simulatedEvent.isStopped()) { 5310 return tabbingConfig.onEscape.bind((f) => f(component, simulatedEvent)); 5311 } 5312 else { 5313 return Optional.none(); 5314 } 5315 }; 5316 const getKeydownRules = constant$1([ 5317 rule(and([isShift$1, inSet(TAB)]), goBackwards), 5318 rule(inSet(TAB), goForwards), 5319 rule(and([isNotShift, inSet(ENTER)]), execute) 5320 ]); 5321 const getKeyupRules = constant$1([ 5322 rule(inSet(ESCAPE), exit), 5323 rule(inSet(TAB), goFromPseudoTabstop), 5324 ]);
vendor: 4,830 bytes, lines 5325-5429
5325 return typical(schema, NoState.init, getKeydownRules, getKeyupRules, () => Optional.some(focusIn)); 5326 }; 5327 5328 var AcyclicType = create$2(customField('cyclic', never)); 5329 5330 var CyclicType = create$2(customField('cyclic', always)); 5331 5332 const inside = (target) => ((isTag('input')(target) && get$g(target, 'type') !== 'radio') || 5333 isTag('textarea')(target)); 5334 5335 const doDefaultExecute = (component, _simulatedEvent, focused) => { 5336 // Note, we use to pass through simulatedEvent here and make target: component. This simplification 5337 // may be a problem 5338 dispatch(component, focused, execute$5()); 5339 return Optional.some(true); 5340 }; 5341 const defaultExecute = (component, simulatedEvent, focused) => { 5342 const isComplex = inside(focused) && inSet(SPACE)(simulatedEvent.event); 5343 return isComplex ? Optional.none() : doDefaultExecute(component, simulatedEvent, focused); 5344 }; 5345 // On Firefox, pressing space fires a click event if the element maintains focus and fires a keyup. This 5346 // stops the keyup, which should stop the click. We might want to make this only work for buttons and Firefox etc, 5347 // but at this stage it's cleaner to just always do it. It makes sense that Keying that handles space should handle 5348 // keyup also. This does make the name confusing, though. 5349 const stopEventForFirefox = (_component, _simulatedEvent) => Optional.some(true); 5350 5351 const schema$z = [ 5352 defaulted('execute', defaultExecute), 5353 defaulted('useSpace', false), 5354 defaulted('useEnter', true), 5355 defaulted('useControlEnter', false), 5356 defaulted('useDown', false) 5357 ]; 5358 const execute$4 = (component, simulatedEvent, executeConfig) => executeConfig.execute(component, simulatedEvent, component.element); 5359 const getKeydownRules$5 = (component, _simulatedEvent, executeConfig, _executeState) => { 5360 const spaceExec = executeConfig.useSpace && !inside(component.element) ? SPACE : []; 5361 const enterExec = executeConfig.useEnter ? ENTER : []; 5362 const downExec = executeConfig.useDown ? DOWN : []; 5363 const execKeys = spaceExec.concat(enterExec).concat(downExec); 5364 return [ 5365 rule(inSet(execKeys), execute$4) 5366 ].concat(executeConfig.useControlEnter ? [ 5367 rule(and([isControl, inSet(ENTER)]), execute$4) 5368 ] : []); 5369 }; 5370 const getKeyupRules$5 = (component, _simulatedEvent, executeConfig, _executeState) => executeConfig.useSpace && !inside(component.element) ? 5371 [rule(inSet(SPACE), stopEventForFirefox)] : 5372 []; 5373 var ExecutionType = typical(schema$z, NoState.init, getKeydownRules$5, getKeyupRules$5, () => Optional.none()); 5374 5375 const flatgrid$1 = () => { 5376 const dimensions = value$2(); 5377 const setGridSize = (numRows, numColumns) => { 5378 dimensions.set({ numRows, numColumns }); 5379 }; 5380 const getNumRows = () => dimensions.get().map((d) => d.numRows); 5381 const getNumColumns = () => dimensions.get().map((d) => d.numColumns); 5382 return nu$4({ 5383 readState: () => dimensions.get().map((d) => ({ 5384 numRows: String(d.numRows), 5385 numColumns: String(d.numColumns) 5386 })).getOr({ 5387 numRows: '?', 5388 numColumns: '?' 5389 }), 5390 setGridSize, 5391 getNumRows, 5392 getNumColumns 5393 }); 5394 }; 5395 const init$g = (spec) => spec.state(spec); 5396 5397 var KeyingState = /*#__PURE__*/Object.freeze({ 5398 __proto__: null, 5399 flatgrid: flatgrid$1, 5400 init: init$g 5401 }); 5402 5403 // Looks up direction (considering LTR and RTL), finds the focused element, 5404 // and tries to move. If it succeeds, triggers focus and kills the event. 5405 const useH = (movement) => (component, simulatedEvent, config, state) => { 5406 const move = movement(component.element); 5407 return use(move, component, simulatedEvent, config, state); 5408 }; 5409 const west = (moveLeft, moveRight) => { 5410 const movement = onDirection(moveLeft, moveRight); 5411 return useH(movement); 5412 }; 5413 const east = (moveLeft, moveRight) => { 5414 const movement = onDirection(moveRight, moveLeft); 5415 return useH(movement); 5416 }; 5417 const useV = (move) => (component, simulatedEvent, config, state) => use(move, component, simulatedEvent, config, state); 5418 const use = (move, component, simulatedEvent, config, state) => { 5419 const outcome = config.focusManager.get(component).bind((focused) => move(component.element, focused, config, state)); 5420 return outcome.map((newFocus) => { 5421 config.focusManager.set(component, newFocus); 5422 return true; 5423 }); 5424 }; 5425 const north = useV; 5426 const south = useV; 5427 const move$1 = useV; 5428 5429 const locate = (candidates, predicate) =>
5429 findIndex$1(candidates, predicate).map((index) => ({ 5430 index, 5431 candidates 5432 })); 5433 5434 const locateVisible = (container, current, selector) => { 5435 const predicate = (x) => eq(x, current); 5436 const candidates = descendants(container, selector); 5437 const visible = filter$2(candidates, isVisible); 5438 return locate(visible, predicate); 5439 }; 5440 const findIndex = (elements, target) => findIndex$1(elements, (elem) => eq(target, elem)); 5441 5442 const withGrid = (values, index, numCols, f) => { 5443 const oldRow = Math.floor(index / numCols); 5444 const oldColumn = index % numCols; 5445 return f(oldRow, oldColumn).bind((address) => { 5446 const newIndex = address.row * numCols + address.column; 5447 return newIndex >= 0 && newIndex < values.length ? Optional.some(values[newIndex]) : Optional.none(); 5448 }); 5449 }; 5450 const cycleHorizontal$1 = (values, index, numRows, numCols, delta) => withGrid(values, index, numCols, (oldRow, oldColumn) => { 5451 const onLastRow = oldRow === numRows - 1; 5452 const colsInRow = onLastRow ? values.length - (oldRow * numCols) : numCols; 5453 const newColumn = cycleBy(oldColumn, delta, 0, colsInRow - 1); 5454 return Optional.some({ 5455 row: oldRow, 5456 column: newColumn 5457 }); 5458 }); 5459 const cycleVertical$1 = (values, index, numRows, numCols, delta) => withGrid(values, index, numCols, (oldRow, oldColumn) => { 5460 const newRow = cycleBy(oldRow, delta, 0, numRows - 1); 5461 const onLastRow = newRow === numRows - 1;
vendor: 14,516 bytes, lines 5462-5719
5462 const colsInRow = onLastRow ? values.length - (newRow * numCols) : numCols; 5463 const newCol = clamp(oldColumn, 0, colsInRow - 1); 5464 return Optional.some({ 5465 row: newRow, 5466 column: newCol 5467 }); 5468 }); 5469 const cycleRight$1 = (values, index, numRows, numCols) => cycleHorizontal$1(values, index, numRows, numCols, +1); 5470 const cycleLeft$1 = (values, index, numRows, numCols) => cycleHorizontal$1(values, index, numRows, numCols, -1); 5471 const cycleUp$1 = (values, index, numRows, numCols) => cycleVertical$1(values, index, numRows, numCols, -1); 5472 const cycleDown$1 = (values, index, numRows, numCols) => cycleVertical$1(values, index, numRows, numCols, +1); 5473 5474 const schema$y = [ 5475 required$1('selector'), 5476 defaulted('execute', defaultExecute), 5477 onKeyboardHandler('onEscape'), 5478 defaulted('captureTab', false), 5479 initSize() 5480 ]; 5481 const focusIn$4 = (component, gridConfig, _gridState) => { 5482 descendant(component.element, gridConfig.selector).each((first) => { 5483 gridConfig.focusManager.set(component, first); 5484 }); 5485 }; 5486 const findCurrent$1 = (component, gridConfig) => gridConfig.focusManager.get(component).bind((elem) => closest$3(elem, gridConfig.selector)); 5487 const execute$3 = (component, simulatedEvent, gridConfig, _gridState) => findCurrent$1(component, gridConfig) 5488 .bind((focused) => gridConfig.execute(component, simulatedEvent, focused)); 5489 const doMove$2 = (cycle) => (element, focused, gridConfig, gridState) => locateVisible(element, focused, gridConfig.selector) 5490 .bind((identified) => cycle(identified.candidates, identified.index, gridState.getNumRows().getOr(gridConfig.initSize.numRows), gridState.getNumColumns().getOr(gridConfig.initSize.numColumns))); 5491 const handleTab = (_component, _simulatedEvent, gridConfig) => gridConfig.captureTab ? Optional.some(true) : Optional.none(); 5492 const doEscape$1 = (component, simulatedEvent, gridConfig) => gridConfig.onEscape(component, simulatedEvent); 5493 const moveLeft$3 = doMove$2(cycleLeft$1); 5494 const moveRight$3 = doMove$2(cycleRight$1); 5495 const moveNorth$1 = doMove$2(cycleUp$1); 5496 const moveSouth$1 = doMove$2(cycleDown$1); 5497 const getKeydownRules$4 = constant$1([ 5498 rule(inSet(LEFT), west(moveLeft$3, moveRight$3)), 5499 rule(inSet(RIGHT), east(moveLeft$3, moveRight$3)), 5500 rule(inSet(UP), north(moveNorth$1)), 5501 rule(inSet(DOWN), south(moveSouth$1)), 5502 rule(and([isShift$1, inSet(TAB)]), handleTab), 5503 rule(and([isNotShift, inSet(TAB)]), handleTab), 5504 // Probably should make whether space is used configurable 5505 rule(inSet(SPACE.concat(ENTER)), execute$3) 5506 ]); 5507 const getKeyupRules$4 = constant$1([ 5508 rule(inSet(ESCAPE), doEscape$1), 5509 rule(inSet(SPACE), stopEventForFirefox) 5510 ]); 5511 var FlatgridType = typical(schema$y, flatgrid$1, getKeydownRules$4, getKeyupRules$4, () => Optional.some(focusIn$4)); 5512 5513 const f = (container, selector, current, delta, getNewIndex) => { 5514 const isDisabledButton = (candidate) => name$3(candidate) === 'button' && get$g(candidate, 'disabled') === 'disabled'; 5515 const tryNewIndex = (initial, index, candidates) => getNewIndex(initial, index, delta, 0, candidates.length - 1, candidates[index], (newIndex) => isDisabledButton(candidates[newIndex]) ? 5516 tryNewIndex(initial, newIndex, candidates) : 5517 Optional.from(candidates[newIndex])); 5518 // I wonder if this will be a problem when the focused element is invisible (shouldn't happen) 5519 return locateVisible(container, current, selector).bind((identified) => { 5520 const index = identified.index; 5521 const candidates = identified.candidates; 5522 return tryNewIndex(index, index, candidates); 5523 }); 5524 }; 5525 const horizontalWithoutCycles = (container, selector, current, delta) => f(container, selector, current, delta, (prevIndex, v, d, min, max, oldCandidate, onNewIndex) => { 5526 const newIndex = clamp(v + d, min, max); 5527 return newIndex === prevIndex ? Optional.from(oldCandidate) : onNewIndex(newIndex); 5528 }); 5529 const horizontal = (container, selector, current, delta) => f(container, selector, current, delta, (prevIndex, v, d, min, max, _oldCandidate, onNewIndex) => { 5530 const newIndex = cycleBy(v, d, min, max); 5531 // If we've cycled back to the original index, we've failed to find a new valid candidate 5532 return newIndex === prevIndex ? Optional.none() : onNewIndex(newIndex); 5533 }); 5534 5535 const schema$x = [ 5536 required$1('selector'), 5537 defaulted('getInitial', Optional.none), 5538 defaulted('execute', defaultExecute), 5539 onKeyboardHandler('onEscape'), 5540 defaulted('executeOnMove', false), 5541 defaulted('allowVertical', true), 5542 defaulted('allowHorizontal', true), 5543 defaulted('cycles', true) 5544 ]; 5545 // TODO: Remove dupe. 5546 // TODO: Probably use this for not just execution. 5547 const findCurrent = (component, flowConfig) => flowConfig.focusManager.get(component).bind((elem) => closest$3(elem, flowConfig.selector)); 5548 const execute$2 = (component, simulatedEvent, flowConfig) => findCurrent(component, flowConfig).bind((focused) => flowConfig.execute(component, simulatedEvent, focused)); 5549 const focusIn$3 = (component, flowConfig, _state) => { 5550 flowConfig.getInitial(component).orThunk(() => descendant(component.element, flowConfig.selector)).each((first) => { 5551 flowConfig.focusManager.set(component, first); 5552 }); 5553 }; 5554 const moveLeft$2 = (element, focused, info) => (info.cycles ? horizontal : horizontalWithoutCycles)(element, info.selector, focused, -1); 5555 const moveRight$2 = (element, focused, info) => (info.cycles ? horizontal : horizontalWithoutCycles)(element, info.selector, focused, +1); 5556 const doMove$1 = (movement) => (component, simulatedEvent, flowConfig, flowState) => movement(component, simulatedEvent, flowConfig, flowState).bind(() => flowConfig.executeOnMove ? 5557 execute$2(component, simulatedEvent, flowConfig) : 5558 Optional.some(true)); 5559 const doEscape = (component, simulatedEvent, flowConfig) => flowConfig.onEscape(component, simulatedEvent); 5560 const getKeydownRules$3 = (_component, _se, flowConfig, _flowState) => { 5561 const westMovers = [...flowConfig.allowHorizontal ? LEFT : []].concat(flowConfig.allowVertical ? UP : []); 5562 const eastMovers = [...flowConfig.allowHorizontal ? RIGHT : []].concat(flowConfig.allowVertical ? DOWN : []); 5563 return [ 5564 rule(inSet(westMovers), doMove$1(west(moveLeft$2, moveRight$2))), 5565 rule(inSet(eastMovers), doMove$1(east(moveLeft$2, moveRight$2))), 5566 rule(inSet(ENTER), execute$2), 5567 rule(inSet(SPACE), execute$2) 5568 ]; 5569 }; 5570 const getKeyupRules$3 = constant$1([ 5571 rule(inSet(SPACE), stopEventForFirefox), 5572 rule(inSet(ESCAPE), doEscape) 5573 ]); 5574 var FlowType = typical(schema$x, NoState.init, getKeydownRules$3, getKeyupRules$3, () => Optional.some(focusIn$3)); 5575 5576 const toCell = (matrix, rowIndex, columnIndex) => Optional.from(matrix[rowIndex]).bind((row) => Optional.from(row[columnIndex]).map((cell) => ({ 5577 rowIndex, 5578 columnIndex, 5579 cell 5580 }))); 5581 const cycleHorizontal = (matrix, rowIndex, startCol, deltaCol) => { 5582 const row = matrix[rowIndex]; 5583 const colsInRow = row.length; 5584 const newColIndex = cycleBy(startCol, deltaCol, 0, colsInRow - 1); 5585 return toCell(matrix, rowIndex, newColIndex); 5586 }; 5587 const cycleVertical = (matrix, colIndex, startRow, deltaRow) => { 5588 const nextRowIndex = cycleBy(startRow, deltaRow, 0, matrix.length - 1); 5589 const colsInNextRow = matrix[nextRowIndex].length; 5590 const nextColIndex = clamp(colIndex, 0, colsInNextRow - 1); 5591 return toCell(matrix, nextRowIndex, nextColIndex); 5592 }; 5593 const moveHorizontal = (matrix, rowIndex, startCol, deltaCol) => { 5594 const row = matrix[rowIndex]; 5595 const colsInRow = row.length; 5596 const newColIndex = clamp(startCol + deltaCol, 0, colsInRow - 1); 5597 return toCell(matrix, rowIndex, newColIndex); 5598 }; 5599 const moveVertical = (matrix, colIndex, startRow, deltaRow) => { 5600 const nextRowIndex = clamp(startRow + deltaRow, 0, matrix.length - 1); 5601 const colsInNextRow = matrix[nextRowIndex].length; 5602 const nextColIndex = clamp(colIndex, 0, colsInNextRow - 1); 5603 return toCell(matrix, nextRowIndex, nextColIndex); 5604 }; 5605 // return address(Math.floor(index / columns), index % columns); 5606 const cycleRight = (matrix, startRow, startCol) => cycleHorizontal(matrix, startRow, startCol, +1); 5607 const cycleLeft = (matrix, startRow, startCol) => cycleHorizontal(matrix, startRow, startCol, -1); 5608 const cycleUp = (matrix, startRow, startCol) => cycleVertical(matrix, startCol, startRow, -1); 5609 const cycleDown = (matrix, startRow, startCol) => cycleVertical(matrix, startCol, startRow, +1); 5610 const moveLeft$1 = (matrix, startRow, startCol) => moveHorizontal(matrix, startRow, startCol, -1); 5611 const moveRight$1 = (matrix, startRow, startCol) => moveHorizontal(matrix, startRow, startCol, +1); 5612 const moveUp$1 = (matrix, startRow, startCol) => moveVertical(matrix, startCol, startRow, -1); 5613 const moveDown$1 = (matrix, startRow, startCol) => moveVertical(matrix, startCol, startRow, +1); 5614 5615 const schema$w = [ 5616 requiredObjOf('selectors', [ 5617 required$1('row'), 5618 required$1('cell') 5619 ]), 5620 // Used to determine whether pressing right/down at the end cycles back to the start/top 5621 defaulted('cycles', true), 5622 defaulted('previousSelector', Optional.none), 5623 defaulted('execute', defaultExecute) 5624 ]; 5625 const focusIn$2 = (component, matrixConfig, _state) => { 5626 const focused = matrixConfig.previousSelector(component).orThunk(() => { 5627 const selectors = matrixConfig.selectors; 5628 return descendant(component.element, selectors.cell); 5629 }); 5630 focused.each((cell) => { 5631 matrixConfig.focusManager.set(component, cell); 5632 }); 5633 }; 5634 const execute$1 = (component, simulatedEvent, matrixConfig) => search(component.element).bind((focused) => matrixConfig.execute(component, simulatedEvent, focused)); 5635 const toMatrix = (rows, matrixConfig) => map$2(rows, (row) => descendants(row, matrixConfig.selectors.cell)); 5636 const doMove = (ifCycle, ifMove) => (element, focused, matrixConfig) => { 5637 const move = matrixConfig.cycles ? ifCycle : ifMove; 5638 return closest$3(focused, matrixConfig.selectors.row).bind((inRow) => { 5639 const cellsInRow = descendants(inRow, matrixConfig.selectors.cell); 5640 return findIndex(cellsInRow, focused).bind((colIndex) => { 5641 const allRows = descendants(element, matrixConfig.selectors.row); 5642 return findIndex(allRows, inRow).bind((rowIndex) => { 5643 // Now, make the matrix. 5644 const matrix = toMatrix(allRows, matrixConfig); 5645 return move(matrix, rowIndex, colIndex).map((next) => next.cell); 5646 }); 5647 }); 5648 }); 5649 }; 5650 const moveLeft = doMove(cycleLeft, moveLeft$1); 5651 const moveRight = doMove(cycleRight, moveRight$1); 5652 const moveNorth = doMove(cycleUp, moveUp$1); 5653 const moveSouth = doMove(cycleDown, moveDown$1); 5654 const getKeydownRules$2 = constant$1([ 5655 rule(inSet(LEFT), west(moveLeft, moveRight)), 5656 rule(inSet(RIGHT), east(moveLeft, moveRight)), 5657 rule(inSet(UP), north(moveNorth)), 5658 rule(inSet(DOWN), south(moveSouth)), 5659 rule(inSet(SPACE.concat(ENTER)), execute$1) 5660 ]); 5661 const getKeyupRules$2 = constant$1([ 5662 rule(inSet(SPACE), stopEventForFirefox) 5663 ]); 5664 var MatrixType = typical(schema$w, NoState.init, getKeydownRules$2, getKeyupRules$2, () => Optional.some(focusIn$2)); 5665 5666 const schema$v = [ 5667 required$1('selector'), 5668 defaulted('execute', defaultExecute) 5669 ]; 5670 const execute = (component, simulatedEvent, menuConfig) => menuConfig.focusManager.get(component).bind((focused) => menuConfig.execute(component, simulatedEvent, focused)); 5671 const focusIn$1 = (component, menuConfig, _state) => { 5672 // Maybe keep selection if it was there before 5673 descendant(component.element, menuConfig.selector).each((first) => { 5674 menuConfig.focusManager.set(component, first); 5675 }); 5676 }; 5677 const moveUp = (element, focused, info) => horizontal(element, info.selector, focused, -1); 5678 const moveDown = (element, focused, info) => horizontal(element, info.selector, focused, +1); 5679 const getKeydownRules$1 = constant$1([ 5680 rule(inSet(UP), move$1(moveUp)), 5681 rule(inSet(DOWN), move$1(moveDown)), 5682 rule(inSet(ENTER), execute), 5683 rule(inSet(SPACE), execute) 5684 ]); 5685 const getKeyupRules$1 = constant$1([ 5686 rule(inSet(SPACE), stopEventForFirefox) 5687 ]); 5688 var MenuType = typical(schema$v, NoState.init, getKeydownRules$1, getKeyupRules$1, () => Optional.some(focusIn$1)); 5689 5690 const schema$u = [ 5691 onKeyboardHandler('onSpace'), 5692 onKeyboardHandler('onEnter'), 5693 onKeyboardHandler('onShiftEnter'), 5694 onKeyboardHandler('onLeft'), 5695 onKeyboardHandler('onRight'), 5696 onKeyboardHandler('onTab'), 5697 onKeyboardHandler('onShiftTab'), 5698 onKeyboardHandler('onUp'), 5699 onKeyboardHandler('onDown'), 5700 onKeyboardHandler('onEscape'), 5701 defaulted('stopSpaceKeyup', false), 5702 option$3('focusIn') 5703 ]; 5704 const getKeydownRules = (component, simulatedEvent, specialInfo) => [ 5705 rule(inSet(SPACE), specialInfo.onSpace), 5706 rule(and([isNotShift, inSet(ENTER)]), specialInfo.onEnter), 5707 rule(and([isShift$1, inSet(ENTER)]), specialInfo.onShiftEnter), 5708 rule(and([isShift$1, inSet(TAB)]), specialInfo.onShiftTab), 5709 rule(and([isNotShift, inSet(TAB)]), specialInfo.onTab), 5710 rule(inSet(UP), specialInfo.onUp), 5711 rule(inSet(DOWN), specialInfo.onDown), 5712 rule(inSet(LEFT), specialInfo.onLeft), 5713 rule(inSet(RIGHT), specialInfo.onRight), 5714 rule(inSet(SPACE), specialInfo.onSpace) 5715 ]; 5716 const getKeyupRules = (component, simulatedEvent, specialInfo) => [ 5717 ...(specialInfo.stopSpaceKeyup ? [rule(inSet(SPACE), stopEventForFirefox)] : []), 5718 rule(inSet(ESCAPE), specialInfo.onEscape) 5719 ];
5720 var SpecialType = typical(schema$u, NoState.init, getKeydownRules, getKeyupRules, (specialInfo) => specialInfo.focusIn); 5721 5722 const acyclic = AcyclicType.schema(); 5723 const cyclic = CyclicType.schema(); 5724 const flow = FlowType.schema(); 5725 const flatgrid = FlatgridType.schema(); 5726 const matrix = MatrixType.schema(); 5727 const execution = ExecutionType.schema(); 5728 const menu = MenuType.schema(); 5729 const special = SpecialType.schema(); 5730 5731 var KeyboardBranches = /*#__PURE__*/Object.freeze({ 5732 __proto__: null, 5733 acyclic: acyclic, 5734 cyclic: cyclic, 5735 flow: flow, 5736 flatgrid: flatgrid, 5737 matrix: matrix, 5738 execution: execution, 5739 menu: menu, 5740 special: special 5741 }); 5742 5743 const isFlatgridState = (keyState) => hasNonNullableKey(keyState, 'setGridSize'); 5744 const Keying = createModes({ 5745 branchKey: 'mode', 5746 branches: KeyboardBranches, 5747 name: 'keying', 5748 active: { 5749 events: (keyingConfig, keyingState) => { 5750 const handler = keyingConfig.handler; 5751 return handler.toEvents(keyingConfig, keyingState); 5752 } 5753 }, 5754 apis: { 5755 focusIn: (component, keyConfig, keyState) => { 5756 // If we have a custom sendFocusIn function, use that. 5757 // Otherwise, we just trigger focus on the outer element. 5758 keyConfig.sendFocusIn(keyConfig).fold(() => { 5759 component.getSystem().triggerFocus(component.element, component.element); 5760 }, (sendFocusIn) => { 5761 sendFocusIn(component, keyConfig, keyState); 5762 }); 5763 }, 5764 // These APIs are going to be interesting because they are not 5765 // available for all keying modes 5766 setGridSize: (component, keyConfig, keyState, numRows, numColumns) => { 5767 if (!isFlatgridState(keyState)) { 5768 // eslint-disable-next-line no-console 5769 console.error('Layout does not support setGridSize'); 5770 } 5771 else { 5772 keyState.setGridSize(numRows, numColumns); 5773 } 5774 } 5775 }, 5776 state: KeyingState 5777 }); 5778 5779 const premadeTag = generate$6('alloy-premade'); 5780 const premade$1 = (comp) => { 5781 Object.defineProperty(comp.element.dom, premadeTag, { 5782 value: comp.uid, 5783 writable: true 5784 }); 5785 return wrap(premadeTag, comp); 5786 }; 5787 const isPremade = (element) => has$2(element.dom, premadeTag); 5788 const getPremade = (spec) => get$h(spec, premadeTag); 5789 const makeApi = (f) => markAsSketchApi((component, ...rest) => f(component.getApis(), component, ...rest), f); 5790 5791 const isConnected = (comp) => comp.getSystem().isConnected(); 5792 const fireDetaching = (component) => { 5793 emit(component, detachedFromDom()); 5794 const children = component.components(); 5795 each$1(children, fireDetaching); 5796 }; 5797 const fireAttaching = (component) => { 5798 const children = component.components(); 5799 each$1(children, fireAttaching); 5800 emit(component, attachedToDom()); 5801 }; 5802 // Unlike attach, a virtualAttach makes no actual DOM changes. 5803 // This is because it should only be used in a situation 5804 // where we are patching an existing element. 5805 const virtualAttach = (parent, child) => { 5806 // So we still add it to the world 5807 parent.getSystem().addToWorld(child); 5808 // And we fire attaching ONLY if it's already in the DOM 5809 if (inBody(parent.element)) { 5810 fireAttaching(child); 5811 } 5812 }; 5813 // Unlike detach, a virtualDetach makes no actual DOM changes. 5814 // This is because it's used in patching circumstances. 5815 const virtualDetach = (comp) => { 5816 fireDetaching(comp); 5817 comp.getSystem().removeFromWorld(comp); 5818 }; 5819 const attach$1 = (parent, child) => { 5820 append$2(parent.element, child.element); 5821 }; 5822 const detachChildren$1 = (component) => { 5823 // This will not detach the component, but will detach its children and sync at the end. 5824 each$1(component.components(), (childComp) => remove$7(childComp.element)); 5825 // Clear the component also. 5826 empty(component.element); 5827 component.syncComponents(); 5828 }; 5829 const replaceChildren = (component, newSpecs, buildNewChildren) => { 5830 // Detach all existing children 5831 const subs = component.components(); 5832 detachChildren$1(component); 5833 const newChildren = buildNewChildren(newSpecs); 5834 // Determine which components have been deleted and remove them from the world 5835 const deleted = difference(subs, newChildren); 5836 each$1(deleted, (comp) => { 5837 fireDetaching(comp); 5838 component.getSystem().removeFromWorld(comp); 5839 }); 5840 // Add all new components 5841 each$1(newChildren, (childComp) => { 5842 // If the component isn't connected, ie is new, then we also need to add it to the world 5843 if (!isConnected(childComp)) { 5844 component.getSystem().addToWorld(childComp); 5845 attach$1(component, childComp); 5846 if (inBody(component.element)) { 5847 fireAttaching(childComp); 5848 } 5849 } 5850 else { 5851 attach$1(component, childComp); 5852 } 5853 }); 5854 component.syncComponents(); 5855 }; 5856 const virtualReplaceChildren = (component, newSpecs, buildNewChildren) => { 5857 // When replacing we don't want to fire detachedFromDom and attachedToDom again for a premade that has just had its position in the children moved around, 5858 // so we only detach initially if we aren't a premade. Premades will be detached later, but only if they are no longer in the child list. 5859 const subs = component.components(); 5860 const existingComps = bind$3(newSpecs, (spec) => getPremade(spec).toArray());
5861 each$1(subs, (childComp) => { 5862 if (!contains$2(existingComps, childComp)) { 5863 virtualDetach(childComp); 5864 } 5865 }); 5866 const newChildren = buildNewChildren(newSpecs); 5867 // Determine which components have been deleted and remove them from the world 5868 // It's probable the component has already been detached beforehand so only 5869 // detach what's still attached to the world (i.e removed premades) 5870 const deleted = difference(subs, newChildren); 5871 each$1(deleted, (deletedComp) => { 5872 if (isConnected(deletedComp)) { 5873 virtualDetach(deletedComp); 5874 } 5875 }); 5876 // Add all new components 5877 each$1(newChildren, (childComp) => { 5878 // If the component isn't connected, ie is new, then we also need to add it to the world 5879 if (!isConnected(childComp)) { 5880 virtualAttach(component, childComp); 5881 } 5882 }); 5883 component.syncComponents(); 5884 }; 5885 5886 const attach = (parent, child) => { 5887 attachWith(parent, child, append$2); 5888 }; 5889 const attachWith = (parent, child, insertion) => { 5890 parent.getSystem().addToWorld(child); 5891 insertion(parent.element, child.element); 5892 if (inBody(parent.element)) { 5893 fireAttaching(child); 5894 } 5895 parent.syncComponents(); 5896 }; 5897 const doDetach = (component) => { 5898 fireDetaching(component); 5899 remove$7(component.element); 5900 component.getSystem().removeFromWorld(component); 5901 }; 5902 const detach = (component) => { 5903 const parent$1 = parent(component.element).bind((p) => component.getSystem().getByDom(p).toOptional()); 5904 doDetach(component); 5905 parent$1.each((p) => { 5906 p.syncComponents(); 5907 }); 5908 }; 5909 const detachChildren = (component) => { 5910 // This will not detach the component, but will detach its children and sync at the end. 5911 const subs = component.components(); 5912 each$1(subs, doDetach); 5913 // Clear the component also. 5914 empty(component.element); 5915 component.syncComponents(); 5916 }; 5917 const attachSystem = (element, guiSystem) => { 5918 attachSystemWith(element, guiSystem, append$2); 5919 }; 5920 const attachSystemAfter = (element, guiSystem) => { 5921 attachSystemWith(element, guiSystem, after$1); 5922 }; 5923 const attachSystemWith = (element, guiSystem, inserter) => { 5924 inserter(element, guiSystem.element); 5925 const children$1 = children(guiSystem.element); 5926 each$1(children$1, (child) => { 5927 guiSystem.getByDom(child).each(fireAttaching); 5928 }); 5929 }; 5930 const detachSystem = (guiSystem) => { 5931 const children$1 = children(guiSystem.element); 5932 each$1(children$1, (child) => { 5933 guiSystem.getByDom(child).each(fireDetaching); 5934 }); 5935 remove$7(guiSystem.element); 5936 }; 5937 5938 const determineObsoleted = (parent, index, oldObsoleted) => { 5939 // When dealing with premades, the process of building something may have moved existing nodes around, so we see 5940 // if the child at the index position is still the same. If it isn't, we need to introduce some complex behaviour 5941 // 5942 // Example: 5943 // ```<div><premade></premade><span></span></div>``` 5944 // and then moving the premade inside a blockquote 5945 // ```<div><blockquote><premade></premade></blockquote><span></span></div>``` 5946 // 5947 // so when you go to replace the first thing it would think there is only 1 child which w
vendor: 4,640 bytes, lines 5947-6044
5947ould be the span, so in 5948 // this case we insert a marker to keep the span in the same spot. 5949 const newObsoleted = child$2(parent, index); 5950 return newObsoleted.map((newObs) => { 5951 const elemChanged = oldObsoleted.exists((o) => !eq(o, newObs)); 5952 // Adding a marker prevents the case where a premade is added to something shifting it from where 5953 // it was. That in turn un-synced all trailing children and made it so they couldn't be patched. 5954 if (elemChanged) { 5955 const oldTag = oldObsoleted.map(name$3).getOr('span'); 5956 const marker = SugarElement.fromTag(oldTag); 5957 before$1(newObs, marker); 5958 return marker; 5959 } 5960 else { 5961 return newObs; 5962 } 5963 }); 5964 }; 5965 const ensureInDom = (parent, child, obsoleted) => { 5966 obsoleted.fold( 5967 // There is nothing here, so just append to the parent 5968 () => append$2(parent, child), (obs) => { 5969 if (!eq(obs, child)) { 5970 // This situation occurs when the DOM element that has been patched when building it is no 5971 // longer the one that we need to replace. This is probably caused by premades. 5972 before$1(obs, child); 5973 remove$7(obs); 5974 } 5975 }); 5976 }; 5977 const patchChildrenWith = (parent, nu, f) => { 5978 const builtChildren = map$2(nu, f); 5979 // Need to regather the children in case some of the previous children have moved 5980 // to an earlier index. So this just prunes any leftover children in the dom. 5981 const currentChildren = children(parent); 5982 each$1(currentChildren.slice(builtChildren.length), remove$7); 5983 return builtChildren; 5984 }; 5985 const patchSpecChild = (parent, index, spec, build) => { 5986 // Before building anything, this is the DOM element we are going to try to use. 5987 const oldObsoleted = child$2(parent, index); 5988 const childComp = build(spec, oldObsoleted); 5989 const obsoleted = determineObsoleted(parent, index, oldObsoleted); 5990 ensureInDom(parent, childComp.element, obsoleted); 5991 return childComp; 5992 }; 5993 const patchSpecChildren = (parent, specs, build) => patchChildrenWith(parent, specs, (spec, index) => patchSpecChild(parent, index, spec, build)); 5994 const patchDomChildren = (parent, nodes) => patchChildrenWith(parent, nodes, (node, index) => { 5995 const optObsoleted = child$2(parent, index); 5996 ensureInDom(parent, node, optObsoleted); 5997 return node; 5998 }); 5999 6000 const preserve = (f, container) => { 6001 const dos = getRootNode(container); 6002 const refocus = active$1(dos).bind((focused) => { 6003 const hasFocus = (elem) => eq(focused, elem); 6004 return hasFocus(container) ? Optional.some(container) : descendant$1(container, hasFocus); 6005 }); 6006 const result = f(container); 6007 // If there is a focussed element, the F function may cause focus to be lost (such as by hiding elements). Restore it afterwards. 6008 refocus.each((oldFocus) => { 6009 active$1(dos).filter((newFocus) => eq(newFocus, oldFocus)).fold(() => { 6010 // Only refocus if the focus has changed, otherwise we break IE 6011 focus$4(oldFocus); 6012 }, noop); 6013 }); 6014 return result; 6015 }; 6016 6017 const withoutReuse = (parent, data) => { 6018 preserve(() => { 6019 replaceChildren(parent, data, () => map$2(data, parent.getSystem().build)); 6020 }, parent.element); 6021 }; 6022 const withReuse = (parent, data) => { 6023 // Note: We shouldn't need AriaPreserve since we're trying to keep the existing elements, 6024 // but let's just do it for now just to be safe. 6025 preserve(() => { 6026 virtualReplaceChildren(parent, data, () => { 6027 // Build the new children 6028 return patchSpecChildren(parent.element, data, parent.getSystem().buildOrPatch); 6029 }); 6030 }, parent.element); 6031 }; 6032 6033 const virtualReplace = (component, replacee, replaceeIndex, childSpec) => { 6034 virtualDetach(replacee); 6035 const child = patchSpecChild(component.element, replaceeIndex, childSpec, component.getSystem().buildOrPatch); 6036 virtualAttach(component, child); 6037 component.syncComponents(); 6038 }; 6039 const insert = (component, insertion, childSpec) => { 6040 const child = component.getSystem().build(childSpec); 6041 attachWith(component, child, insertion); 6042 }; 6043 const replace = (component, replacee, replaceeIndex, childSpec) => { 6044 detach(replacee);
6045 insert(component, (p, c) => appendAt(p, c, replaceeIndex), childSpec); 6046 }; 6047 const set$3 = (component, replaceConfig, replaceState, data) => { 6048 const replacer = replaceConfig.reuseDom ? withReuse : withoutReuse; 6049 return replacer(component, data); 6050 }; 6051 const append = (component, replaceConfig, replaceState, appendee) => { 6052 insert(component, append$2, appendee); 6053 }; 6054 const prepend = (component, replaceConfig, replaceState, prependee) => { 6055 insert(component, prepend$1, prependee); 6056 }; 6057 // NOTE: Removee is going to be a component, not a spec. 6058 const remove = (component, replaceConfig, replaceState, removee) => { 6059 const children = contents(component); 6060 const foundChild = find$5(children, (child) => eq(removee.element, child.element)); 6061 foundChild.each(detach); 6062 }; 6063 // TODO: Rename 6064 const contents = (component, _replaceConfig) => component.components(); 6065 const replaceAt = (component, replaceConfig, replaceState, replaceeIndex, replacer) => { 6066 const children = contents(component); 6067 return Optional.from(children[replaceeIndex]).map((replacee) => { 6068 replacer.fold(() => detach(replacee), (r) => { 6069 const replacer = replaceConfig.reuseDom ? virtualReplace : replace; 6070 replacer(component, replacee, replaceeIndex, r); 6071 }); 6072 return replacee; 6073 }); 6074 }; 6075 const replaceBy = (component, replaceConfig, replaceState, replaceePred, replacer) => { 6076 const children = contents(component); 6077 return findIndex$1(children, replaceePred).bind((replaceeIndex) => replaceAt(component, replaceConfig, replaceState, replaceeIndex, replacer)); 6078 }; 6079 6080 var ReplaceApis = /*#__PURE__*/Object.freeze({ 6081 __proto__: null, 6082 append: append, 6083 prepend: prepend, 6084 remove: remove, 6085 replaceAt: replaceAt, 6086 replaceBy: replaceBy, 6087 set: set$3, 6088 contents: contents 6089 }); 6090 6091 const Replacing = create$3({ 6092 fields: [ 6093 defaultedBoolean('reuseDom', true) 6094 ], 6095 name: 'replacing', 6096 apis: ReplaceApis 6097 }); 6098 6099 // The purpose of this check is to ensure that a simulated focus call is not going 6100 // to recurse infinitely. Essentially, if the originator of the focus call is the same 6101 // as the element receiving it, and it wasn't its own target, then stop the focus call 6102 // and log a warning. 6103 const isRecursive = (component, originator, target) => eq(originator, component.element) && !eq(originator, target); 6104 const events$h = derive$2([ 6105 can(focus$3(), (component, simulatedEvent) => { 6106 // originator may not always be there. Will need to check this. 6107 const event = simulatedEvent.event; 6108 const originator = event.originator; 6109 const target = event.target; 6110 if (isRecursive(component, originator, target)) { 6111 // eslint-disable-next-line no-console 6112 console.warn(focus$3() + ' did not get interpreted by the desired target. ' + 6113 '\nOriginator: ' + element(originator) + 6114 '\nTarget: ' + element(target) + 6115 '\nCheck the ' + focus$3() + ' event handlers'); 6116 return false; 6117 } 6118 else { 6119 return true; 6120 } 6121 }) 6122 ]); 6123 6124 var DefaultEvents = /*#__PURE__*/Object.freeze({ 6125 __proto__: null, 6126 events: events$h 6127 }); 6128 6129 const prefix$1 = constant$1('alloy-id-'); 6130 const idAttr$1 = constant$1('data-alloy-id'); 6131 6132 const prefix = prefix$1(); 6133 const idAttr = idAttr$1(); 6134 const write = (label, elem) => { 6135 const id = generate$6(prefix + label); 6136 writeOnly(elem, id); 6137 return id; 6138 }; 6139 const writeOnly = (elem, uid) => { 6140 Object.defineProperty(elem.dom, idAttr, { 6141 value: uid, 6142 writable: true 6143 }); 6144 }; 6145 const read = (elem) => { 6146 const id = isElement$1(elem) ? elem.dom[idAttr] : null; 6147 return Optional.from(id); 6148 }; 6149 const generate$4 = (prefix) => generate$6(prefix); 6150 6151 const make$7 = identity; 6152 6153 const NoContextApi = (getComp) => { 6154 const getMessage = (event) => `The component must be in a context to execute: ${event}` + 6155 (getComp ? '\n' + element(getComp().element) + ' is not in context.' : ''); 6156 const fail = (event) => () => { 6157 throw new Error(getMessage(event)); 6158 }; 6159 const warn = (event) => () => { 6160 // eslint-disable-next-line no-console 6161 console.warn(getMessage(event)); 6162 }; 6163 return { 6164 debugInfo: constant$1('fake'), 6165 triggerEvent: warn('triggerEvent'), 6166 triggerFocus: warn('triggerFocus'), 6167 triggerEscape: warn('triggerEscape'), 6168 broadcast: warn('broadcast'), 6169 broadcastOn: warn('broadcastOn'), 6170 broadcastEvent: warn('broadcastEvent'), 6171 build: fail('build'), 6172 buildOrPatch: fail('buildOrPatch'), 6173 addToWorld: fail('addToWorld'), 6174 removeFromWorld: fail('removeFromWorld'), 6175 addToGui: fail('addToGui'), 6176 removeFromGui: fail('removeFromGui'), 6177 getByUid: fail('getByUid'), 6178 getByDom: fail('getByDom'), 6179 isConnected: never 6180 }; 6181 }; 6182 const singleton = NoContextApi(); 6183 6184 const generateFrom$1 = (spec, all) => { 6185 /* 6186 * This takes a basic record of configured behaviours, defaults their state 6187 * and ensures that all the behaviours were valid. Will need to document 6188 * this entire process. Let's see where this is used. 6189 */ 6190 const schema = map$2(all, (a) => 6191 // Optional here probably just due to ForeignGui listing everything it supports. Can most likely 6192 // change it to strict once I fix the other errors. 6193 optionObjOf(a.name(), [ 6194 required$1('config'), 6195 defaulted('state', NoState) 6196 ])); 6197 const validated = asRaw('component.behaviours', objOf(schema), spec.behaviours).fold((errInfo) => { 6198 throw new Error(formatError(errInfo) + '\nComplete spec:\n' + 6199 JSON.stringify(spec, null, 2)); 6200 }, identity); 6201 return { 6202 list: all, 6203 data: map$1(validated, (optBlobThunk) => { 6204 const output = optBlobThunk.map((blob) => ({ 6205 config: blob.config, 6206 state: blob.state.init(blob.config) 6207 })); 6208 return constant$1(output); 6209 }) 6210 }; 6211 }; 6212 const getBehaviours$3 = (bData) => bData.list; 6213 const getData$3 = (bData) => bData.data; 6214
vendor: 4,670 bytes, lines 6215-6329
6215 const byInnerKey = (data, tuple) => { 6216 const r = {}; 6217 each(data, (detail, key) => { 6218 each(detail, (value, indexKey) => { 6219 const chain = get$h(r, indexKey).getOr([]); 6220 r[indexKey] = chain.concat([ 6221 tuple(key, value) 6222 ]); 6223 }); 6224 }); 6225 return r; 6226 }; 6227 6228 // Based on all the behaviour exhibits, and the original dom modification, identify 6229 // the overall combined dom modification that needs to occur 6230 const combine$1 = (info, baseMod, behaviours, base) => { 6231 // Clone the object so we can change it. 6232 const modsByBehaviour = { ...baseMod }; 6233 each$1(behaviours, (behaviour) => { 6234 modsByBehaviour[behaviour.name()] = behaviour.exhibit(info, base); 6235 }); 6236 // byAspect format: { classes: [ { name: Toggling, modification: [ 'selected' ] } ] } 6237 const byAspect = byInnerKey(modsByBehaviour, (name, modification) => ({ name, modification })); 6238 const combineObjects = (objects) => foldr(objects, (b, a) => ({ ...a.modification, ...b }), {}); 6239 const combinedClasses = foldr(byAspect.classes, (b, a) => a.modification.concat(b), []); 6240 const combinedAttributes = combineObjects(byAspect.attributes); 6241 const combinedStyles = combineObjects(byAspect.styles); 6242 return nu$2({ 6243 classes: combinedClasses, 6244 attributes: combinedAttributes, 6245 styles: combinedStyles 6246 }); 6247 }; 6248 6249 const sortKeys = (label, keyName, array, order) => { 6250 try { 6251 const sorted = sort(array, (a, b) => { 6252 const aKey = a[keyName]; 6253 const bKey = b[keyName]; 6254 const aIndex = order.indexOf(aKey); 6255 const bIndex = order.indexOf(bKey); 6256 if (aIndex === -1) { 6257 throw new Error('The ordering for ' + label + ' does not have an entry for ' + aKey + 6258 '.\nOrder specified: ' + JSON.stringify(order, null, 2)); 6259 } 6260 if (bIndex === -1) { 6261 throw new Error('The ordering for ' + label + ' does not have an entry for ' + bKey + 6262 '.\nOrder specified: ' + JSON.stringify(order, null, 2)); 6263 } 6264 if (aIndex < bIndex) { 6265 return -1; 6266 } 6267 else if (bIndex < aIndex) { 6268 return 1; 6269 } 6270 else { 6271 return 0; 6272 } 6273 }); 6274 return Result.value(sorted); 6275 } 6276 catch (err) { 6277 return Result.error([err]); 6278 } 6279 }; 6280 6281 const uncurried = (handler, purpose) => ({ 6282 handler, 6283 purpose 6284 }); 6285 const curried = (handler, purpose) => ({ 6286 cHandler: handler, 6287 purpose 6288 }); 6289 const curryArgs = (descHandler, extraArgs) => curried(curry.apply(undefined, [descHandler.handler].concat(extraArgs)), descHandler.purpose); 6290 const getCurried = (descHandler) => descHandler.cHandler; 6291 6292 const behaviourTuple = (name, handler) => ({ 6293 name, 6294 handler 6295 }); 6296 const nameToHandlers = (behaviours, info) => { 6297 const r = {}; 6298 each$1(behaviours, (behaviour) => { 6299 r[behaviour.name()] = behaviour.handlers(info); 6300 }); 6301 return r; 6302 }; 6303 const groupByEvents = (info, behaviours, base) => { 6304 const behaviourEvents = { 6305 ...base, 6306 ...nameToHandlers(behaviours, info) 6307 }; 6308 // Now, with all of these events, we need to index by event name 6309 return byInnerKey(behaviourEvents, behaviourTuple); 6310 }; 6311 const combine = (info, eventOrder, behaviours, base) => { 6312 const byEventName = groupByEvents(info, behaviours, base); 6313 return combineGroups(byEventName, eventOrder); 6314 }; 6315 const assemble = (rawHandler) => { 6316 const handler = read$1(rawHandler); 6317 return (component, simulatedEvent, ...rest) => { 6318 const args = [component, simulatedEvent].concat(rest); 6319 if (handler.abort.apply(undefined, args)) { 6320 simulatedEvent.stop(); 6321 } 6322 else if (handler.can.apply(undefined, args)) { 6323 handler.run.apply(undefined, args); 6324 } 6325 }; 6326 }; 6327 const missingOrderError = (eventName, tuples) => Result.error([ 6328 'The event (' + eventName + ') has more than one behaviour that listens to it.\nWhen this occurs, you must ' + 6329 'specify an event ordering for the behaviours in your spec (e.g. [ "listing", "toggling" ]).\nThe behaviours that ' +
6330 'can trigger it are: ' + JSON.stringify(map$2(tuples, (c) => c.name), null, 2) 6331 ]); 6332 const fuse = (tuples, eventOrder, eventName) => { 6333 // ASSUMPTION: tuples.length will never be 0, because it wouldn't have an entry if it was 0 6334 const order = eventOrder[eventName]; 6335 if (!order) { 6336 return missingOrderError(eventName, tuples); 6337 } 6338 else { 6339 return sortKeys('Event: ' + eventName, 'name', tuples, order).map((sortedTuples) => { 6340 const handlers = map$2(sortedTuples, (tuple) => tuple.handler); 6341 return fuse$1(handlers); 6342 }); 6343 } 6344 }; 6345 const combineGroups = (byEventName, eventOrder) => { 6346 const r = mapToArray(byEventName, (tuples, eventName) => { 6347 const combined = tuples.length === 1 ? Result.value(tuples[0].handler) : fuse(tuples, eventOrder, eventName); 6348 return combined.map((handler) => { 6349 const assembled = assemble(handler); 6350 const purpose = tuples.length > 1 ? filter$2(eventOrder[eventName], (o) => exists(tuples, (t) => t.name === o)).join(' > ') : tuples[0].name; 6351 return wrap(eventName, uncurried(assembled, purpose)); 6352 }); 6353 }); 6354 return consolidate(r, {}); 6355 }; 6356 6357 const baseBehaviour = 'alloy.base.behaviour'; 6358 const schema$t = objOf([ 6359 field$1('dom', 'dom', required$2(), objOf([ 6360 // Note, no children. 6361 required$1('tag'), 6362 defaulted('styles', {}), 6363 defaulted('classes', []), 6364 defaulted('attributes', {}), 6365 option$3('value'), 6366 option$3('innerHtml') 6367 ])), 6368 required$1('components'), 6369 required$1('uid'), 6370 defaulted('events', {}), 6371 defaulted('apis', {}), 6372 // Use mergeWith in the future when pre-built behaviours conflict 6373 field$1('eventOrder', 'eventOrder', mergeWith({ 6374 // Note, not using constant behaviour names to avoid code size of unused behaviours 6375 [execute$5()]: ['disabling', baseBehaviour, 'toggling', 'typeaheadevents'], 6376 [focus$3()]: [baseBehaviour, 'focusing', 'keying'], 6377 [systemInit()]: [baseBehaviour, 'disabling', 'toggling', 'representing', 'tooltipping'], 6378 [input()]: [baseBehaviour, 'representing', 'streaming', 'invalidating'], 6379 [detachedFromDom()]: [baseBehaviour, 'representing', 'item-events', 'toolbar-button-events', 'tooltipping'], 6380 [mousedown()]: ['focusing', baseBehaviour, 'item-type-events'], 6381 [touchstart()]: ['focusing', baseBehaviour, 'item-type-events'], 6382 [mouseover()]: ['item-type-events', 'tooltipping'], 6383 [receive()]: ['receiving', 'reflecting', 'tooltipping'] 6384 }), anyValue()), 6385 option$3('domModification') 6386 ]); 6387 const toInfo = (spec) => asRaw('custom.definition', schema$t, spec); 6388 const toDefinition = (detail) => 6389 // EFFICIENCY: Consider not merging here. 6390 ({ 6391 ...detail.dom, 6392 uid: detail.uid, 6393 domChildren: map$2(detail.components, (comp) => comp.element) 6394 }); 6395 const toModification = (detail) => detail.domModification.fold(() => nu$2({}), nu$2); 6396 const toEvents = (info) => info.events; 6397 6398 const diffKeyValueSet = (newObj, oldObj) => { 6399 const newKeys = keys(newObj); 6400 const oldKeys = keys(oldObj); 6401 const toRemove = difference(oldKeys, newKeys); 6402 const toSet = bifilter(newObj, (v, k) => { 6403 return !has$2(oldObj, k) || v !== oldObj[k]; 6404 }).t; 6405 return { toRemove, toSet }; 6406 }; 6407 const reconcileToDom = (definition, obsoleted) => { 6408 const { class: clazz, style, ...existingAttributes } = clone$2(obsoleted); 6409 const { toSet: attrsToSet, toRemove: attrsToRemove } = diffKeyValueSet(definition.attributes, existingAttributes); 6410 const updateAttrs = () => { 6411 each$1(attrsToRemove, (a) => remove$8(obsoleted, a)); 6412 setAll$1(obsoleted, attrsToSet); 6413 }; 6414 const existingStyles = getAllRaw(obsoleted); 6415 const { toSet: stylesToSet, toRemove: stylesToRemove } = diffKeyValueSet(definition.styles, existingStyles); 6416 const updateStyles = () => { 6417 each$1(stylesToRemove, (s) => remove$6(obsoleted, s)); 6418 setAll(obsoleted, stylesToSet); 6419 }; 6420 const existingClasses = get$7(obsoleted); 6421 const classesToRemove = difference(existingClasses, definition.classes); 6422 const classesToAdd = difference(definition.classes, existingClasses); 6423 const updateClasses = () => { 6424 add$1(obsoleted, classesToAdd); 6425 remove$2(obsoleted, classesToRemove); 6426 }; 6427 const updateHtml = (html) => { 6428 set$8(obsoleted, html); 6429 }; 6430 const updateChildren = () => { 6431 const children = definition.domChildren; 6432 patchDomChildren(obsoleted, children); 6433 };
vendor: 52,439 bytes, lines 6434-7594
6434 const updateValue = () => { 6435 const valueElement = obsoleted; 6436 const value = definition.value.getOrUndefined(); 6437 if (value !== get$5(valueElement)) { 6438 // TINY-8736: Value.set throws an error in case the value is undefined 6439 set$4(valueElement, value ?? ''); 6440 } 6441 }; 6442 updateAttrs(); 6443 updateClasses(); 6444 updateStyles(); 6445 // Patching can only support one form of children, so we only update the html or the children, but never both 6446 definition.innerHtml.fold(updateChildren, updateHtml); 6447 updateValue(); 6448 return obsoleted; 6449 }; 6450 6451 const introduceToDom = (definition) => { 6452 const subject = SugarElement.fromTag(definition.tag); 6453 setAll$1(subject, definition.attributes); 6454 add$1(subject, definition.classes); 6455 setAll(subject, definition.styles); 6456 // Remember: Order of innerHtml vs children is important. 6457 definition.innerHtml.each((html) => set$8(subject, html)); 6458 // Children are already elements. 6459 const children = definition.domChildren; 6460 append$1(subject, children); 6461 definition.value.each((value) => { 6462 set$4(subject, value); 6463 }); 6464 return subject; 6465 }; 6466 const attemptPatch = (definition, obsoleted) => { 6467 try { 6468 const e = reconcileToDom(definition, obsoleted); 6469 return Optional.some(e); 6470 } 6471 catch { 6472 return Optional.none(); 6473 } 6474 }; 6475 // If a component has both innerHtml and children then we can't patch it 6476 const hasMixedChildren = (definition) => definition.innerHtml.isSome() && definition.domChildren.length > 0; 6477 const renderToDom = (definition, optObsoleted) => { 6478 // If the current tag doesn't match, let's not try to add anything further down the tree. 6479 // If it does match though and we don't have mixed children then attempt to patch attributes etc... 6480 const canBePatched = (candidate) => name$3(candidate) === definition.tag && !hasMixedChildren(definition) && !isPremade(candidate); 6481 const elem = optObsoleted 6482 .filter(canBePatched) 6483 .bind((obsoleted) => attemptPatch(definition, obsoleted)) 6484 .getOrThunk(() => introduceToDom(definition)); 6485 writeOnly(elem, definition.uid); 6486 return elem; 6487 }; 6488 6489 // This goes through the list of behaviours defined for a particular spec (removing anything 6490 // that has been revoked), and returns the BehaviourType (e.g. Sliding) 6491 const getBehaviours$2 = (spec) => { 6492 const behaviours = get$h(spec, 'behaviours').getOr({}); 6493 return bind$3(keys(behaviours), (name) => { 6494 const behaviour = behaviours[name]; 6495 return isNonNullable(behaviour) ? [behaviour.me] : []; 6496 }); 6497 }; 6498 const generateFrom = (spec, all) => generateFrom$1(spec, all); 6499 const generate$3 = (spec) => { 6500 const all = getBehaviours$2(spec); 6501 return generateFrom(spec, all); 6502 }; 6503 6504 // This is probably far too complicated. I think DomModification is probably 6505 // questionable as a concept. Maybe it should be deprecated. 6506 const getDomDefinition = (info, bList, bData) => { 6507 // Get the current DOM definition from the spec 6508 const definition = toDefinition(info); 6509 // Get the current DOM modification definition from the spec 6510 const infoModification = toModification(info); 6511 // Treat the DOM modification like it came from a behaviour 6512 const baseModification = { 6513 'alloy.base.modification': infoModification 6514 }; 6515 // Combine the modifications from any defined behaviours 6516 const modification = bList.length > 0 ? combine$1(bData, baseModification, bList, definition) : infoModification; 6517 // Transform the DOM definition with the combined dom modifications to make a new DOM definition 6518 return merge(definition, modification); 6519 }; 6520 const getEvents = (info, bList, bData) => { 6521 const baseEvents = { 6522 'alloy.base.behaviour': toEvents(info) 6523 }; 6524 return combine(bData, info.eventOrder, bList, baseEvents).getOrDie(); 6525 }; 6526 const build$2 = (spec, obsoleted) => { 6527 const getMe = () => me; 6528 const systemApi = Cell(singleton); 6529 const info = getOrDie(toInfo(spec)); 6530 const bBlob = generate$3(spec); 6531 const bList = getBehaviours$3(bBlob); 6532 const bData = getData$3(bBlob); 6533 const modDefinition = getDomDefinition(info, bList, bData); 6534 const item = renderToDom(modDefinition, obsoleted); 6535 const events = getEvents(info, bList, bData); 6536 const subcomponents = Cell(info.components); 6537 const connect = (newApi) => { 6538 systemApi.set(newApi); 6539 }; 6540 const disconnect = () => { 6541 systemApi.set(NoContextApi(getMe)); 6542 }; 6543 const syncComponents = () => { 6544 // Update the component list with the current children 6545 const children$1 = children(item); 6546 // INVESTIGATE: Not sure about how to handle text nodes here. 6547 const subs = bind$3(children$1, (child) => systemApi.get().getByDom(child).fold(() => [], pure$2)); 6548 subcomponents.set(subs); 6549 }; 6550 // TYPIFY (any here is for the info.apis() pathway) 6551 const config = (behaviour) => { 6552 const b = bData; 6553 const f = isFunction(b[behaviour.name()]) ? b[behaviour.name()] : () => { 6554 throw new Error('Could not find ' + behaviour.name() + ' in ' + JSON.stringify(spec, null, 2)); 6555 }; 6556 return f(); 6557 }; 6558 const hasConfigured = (behaviour) => isFunction(bData[behaviour.name()]); 6559 const getApis = () => info.apis; 6560 const readState = (behaviourName) => bData[behaviourName]().map((b) => b.state.readState()).getOr('not enabled'); 6561 const me = { 6562 uid: spec.uid, 6563 getSystem: systemApi.get, 6564 config, 6565 hasConfigured, 6566 spec, 6567 readState, 6568 getApis, 6569 connect, 6570 disconnect, 6571 element: item, 6572 syncComponents, 6573 components: subcomponents.get, 6574 events 6575 }; 6576 return me; 6577 }; 6578 6579 const buildSubcomponents = (spec, obsoleted) => { 6580 const components = get$h(spec, 'components').getOr([]); 6581 return obsoleted.fold(() => map$2(components, build$1), (obs) => map$2(components, (c, i) => { 6582 return buildOrPatch(c, child$2(obs, i)); 6583 })); 6584 }; 6585 const buildFromSpec = (userSpec, obsoleted) => { 6586 const { events: specEvents, ...spec } = make$7(userSpec); 6587 // Build the subcomponents. A spec hierarchy is built from the bottom up. 6588 // obsoleted is used to define which element we are attempting to replace 6589 // so that it might be used to patch the DOM instead of recreate it. 6590 const components = buildSubcomponents(spec, obsoleted); 6591 const completeSpec = { 6592 ...spec, 6593 events: { ...DefaultEvents, ...specEvents }, 6594 components 6595 }; 6596 return Result.value( 6597 // Note, this isn't a spec any more, because it has built children 6598 build$2(completeSpec, obsoleted)); 6599 }; 6600 const text$2 = (textContent) => { 6601 const element = SugarElement.fromText(textContent); 6602 return external({ 6603 element 6604 }); 6605 }; 6606 const external = (spec) => { 6607 const extSpec = asRawOrDie$1('external.component', objOfOnly([ 6608 required$1('element'), 6609 option$3('uid') 6610 ]), spec); 6611 const systemApi = Cell(NoContextApi()); 6612 const connect = (newApi) => { 6613 systemApi.set(newApi); 6614 }; 6615 const disconnect = () => { 6616 systemApi.set(NoContextApi(() => me)); 6617 }; 6618 const uid = extSpec.uid.getOrThunk(() => generate$4('external')); 6619 writeOnly(extSpec.element, uid); 6620 const me = { 6621 uid, 6622 getSystem: systemApi.get, 6623 config: Optional.none, 6624 hasConfigured: never, 6625 connect, 6626 disconnect, 6627 getApis: () => ({}), 6628 element: extSpec.element, 6629 spec, 6630 readState: constant$1('No state'), 6631 syncComponents: noop, 6632 components: constant$1([]), 6633 events: {} 6634 }; 6635 return premade$1(me); 6636 }; 6637 // We experimented with just having a counter for efficiency, but that fails for situations 6638 // where an external JS file is using alloy, and is contained within another 6639 // alloy root container. The ids can conflict, because the counters do not 6640 // know about each other (being parts of separate scripts). 6641 // 6642 // There are other solutions than this ... not sure if they are going to have better performance, though 6643 const uids = generate$4; 6644 const isSketchSpec$1 = (spec) => has$2(spec, 'uid'); 6645 // INVESTIGATE: A better way to provide 'meta-specs' 6646 const buildOrPatch = (spec, obsoleted) => getPremade(spec).getOrThunk(() => { 6647 // EFFICIENCY: Consider not merging here, and passing uid through separately 6648 const userSpecWithUid = isSketchSpec$1(spec) ? spec : { 6649 uid: uids(''), 6650 ...spec 6651 }; 6652 return buildFromSpec(userSpecWithUid, obsoleted).getOrDie(); 6653 }); 6654 const build$1 = (spec) => buildOrPatch(spec, Optional.none()); 6655 const premade = premade$1; 6656 6657 // Mark this component as busy, or blocked. 6658 const block = (component, config, state, 6659 // This works in conjunction with the 'getRoot' function in the config. To 6660 // attach a blocker component to the dom, ensure that 'getRoot' returns a 6661 // component, and this function returns the specification of the component to 6662 // attach. 6663 getBusySpec) => { 6664 set$9(component.element, 'aria-busy', true); 6665 const root = config.getRoot(component).getOr(component); 6666 const blockerBehaviours = derive$1([ 6667 // Trap the "Tab" key and don't let it escape. 6668 Keying.config({ 6669 mode: 'special', 6670 onTab: () => Optional.some(true), 6671 onShiftTab: () => Optional.some(true) 6672 }), 6673 Focusing.config({}) 6674 ]); 6675 const blockSpec = getBusySpec(root, blockerBehaviours); 6676 const blocker = root.getSystem().build(blockSpec); 6677 Replacing.append(root, premade(blocker)); 6678 if (blocker.hasConfigured(Keying) && config.focus) { 6679 Keying.focusIn(blocker); 6680 } 6681 if (!state.isBlocked()) { 6682 config.onBlock(component); 6683 } 6684 state.blockWith(() => Replacing.remove(root, blocker)); 6685 }; 6686 // Mark this component as unblocked, or not busy. This is a noop on a component 6687 // that isn't blocked. 6688 const unblock = (component, config, state) => { 6689 remove$8(component.element, 'aria-busy'); 6690 if (state.isBlocked()) { 6691 config.onUnblock(component); 6692 } 6693 state.clear(); 6694 }; 6695 const isBlocked = (component, blockingConfig, blockingState) => blockingState.isBlocked(); 6696 6697 var BlockingApis = /*#__PURE__*/Object.freeze({ 6698 __proto__: null, 6699 block: block, 6700 unblock: unblock, 6701 isBlocked: isBlocked 6702 }); 6703 6704 var BlockingSchema = [ 6705 // The blocking behaviour places a blocking element over the DOM while the 6706 // component is in the blocked state. If a function is provided here that 6707 // returns Some, then the blocking element will be added as a child of the 6708 // element returned. Otherwise, it will be added as a child of the main 6709 // component. 6710 defaultedFunction('getRoot', Optional.none), 6711 // This boolean, if provided, will specify whether the blocking element is 6712 // focused when the component is first blocked 6713 defaultedBoolean('focus', true), 6714 // This function, if provided, will be called any time the component is 6715 // blocked (unless it was already blocked). 6716 onHandler('onBlock'), 6717 // This function, if provided, will be called any time the component is 6718 // unblocked (unless it was already unblocked). 6719 onHandler('onUnblock') 6720 ]; 6721 6722 const init$f = () => { 6723 const blocker = destroyable(); 6724 const blockWith = (destroy) => { 6725 blocker.set({ destroy }); 6726 }; 6727 return nu$4({ 6728 readState: blocker.isSet, 6729 blockWith, 6730 clear: blocker.clear, 6731 isBlocked: blocker.isSet 6732 }); 6733 }; 6734 6735 var BlockingState = /*#__PURE__*/Object.freeze({ 6736 __proto__: null, 6737 init: init$f 6738 }); 6739 6740 // Mark a component as able to be "Blocked" or able to enter a busy state. See 6741 // BlockingSchema and BlockingApis for more details on how to configure this. 6742 const Blocking = create$3({ 6743 fields: BlockingSchema, 6744 name: 'blocking', 6745 apis: BlockingApis, 6746 state: BlockingState 6747 }); 6748 6749 const getCurrent = (component, composeConfig, _composeState) => composeConfig.find(component); 6750 6751 var ComposeApis = /*#__PURE__*/Object.freeze({ 6752 __proto__: null, 6753 getCurrent: getCurrent 6754 }); 6755 6756 const ComposeSchema = [ 6757 required$1('find') 6758 ]; 6759 6760 const Composing = create$3({ 6761 fields: ComposeSchema, 6762 name: 'composing', 6763 apis: ComposeApis 6764 }); 6765 6766 const getCoupled = (component, coupleConfig, coupleState, name) => coupleState.getOrCreate(component, coupleConfig, name); 6767 const getExistingCoupled = (component, coupleConfig, coupleState, name) => coupleState.getExisting(component, coupleConfig, name); 6768 6769 var CouplingApis = /*#__PURE__*/Object.freeze({ 6770 __proto__: null, 6771 getCoupled: getCoupled, 6772 getExistingCoupled: getExistingCoupled 6773 }); 6774 6775 var CouplingSchema = [ 6776 requiredOf('others', setOf(Result.value, anyValue())) 6777 ]; 6778 6779 // Unfortunately, the Coupling APIs currently throw errors when the coupled name 6780 // is not recognised. This is because if the wrong name is used, it is a 6781 // non-recoverable error, and the developer should be notified. However, there are 6782 // better ways to do this: (removing this API and only returning Optionals/Results) 6783 const init$e = () => { 6784 const coupled = {}; 6785 const lookupCoupled = (coupleConfig, coupledName) => { 6786 const available = keys(coupleConfig.others); 6787 if (available.length === 0) { 6788 throw new Error('Cannot find any known coupled components'); 6789 } 6790 else { 6791 return get$h(coupled, coupledName); 6792 } 6793 }; 6794 const getOrCreate = (component, coupleConfig, name) => { 6795 return lookupCoupled(coupleConfig, name).getOrThunk(() => { 6796 // TODO: TINY-9014 Likely type error. coupleConfig.others[key] is 6797 // `() => ((comp: AlloyComponent) => AlloySpec)`, 6798 // but builder is being treated as a `(comp: AlloyComponent) => AlloySpec` 6799 const builder = get$h(coupleConfig.others, name).getOrDie('No information found for coupled component: ' + name); 6800 const spec = builder(component); 6801 const built = component.getSystem().build(spec); 6802 coupled[name] = built; 6803 return built; 6804 }); 6805 }; 6806 const getExisting = (component, coupleConfig, name) => { 6807 return lookupCoupled(coupleConfig, name).orThunk(() => { 6808 // Validate we recognise this coupled component's name. 6809 get$h(coupleConfig.others, name).getOrDie('No information found for coupled component: ' + name); 6810 // It's a valid name, so return None, because it hasn't been built yet. 6811 return Optional.none(); 6812 }); 6813 }; 6814 const readState = constant$1({}); 6815 return nu$4({ 6816 readState, 6817 getExisting, 6818 getOrCreate 6819 }); 6820 }; 6821 6822 var CouplingState = /*#__PURE__*/Object.freeze({ 6823 __proto__: null, 6824 init: init$e 6825 }); 6826 6827 const Coupling = create$3({ 6828 fields: CouplingSchema, 6829 name: 'coupling', 6830 apis: CouplingApis, 6831 state: CouplingState 6832 }); 6833 6834 // Just use "disabled" attribute for these, not "aria-disabled" 6835 const nativeDisabled = [ 6836 'input', 6837 'button', 6838 'textarea', 6839 'select' 6840 ]; 6841 const onLoad$5 = (component, disableConfig, disableState) => { 6842 const f = disableConfig.disabled() ? disable : enable; 6843 f(component, disableConfig); 6844 }; 6845 const hasNative = (component, config) => config.useNative === true && contains$2(nativeDisabled, name$3(component.element)); 6846 const nativeIsDisabled = (component) => has$1(component.element, 'disabled'); 6847 const nativeDisable = (component) => { 6848 set$9(component.element, 'disabled', 'disabled'); 6849 }; 6850 const nativeEnable = (component) => { 6851 remove$8(component.element, 'disabled'); 6852 }; 6853 const ariaIsDisabled = (component) => get$g(component.element, 'aria-disabled') === 'true'; 6854 const ariaDisable = (component) => { 6855 set$9(component.element, 'aria-disabled', 'true'); 6856 }; 6857 const ariaEnable = (component) => { 6858 set$9(component.element, 'aria-disabled', 'false'); 6859 }; 6860 const disable = (component, disableConfig, _disableState) => { 6861 disableConfig.disableClass.each((disableClass) => { 6862 add$2(component.element, disableClass); 6863 }); 6864 const f = hasNative(component, disableConfig) ? nativeDisable : ariaDisable; 6865 f(component); 6866 disableConfig.onDisabled(component); 6867 }; 6868 const enable = (component, disableConfig, _disableState) => { 6869 disableConfig.disableClass.each((disableClass) => { 6870 remove$3(component.element, disableClass); 6871 }); 6872 const f = hasNative(component, disableConfig) ? nativeEnable : ariaEnable; 6873 f(component); 6874 disableConfig.onEnabled(component); 6875 }; 6876 const isDisabled$1 = (component, disableConfig) => hasNative(component, disableConfig) ? nativeIsDisabled(component) : ariaIsDisabled(component); 6877 const set$2 = (component, disableConfig, disableState, disabled) => { 6878 const f = disabled ? disable : enable; 6879 f(component, disableConfig); 6880 }; 6881 6882 var DisableApis = /*#__PURE__*/Object.freeze({ 6883 __proto__: null, 6884 enable: enable, 6885 disable: disable, 6886 isDisabled: isDisabled$1, 6887 onLoad: onLoad$5, 6888 set: set$2 6889 }); 6890 6891 const exhibit$5 = (base, disableConfig) => nu$2({ 6892 // Do not add the attribute yet, because it will depend on the node name 6893 // if we use "aria-disabled" or just "disabled" 6894 classes: disableConfig.disabled() ? disableConfig.disableClass.toArray() : [] 6895 }); 6896 const events$g = (disableConfig, disableState) => derive$2([ 6897 abort(execute$5(), (component, _simulatedEvent) => isDisabled$1(component, disableConfig)), 6898 loadEvent(disableConfig, disableState, onLoad$5) 6899 ]); 6900 6901 var ActiveDisable = /*#__PURE__*/Object.freeze({ 6902 __proto__: null, 6903 exhibit: exhibit$5, 6904 events: events$g 6905 }); 6906 6907 var DisableSchema = [ 6908 defaultedFunction('disabled', never), 6909 defaulted('useNative', true), 6910 option$3('disableClass'), 6911 onHandler('onDisabled'), 6912 onHandler('onEnabled') 6913 ]; 6914 6915 const Disabling = create$3({ 6916 fields: DisableSchema, 6917 name: 'disabling', 6918 active: ActiveDisable, 6919 apis: DisableApis 6920 }); 6921 6922 const NuPositionCss = (position, left, top, right, bottom) => { 6923 const toPx = (num) => num + 'px'; 6924 return { 6925 position, 6926 left: left.map(toPx), 6927 top: top.map(toPx), 6928 right: right.map(toPx), 6929 bottom: bottom.map(toPx) 6930 }; 6931 }; 6932 const toOptions = (position) => ({ 6933 ...position, 6934 position: Optional.some(position.position) 6935 }); 6936 const applyPositionCss = (element, position) => { 6937 setOptions(element, toOptions(position)); 6938 }; 6939 6940 const appear = (component, contextualInfo) => { 6941 const elem = component.element; 6942 add$2(elem, contextualInfo.transitionClass); 6943 remove$3(elem, contextualInfo.fadeOutClass); 6944 add$2(elem, contextualInfo.fadeInClass); 6945 contextualInfo.onShow(component); 6946 }; 6947 const disappear = (component, contextualInfo) => { 6948 const elem = component.element; 6949 add$2(elem, contextualInfo.transitionClass); 6950 remove$3(elem, contextualInfo.fadeInClass); 6951 add$2(elem, contextualInfo.fadeOutClass); 6952 contextualInfo.onHide(component); 6953 }; 6954 const isPartiallyVisible = (box, bounds) => box.y < bounds.bottom && box.bottom > bounds.y; 6955 const isTopCompletelyVisible = (box, bounds) => box.y >= bounds.y; 6956 const isBottomCompletelyVisible = (box, bounds) => box.bottom <= bounds.bottom; 6957 const forceTopPosition = (winBox, leftX, viewport) => ({ 6958 location: 'top', 6959 leftX, 6960 topY: viewport.bounds.y - winBox.y 6961 }); 6962 const forceBottomPosition = (winBox, leftX, viewport) => ({ 6963 location: 'bottom', 6964 leftX, 6965 bottomY: winBox.bottom - viewport.bounds.bottom 6966 }); 6967 const getDockedLeftPosition = (bounds) => { 6968 // Essentially, we are just getting the bounding client rect left here, 6969 // because winBox.x will be the scroll value. 6970 return bounds.box.x - bounds.win.x; 6971 }; 6972 const tryDockingPosition = (modes, bounds, viewport) => { 6973 const winBox = bounds.win; 6974 const box = bounds.box; 6975 const leftX = getDockedLeftPosition(bounds); 6976 return findMap(modes, (mode) => { 6977 switch (mode) { 6978 case 'bottom': 6979 return !isBottomCompletelyVisible(box, viewport.bounds) ? Optional.some(forceBottomPosition(winBox, leftX, viewport)) : Optional.none(); 6980 case 'top': 6981 return !isTopCompletelyVisible(box, viewport.bounds) ? Optional.some(forceTopPosition(winBox, leftX, viewport)) : Optional.none(); 6982 default: 6983 return Optional.none(); 6984 } 6985 }).getOr({ 6986 location: 'no-dock' 6987 }); 6988 }; 6989 const isVisibleForModes = (modes, box, viewport) => forall(modes, (mode) => { 6990 switch (mode) { 6991 case 'bottom': 6992 return isBottomCompletelyVisible(box, viewport.bounds); 6993 case 'top': 6994 return isTopCompletelyVisible(box, viewport.bounds); 6995 } 6996 }); 6997 const getXYForRestoring = (pos, viewport) => { 6998 const priorY = viewport.optScrollEnv.fold(constant$1(pos.bounds.y), (scrollEnv) => scrollEnv.scrollElmTop + (pos.bounds.y - scrollEnv.currentScrollTop)); 6999 return SugarPosition(pos.bounds.x, priorY); 7000 }; 7001 const getXYForSaving = (box, viewport) => { 7002 const priorY = viewport.optScrollEnv.fold(constant$1(box.y), (scrollEnv) => box.y + scrollEnv.currentScrollTop - scrollEnv.scrollElmTop); 7003 return SugarPosition(box.x, priorY); 7004 }; 7005 const getPrior = (elem, viewport, state) => state.getInitialPos().map((pos) => { 7006 const xy = getXYForRestoring(pos, viewport); 7007 return { 7008 box: bounds(xy.left, xy.top, get$c(elem), get$d(elem)), 7009 location: pos.location 7010 }; 7011 }); 7012 const storePrior = (elem, box, viewport, state, decision) => { 7013 const xy = getXYForSaving(box, viewport); 7014 const bounds$1 = bounds(xy.left, xy.top, box.width, box.height); 7015 state.setInitialPos({ 7016 style: getAllRaw(elem), 7017 position: get$e(elem, 'position') || 'static', 7018 bounds: bounds$1, 7019 location: decision.location 7020 }); 7021 }; 7022 // When we are using APIs like forceDockToTop, then we only want to store the previous position 7023 // if we weren't already docked. Otherwise, we still want to move the component, but keep its old 7024 // restore values 7025 const storePriorIfNone = (elem, box, viewport, state, decision) => { 7026 state.getInitialPos().fold(() => storePrior(elem, box, viewport, state, decision), () => noop); 7027 }; 7028 const revertToOriginal = (elem, box, state) => state.getInitialPos().bind((position) => { 7029 state.clearInitialPos(); 7030 switch (position.position) { 7031 case 'static': 7032 return Optional.some({ 7033 morph: 'static' 7034 }); 7035 case 'absolute': 7036 const offsetParent = getOffsetParent(elem).getOr(body()); 7037 const offsetBox = box$1(offsetParent); 7038 // Adding the scrollDelta here may not be the right solution. The basic problem is that the 7039 // rest of the code isn't considering whether its absolute or not, and where the offset parent 7040 // is. In the situation where the offset parent is *inside* the scrolling environment, then 7041 // we don't need to consider the scroll, and that's what getXYForRestoring does ... it removes 7042 // the scroll. We don't need to consider the scroll because the sink is already affected by the 7043 // scroll. However, when the sink IS the scroller, its position is not moved by scrolling. But the 7044 // positions of everything inside it needs to consider the scroll. So we add the scroll value. 7045 // 7046 // This might also be a bit naive. It's possible that we need to check that the offsetParent 7047 // is THE scroller, not just that it has a scroll value. For example, if the offset parent 7048 // was the body, and the body had a scroll, this might give unexpected results. That's somewhat 7049 // countered by the fact that if the offset parent is outside the scroller, then you don't really 7050 // have a scrolling environment any more, because the offset parent isn't going to be impacted 7051 // at all by the scroller 7052 const scrollDelta = offsetParent.dom.scrollTop ?? 0; 7053 return Optional.some({ 7054 morph: 'absolute', 7055 positionCss: NuPositionCss('absolute', get$h(position.style, 'left').map((_left) => box.x - offsetBox.x), get$h(position.style, 'top').map((_top) => box.y - offsetBox.y + scrollDelta), get$h(position.style, 'right').map((_right) => offsetBox.right - box.right), get$h(position.style, 'bottom').map((_bottom) => offsetBox.bottom - box.bottom)) 7056 }); 7057 default: 7058 return Optional.none(); 7059 } 7060 }); 7061 const tryMorphToOriginal = (elem, viewport, state) => getPrior(elem, viewport, state) 7062 .filter(({ box }) => isVisibleForModes(state.getModes(), box, viewport)) 7063 .bind(({ box }) => revertToOriginal(elem, box, state)); 7064 const tryDecisionToFixedMorph = (decision) => { 7065 switch (decision.location) { 7066 case 'top': { 7067 // We store our current position so we can revert to it once it's 7068 // visible again. 7069 return Optional.some({ 7070 morph: 'fixed', 7071 positionCss: NuPositionCss('fixed', Optional.some(decision.leftX), Optional.some(decision.topY), Optional.none(), Optional.none()) 7072 }); 7073 } 7074 case 'bottom': { 7075 // We store our current position so we can revert to it once it's 7076 // visible again. 7077 return Optional.some({ 7078 morph: 'fixed', 7079 positionCss: NuPositionCss('fixed', Optional.some(decision.leftX), Optional.none(), Optional.none(), Optional.some(decision.bottomY)) 7080 }); 7081 } 7082 default: 7083 return Optional.none(); 7084 } 7085 }; 7086 const tryMorphToFixed = (elem, viewport, state) => { 7087 const box = box$1(elem); 7088 const winBox = win(); 7089 const decision = tryDockingPosition(state.getModes(), { 7090 win: winBox, 7091 box 7092 }, viewport); 7093 if (decision.location === 'top' || decision.location === 'bottom') { 7094 // We are moving from undocked to docked, so store the previous location 7095 // so that we can restore it when we switch out of docking (back to undocked) 7096 storePrior(elem, box, viewport, state, decision); 7097 return tryDecisionToFixedMorph(decision); 7098 } 7099 else { 7100 return Optional.none(); 7101 } 7102 }; 7103 const tryMorphToOriginalOrUpdateFixed = (elem, viewport, state) => { 7104 // When a "docked" element is docked to the top of a scroll container (due to optScrollEnv in 7105 // viewport), we need to reposition its fixed if the scroll container has itself moved its top position. 7106 // This isn't required when the docking is to the top of the window, because the entire window cannot 7107 // be scrolled up and down the page - it is the page. 7108 // 7109 // Imagine a situation where the toolbar has docked to the top of the scroll container, which is at 7110 // y = 200. Now, when the user scrolls the page another 50px down the page, the top of the scroll 7111 // container will now be 150px, but the "fixed" toolbar will still be at "200px". So this is a morph 7112 // from "fixed" to "fixed", but with new coordinates. So if we can't morph to original from "fixed", 7113 // we try to update our "fixed" position (if we have a scrolling environment in the viewport) 7114 return tryMorphToOriginal(elem, viewport, state) 7115 .orThunk(() => { 7116 // Importantly, we don't update our stored position for the element before "docking", because 7117 // this is a transition between "docked" and "docked", not "undocked" and "docked". We want to 7118 // keep our undocked position in our store, not a docked position. 7119 // So we don't change our stored position. We just improve our fixed. 7120 return viewport.optScrollEnv 7121 .bind((_) => getPrior(elem, viewport, state)) 7122 .bind(({ box, location }) => { 7123 const winBox = win(); 7124 const leftX = getDockedLeftPosition({ win: winBox, box }); 7125 // Keep the same docking location 7126 const decision = location === 'top' 7127 ? forceTopPosition(winBox, leftX, viewport) 7128 : forceBottomPosition(winBox, leftX, viewport); 7129 return tryDecisionToFixedMorph(decision); 7130 }); 7131 }); 7132 }; 7133 const tryMorph = (component, viewport, state) => { 7134 const elem = component.element; 7135 const isDocked = is$1(getRaw(elem, 'position'), 'fixed'); 7136 return isDocked 7137 ? tryMorphToOriginalOrUpdateFixed(elem, viewport, state) 7138 : tryMorphToFixed(elem, viewport, state); 7139 }; 7140 // The difference between the "calculate" functions and the "try" functions is that the "try" functions 7141 // will first consider whether there is a need to morph, whereas the "calculate" functions will just 7142 // give you the morph details, bypassing the check to see if it's needed 7143 const calculateMorphToOriginal = (component, viewport, state) => { 7144 const elem = component.element; 7145 return getPrior(elem, viewport, state) 7146 .bind(({ box }) => revertToOriginal(elem, box, state)); 7147 }; 7148 const forceDockWith = (elem, viewport, state, getDecision) => { 7149 const box = box$1(elem); 7150 const winBox = win(); 7151 const leftX = getDockedLeftPosition({ win: winBox, box }); 7152 const decision = getDecision(winBox, leftX, viewport); 7153 if (decision.location === 'bottom' || decision.location === 'top') { 7154 // We only want to store the values if we aren't already docking. If we are already docking, then 7155 // we just want to move the element, without updating where it started originally 7156 storePriorIfNone(elem, box, viewport, state, decision); 7157 return tryDecisionToFixedMorph(decision); 7158 } 7159 else { 7160 return Optional.none(); 7161 } 7162 }; 7163 7164 const morphToStatic = (component, config, state) => { 7165 state.setDocked(false); 7166 each$1(['left', 'right', 'top', 'bottom', 'position'], (prop) => remove$6(component.element, prop)); 7167 config.onUndocked(component); 7168 }; 7169 const morphToCoord = (component, config, state, position) => { 7170 const isDocked = position.position === 'fixed'; 7171 state.setDocked(isDocked); 7172 applyPositionCss(component.element, position); 7173 const method = isDocked ? config.onDocked : config.onUndocked; 7174 method(component); 7175 }; 7176 const updateVisibility = (component, config, state, viewport, morphToDocked = false) => { 7177 config.contextual.each((contextInfo) => { 7178 // Make the dockable component disappear if the context is outside the viewport 7179 contextInfo.lazyContext(component).each((box) => { 7180 const isVisible = isPartiallyVisible(box, viewport.bounds); 7181 if (isVisible !== state.isVisible()) { 7182 state.setVisible(isVisible); 7183 // If morphing to docked and the context isn't visible then immediately set 7184 // the fadeout class and don't worry about transitioning, as the context 7185 // would never have been in view while docked 7186 if (morphToDocked && !isVisible) { 7187 add$1(component.element, [contextInfo.fadeOutClass]); 7188 contextInfo.onHide(component); 7189 } 7190 else { 7191 const method = isVisible ? appear : disappear; 7192 method(component, contextInfo); 7193 } 7194 } 7195 }); 7196 }); 7197 }; 7198 const applyFixedMorph = (component, config, state, viewport, morph) => { 7199 // This "updateVisibility" call is potentially duplicated with the 7200 // call in refreshInternal for isDocked. We might want to consolidate them. 7201 // The difference between them is the "morphToDocked" flag. 7202 updateVisibility(component, config, state, viewport, true); 7203 morphToCoord(component, config, state, morph.positionCss); 7204 }; 7205 const applyMorph = (component, config, state, viewport, morph) => { 7206 // Apply the morph result depending on its type 7207 switch (morph.morph) { 7208 case 'static': { 7209 return morphToStatic(component, config, state); 7210 } 7211 case 'absolute': { 7212 return morphToCoord(component, config, state, morph.positionCss); 7213 } 7214 case 'fixed': { 7215 return applyFixedMorph(component, config, state, viewport, morph); 7216 } 7217 } 7218 }; 7219 const refreshInternal = (component, config, state) => { 7220 // Absolute coordinates (considers scroll) 7221 const viewport = config.lazyViewport(component); 7222 updateVisibility(component, config, state, viewport); 7223 tryMorph(component, viewport, state).each((morph) => { 7224 applyMorph(component, config, state, viewport, morph); 7225 }); 7226 }; 7227 const resetInternal = (component, config, state) => { 7228 // Morph back to the original position 7229 const elem = component.element; 7230 state.setDocked(false); 7231 const viewport = config.lazyViewport(component); 7232 calculateMorphToOriginal(component, viewport, state).each((staticOrAbsoluteMorph) => { 7233 // This code is very similar to the "applyMorph" function above. The main difference 7234 // is that it doesn't consider fixed position, because something that is docking 7235 // can't currently start with fixed position 7236 switch (staticOrAbsoluteMorph.morph) { 7237 case 'static': { 7238 morphToStatic(component, config, state); 7239 break; 7240 } 7241 case 'absolute': { 7242 morphToCoord(component, config, state, staticOrAbsoluteMorph.positionCss); 7243 break; 7244 } 7245 } 7246 }); 7247 // Remove contextual visibility classes 7248 state.setVisible(true); 7249 config.contextual.each((contextInfo) => { 7250 remove$2(elem, [contextInfo.fadeInClass, contextInfo.fadeOutClass, contextInfo.transitionClass]); 7251 contextInfo.onShow(component); 7252 }); 7253 // Apply docking again to reset the position 7254 refresh$4(component, config, state); 7255 }; 7256 const refresh$4 = (component, config, state) => { 7257 // Ensure the component is attached to the document/world, if not then do nothing as we can't 7258 // check if the component should be docked or not when in a detached state 7259 if (component.getSystem().isConnected()) { 7260 refreshInternal(component, config, state); 7261 } 7262 }; 7263 const reset$1 = (component, config, state) => { 7264 // If the component is not docked then there's no need to reset the state, 7265 // so only reset when docked 7266 if (state.isDocked()) { 7267 resetInternal(component, config, state); 7268 } 7269 }; 7270 const forceDockWithDecision = (getDecision) => (component, config, state) => { 7271 const viewport = config.lazyViewport(component); 7272 const optMorph = forceDockWith(component.element, viewport, state, getDecision); 7273 optMorph.each((morph) => { 7274 // ASSUMPTION: This "applyFixedMorph" sets state.setDocked to true. 7275 applyFixedMorph(component, config, state, viewport, morph); 7276 }); 7277 }; 7278 const forceDockToTop = forceDockWithDecision(forceTopPosition); 7279 const forceDockToBottom = forceDockWithDecision(forceBottomPosition); 7280 const isDocked$2 = (component, config, state) => state.isDocked(); 7281 const setModes = (component, config, state, modes) => state.setModes(modes); 7282 const getModes = (component, config, state) => state.getModes(); 7283 7284 var DockingApis = /*#__PURE__*/Object.freeze({ 7285 __proto__: null, 7286 refresh: refresh$4, 7287 reset: reset$1, 7288 isDocked: isDocked$2, 7289 getModes: getModes, 7290 setModes: setModes, 7291 forceDockToTop: forceDockToTop, 7292 forceDockToBottom: forceDockToBottom 7293 }); 7294 7295 const events$f = (dockInfo, dockState) => derive$2([ 7296 runOnSource(transitionend(), (component, simulatedEvent) => { 7297 dockInfo.contextual.each((contextInfo) => { 7298 if (has(component.element, contextInfo.transitionClass)) { 7299 remove$2(component.element, [contextInfo.transitionClass, contextInfo.fadeInClass]); 7300 const notify = dockState.isVisible() ? contextInfo.onShown : contextInfo.onHidden; 7301 notify(component); 7302 } 7303 simulatedEvent.stop(); 7304 }); 7305 }), 7306 run$1(windowScroll(), (component, _) => { 7307 refresh$4(component, dockInfo, dockState); 7308 }), 7309 run$1(externalElementScroll(), (component, _) => { 7310 refresh$4(component, dockInfo, dockState); 7311 }), 7312 run$1(windowResize(), (component, _) => { 7313 reset$1(component, dockInfo, dockState); 7314 }) 7315 ]); 7316 7317 var ActiveDocking = /*#__PURE__*/Object.freeze({ 7318 __proto__: null, 7319 events: events$f 7320 }); 7321 7322 var DockingSchema = [ 7323 optionObjOf('contextual', [ 7324 requiredString('fadeInClass'), 7325 requiredString('fadeOutClass'), 7326 requiredString('transitionClass'), 7327 requiredFunction('lazyContext'), 7328 onHandler('onShow'), 7329 onHandler('onShown'), 7330 onHandler('onHide'), 7331 onHandler('onHidden') 7332 ]), 7333 defaultedFunction('lazyViewport', () => ({ 7334 bounds: win(), 7335 optScrollEnv: Optional.none() 7336 })), 7337 defaultedArrayOf('modes', ['top', 'bottom'], string), 7338 onHandler('onDocked'), 7339 onHandler('onUndocked') 7340 ]; 7341 7342 const init$d = (spec) => { 7343 const docked = Cell(false); 7344 const visible = Cell(true); 7345 const initialBounds = value$2(); 7346 const modes = Cell(spec.modes); 7347 const readState = () => `docked: ${docked.get()}, visible: ${visible.get()}, modes: ${modes.get().join(',')}`; 7348 return nu$4({ 7349 isDocked: docked.get, 7350 setDocked: docked.set, 7351 getInitialPos: initialBounds.get, 7352 setInitialPos: initialBounds.set, 7353 clearInitialPos: initialBounds.clear, 7354 isVisible: visible.get, 7355 setVisible: visible.set, 7356 getModes: modes.get, 7357 setModes: modes.set, 7358 readState 7359 }); 7360 }; 7361 7362 var DockingState = /*#__PURE__*/Object.freeze({ 7363 __proto__: null, 7364 init: init$d 7365 }); 7366 7367 const Docking = create$3({ 7368 fields: DockingSchema, 7369 name: 'docking', 7370 active: ActiveDocking, 7371 apis: DockingApis, 7372 state: DockingState 7373 }); 7374 7375 /* 7376 * origin: the position (without scroll) of the offset parent 7377 * scroll: the scrolling position of the window 7378 * 7379 * fixed: the fixed coordinates to show for css 7380 * offset: the absolute coordinates to show for css when inside an offset parent 7381 * absolute: the absolute coordinates to show before considering the offset parent 7382 */ 7383 const adt$4 = Adt.generate([ 7384 { offset: ['x', 'y'] }, 7385 { absolute: ['x', 'y'] }, 7386 { fixed: ['x', 'y'] } 7387 ]); 7388 const subtract = (change) => (point) => point.translate(-change.left, -change.top); 7389 const add = (change) => (point) => point.translate(change.left, change.top); 7390 const transform = (changes) => (x, y) => foldl(changes, (rest, f) => f(rest), SugarPosition(x, y)); 7391 const asFixed = (coord, scroll, origin) => coord.fold( 7392 // offset to fixed 7393 transform([add(origin), subtract(scroll)]), 7394 // absolute to fixed 7395 transform([subtract(scroll)]), 7396 // fixed to fixed 7397 transform([])); 7398 const asAbsolute = (coord, scroll, origin) => coord.fold( 7399 // offset to absolute 7400 transform([add(origin)]), 7401 // absolute to absolute 7402 transform([]), 7403 // fixed to absolute 7404 transform([add(scroll)])); 7405 const asOffset = (coord, scroll, origin) => coord.fold( 7406 // offset to offset 7407 transform([]), 7408 // absolute to offset 7409 transform([subtract(origin)]), 7410 // fixed to offset 7411 transform([add(scroll), subtract(origin)])); 7412 const withinRange = (coord1, coord2, xRange, yRange, scroll, origin) => { 7413 const a1 = asAbsolute(coord1, scroll, origin); 7414 const a2 = asAbsolute(coord2, scroll, origin); 7415 // console.log(`a1.left: ${a1.left}, a2.left: ${a2.left}, leftDelta: ${a1.left - a2.left}, xRange: ${xRange}, lD <= xRange: ${Math.abs(a1.left - a2.left) <= xRange}`); 7416 // console.log(`a1.top: ${a1.top}, a2.top: ${a2.top}, topDelta: ${a1.top - a2.top}, yRange: ${yRange}, lD <= xRange: ${Math.abs(a1.top - a2.top) <= yRange}`); 7417 return Math.abs(a1.left - a2.left) <= xRange && 7418 Math.abs(a1.top - a2.top) <= yRange; 7419 }; 7420 const getDeltas = (coord1, coord2, xRange, yRange, scroll, origin) => { 7421 const a1 = asAbsolute(coord1, scroll, origin); 7422 const a2 = asAbsolute(coord2, scroll, origin); 7423 const left = Math.abs(a1.left - a2.left); 7424 const top = Math.abs(a1.top - a2.top); 7425 return SugarPosition(left, top); 7426 }; 7427 const toStyles = (coord, scroll, origin) => { 7428 const stylesOpt = coord.fold((x, y) => ({ position: Optional.some('absolute'), left: Optional.some(x + 'px'), top: Optional.some(y + 'px') }), // offset 7429 (x, y) => ({ position: Optional.some('absolute'), left: Optional.some((x - origin.left) + 'px'), top: Optional.some((y - origin.top) + 'px') }), // absolute 7430 (x, y) => ({ position: Optional.some('fixed'), left: Optional.some(x + 'px'), top: Optional.some(y + 'px') }) // fixed 7431 ); 7432 return { right: Optional.none(), bottom: Optional.none(), ...stylesOpt }; 7433 }; 7434 const translate$2 = (coord, deltaX, deltaY) => coord.fold((x, y) => offset(x + deltaX, y + deltaY), (x, y) => absolute$1(x + deltaX, y + deltaY), (x, y) => fixed$1(x + deltaX, y + deltaY)); 7435 const absorb = (partialCoord, originalCoord, scroll, origin) => { 7436 const absorbOne = (stencil, nu) => (optX, optY) => { 7437 const original = stencil(originalCoord, scroll, origin); 7438 return nu(optX.getOr(original.left), optY.getOr(original.top)); 7439 }; 7440 return partialCoord.fold(absorbOne(asOffset, offset), absorbOne(asAbsolute, absolute$1), absorbOne(asFixed, fixed$1)); 7441 }; 7442 const offset = adt$4.offset; 7443 const absolute$1 = adt$4.absolute; 7444 const fixed$1 = adt$4.fixed; 7445 7446 const parseAttrToInt = (element, name) => { 7447 const value = get$g(element, name); 7448 return isUndefined(value) ? NaN : parseInt(value, 10); 7449 }; 7450 // NOTE: Moved from ego with some parameterisation 7451 const get$3 = (component, snapsInfo) => { 7452 const element = component.element; 7453 const x = parseAttrToInt(element, snapsInfo.leftAttr); 7454 const y = parseAttrToInt(element, snapsInfo.topAttr); 7455 return isNaN(x) || isNaN(y) ? Optional.none() : Optional.some(SugarPosition(x, y)); 7456 }; 7457 const set$1 = (component, snapsInfo, pt) => { 7458 const element = component.element; 7459 set$9(element, snapsInfo.leftAttr, pt.left + 'px'); 7460 set$9(element, snapsInfo.topAttr, pt.top + 'px'); 7461 }; 7462 const clear = (component, snapsInfo) => { 7463 const element = component.element; 7464 remove$8(element, snapsInfo.leftAttr); 7465 remove$8(element, snapsInfo.topAttr); 7466 }; 7467 7468 // Types of coordinates 7469 // SugarLocation: This is the position on the screen including scroll. 7470 // Absolute: This is the css setting that would be applied. Therefore, it subtracts 7471 // the origin of the relative offsetParent. 7472 // Fixed: This is the fixed position. 7473 /* 7474 So in attempt to make this more understandable, let's use offset, absolute, and fixed. 7475 and try and model individual combinators 7476 */ 7477 /* 7478 7479 Relationships: 7480 - location -> absolute: should just need to subtract the position of the offset parent (origin) 7481 - location -> fixed: subtract the scrolling 7482 - absolute -> fixed: add the origin, and subtract the scrolling 7483 - absolute -> location: add the origin 7484 - fixed -> absolute: add the scrolling, remove the origin 7485 - fixed -> location: add the scrolling 7486 7487 /* 7488 * When the user is dragging around the element, and it snaps into place, it is important 7489 * for the next movement to be from its pre-snapped location, rather than the snapped location. 7490 * This is because if it is from the snapped location the next delta movement may not actually 7491 * be high enough to get it out of the snap area, and hence, it will just snap again (and again). 7492 */ 7493 // This identifies the position of the draggable element as either its current position, or the position 7494 // that we put on it before we snapped it into place (before dropping). Once it's dropped, the presnap 7495 // position will go away. It is used to avoid the situation where you can't escape the snap unless you 7496 // move the mouse really quickly :) 7497 const getCoords = (component, snapInfo, coord, delta) => get$3(component, snapInfo).fold(() => coord, (fixed) => 7498 // We have a pre-snap position, so we have to apply the delta ourselves 7499 fixed$1(fixed.left + delta.left, fixed.top + delta.top)); 7500 const moveOrSnap = (component, snapInfo, coord, delta, scroll, origin) => { 7501 const newCoord = getCoords(component, snapInfo, coord, delta); 7502 const snap = snapInfo.mustSnap ? findClosestSnap(component, snapInfo, newCoord, scroll, origin) : 7503 findSnap(component, snapInfo, newCoord, scroll, origin); 7504 const fixedCoord = asFixed(newCoord, scroll, origin); 7505 set$1(component, snapInfo, fixedCoord); 7506 return snap.fold(() => ({ 7507 coord: fixed$1(fixedCoord.left, fixedCoord.top), 7508 extra: Optional.none() 7509 }) 7510 // No snap. 7511 // var newfixed = graph.boundToFixed(theatre, element, loc.left, loc.top, fixed.left, fixed.top, height); 7512 // presnaps.set(element, 'fixed', newfixed.left, newfixed.top); 7513 // return { position: 'fixed', left: newfixed.left + 'px', top: newfixed.top + 'px' }; 7514 , (spanned) => ({ 7515 coord: spanned.output, 7516 extra: spanned.extra 7517 })); 7518 }; 7519 const stopDrag = (component, snapInfo) => { 7520 clear(component, snapInfo); 7521 }; 7522 const findMatchingSnap = (snaps, newCoord, scroll, origin) => findMap(snaps, (snap) => { 7523 const sensor = snap.sensor; 7524 const inRange = withinRange(newCoord, sensor, snap.range.left, snap.range.top, scroll, origin); 7525 return inRange ? Optional.some({ 7526 output: absorb(snap.output, newCoord, scroll, origin), 7527 extra: snap.extra 7528 }) : Optional.none(); 7529 }); 7530 const findClosestSnap = (component, snapInfo, newCoord, scroll, origin) => { 7531 // You need to pass in the absX and absY so that they can be used for things which only care about snapping one axis and keeping the other one. 7532 const snaps = snapInfo.getSnapPoints(component); 7533 const matchSnap = findMatchingSnap(snaps, newCoord, scroll, origin); 7534 return matchSnap.orThunk(() => { 7535 const bestSnap = foldl(snaps, (acc, snap) => { 7536 const sensor = snap.sensor; 7537 const deltas = getDeltas(newCoord, sensor, snap.range.left, snap.range.top, scroll, origin); 7538 return acc.deltas.fold(() => ({ 7539 deltas: Optional.some(deltas), 7540 snap: Optional.some(snap) 7541 }), (bestDeltas) => { 7542 const currAvg = (deltas.left + deltas.top) / 2; 7543 const bestAvg = (bestDeltas.left + bestDeltas.top) / 2; 7544 if (currAvg <= bestAvg) { 7545 return { 7546 deltas: Optional.some(deltas), 7547 snap: Optional.some(snap) 7548 }; 7549 } 7550 else { 7551 return acc; 7552 } 7553 }); 7554 }, { 7555 deltas: Optional.none(), 7556 snap: Optional.none() 7557 }); 7558 return bestSnap.snap.map((snap) => ({ 7559 output: absorb(snap.output, newCoord, scroll, origin), 7560 extra: snap.extra 7561 })); 7562 }); 7563 }; 7564 // x: the absolute position.left of the draggable element 7565 // y: the absolute position.top of the draggable element 7566 // deltaX: the amount the mouse has moved horizontally 7567 // deltaY: the amount the mouse has moved vertically 7568 const findSnap = (component, snapInfo, newCoord, scroll, origin) => { 7569 // You need to pass in the absX and absY so that they can be used for things which only care about snapping one axis and keeping the other one. 7570 const snaps = snapInfo.getSnapPoints(component); 7571 // HERE 7572 return findMatchingSnap(snaps, newCoord, scroll, origin); 7573 }; 7574 const snapTo$1 = (snap, scroll, origin) => ({ 7575 // TODO: This looks to be incorrect and needs fixing as DragCoord definitely needs a number 7576 // based drag coord for the second argument here, so this is probably a bug. 7577 coord: absorb(snap.output, snap.output, scroll, origin), 7578 extra: snap.extra 7579 }); 7580 7581 const snapTo = (component, dragConfig, _state, snap) => { 7582 const target = dragConfig.getTarget(component.element); 7583 if (dragConfig.repositionTarget) { 7584 const doc = owner$4(component.element); 7585 const scroll = get$b(doc); 7586 const origin = getOrigin(target); 7587 const snapPin = snapTo$1(snap, scroll, origin); 7588 const styles = toStyles(snapPin.coord, scroll, origin); 7589 setOptions(target, styles); 7590 } 7591 }; 7592 7593 var DraggingApis = /*#__PURE__*/Object.freeze({ 7594 __proto__: null,
7595 snapTo: snapTo 7596 }); 7597 7598 const field = (name, forbidden) => defaultedObjOf(name, {}, map$2(forbidden, (f) => forbid(f.name(), 'Cannot configure ' + f.name() + ' for ' + name)).concat([ 7599 customField('dump', identity) 7600 ])); 7601 const get$2 = (data) => data.dump; 7602 const augment = (data, original) => ({ 7603 ...derive$1(original), 7604 ...data.dump 7605 }); 7606 // Is this used? 7607 const SketchBehaviours = { 7608 field, 7609 augment, 7610 get: get$2 7611 }; 7612 7613 const base = (partSchemas, partUidsSchemas) => { 7614 const ps = partSchemas.length > 0 ? [ 7615 requiredObjOf('parts', partSchemas) 7616 ] : []; 7617 return ps.concat([ 7618 required$1('uid'), 7619 defaulted('dom', {}), // Maybe get rid of. 7620 defaulted('components', []), 7621 snapshot('originalSpec'), 7622 defaulted('debug.sketcher', {}) 7623 ]).concat(partUidsSchemas); 7624 }; 7625 const asRawOrDie = (label, schema, spec, partSchemas, partUidsSchemas) => { 7626 const baseS = base(partSchemas, partUidsSchemas); 7627 return asRawOrDie$1(label + ' [SpecSchema]', objOfOnly(baseS.concat(schema)), spec); 7628 }; 7629 7630 const single$1 = (owner, schema, factory, spec) => { 7631 const specWithUid = supplyUid(spec); 7632 const detail = asRawOrDie(owner, schema, specWithUid, [], []); 7633 return factory(detail, specWithUid); 7634 }; 7635 const composite$1 = (owner, schema, partTypes, factory, spec) => { 7636 const specWithUid = supplyUid(spec); 7637 // Identify any information required for external parts 7638 const partSchemas = schemas(partTypes); 7639 // Generate partUids for all parts (external and otherwise) 7640 const partUidsSchema = defaultUidsSchema(partTypes); 7641 const detail = asRawOrDie(owner, schema, specWithUid, partSchemas, [partUidsSchema]); 7642 // Create (internals, externals) substitutions 7643 const subs = substitutes(owner, detail, partTypes); 7644 // Work out the components by substituting internals 7645 const components = components$1(owner, detail, subs.internals()); 7646 return factory(detail, components, specWithUid, subs.externals()); 7647 }; 7648 const hasUid = (spec) => has$2(spec, 'uid'); 7649 const supplyUid = (spec) => { 7650 return hasUid(spec) ? spec : { 7651 ...spec, 7652 uid: generate$4('uid') 7653 }; 7654 }; 7655 7656 const isSketchSpec = (spec) => { 7657 return spec.uid !== undefined; 7658 }; 7659 const singleSchema = objOfOnly([ 7660 required$1('name'), 7661 required$1('factory'), 7662 required$1('configFields'), 7663 defaulted('apis', {}), 7664 defaulted('extraApis', {}) 7665 ]); 7666 const compositeSchema = objOfOnly([ 7667 required$1('name'), 7668 required$1('factory'), 7669 required$1('configFields'), 7670 required$1('partFields'), 7671 defaulted('apis', {}), 7672 defaulted('extraApis', {}) 7673 ]); 7674 const single = (rawConfig) => { 7675 const config = asRawOrDie$1('Sketcher for ' + rawConfig.name, singleSchema, rawConfig); 7676 const sketch = (spec) => single$1(config.name, config.configFields, config.factory, spec); 7677 const apis = map$1(config.apis, makeApi); 7678 const extraApis = map$1(config.extraApis, (f, k) => markAsExtraApi(f, k)); 7679 return { 7680 name: config.name, 7681 configFields: config.configFields, 7682 sketch, 7683 ...apis, 7684 ...extraApis 7685 }; 7686 }; 7687 const composite = (rawConfig) => { 7688 const config = asRawOrDie$1('Sketcher for ' + rawConfig.name, compositeSchema, rawConfig); 7689 const sketch = (spec) => composite$1(config.name, config.configFields, config.partFields, config.factory, spec); 7690 // These are constructors that will store their configuration. 7691 const parts = generate$5(config.name, config.partFields); 7692 const apis = map$1(config.apis, makeApi); 7693 const extraApis = map$1(config.extraApis, (f, k) => markAsExtraApi(f, k)); 7694 return { 7695 name: config.name, 7696 partFields: config.partFields, 7697 configFields: config.configFields, 7698 sketch, 7699 parts, 7700 ...apis, 7701 ...extraApis 7702 }; 7703 }; 7704
vendor: 16,384 bytes, lines 7705-8097
7705 const factory$n = (detail) => { 7706 const { attributes, ...domWithoutAttributes } = detail.dom; 7707 return { 7708 uid: detail.uid, 7709 dom: { 7710 tag: 'div', 7711 attributes: { 7712 role: 'presentation', 7713 ...attributes 7714 }, 7715 ...domWithoutAttributes 7716 }, 7717 components: detail.components, 7718 behaviours: get$2(detail.containerBehaviours), 7719 events: detail.events, 7720 domModification: detail.domModification, 7721 eventOrder: detail.eventOrder 7722 }; 7723 }; 7724 const Container = single({ 7725 name: 'Container', 7726 factory: factory$n, 7727 configFields: [ 7728 defaulted('components', []), 7729 field('containerBehaviours', []), 7730 // TODO: Deprecate 7731 defaulted('events', {}), 7732 defaulted('domModification', {}), 7733 defaulted('eventOrder', {}) 7734 ] 7735 }); 7736 7737 const initialAttribute = 'data-initial-z-index'; 7738 // We have to alter the z index of the alloy root of the blocker so that 7739 // it can have a z-index high enough to act as the "blocker". Just before 7740 // discarding it, we need to reset those z-indices back to what they 7741 // were. ASSUMPTION: the blocker has been added as a direct child of the root 7742 const resetZIndex = (blocker) => { 7743 parent(blocker.element).filter(isElement$1).each((root) => { 7744 getOpt(root, initialAttribute).fold(() => remove$6(root, 'z-index'), (zIndex) => set$7(root, 'z-index', zIndex)); 7745 remove$8(root, initialAttribute); 7746 }); 7747 }; 7748 const changeZIndex = (blocker) => { 7749 parent(blocker.element).filter(isElement$1).each((root) => { 7750 getRaw(root, 'z-index').each((zindex) => { 7751 set$9(root, initialAttribute, zindex); 7752 }); 7753 // Used to be a really high number, but it probably just has 7754 // to match the blocker 7755 set$7(root, 'z-index', get$e(blocker.element, 'z-index')); 7756 }); 7757 }; 7758 const instigate = (anyComponent, blocker) => { 7759 anyComponent.getSystem().addToGui(blocker); 7760 changeZIndex(blocker); 7761 }; 7762 const discard = (blocker) => { 7763 resetZIndex(blocker); 7764 blocker.getSystem().removeFromGui(blocker); 7765 }; 7766 const createComponent = (component, blockerClass, blockerEvents) => component.getSystem().build(Container.sketch({ 7767 dom: { 7768 // Probably consider doing with classes? 7769 styles: { 7770 'left': '0px', 7771 'top': '0px', 7772 'width': '100%', 7773 'height': '100%', 7774 'position': 'fixed', 7775 'z-index': '1000000000000000' 7776 }, 7777 classes: [blockerClass] 7778 }, 7779 events: blockerEvents 7780 })); 7781 7782 var SnapSchema = optionObjOf('snaps', [ 7783 required$1('getSnapPoints'), 7784 onHandler('onSensor'), 7785 required$1('leftAttr'), 7786 required$1('topAttr'), 7787 defaulted('lazyViewport', win), 7788 defaulted('mustSnap', false) 7789 ]); 7790 7791 const schema$s = [ 7792 // Is this used? 7793 defaulted('useFixed', never), 7794 defaulted('getTarget', identity), 7795 defaulted('onDragStart', noop), 7796 defaulted('onDrag', noop), 7797 defaulted('repositionTarget', true), 7798 defaulted('onDrop', noop), 7799 defaultedFunction('getBounds', win), 7800 SnapSchema 7801 ]; 7802 7803 const getCurrentCoord = (target) => lift3(getRaw(target, 'left'), getRaw(target, 'top'), getRaw(target, 'position'), (left, top, position) => { 7804 const nu = position === 'fixed' ? fixed$1 : offset; 7805 return nu(parseInt(left, 10), parseInt(top, 10)); 7806 }).getOrThunk(() => { 7807 const location = absolute$3(target); 7808 return absolute$1(location.left, location.top); 7809 }); 7810 const clampCoords = (component, coords, scroll, origin, startData) => { 7811 const bounds = startData.bounds; 7812 const absoluteCoord = asAbsolute(coords, scroll, origin); 7813 const newX = clamp(absoluteCoord.left, bounds.x, bounds.x + bounds.width - startData.width); 7814 const newY = clamp(absoluteCoord.top, bounds.y, bounds.y + bounds.height - startData.height); 7815 const newCoords = absolute$1(newX, newY); 7816 // Translate the absolute coord back into the previous type 7817 return coords.fold( 7818 // offset 7819 () => { 7820 const offset$1 = asOffset(newCoords, scroll, origin); 7821 return offset(offset$1.left, offset$1.top); 7822 }, 7823 // absolute 7824 constant$1(newCoords), 7825 // fixed 7826 () => { 7827 const fixed = asFixed(newCoords, scroll, origin); 7828 return fixed$1(fixed.left, fixed.top); 7829 }); 7830 }; 7831 const calcNewCoord = (component, optSnaps, currentCoord, scroll, origin, delta, startData) => { 7832 const newCoord = optSnaps.fold(() => { 7833 // When not docking, use fixed coordinates. 7834 const translated = translate$2(currentCoord, delta.left, delta.top); 7835 const fixedCoord = asFixed(translated, scroll, origin); 7836 return fixed$1(fixedCoord.left, fixedCoord.top); 7837 }, (snapInfo) => { 7838 const snapping = moveOrSnap(component, snapInfo, currentCoord, delta, scroll, origin); 7839 snapping.extra.each((extra) => { 7840 snapInfo.onSensor(component, extra); 7841 }); 7842 return snapping.coord; 7843 }); 7844 // Clamp the coords so that they are within the bounds 7845 return clampCoords(component, newCoord, scroll, origin, startData); 7846 }; 7847 const dragBy = (component, dragConfig, startData, delta) => { 7848 const target = dragConfig.getTarget(component.element); 7849 if (dragConfig.repositionTarget) { 7850 const doc = owner$4(component.element); 7851 const scroll = get$b(doc); 7852 const origin = getOrigin(target); 7853 const currentCoord = getCurrentCoord(target); 7854 const newCoord = calcNewCoord(component, dragConfig.snaps, currentCoord, scroll, origin, delta, startData); 7855 const styles = toStyles(newCoord, scroll, origin); 7856 setOptions(target, styles); 7857 } 7858 // NOTE: On drag just goes with the original delta. It does not know about snapping. 7859 dragConfig.onDrag(component, target, delta); 7860 }; 7861 7862 const calcStartData = (dragConfig, comp) => ({ 7863 bounds: dragConfig.getBounds(), 7864 height: getOuter$1(comp.element), 7865 width: getOuter(comp.element) 7866 }); 7867 const move = (component, dragConfig, dragState, dragMode, event) => { 7868 const delta = dragState.update(dragMode, event); 7869 const dragStartData = dragState.getStartData().getOrThunk(() => calcStartData(dragConfig, component)); 7870 delta.each((dlt) => { 7871 dragBy(component, dragConfig, dragStartData, dlt); 7872 }); 7873 }; 7874 const stop = (component, blocker, dragConfig, dragState) => { 7875 blocker.each(discard); 7876 dragConfig.snaps.each((snapInfo) => { 7877 stopDrag(component, snapInfo); 7878 }); 7879 const target = dragConfig.getTarget(component.element); 7880 dragState.reset(); 7881 dragConfig.onDrop(component, target); 7882 }; 7883 const handlers = (events) => (dragConfig, dragState) => { 7884 const updateStartState = (comp) => { 7885 const isBeingDragged = dragState.getStartData().isSome(); 7886 dragState.setStartData(calcStartData(dragConfig, comp)); 7887 if (!isBeingDragged) { 7888 const target = dragConfig.getTarget(comp.element); 7889 dragConfig.onDragStart(comp, target); 7890 } 7891 }; 7892 return derive$2([ 7893 run$1(windowScroll(), (comp) => { 7894 // Only update if we have some start data 7895 dragState.getStartData().each(() => updateStartState(comp)); 7896 }), 7897 ...events(dragConfig, dragState, updateStartState) 7898 ]); 7899 }; 7900 7901 const init$c = (dragApi) => derive$2([ 7902 // When the user clicks on the blocker, something has probably gone slightly 7903 // wrong, so we'll just drop for safety. The blocker should really only 7904 // be there when the mouse is already down and not released, so a 'click' 7905 run$1(mousedown(), dragApi.forceDrop), 7906 // When the user releases the mouse on the blocker, that is a drop 7907 run$1(mouseup(), dragApi.drop), 7908 // As the user moves the mouse around (while pressed down), we move the 7909 // component around 7910 run$1(mousemove(), (comp, simulatedEvent) => { 7911 dragApi.move(simulatedEvent.event); 7912 }), 7913 // When the use moves outside the range, schedule a block to occur but 7914 // give it a chance to be cancelled. 7915 run$1(mouseout(), dragApi.delayDrop) 7916 ]); 7917 7918 const getData$2 = (event) => Optional.from(SugarPosition(event.x, event.y)); 7919 // When dragging with the mouse, the delta is simply the difference 7920 // between the two position (previous/old and next/nu) 7921 const getDelta$2 = (old, nu) => SugarPosition(nu.left - old.left, nu.top - old.top); 7922 7923 var MouseData = /*#__PURE__*/Object.freeze({ 7924 __proto__: null, 7925 getData: getData$2, 7926 getDelta: getDelta$2 7927 }); 7928 7929 const events$e = (dragConfig, dragState, updateStartState) => [ 7930 run$1(mousedown(), (component, simulatedEvent) => { 7931 const raw = simulatedEvent.event.raw; 7932 if (raw.button !== 0) { 7933 return; 7934 } 7935 simulatedEvent.stop(); 7936 const stop$1 = () => stop(component, Optional.some(blocker), dragConfig, dragState); 7937 // If the user has moved something outside the area, and has not come back within 7938 // 200 ms, then drop 7939 const delayDrop = DelayedFunction(stop$1, 200); 7940 const dragApi = { 7941 drop: stop$1, 7942 delayDrop: delayDrop.schedule, 7943 forceDrop: stop$1, 7944 move: (event) => { 7945 // Stop any pending drops caused by mouseout 7946 delayDrop.cancel(); 7947 move(component, dragConfig, dragState, MouseData, event); 7948 } 7949 }; 7950 const blocker = createComponent(component, dragConfig.blockerClass, init$c(dragApi)); 7951 const start = () => { 7952 updateStartState(component); 7953 instigate(component, blocker); 7954 }; 7955 start(); 7956 }) 7957 ]; 7958 const schema$r = [ 7959 ...schema$s, 7960 required$1('blockerClass'), 7961 output$1('dragger', { 7962 handlers: handlers(events$e) 7963 }) 7964 ]; 7965 7966 const init$b = (dragApi) => derive$2([ 7967 // When the user taps on the blocker, something has probably gone slightly 7968 // wrong, so we'll just drop for safety. The blocker should really only 7969 // be there when their finger is already down and not released, so a 'tap' 7970 run$1(touchstart(), dragApi.forceDrop), 7971 // When the user releases their finger on the blocker, that is a drop 7972 run$1(touchend(), dragApi.drop), 7973 run$1(touchcancel(), dragApi.drop), 7974 // As the user moves their finger around (while pressed down), we move the 7975 // component around 7976 run$1(touchmove(), (comp, simulatedEvent) => { 7977 dragApi.move(simulatedEvent.event); 7978 }) 7979 ]); 7980 7981 const getDataFrom = (touches) => { 7982 const touch = touches[0]; 7983 return Optional.some(SugarPosition(touch.clientX, touch.clientY)); 7984 }; 7985 const getData$1 = (event) => { 7986 const raw = event.raw; 7987 const touches = raw.touches; 7988 return touches.length === 1 ? getDataFrom(touches) : Optional.none(); 7989 }; 7990 // When dragging the touch, the delta is simply the difference 7991 // between the two touch positions (previous/old and next/nu) 7992 const getDelta$1 = (old, nu) => SugarPosition(nu.left - old.left, nu.top - old.top); 7993 7994 var TouchData = /*#__PURE__*/Object.freeze({ 7995 __proto__: null, 7996 getData: getData$1, 7997 getDelta: getDelta$1 7998 }); 7999 8000 const events$d = (dragConfig, dragState, updateStartState) => { 8001 const blockerSingleton = value$2(); 8002 const stopBlocking = (component) => { 8003 stop(component, blockerSingleton.get(), dragConfig, dragState); 8004 blockerSingleton.clear(); 8005 }; 8006 // Android fires events on the component at all times, while iOS initially fires on the component 8007 // but once moved off the component then fires on the element behind. As such we need to use 8008 // a blocker and then listen to both touchmove/touchend on both the component and blocker. 8009 return [ 8010 run$1(touchstart(), (component, simulatedEvent) => { 8011 simulatedEvent.stop(); 8012 const stop = () => stopBlocking(component); 8013 const dragApi = { 8014 drop: stop, 8015 // delayDrop is not used by touch 8016 delayDrop: noop, 8017 forceDrop: stop, 8018 move: (event) => { 8019 move(component, dragConfig, dragState, TouchData, event); 8020 } 8021 }; 8022 const blocker = createComponent(component, dragConfig.blockerClass, init$b(dragApi)); 8023 blockerSingleton.set(blocker); 8024 const start = () => { 8025 updateStartState(component); 8026 instigate(component, blocker); 8027 }; 8028 start(); 8029 }), 8030 run$1(touchmove(), (component, simulatedEvent) => { 8031 simulatedEvent.stop(); 8032 move(component, dragConfig, dragState, TouchData, simulatedEvent.event); 8033 }), 8034 run$1(touchend(), (component, simulatedEvent) => { 8035 simulatedEvent.stop(); 8036 stopBlocking(component); 8037 }), 8038 run$1(touchcancel(), stopBlocking) 8039 ]; 8040 }; 8041 const schema$q = [ 8042 ...schema$s, 8043 required$1('blockerClass'), 8044 output$1('dragger', { 8045 handlers: handlers(events$d) 8046 }) 8047 ]; 8048 8049 const events$c = (dragConfig, dragState, updateStartState) => [ 8050 ...events$e(dragConfig, dragState, updateStartState), 8051 ...events$d(dragConfig, dragState, updateStartState) 8052 ]; 8053 const schema$p = [ 8054 ...schema$s, 8055 required$1('blockerClass'), 8056 output$1('dragger', { 8057 handlers: handlers(events$c) 8058 }) 8059 ]; 8060 8061 const getData = (event) => Optional.from(SugarPosition(event.x, event.y)); 8062 const getDelta = (old, nu) => SugarPosition(nu.left - old.left, nu.top - old.top); 8063 8064 var PointerData = /*#__PURE__*/Object.freeze({ 8065 __proto__: null, 8066 getData: getData, 8067 getDelta: getDelta 8068 }); 8069 8070 const isLeftClick = (e) => e.button === 0; 8071 const events$b = (dragConfig, dragState, updateStartState) => { 8072 return [ 8073 run$1(pointerdown(), (component, simulatedEvent) => { 8074 const raw = simulatedEvent.event.raw; 8075 if (!isLeftClick(raw)) { 8076 return; 8077 } 8078 if (dragState.getStartData().isNone()) { 8079 simulatedEvent.stop(); 8080 component.element.dom.setPointerCapture(raw.pointerId); 8081 dragState.setActivePointerId(raw.pointerId); 8082 updateStartState(component); 8083 } 8084 }), 8085 run$1(pointermove(), (component, simulatedEvent) => { 8086 const pointerId = simulatedEvent.event.raw.pointerId; 8087 lift2(dragState.getStartData(), dragState.getActivePointerId(), (_startData, activePointerId) => { 8088 if (pointerId === activePointerId) { 8089 move(component, dragConfig, dragState, PointerData, simulatedEvent.event); 8090 } 8091 }); 8092 }), 8093 run$1(pointerup(), (component, simulatedEvent) => { 8094 const pointerId = simulatedEvent.event.raw.pointerId; 8095 lift2(dragState.getStartData(), dragState.getActivePointerId(), (_startData, activePointerId) => { 8096 if (pointerId === activePointerId) { 8097 componen
8097t.element.dom.releasePointerCapture(pointerId); 8098 stop(component, Optional.none(), dragConfig, dragState); 8099 } 8100 }); 8101 }), 8102 // Safety net â if capture is lost unexpectedly. 8103 // It sometimes happens that pointer capture is lost without pointerup event being emitted. 8104 // I could observe that using touchpad in chrome 8105 run$1(lostpointercapture(), (component) => { 8106 dragState.getStartData().each(() => { 8107 stop(component, Optional.none(), dragConfig, dragState); 8108 }); 8109 }) 8110 ]; 8111 }; 8112 const schema$o = [ 8113 ...schema$s, 8114 output$1('dragger', { 8115 handlers: handlers(events$b) 8116 }) 8117 ]; 8118 8119 const mouse = schema$r; 8120 const touch = schema$q; 8121 const mouseOrTouch = schema$p; 8122 const pointer = schema$o; 8123 8124 var DraggingBranches = /*#__PURE__*/Object.freeze({ 8125 __proto__: null, 8126 mouse: mouse, 8127 touch: touch, 8128 mouseOrTouch: mouseOrTouch, 8129 pointer: pointer 8130 }); 8131 8132 // NOTE: mode refers to the way that information is retrieved from 8133 // the user interaction. It can be things like MouseData, TouchData etc. 8134 const init$a = () => { 8135 // Dragging operates on the difference between the previous user 8136 // interaction and the next user interaction. Therefore, we store 8137 // the previous interaction so that we can compare it. 8138 const previous = value$2(); 8139 // Dragging requires calculating the bounds, so we store that data initially 8140 // to reduce the amount of computation each mouse movement 8141 const startData = value$2(); 8142 // In a multitouch environment, `pointerId` is used to distinguish pointers 8143 // (e.g. different fingers on a touchscreen). 8144 // This property is only used by pointer-event branches. 8145 const activePointerId = value$2(); 8146 const reset = () => { 8147 previous.clear(); 8148 startData.clear(); 8149 activePointerId.clear(); 8150 }; 8151 // Return position delta between previous position and nu position, 8152 // or None if this is the first. Set the previous position to nu. 8153 const calculateDelta = (mode, nu) => { 8154 const result = previous.get().map((old) => mode.getDelta(old, nu)); 8155 previous.set(nu); 8156 return result; 8157 }; 8158 // NOTE: This dragEvent is the DOM touch event or mouse event 8159 const update = (mode, dragEvent) => mode.getData(dragEvent).bind((nuData) => calculateDelta(mode, nuData)); 8160 const setStartData = (data) => { 8161 startData.set(data); 8162 }; 8163 const getStartData = () => startData.get(); 8164 const setActivePointerId = (id) => { 8165 activePointerId.set(id); 8166 }; 8167 const getActivePointerId = () => activePointerId.get(); 8168 const readState = constant$1({}); 8169 return nu$4({ 8170 readState, 8171 reset, 8172 update, 8173 getStartData, 8174 setStartData,
8175 setActivePointerId, 8176 getActivePointerId 8177 }); 8178 }; 8179 8180 var DragState = /*#__PURE__*/Object.freeze({ 8181 __proto__: null, 8182 init: init$a 8183 }); 8184 8185 const Dragging = createModes({ 8186 branchKey: 'mode', 8187 branches: DraggingBranches, 8188 name: 'dragging', 8189 active: { 8190 events: (dragConfig, dragState) => { 8191 const dragger = dragConfig.dragger; 8192 return dragger.handlers(dragConfig, dragState); 8193 } 8194 }, 8195 extra: { 8196 // Extra. Does not need component as input. 8197 snap: (sConfig) => ({ 8198 sensor: sConfig.sensor, 8199 range: sConfig.range, 8200 output: sConfig.output, 8201 extra: Optional.from(sConfig.extra) 8202 }) 8203 }, 8204 state: DragState, 8205 apis: DraggingApis 8206 }); 8207 8208 const ariaElements = [ 8209 'input', 8210 'textarea' 8211 ]; 8212 const isAriaElement = (elem) => { 8213 const name = name$3(elem); 8214 return contains$2(ariaElements, name); 8215 }; 8216 const markValid = (component, invalidConfig) => { 8217 const elem = invalidConfig.getRoot(component).getOr(component.element); 8218 remove$3(elem, invalidConfig.invalidClass); 8219 invalidConfig.notify.each((notifyInfo) => { 8220 if (isAriaElement(component.element)) { 8221 set$9(component.element, 'aria-invalid', false); 8222 } 8223 notifyInfo.getContainer(component).each((container) => { 8224 set$8(container, notifyInfo.validHtml); 8225 }); 8226 notifyInfo.onValid(component); 8227 }); 8228 }; 8229 const markInvalid = (component, invalidConfig, invalidState, text) => { 8230 const elem = invalidConfig.getRoot(component).getOr(component.element); 8231 add$2(elem, invalidConfig.invalidClass); 8232 invalidConfig.notify.each((notifyInfo) => { 8233 if (isAriaElement(component.element)) { 8234 set$9(component.element, 'aria-invalid', true); 8235 } 8236 notifyInfo.getContainer(component).each((container) => { 8237 // TODO: Should we just use Text here, not HTML? 8238 set$8(container, text); 8239 }); 8240 notifyInfo.onInvalid(component, text); 8241 }); 8242 }; 8243 const query = (component, invalidConfig, _invalidState) => invalidConfig.validator.fold(() => Future.pure(Result.value(true)), (validatorInfo) => validatorInfo.validate(component)); 8244 const run = (component, invalidConfig, invalidState) => { 8245 invalidConfig.notify.each((notifyInfo) => { 8246 notifyInfo.onValidate(component); 8247 }); 8248 return query(component, invalidConfig).map((valid) => { 8249 if (component.getSystem().isConnected()) { 8250 return valid.fold((err) => { 8251 markInvalid(component, invalidConfig, invalidState, err); 8252 return Result.error(err); 8253 }, (v) => { 8254 markValid(component, invalidConfig); 8255 return Result.value(v); 8256 }); 8257 } 8258 else { 8259 return Result.error('No longer in system'); 8260 } 8261 }); 8262 }; 8263 const isInvalid = (component, invalidConfig) => { 8264 const elem = invalidConfig.getRoot(component).getOr(component.element); 8265 return has(elem, invalidConfig.invalidClass); 8266 }; 8267 8268 var InvalidateApis = /*#__PURE__*/Object.freeze({ 8269 __proto__: null, 8270 markValid: markValid, 8271 markInvalid: markInvalid, 8272 query: query, 8273 run: run, 8274 isInvalid: isInvalid 8275 }); 8276 8277 const events$a = (invalidConfig, invalidState) => invalidConfig.validator.map((validatorInfo) => derive$2([ 8278 run$1(validatorInfo.onEvent, (component) => { 8279 run(component, invalidConfig, invalidState).get(identity); 8280 }) 8281 ].concat(validatorInfo.validateOnLoad ? [ 8282 runOnAttached((component) => { 8283 run(component, invalidConfig, invalidState).get(noop); 8284 }) 8285 ] : []))).getOr({}); 8286 8287 var ActiveInvalidate = /*#__PURE__*/Object.freeze({ 8288 __proto__: null, 8289 events: events$a 8290 }); 8291 8292 var InvalidateSchema = [ 8293 required$1('invalidClass'),
vendor: 18,572 bytes, lines 8294-8749
8294 defaulted('getRoot', Optional.none), 8295 // TODO: Completely rework the notify API 8296 optionObjOf('notify', [ 8297 defaulted('aria', 'alert'), 8298 // Maybe we should use something else. 8299 defaulted('getContainer', Optional.none), 8300 defaulted('validHtml', ''), 8301 onHandler('onValid'), 8302 onHandler('onInvalid'), 8303 onHandler('onValidate') 8304 ]), 8305 optionObjOf('validator', [ 8306 required$1('validate'), 8307 defaulted('onEvent', 'input'), 8308 defaulted('validateOnLoad', true) 8309 ]) 8310 ]; 8311 8312 const onLoad$4 = (component, repConfig, repState) => { 8313 repConfig.store.manager.onLoad(component, repConfig, repState); 8314 }; 8315 const onUnload$2 = (component, repConfig, repState) => { 8316 repConfig.store.manager.onUnload(component, repConfig, repState); 8317 }; 8318 const setValue$3 = (component, repConfig, repState, data) => { 8319 repConfig.store.manager.setValue(component, repConfig, repState, data); 8320 }; 8321 const getValue$3 = (component, repConfig, repState) => repConfig.store.manager.getValue(component, repConfig, repState); 8322 const getState$2 = (component, repConfig, repState) => repState; 8323 8324 var RepresentApis = /*#__PURE__*/Object.freeze({ 8325 __proto__: null, 8326 onLoad: onLoad$4, 8327 onUnload: onUnload$2, 8328 setValue: setValue$3, 8329 getValue: getValue$3, 8330 getState: getState$2 8331 }); 8332 8333 const events$9 = (repConfig, repState) => { 8334 const es = repConfig.resetOnDom ? [ 8335 runOnAttached((comp, _se) => { 8336 onLoad$4(comp, repConfig, repState); 8337 }), 8338 runOnDetached((comp, _se) => { 8339 onUnload$2(comp, repConfig, repState); 8340 }) 8341 ] : [ 8342 loadEvent(repConfig, repState, onLoad$4) 8343 ]; 8344 return derive$2(es); 8345 }; 8346 8347 var ActiveRepresenting = /*#__PURE__*/Object.freeze({ 8348 __proto__: null, 8349 events: events$9 8350 }); 8351 8352 const memory$1 = () => { 8353 const data = Cell(null); 8354 const readState = () => ({ 8355 mode: 'memory', 8356 value: data.get() 8357 }); 8358 const isNotSet = () => data.get() === null; 8359 const clear = () => { 8360 data.set(null); 8361 }; 8362 return nu$4({ 8363 set: data.set, 8364 get: data.get, 8365 isNotSet, 8366 clear, 8367 readState 8368 }); 8369 }; 8370 const manual = () => { 8371 const readState = noop; 8372 return nu$4({ 8373 readState 8374 }); 8375 }; 8376 const dataset = () => { 8377 const dataByValue = Cell({}); 8378 const dataByText = Cell({}); 8379 const readState = () => ({ 8380 mode: 'dataset', 8381 dataByValue: dataByValue.get(), 8382 dataByText: dataByText.get() 8383 }); 8384 const clear = () => { 8385 dataByValue.set({}); 8386 dataByText.set({}); 8387 }; 8388 // itemString can be matching value or text. 8389 // TODO: type problem - impossible to correctly return value when type parameter only exists in return type 8390 const lookup = (itemString) => get$h(dataByValue.get(), itemString).orThunk(() => get$h(dataByText.get(), itemString)); 8391 const update = (items) => { 8392 const currentDataByValue = dataByValue.get(); 8393 const currentDataByText = dataByText.get(); 8394 const newDataByValue = {}; 8395 const newDataByText = {}; 8396 each$1(items, (item) => { 8397 newDataByValue[item.value] = item; 8398 get$h(item, 'meta').each((meta) => { 8399 get$h(meta, 'text').each((text) => { 8400 newDataByText[text] = item; 8401 }); 8402 }); 8403 }); 8404 dataByValue.set({ 8405 ...currentDataByValue, 8406 ...newDataByValue 8407 }); 8408 dataByText.set({ 8409 ...currentDataByText, 8410 ...newDataByText 8411 }); 8412 }; 8413 return nu$4({ 8414 readState, 8415 lookup, 8416 update, 8417 clear 8418 }); 8419 }; 8420 const init$9 = (spec) => spec.store.manager.state(spec); 8421 8422 var RepresentState = /*#__PURE__*/Object.freeze({ 8423 __proto__: null, 8424 memory: memory$1, 8425 dataset: dataset, 8426 manual: manual, 8427 init: init$9 8428 }); 8429 8430 const setValue$2 = (component, repConfig, repState, data) => { 8431 const store = repConfig.store; 8432 repState.update([data]); 8433 store.setValue(component, data); 8434 repConfig.onSetValue(component, data); 8435 }; 8436 const getValue$2 = (component, repConfig, repState) => { 8437 const store = repConfig.store; 8438 const key = store.getDataKey(component); 8439 return repState.lookup(key).getOrThunk(() => store.getFallbackEntry(key)); 8440 }; 8441 const onLoad$3 = (component, repConfig, repState) => { 8442 const store = repConfig.store; 8443 store.initialValue.each((data) => { 8444 setValue$2(component, repConfig, repState, data); 8445 }); 8446 }; 8447 const onUnload$1 = (component, repConfig, repState) => { 8448 repState.clear(); 8449 }; 8450 var DatasetStore = [ 8451 option$3('initialValue'), 8452 required$1('getFallbackEntry'), 8453 required$1('getDataKey'), 8454 required$1('setValue'), 8455 output$1('manager', { 8456 setValue: setValue$2, 8457 getValue: getValue$2, 8458 onLoad: onLoad$3, 8459 onUnload: onUnload$1, 8460 state: dataset 8461 }) 8462 ]; 8463 8464 const getValue$1 = (component, repConfig, _repState) => repConfig.store.getValue(component); 8465 const setValue$1 = (component, repConfig, _repState, data) => { 8466 repConfig.store.setValue(component, data); 8467 repConfig.onSetValue(component, data); 8468 }; 8469 const onLoad$2 = (component, repConfig, _repState) => { 8470 repConfig.store.initialValue.each((data) => { 8471 repConfig.store.setValue(component, data); 8472 }); 8473 }; 8474 var ManualStore = [ 8475 required$1('getValue'), 8476 defaulted('setValue', noop), 8477 option$3('initialValue'), 8478 output$1('manager', { 8479 setValue: setValue$1, 8480 getValue: getValue$1, 8481 onLoad: onLoad$2, 8482 onUnload: noop, 8483 state: NoState.init 8484 }) 8485 ]; 8486 8487 const setValue = (component, repConfig, repState, data) => { 8488 repState.set(data); 8489 repConfig.onSetValue(component, data); 8490 }; 8491 const getValue = (component, repConfig, repState) => repState.get(); 8492 const onLoad$1 = (component, repConfig, repState) => { 8493 repConfig.store.initialValue.each((initVal) => { 8494 if (repState.isNotSet()) { 8495 repState.set(initVal); 8496 } 8497 }); 8498 }; 8499 const onUnload = (component, repConfig, repState) => { 8500 repState.clear(); 8501 }; 8502 var MemoryStore = [ 8503 option$3('initialValue'), 8504 output$1('manager', { 8505 setValue, 8506 getValue, 8507 onLoad: onLoad$1, 8508 onUnload, 8509 state: memory$1 8510 }) 8511 ]; 8512 8513 var RepresentSchema = [ 8514 defaultedOf('store', { mode: 'memory' }, choose$1('mode', { 8515 memory: MemoryStore, 8516 manual: ManualStore, 8517 dataset: DatasetStore 8518 })), 8519 onHandler('onSetValue'), 8520 defaulted('resetOnDom', false) 8521 ]; 8522 8523 // The self-reference is clumsy. 8524 const Representing = create$3({ 8525 fields: RepresentSchema, 8526 name: 'representing', 8527 active: ActiveRepresenting, 8528 apis: RepresentApis, 8529 extra: { 8530 setValueFrom: (component, source) => { 8531 const value = Representing.getValue(source); 8532 Representing.setValue(component, value); 8533 } 8534 }, 8535 state: RepresentState 8536 }); 8537 8538 const Invalidating = create$3({ 8539 fields: InvalidateSchema, 8540 name: 'invalidating', 8541 active: ActiveInvalidate, 8542 apis: InvalidateApis, 8543 extra: { 8544 // Note, this requires representing to be on the validatee 8545 validation: (validator) => { 8546 return (component) => { 8547 const v = Representing.getValue(component); 8548 return Future.pure(validator(v)); 8549 }; 8550 } 8551 } 8552 }); 8553 8554 const exhibit$4 = (base, posConfig) => nu$2({ 8555 classes: [], 8556 styles: posConfig.useFixed() ? {} : { position: 'relative' } 8557 }); 8558 8559 var ActivePosition = /*#__PURE__*/Object.freeze({ 8560 __proto__: null, 8561 exhibit: exhibit$4 8562 }); 8563 8564 const adt$3 = Adt.generate([ 8565 { none: [] }, 8566 { relative: ['x', 'y', 'width', 'height'] }, 8567 { fixed: ['x', 'y', 'width', 'height'] } 8568 ]); 8569 const positionWithDirection = (posName, decision, x, y, width, height) => { 8570 const decisionRect = decision.rect; 8571 const decisionX = decisionRect.x - x; 8572 const decisionY = decisionRect.y - y; 8573 const decisionWidth = decisionRect.width; 8574 const decisionHeight = decisionRect.height; 8575 const decisionRight = width - (decisionX + decisionWidth); 8576 const decisionBottom = height - (decisionY + decisionHeight); 8577 const left = Optional.some(decisionX); 8578 const top = Optional.some(decisionY); 8579 const right = Optional.some(decisionRight); 8580 const bottom = Optional.some(decisionBottom); 8581 const none = Optional.none(); 8582 return cata$1(decision.direction, () => NuPositionCss(posName, left, top, none, none), // southeast 8583 () => NuPositionCss(posName, none, top, right, none), // southwest 8584 () => NuPositionCss(posName, left, none, none, bottom), // northeast 8585 () => NuPositionCss(posName, none, none, right, bottom), // northwest 8586 () => NuPositionCss(posName, left, top, none, none), // south 8587 () => NuPositionCss(posName, left, none, none, bottom), // north 8588 () => NuPositionCss(posName, left, top, none, none), // east 8589 () => NuPositionCss(posName, none, top, right, none) // west 8590 ); 8591 }; 8592 const reposition = (origin, decision) => origin.fold(() => { 8593 const decisionRect = decision.rect; 8594 return NuPositionCss('absolute', Optional.some(decisionRect.x), Optional.some(decisionRect.y), Optional.none(), Optional.none()); 8595 }, (x, y, width, height) => { 8596 return positionWithDirection('absolute', decision, x, y, width, height); 8597 }, (x, y, width, height) => { 8598 return positionWithDirection('fixed', decision, x, y, width, height); 8599 }); 8600 const toBox = (origin, element) => { 8601 const rel = curry(find$2, element); 8602 const position = origin.fold(rel, rel, () => { 8603 const scroll = get$b(); 8604 // TODO: Make adding the scroll in OuterPosition.find optional. 8605 return find$2(element).translate(-scroll.left, -scroll.top); 8606 }); 8607 const width = getOuter(element); 8608 const height = getOuter$1(element); 8609 return bounds(position.left, position.top, width, height); 8610 }; 8611 const viewport = (origin, optBounds) => optBounds.fold( 8612 /* There are no bounds supplied */ 8613 () => origin.fold(win, win, bounds), (bounds$1) => 8614 /* Use any bounds supplied or remove the scroll position of the bounds for fixed. */ 8615 origin.fold(constant$1(bounds$1), constant$1(bounds$1), () => { 8616 const pos = translate$1(origin, bounds$1.x, bounds$1.y); 8617 return bounds(pos.left, pos.top, bounds$1.width, bounds$1.height); 8618 })); 8619 const translate$1 = (origin, x, y) => { 8620 const pos = SugarPosition(x, y); 8621 const removeScroll = () => { 8622 const outerScroll = get$b(); 8623 return pos.translate(-outerScroll.left, -outerScroll.top); 8624 }; 8625 // This could use cata if it wasn't a circular reference 8626 return origin.fold(constant$1(pos), constant$1(pos), removeScroll); 8627 }; 8628 const cata = (subject, onNone, onRelative, onFixed) => subject.fold(onNone, onRelative, onFixed); 8629 adt$3.none; 8630 const relative = adt$3.relative; 8631 const fixed = adt$3.fixed; 8632 8633 const anchor = (anchorBox, origin) => ({ 8634 anchorBox, 8635 origin 8636 }); 8637 const box = (anchorBox, origin) => anchor(anchorBox, origin); 8638 8639 const adt$2 = Adt.generate([ 8640 { fit: ['reposition'] }, 8641 { nofit: ['reposition', 'visibleW', 'visibleH', 'isVisible'] } 8642 ]); 8643 /** 8644 * This will attempt to determine if the box will fit within the specified bounds or if it needs to be repositioned. 8645 * It will return the following details: 8646 * - if the original rect was in bounds (originInBounds & sizeInBounds). This is used to determine if we fitted 8647 * without having to make adjustments. 8648 * - the height and width that would be visible in the original location. (ie the overlap between the rect and 8649 * the bounds or the distance between the boxes if there is no overlap) 8650 */ 8651 const determinePosition = (box, bounds) => { 8652 const { x: boundsX, y: boundsY, right: boundsRight, bottom: boundsBottom } = bounds; 8653 const { x, y, right, bottom, width, height } = box; 8654 // simple checks for "is the top left inside the view" 8655 const xInBounds = x >= boundsX && x <= boundsRight; 8656 const yInBounds = y >= boundsY && y <= boundsBottom; 8657 const originInBounds = xInBounds && yInBounds; 8658 // simple checks for "is the bottom right inside the view" 8659 const rightInBounds = right <= boundsRight && right >= boundsX; 8660 const bottomInBounds = bottom <= boundsBottom && bottom >= boundsY; 8661 const sizeInBounds = rightInBounds && bottomInBounds; 8662 // measure how much of the width and height are visible. This should never be larger than the actual width or height 8663 // however it can be a negative value when offscreen. These values are generally are only needed for the "nofit" case 8664 const visibleW = Math.min(width, x >= boundsX ? boundsRight - x : right - boundsX); 8665 const visibleH = Math.min(height, y >= boundsY ? boundsBottom - y : bottom - boundsY); 8666 return { 8667 originInBounds, 8668 sizeInBounds, 8669 visibleW, 8670 visibleH 8671 }; 8672 }; 8673 /** 8674 * This will attempt to calculate and adjust the position of the box so that is stays within the specified bounds. 8675 * The end result will be a new restricted box of where it can safely be placed within the bounds. 8676 */ 8677 const calcReposition = (box, bounds$1) => { 8678 const { x: boundsX, y: boundsY, right: boundsRight, bottom: boundsBottom } = bounds$1; 8679 const { x, y, width, height } = box; 8680 // measure the maximum x and y taking into account the height and width of the box 8681 const maxX = Math.max(boundsX, boundsRight - width); 8682 const maxY = Math.max(boundsY, boundsBottom - height); 8683 // Futz with the X value to ensure that we're not off the left or right of the screen 8684 const restrictedX = clamp(x, boundsX, maxX); 8685 // Futz with the Y value to ensure that we're not off the top or bottom of the screen 8686 const restrictedY = clamp(y, boundsY, maxY); 8687 // Determine the new height and width based on the restricted X/Y to keep the element in bounds 8688 const restrictedWidth = Math.min(restrictedX + width, boundsRight) - restrictedX; 8689 const restrictedHeight = Math.min(restrictedY + height, boundsBottom) - restrictedY; 8690 return bounds(restrictedX, restrictedY, restrictedWidth, restrictedHeight); 8691 }; 8692 /** 8693 * Determine the maximum height and width available for where the box is positioned in the bounds, making sure 8694 * to account for which direction it's rendering in. 8695 */ 8696 const calcMaxSizes = (direction, box, bounds) => { 8697 // Futz with the "height" of the popup to ensure if it doesn't fit it's capped at the available height. 8698 const upAvailable = constant$1(box.bottom - bounds.y); 8699 const downAvailable = constant$1(bounds.bottom - box.y); 8700 const maxHeight = cataVertical(direction, downAvailable, /* middle */ downAvailable, upAvailable); 8701 // Futz with the "width" of the popup to ensure if it doesn't fit it's capped at the available width. 8702 const westAvailable = constant$1(box.right - bounds.x); 8703 const eastAvailable = constant$1(bounds.right - box.x); 8704 const maxWidth = cataHorizontal(direction, eastAvailable, /* middle */ eastAvailable, westAvailable); 8705 return { 8706 maxWidth, 8707 maxHeight 8708 }; 8709 }; 8710 const attempt = (candidate, width, height, bounds$1) => { 8711 const bubble = candidate.bubble; 8712 const bubbleOffset = bubble.offset; 8713 // adjust the bounds to account for the layout and bubble restrictions 8714 const adjustedBounds = adjustBounds(bounds$1, candidate.restriction, bubbleOffset); 8715 // candidate position is excluding the bubble, so add those values as well 8716 const newX = candidate.x + bubbleOffset.left; 8717 const newY = candidate.y + bubbleOffset.top; 8718 const box = bounds(newX, newY, width, height); 8719 // determine the position of the box in relation to the bounds 8720 const { originInBounds, sizeInBounds, visibleW, visibleH } = determinePosition(box, adjustedBounds); 8721 // restrict the box if it won't fit in the bounds 8722 const fits = originInBounds && sizeInBounds; 8723 const fittedBox = fits ? box : calcReposition(box, adjustedBounds); 8724 // Determine if the box is at least partly visible in the bounds after applying the restrictions 8725 const isPartlyVisible = fittedBox.width > 0 && fittedBox.height > 0; 8726 // Determine the maximum height and width available in the bounds 8727 const { maxWidth, maxHeight } = calcMaxSizes(candidate.direction, fittedBox, bounds$1); 8728 const reposition = { 8729 rect: fittedBox, 8730 maxHeight, 8731 maxWidth, 8732 direction: candidate.direction, 8733 placement: candidate.placement, 8734 classes: { 8735 on: bubble.classesOn, 8736 off: bubble.classesOff 8737 }, 8738 layout: candidate.label, 8739 testY: newY 8740 }; 8741 // useful debugging that I don't want to lose 8742 // console.log(candidate.label); 8743 // console.table([{ 8744 // newY, 8745 // limitY: fittedBox.y, 8746 // boundsY: bounds.y, 8747 // boundsBottom: bounds.bottom, 8748 // newX, 8749 // limitX: fittedBox.x,
8750 // boundsX: bounds.x, 8751 // boundsRight: bounds.right, 8752 // candidateX: candidate.x, 8753 // candidateY: candidate.y, 8754 // width, 8755 // height, 8756 // isPartlyVisible 8757 // }]); 8758 // console.log(`maxWidth: ${maxWidth}, visibleW: ${visibleW}`); 8759 // console.log(`maxHeight: ${maxHeight}, visibleH: ${visibleH}`); 8760 // console.log('originInBounds:', originInBounds); 8761 // console.log('sizeInBounds:', sizeInBounds); 8762 // console.log(originInBounds && sizeInBounds ? 'fit' : 'nofit'); 8763 // Take special note that we don't use the futz values in the nofit case; whether this position is a good fit is separate 8764 // to ensuring that if we choose it the popup is actually on screen properly. 8765 return fits || candidate.alwaysFit ? adt$2.fit(reposition) : adt$2.nofit(reposition, visibleW, visibleH, isPartlyVisible); 8766 }; 8767 /** 8768 * Attempts to fit a box (generally a menu). 8769 * 8770 * candidates: an array of layout generators, generally obtained via api.Layout or api.LinkedLayout 8771 * anchorBox: the box on screen that triggered the menu, we must touch one of the edges as defined by the candidate layouts 8772 * elementBox: the popup (only width and height matter) 8773 * bubbles: the bubbles for the popup (see api.Bubble) 8774 * bounds: the screen 8775 */ 8776 const attempts = (element, candidates, anchorBox, elementBox, bubbles, bounds) => { 8777 const panelWidth = elementBox.width; 8778 const panelHeight = elementBox.height; 8779 const attemptBestFit = (layout, reposition, visibleW, visibleH, isVisible) => { 8780 const next = layout(anchorBox, elementBox, bubbles, element, bounds); 8781 const attemptLayout = attempt(next, panelWidth, panelHeight, bounds); 8782 return attemptLayout.fold(constant$1(attemptLayout), (newReposition, newVisibleW, newVisibleH, newIsVisible) => { 8783 // console.log(`label: ${next.label}, newVisibleW: ${newVisibleW}, visibleW: ${visibleW}, newVisibleH: ${newVisibleH}, visibleH: ${visibleH}, newIsVisible: ${newIsVisible}, isVisible: ${isVisible}`); 8784 const improved = isVisible === newIsVisible ? (newVisibleH > visibleH || newVisibleW > visibleW) : (!isVisible && newIsVisible); 8785 // console.log('improved? ', improved);
vendor: 15,731 bytes, lines 8786-9135
8786 return improved ? attemptLayout : adt$2.nofit(reposition, visibleW, visibleH, isVisible); 8787 }); 8788 }; 8789 const abc = foldl(candidates, (b, a) => { 8790 const bestNext = curry(attemptBestFit, a); 8791 return b.fold(constant$1(b), bestNext); 8792 }, 8793 // fold base case: No candidates, it's never going to be correct, so do whatever 8794 adt$2.nofit({ 8795 rect: anchorBox, 8796 maxHeight: elementBox.height, 8797 maxWidth: elementBox.width, 8798 direction: southeast$3(), 8799 placement: "southeast" /* Placement.Southeast */, 8800 classes: { 8801 on: [], 8802 off: [] 8803 }, 8804 layout: 'none', 8805 testY: anchorBox.y 8806 }, -1, -1, false)); 8807 // unwrapping 'reposition' from the adt, for both fit & nofit the first arg is the one we need, 8808 // so we can cheat and use Fun.identity 8809 return abc.fold(identity, identity); 8810 }; 8811 8812 const properties = ['top', 'bottom', 'right', 'left']; 8813 const timerAttr = 'data-alloy-transition-timer'; 8814 const isTransitioning$1 = (element, transition) => hasAll(element, transition.classes); 8815 const shouldApplyTransitionCss = (transition, decision, lastPlacement) => { 8816 // Don't apply transitions if there was no previous placement as it's transitioning from offscreen 8817 return lastPlacement.exists((placer) => { 8818 const mode = transition.mode; 8819 return mode === 'all' ? true : placer[mode] !== decision[mode]; 8820 }); 8821 }; 8822 const hasChanges = (position, intermediate) => { 8823 // Round to 3 decimal points 8824 const round = (value) => parseFloat(value).toFixed(3); 8825 return find$4(intermediate, (value, key) => { 8826 const newValue = position[key].map(round); 8827 const val = value.map(round); 8828 return !equals(newValue, val); 8829 }).isSome(); 8830 }; 8831 const getTransitionDuration = (element) => { 8832 const get = (name) => { 8833 const style = get$e(element, name); 8834 const times = style.split(/\s*,\s*/); 8835 return filter$2(times, isNotEmpty); 8836 }; 8837 const parse = (value) => { 8838 if (isString(value) && /^[\d.]+/.test(value)) { 8839 const num = parseFloat(value); 8840 return endsWith(value, 'ms') ? num : num * 1000; 8841 } 8842 else { 8843 return 0; 8844 } 8845 }; 8846 const delay = get('transition-delay'); 8847 const duration = get('transition-duration'); 8848 return foldl(duration, (acc, dur, i) => { 8849 const time = parse(delay[i]) + parse(dur); 8850 return Math.max(acc, time); 8851 }, 0); 8852 }; 8853 const setupTransitionListeners = (element, transition) => { 8854 const transitionEnd = unbindable(); 8855 const transitionCancel = unbindable(); 8856 let timer; 8857 const isSourceTransition = (e) => { 8858 // Ensure the transition event isn't from a pseudo element 8859 const pseudoElement = e.raw.pseudoElement ?? ''; 8860 return eq(e.target, element) && isEmpty(pseudoElement) && contains$2(properties, e.raw.propertyName); 8861 }; 8862 const transitionDone = (e) => { 8863 if (isNullable(e) || isSourceTransition(e)) { 8864 transitionEnd.clear(); 8865 transitionCancel.clear(); 8866 // Only cleanup the class/timer on transitionend not on a cancel. This is done as cancel 8867 // means the element has been repositioned and would need to keep transitioning 8868 const type = e?.raw.type; 8869 if (isNullable(type) || type === transitionend()) { 8870 clearTimeout(timer); 8871 remove$8(element, timerAttr); 8872 remove$2(element, transition.classes); 8873 } 8874 } 8875 }; 8876 const transitionStart = bind$1(element, transitionstart(), (e) => { 8877 if (isSourceTransition(e)) { 8878 transitionStart.unbind(); 8879 transitionEnd.set(bind$1(element, transitionend(), transitionDone)); 8880 transitionCancel.set(bind$1(element, transitioncancel(), transitionDone)); 8881 } 8882 }); 8883 // Request the next animation frame so we can roughly determine when the transition starts and then ensure 8884 // the transition is cleaned up. In addition add ~17ms to the delay as that's about about 1 frame at 60fps 8885 const duration = getTransitionDuration(element); 8886 window.requestAnimationFrame(() => { 8887 timer = setTimeout(transitionDone, duration + 17); 8888 set$9(element, timerAttr, timer); 8889 }); 8890 }; 8891 const startTransitioning = (element, transition) => { 8892 add$1(element, transition.classes); 8893 // Clear any existing cleanup timers 8894 getOpt(element, timerAttr).each((timerId) => { 8895 clearTimeout(parseInt(timerId, 10)); 8896 remove$8(element, timerAttr); 8897 }); 8898 setupTransitionListeners(element, transition); 8899 }; 8900 const applyTransitionCss = (element, origin, position, transition, decision, lastPlacement) => { 8901 const shouldTransition = shouldApplyTransitionCss(transition, decision, lastPlacement); 8902 if (shouldTransition || isTransitioning$1(element, transition)) { 8903 // Set the new position first so we can calculate the computed position 8904 set$7(element, 'position', position.position); 8905 // Get the computed positions for the current element based on the new position CSS being applied 8906 const rect = toBox(origin, element); 8907 const intermediatePosition = reposition(origin, { ...decision, rect }); 8908 const intermediateCssOptions = mapToObject(properties, (prop) => intermediatePosition[prop]); 8909 // Apply the intermediate styles and transition classes if something has changed 8910 if (hasChanges(position, intermediateCssOptions)) { 8911 setOptions(element, intermediateCssOptions); 8912 if (shouldTransition) { 8913 startTransitioning(element, transition); 8914 } 8915 reflow(element); 8916 } 8917 } 8918 else { 8919 remove$2(element, transition.classes); 8920 } 8921 }; 8922 8923 /* 8924 * This is the old repartee API. It is retained in a similar structure to the original form, 8925 * in case we decide to bring back the flexibility of working with non-standard positioning. 8926 */ 8927 const elementSize = (p) => ({ 8928 width: Math.ceil(getOuter(p)), 8929 height: getOuter$1(p) 8930 }); 8931 const layout = (anchorBox, element, bubbles, options) => { 8932 // clear the potentially limiting factors before measuring 8933 remove$6(element, 'max-height'); 8934 remove$6(element, 'max-width'); 8935 const elementBox = elementSize(element); 8936 return attempts(element, options.preference, anchorBox, elementBox, bubbles, options.bounds); 8937 }; 8938 const setClasses = (element, decision) => { 8939 const classInfo = decision.classes; 8940 remove$2(element, classInfo.off); 8941 add$1(element, classInfo.on); 8942 }; 8943 /* 8944 * maxHeightFunction is a MaxHeight instance. 8945 * max-height is usually the distance between the edge of the popup and the screen; top of popup to bottom of screen for south, bottom of popup to top of screen for north. 8946 * 8947 * There are a few cases where we specifically don't want a max-height, which is why it's optional. 8948 */ 8949 const setHeight = (element, decision, options) => { 8950 // The old API enforced MaxHeight.anchored() for fixed position. That no longer seems necessary. 8951 const maxHeightFunction = options.maxHeightFunction; 8952 maxHeightFunction(element, decision.maxHeight); 8953 }; 8954 const setWidth = (element, decision, options) => { 8955 const maxWidthFunction = options.maxWidthFunction; 8956 maxWidthFunction(element, decision.maxWidth); 8957 }; 8958 const position$2 = (element, decision, options) => { 8959 // This is a point of difference between Alloy and Repartee. Repartee appears to use Measure to calculate the available space for fixed origin 8960 // That is not ported yet. 8961 const positionCss = reposition(options.origin, decision); 8962 options.transition.each((transition) => { 8963 applyTransitionCss(element, options.origin, positionCss, transition, decision, options.lastPlacement); 8964 }); 8965 applyPositionCss(element, positionCss); 8966 }; 8967 const setPlacement = (element, decision) => { 8968 setPlacement$1(element, decision.placement); 8969 }; 8970 8971 const defaultOr = (options, key, dephault) => options[key] === undefined ? dephault : options[key]; 8972 // This takes care of everything when you are positioning UI that can go anywhere on the screen 8973 const simple = (anchor, element, bubble, layouts, lastPlacement, optBounds, overrideOptions, transition) => { 8974 // the only supported override at the moment. Once relative has been deleted, maybe this can be optional in the bag 8975 const maxHeightFunction = defaultOr(overrideOptions, 'maxHeightFunction', anchored()); 8976 const maxWidthFunction = defaultOr(overrideOptions, 'maxWidthFunction', noop); 8977 const anchorBox = anchor.anchorBox; 8978 const origin = anchor.origin; 8979 const options = { 8980 bounds: viewport(origin, optBounds), 8981 origin, 8982 preference: layouts, 8983 maxHeightFunction, 8984 maxWidthFunction, 8985 lastPlacement, 8986 transition 8987 }; 8988 return go(anchorBox, element, bubble, options); 8989 }; 8990 // This is the old public API. If we ever need full customisability again, this is how to expose it 8991 const go = (anchorBox, element, bubble, options) => { 8992 const decision = layout(anchorBox, element, bubble, options); 8993 position$2(element, decision, options); 8994 setPlacement(element, decision); 8995 setClasses(element, decision); 8996 setHeight(element, decision, options); 8997 setWidth(element, decision, options); 8998 return { 8999 layout: decision.layout, 9000 placement: decision.placement 9001 }; 9002 }; 9003 9004 const nu$1 = identity; 9005 9006 const schema$n = () => optionObjOf('layouts', [ 9007 required$1('onLtr'), 9008 required$1('onRtl'), 9009 option$3('onBottomLtr'), 9010 option$3('onBottomRtl') 9011 ]); 9012 const get$1 = (elem, info, defaultLtr, defaultRtl, defaultBottomLtr, defaultBottomRtl, dirElement) => { 9013 const isBottomToTop = dirElement.map(isBottomToTopDir).getOr(false); 9014 const customLtr = info.layouts.map((ls) => ls.onLtr(elem)); 9015 const customRtl = info.layouts.map((ls) => ls.onRtl(elem)); 9016 const ltr = isBottomToTop ? 9017 info.layouts.bind((ls) => ls.onBottomLtr.map((f) => f(elem))) 9018 .or(customLtr) 9019 .getOr(defaultBottomLtr) : 9020 customLtr.getOr(defaultLtr); 9021 const rtl = isBottomToTop ? 9022 info.layouts.bind((ls) => ls.onBottomRtl.map((f) => f(elem))) 9023 .or(customRtl) 9024 .getOr(defaultBottomRtl) : 9025 customRtl.getOr(defaultRtl); 9026 const f = onDirection(ltr, rtl); 9027 return f(elem); 9028 }; 9029 9030 const placement$4 = (component, anchorInfo, origin) => { 9031 const hotspot = anchorInfo.hotspot; 9032 const anchorBox = toBox(origin, hotspot.element); 9033 const layouts = get$1(component.element, anchorInfo, belowOrAbove(), belowOrAboveRtl(), aboveOrBelow(), aboveOrBelowRtl(), Optional.some(anchorInfo.hotspot.element)); 9034 return Optional.some(nu$1({ 9035 anchorBox, 9036 bubble: anchorInfo.bubble.getOr(fallback()), 9037 overrides: anchorInfo.overrides, 9038 layouts 9039 })); 9040 }; 9041 var HotspotAnchor = [ 9042 required$1('hotspot'), 9043 option$3('bubble'), 9044 defaulted('overrides', {}), 9045 schema$n(), 9046 output$1('placement', placement$4) 9047 ]; 9048 9049 const placement$3 = (component, anchorInfo, origin) => { 9050 const pos = translate$1(origin, anchorInfo.x, anchorInfo.y); 9051 const anchorBox = bounds(pos.left, pos.top, anchorInfo.width, anchorInfo.height); 9052 const layouts = get$1(component.element, anchorInfo, all$2(), allRtl$1(), 9053 // No default bottomToTop layouts currently needed 9054 all$2(), allRtl$1(), Optional.none()); 9055 return Optional.some(nu$1({ 9056 anchorBox, 9057 bubble: anchorInfo.bubble, 9058 overrides: anchorInfo.overrides, 9059 layouts 9060 })); 9061 }; 9062 var MakeshiftAnchor = [ 9063 required$1('x'), 9064 required$1('y'), 9065 defaulted('height', 0), 9066 defaulted('width', 0), 9067 defaulted('bubble', fallback()), 9068 defaulted('overrides', {}), 9069 schema$n(), 9070 output$1('placement', placement$3) 9071 ]; 9072 9073 const adt$1 = Adt.generate([ 9074 { screen: ['point'] }, 9075 { absolute: ['point', 'scrollLeft', 'scrollTop'] } 9076 ]); 9077 const toFixed = (pos) => 9078 // TODO: Use new ADT methods 9079 pos.fold(identity, (point, scrollLeft, scrollTop) => point.translate(-scrollLeft, -scrollTop)); 9080 const toAbsolute = (pos) => pos.fold(identity, identity); 9081 const sum = (points) => foldl(points, (b, a) => b.translate(a.left, a.top), SugarPosition(0, 0)); 9082 const sumAsFixed = (positions) => { 9083 const points = map$2(positions, toFixed); 9084 return sum(points); 9085 }; 9086 const sumAsAbsolute = (positions) => { 9087 const points = map$2(positions, toAbsolute); 9088 return sum(points); 9089 }; 9090 const screen = adt$1.screen; 9091 const absolute = adt$1.absolute; 9092 9093 // In one mode, the window is inside an iframe. If that iframe is in the 9094 // same document as the positioning element (component), then identify the offset 9095 // difference between the iframe and the component. 9096 const getOffset = (component, origin, anchorInfo) => { 9097 const win = defaultView(anchorInfo.root).dom; 9098 const hasSameOwner = (frame) => { 9099 const frameOwner = owner$4(frame); 9100 const compOwner = owner$4(component.element); 9101 return eq(frameOwner, compOwner); 9102 }; 9103 return Optional.from(win.frameElement).map(SugarElement.fromDom) 9104 .filter(hasSameOwner).map(absolute$3); 9105 }; 9106 const getRootPoint = (component, origin, anchorInfo) => { 9107 const doc = owner$4(component.element); 9108 const outerScroll = get$b(doc); 9109 const offset = getOffset(component, origin, anchorInfo).getOr(outerScroll); 9110 return absolute(offset, outerScroll.left, outerScroll.top); 9111 }; 9112 9113 const getBox = (left, top, width, height) => { 9114 const point = screen(SugarPosition(left, top)); 9115 return Optional.some(pointed(point, width, height)); 9116 }; 9117 const calcNewAnchor = (optBox, rootPoint, anchorInfo, origin, elem) => optBox.map((box) => { 9118 const points = [rootPoint, box.point]; 9119 const topLeft = cata(origin, () => sumAsAbsolute(points), () => sumAsAbsolute(points), () => sumAsFixed(points)); 9120 const anchorBox = rect(topLeft.left, topLeft.top, box.width, box.height); 9121 const layoutsLtr = anchorInfo.showAbove ? 9122 aboveOrBelow() : 9123 belowOrAbove(); 9124 const layoutsRtl = anchorInfo.showAbove ? 9125 aboveOrBelowRtl() : 9126 belowOrAboveRtl(); 9127 const layouts = get$1(elem, anchorInfo, layoutsLtr, layoutsRtl, layoutsLtr, layoutsRtl, Optional.none()); 9128 return nu$1({ 9129 anchorBox, 9130 bubble: anchorInfo.bubble.getOr(fallback()), 9131 overrides: anchorInfo.overrides, 9132 layouts 9133 }); 9134 }); 9135
9136 const placement$2 = (component, anchorInfo, origin) => { 9137 const rootPoint = getRootPoint(component, origin, anchorInfo); 9138 return anchorInfo.node 9139 // Ensure the node is still attached, otherwise we can't get a valid client rect for a detached node 9140 .filter(inBody) 9141 .bind((target) => { 9142 const rect = target.dom.getBoundingClientRect(); 9143 const nodeBox = getBox(rect.left, rect.top, rect.width, rect.height); 9144 const elem = anchorInfo.node.getOr(component.element); 9145 return calcNewAnchor(nodeBox, rootPoint, anchorInfo, origin, elem); 9146 }); 9147 }; 9148 var NodeAnchor = [ 9149 required$1('node'), 9150 required$1('root'), 9151 option$3('bubble'), 9152 schema$n(), 9153 // chiefly MaxHeight.expandable() 9154 defaulted('overrides', {}), 9155 defaulted('showAbove', false), 9156 output$1('placement', placement$2) 9157 ]; 9158 9159 const point = (element, offset) => ({ 9160 element, 9161 offset 9162 }); 9163 // NOTE: This only descends once. 9164 const descendOnce$1 = (element, offset) => { 9165 const children$1 = children(element); 9166 if (children$1.length === 0) { 9167 return point(element, offset); 9168 } 9169 else if (offset < children$1.length) { 9170 return point(children$1[offset], 0); 9171 } 9172 else { 9173 const last = children$1[children$1.length - 1]; 9174 const len = isText(last) ? get$a(last).length : children(last).length; 9175 return point(last, len); 9176 } 9177 }; 9178 9179 // A range from (a, 1) to (body, end) was giving the wrong bounds. 9180 const descendOnce = (element, offset) => isText(element) ? point(element, offset) : descendOnce$1(element, offset); 9181 const isSimRange = (detail) => detail.foffset !== undefined; 9182 const getAnchorSelection = (win, anchorInfo) => { 9183 // FIX TEST Test both providing a getSelection and not providing a getSelection 9184 const getSelection = anchorInfo.getSelection.getOrThunk(() => () => getExact(win)); 9185 return getSelection().map((sel) => { 9186 if (isSimRange(sel)) { 9187 const modStart = descendOnce(sel.start, sel.soffset); 9188 const modFinish = descendOnce(sel.finish, sel.foffset); 9189 return SimSelection.range(modStart.element, modStart.offset, modFinish.element, modFinish.offset); 9190 } 9191 else { 9192 return sel; 9193 } 9194 }); 9195 }; 9196 const placement$1 = (component, anchorInfo, origin) => { 9197 const win = defaultView(anchorInfo.root).dom; 9198 const rootPoint = getRootPoint(component, origin, anchorInfo); 9199 const selectionBox = getAnchorSelection(win, anchorInfo).bind((sel) => { 9200 // This represents the *visual* rectangle of the selection. 9201 if (isSimRange(sel)) { 9202 const optRect = getBounds$2(win, SimSelection.exactFromRange(sel)).orThunk(() => { 9203 const zeroWidth$1 = SugarElement.fromText(zeroWidth); 9204 const beforeMutation = getExact(win); 9205 before$1(sel.start, zeroWidth$1); 9206 // Certain things like <p><br/></p> with (p, 0) or <br>) as collapsed selection do not return a client rectangle 9207 const rect = getFirstRect(win, SimSelection.exact(zeroWidth$1, 0, zeroWidth$1, 1)); 9208 remove$7(zeroWidth$1); 9209 beforeMutation.each((range) => setExact(win, range.start, range.soffset, range.finish, range.foffset)); 9210 return rect; 9211 }); 9212 return optRect.bind((rawRect) => { 9213 return getBox(rawRect.left, rawRect.top, rawRect.width, rawRect.height); 9214 }); 9215 } 9216 else { 9217 const selectionRect = map$1(sel, (cell) => cell.dom.getBoundingClientRect()); 9218 const bounds = { 9219 left: Math.min(selectionRect.firstCell.left, selectionRect.lastCell.left), 9220 right: Math.max(selectionRect.firstCell.right, selectionRect.lastCell.right),
vendor: 43,262 bytes, lines 9221-10256
9221 top: Math.min(selectionRect.firstCell.top, selectionRect.lastCell.top), 9222 bottom: Math.max(selectionRect.firstCell.bottom, selectionRect.lastCell.bottom) 9223 }; 9224 return getBox(bounds.left, bounds.top, bounds.right - bounds.left, bounds.bottom - bounds.top); 9225 } 9226 }); 9227 const targetElement = getAnchorSelection(win, anchorInfo) 9228 .bind((sel) => { 9229 if (isSimRange(sel)) { 9230 return isElement$1(sel.start) ? Optional.some(sel.start) : parentElement(sel.start); 9231 } 9232 else { 9233 return Optional.some(sel.firstCell); 9234 } 9235 }); 9236 const elem = targetElement.getOr(component.element); 9237 return calcNewAnchor(selectionBox, rootPoint, anchorInfo, origin, elem); 9238 }; 9239 var SelectionAnchor = [ 9240 option$3('getSelection'), 9241 required$1('root'), 9242 option$3('bubble'), 9243 schema$n(), 9244 defaulted('overrides', {}), 9245 defaulted('showAbove', false), 9246 output$1('placement', placement$1) 9247 ]; 9248 9249 /* 9250 Layout for submenus; 9251 Either left or right of the anchor menu item. Never above or below. 9252 Aligned to the top or bottom of the anchor as appropriate. 9253 */ 9254 const labelPrefix = 'link-layout'; 9255 // display element to the right, left edge against the right of the menu 9256 const eastX = (anchor) => anchor.x + anchor.width; 9257 // display element to the left, right edge against the left of the menu 9258 const westX = (anchor, element) => anchor.x - element.width; 9259 // display element pointing up, bottom edge against the bottom of the menu (usually to one side) 9260 const northY = (anchor, element) => anchor.y - element.height + anchor.height; 9261 // display element pointing down, top edge against the top of the menu (usually to one side) 9262 const southY = (anchor) => anchor.y; 9263 const southeast = (anchor, element, bubbles) => nu$5(eastX(anchor), southY(anchor), bubbles.southeast(), southeast$3(), "southeast" /* Placement.Southeast */, boundsRestriction(anchor, { left: 0 /* AnchorBoxBounds.RightEdge */, top: 2 /* AnchorBoxBounds.TopEdge */ }), labelPrefix); 9264 const southwest = (anchor, element, bubbles) => nu$5(westX(anchor, element), southY(anchor), bubbles.southwest(), southwest$3(), "southwest" /* Placement.Southwest */, boundsRestriction(anchor, { right: 1 /* AnchorBoxBounds.LeftEdge */, top: 2 /* AnchorBoxBounds.TopEdge */ }), labelPrefix); 9265 const northeast = (anchor, element, bubbles) => nu$5(eastX(anchor), northY(anchor, element), bubbles.northeast(), northeast$3(), "northeast" /* Placement.Northeast */, boundsRestriction(anchor, { left: 0 /* AnchorBoxBounds.RightEdge */, bottom: 3 /* AnchorBoxBounds.BottomEdge */ }), labelPrefix); 9266 const northwest = (anchor, element, bubbles) => nu$5(westX(anchor, element), northY(anchor, element), bubbles.northwest(), northwest$3(), "northwest" /* Placement.Northwest */, boundsRestriction(anchor, { right: 1 /* AnchorBoxBounds.LeftEdge */, bottom: 3 /* AnchorBoxBounds.BottomEdge */ }), labelPrefix); 9267 const all = () => [southeast, southwest, northeast, northwest]; 9268 const allRtl = () => [southwest, southeast, northwest, northeast]; 9269 9270 const placement = (component, submenuInfo, origin) => { 9271 const anchorBox = toBox(origin, submenuInfo.item.element); 9272 const layouts = get$1(component.element, submenuInfo, all(), allRtl(), 9273 // No default bottomToTop layouts currently needed 9274 all(), allRtl(), Optional.none()); 9275 return Optional.some(nu$1({ 9276 anchorBox, 9277 bubble: fallback(), 9278 overrides: submenuInfo.overrides, 9279 layouts 9280 })); 9281 }; 9282 var SubmenuAnchor = [ 9283 required$1('item'), 9284 schema$n(), 9285 defaulted('overrides', {}), 9286 output$1('placement', placement) 9287 ]; 9288 9289 var AnchorSchema = choose$1('type', { 9290 selection: SelectionAnchor, 9291 node: NodeAnchor, 9292 hotspot: HotspotAnchor, 9293 submenu: SubmenuAnchor, 9294 makeshift: MakeshiftAnchor 9295 }); 9296 9297 const TransitionSchema = [ 9298 requiredArrayOf('classes', string), 9299 defaultedStringEnum('mode', 'all', ['all', 'layout', 'placement']) 9300 ]; 9301 const PositionSchema = [ 9302 defaulted('useFixed', never), 9303 option$3('getBounds') 9304 ]; 9305 const PlacementSchema = [ 9306 requiredOf('anchor', AnchorSchema), 9307 optionObjOf('transition', TransitionSchema) 9308 ]; 9309 9310 const getFixedOrigin = () => { 9311 // Don't use window.innerWidth/innerHeight here, as we don't want to include scrollbars 9312 // since the right/bottom position is based on the edge of the scrollbar not the window 9313 const html = document.documentElement; 9314 return fixed(0, 0, html.clientWidth, html.clientHeight); 9315 }; 9316 const getRelativeOrigin = (component) => { 9317 const position = absolute$3(component.element); 9318 const bounds = component.element.dom.getBoundingClientRect(); 9319 // We think that this just needs to be kept consistent with Boxes.win. If we remove the scroll values from Boxes.win, we 9320 // should change this to just bounds.left and bounds.top from getBoundingClientRect 9321 return relative(position.left, position.top, bounds.width, bounds.height); 9322 }; 9323 const place = (origin, anchoring, optBounds, placee, lastPlace, transition) => { 9324 const anchor = box(anchoring.anchorBox, origin); 9325 return simple(anchor, placee.element, anchoring.bubble, anchoring.layouts, lastPlace, optBounds, anchoring.overrides, transition); 9326 }; 9327 const position$1 = (component, posConfig, posState, placee, placementSpec) => { 9328 const optWithinBounds = Optional.none(); 9329 positionWithinBounds(component, posConfig, posState, placee, placementSpec, optWithinBounds); 9330 }; 9331 const positionWithinBounds = (component, posConfig, posState, placee, placementSpec, optWithinBounds) => { 9332 const placeeDetail = asRawOrDie$1('placement.info', objOf(PlacementSchema), placementSpec); 9333 const anchorage = placeeDetail.anchor; 9334 const element = placee.element; 9335 const placeeState = posState.get(placee.uid); 9336 // Preserve the focus as IE 11 loses it when setting visibility to hidden 9337 preserve(() => { 9338 // We set it to be fixed, so that it doesn't interfere with the layout of anything 9339 // when calculating anchors 9340 set$7(element, 'position', 'fixed'); 9341 const oldVisibility = getRaw(element, 'visibility'); 9342 set$7(element, 'visibility', 'hidden'); 9343 // We need to calculate the origin (esp. the bounding client rect) *after* we have done 9344 // all the preprocessing of the component and placee. Otherwise, the relative positions 9345 // (bottom and right) will be using the wrong dimensions 9346 const origin = posConfig.useFixed() ? getFixedOrigin() : getRelativeOrigin(component); 9347 anchorage.placement(component, anchorage, origin).each((anchoring) => { 9348 // If "within bounds" is specified, it overrides any Positioning config. Otherwise, we 9349 // use the Positioning config. We don't try to combine automatically here because they are 9350 // sometimes serving different purposes. If the Positioning config getBounds needs to be 9351 // combined with the optWithinBounds bounds, then it is the responsibility of the calling 9352 // code to combine them, and pass in the combined value as optWithinBounds. The optWithinBounds 9353 // will *always* override the Positioning config. 9354 const optBounds = optWithinBounds.orThunk(() => posConfig.getBounds.map(apply$1)); 9355 // Place the element and then update the state for the placee 9356 const newState = place(origin, anchoring, optBounds, placee, placeeState, placeeDetail.transition); 9357 posState.set(placee.uid, newState); 9358 }); 9359 oldVisibility.fold(() => { 9360 remove$6(element, 'visibility'); 9361 }, (vis) => { 9362 set$7(element, 'visibility', vis); 9363 }); 9364 // We need to remove position: fixed put on by above code if it is not needed. 9365 if (getRaw(element, 'left').isNone() && 9366 getRaw(element, 'top').isNone() && 9367 getRaw(element, 'right').isNone() && 9368 getRaw(element, 'bottom').isNone() && 9369 is$1(getRaw(element, 'position'), 'fixed')) { 9370 remove$6(element, 'position'); 9371 } 9372 }, element); 9373 }; 9374 const getMode = (component, pConfig, _pState) => pConfig.useFixed() ? 'fixed' : 'absolute'; 9375 const reset = (component, pConfig, posState, placee) => { 9376 const element = placee.element; 9377 each$1(['position', 'left', 'right', 'top', 'bottom'], (prop) => remove$6(element, prop)); 9378 reset$2(element); 9379 posState.clear(placee.uid); 9380 }; 9381 9382 var PositionApis = /*#__PURE__*/Object.freeze({ 9383 __proto__: null, 9384 position: position$1, 9385 positionWithinBounds: positionWithinBounds, 9386 getMode: getMode, 9387 reset: reset 9388 }); 9389 9390 const init$8 = () => { 9391 let state = {}; 9392 const set = (id, data) => { 9393 state[id] = data; 9394 }; 9395 const get = (id) => get$h(state, id); 9396 const clear = (id) => { 9397 if (isNonNullable(id)) { 9398 delete state[id]; 9399 } 9400 else { 9401 state = {}; 9402 } 9403 }; 9404 return nu$4({ 9405 readState: () => state, 9406 clear, 9407 set, 9408 get 9409 }); 9410 }; 9411 9412 var PositioningState = /*#__PURE__*/Object.freeze({ 9413 __proto__: null, 9414 init: init$8 9415 }); 9416 9417 const Positioning = create$3({ 9418 fields: PositionSchema, 9419 name: 'positioning', 9420 active: ActivePosition, 9421 apis: PositionApis, 9422 state: PositioningState 9423 }); 9424 9425 const chooseChannels = (channels, message) => message.universal ? channels : filter$2(channels, (ch) => contains$2(message.channels, ch)); 9426 const events$8 = (receiveConfig) => derive$2([ 9427 run$1(receive(), (component, message) => { 9428 const channelMap = receiveConfig.channels; 9429 const channels = keys(channelMap); 9430 // NOTE: Receiving event ignores the whole simulated event part. 9431 // TODO: Think about the types for this, or find a better way for this to rely on receiving. 9432 const receivingData = message; 9433 const targetChannels = chooseChannels(channels, receivingData); 9434 each$1(targetChannels, (ch) => { 9435 const channelInfo = channelMap[ch]; 9436 const channelSchema = channelInfo.schema; 9437 const data = asRawOrDie$1('channel[' + ch + '] data\nReceiver: ' + element(component.element), channelSchema, receivingData.data); 9438 channelInfo.onReceive(component, data); 9439 }); 9440 }) 9441 ]); 9442 9443 var ActiveReceiving = /*#__PURE__*/Object.freeze({ 9444 __proto__: null, 9445 events: events$8 9446 }); 9447 9448 var ReceivingSchema = [ 9449 requiredOf('channels', setOf( 9450 // Allow any keys. 9451 Result.value, objOfOnly([ 9452 onStrictHandler('onReceive'), 9453 defaulted('schema', anyValue()) 9454 ]))) 9455 ]; 9456 9457 const Receiving = create$3({ 9458 fields: ReceivingSchema, 9459 name: 'receiving', 9460 active: ActiveReceiving 9461 }); 9462 9463 const events$7 = (reflectingConfig, reflectingState) => { 9464 const update = (component, data) => { 9465 reflectingConfig.updateState.each((updateState) => { 9466 const newState = updateState(component, data); 9467 reflectingState.set(newState); 9468 }); 9469 // FIX: Partial duplication of Replacing + Receiving 9470 reflectingConfig.renderComponents.each((renderComponents) => { 9471 const newComponents = renderComponents(data, reflectingState.get()); 9472 const replacer = reflectingConfig.reuseDom ? withReuse : withoutReuse; 9473 replacer(component, newComponents); 9474 }); 9475 }; 9476 return derive$2([ 9477 run$1(receive(), (component, message) => { 9478 // NOTE: Receiving event ignores the whole simulated event part. 9479 // TODO: Think about the types for this, or find a better way for this to rely on receiving. 9480 const receivingData = message; 9481 if (!receivingData.universal) { 9482 const channel = reflectingConfig.channel; 9483 if (contains$2(receivingData.channels, channel)) { 9484 update(component, receivingData.data); 9485 } 9486 } 9487 }), 9488 runOnAttached((comp, _se) => { 9489 reflectingConfig.initialData.each((rawData) => { 9490 update(comp, rawData); 9491 }); 9492 }) 9493 ]); 9494 }; 9495 9496 var ActiveReflecting = /*#__PURE__*/Object.freeze({ 9497 __proto__: null, 9498 events: events$7 9499 }); 9500 9501 const getState$1 = (component, replaceConfig, reflectState) => reflectState; 9502 9503 var ReflectingApis = /*#__PURE__*/Object.freeze({ 9504 __proto__: null, 9505 getState: getState$1 9506 }); 9507 9508 var ReflectingSchema = [ 9509 required$1('channel'), 9510 option$3('renderComponents'), 9511 option$3('updateState'), 9512 option$3('initialData'), 9513 defaultedBoolean('reuseDom', true) 9514 ]; 9515 9516 const init$7 = () => { 9517 const cell = Cell(Optional.none()); 9518 const clear = () => cell.set(Optional.none()); 9519 const readState = () => cell.get().getOr('none'); 9520 return { 9521 readState, 9522 get: cell.get, 9523 set: cell.set, 9524 clear 9525 }; 9526 }; 9527 9528 var ReflectingState = /*#__PURE__*/Object.freeze({ 9529 __proto__: null, 9530 init: init$7 9531 }); 9532 9533 const Reflecting = create$3({ 9534 fields: ReflectingSchema, 9535 name: 'reflecting', 9536 active: ActiveReflecting, 9537 apis: ReflectingApis, 9538 state: ReflectingState 9539 }); 9540 9541 // NOTE: A sandbox should not start as part of the world. It is expected to be 9542 // added to the sink on rebuild. 9543 const rebuild = (sandbox, sConfig, sState, data) => { 9544 sState.get().each((_data) => { 9545 // If currently has data, so it hasn't been removed yet. It is 9546 // being "re-opened" 9547 detachChildren(sandbox); 9548 }); 9549 const point = sConfig.getAttachPoint(sandbox); 9550 attach(point, sandbox); 9551 // Must be after the sandbox is in the system 9552 const built = sandbox.getSystem().build(data); 9553 attach(sandbox, built); 9554 sState.set(built); 9555 return built; 9556 }; 9557 // Open sandbox transfers focus to the opened menu 9558 const open$1 = (sandbox, sConfig, sState, data) => { 9559 const newState = rebuild(sandbox, sConfig, sState, data); 9560 sConfig.onOpen(sandbox, newState); 9561 return newState; 9562 }; 9563 const setContent = (sandbox, sConfig, sState, data) => sState.get().map(() => rebuild(sandbox, sConfig, sState, data)); 9564 // TODO AP-191 write a test for openWhileCloaked 9565 const openWhileCloaked = (sandbox, sConfig, sState, data, transaction) => { 9566 cloak(sandbox, sConfig); 9567 open$1(sandbox, sConfig, sState, data); 9568 transaction(); 9569 decloak(sandbox, sConfig); 9570 }; 9571 const close$1 = (sandbox, sConfig, sState) => { 9572 sState.get().each((data) => { 9573 detachChildren(sandbox); 9574 detach(sandbox); 9575 sConfig.onClose(sandbox, data); 9576 sState.clear(); 9577 }); 9578 }; 9579 const isOpen$1 = (_sandbox, _sConfig, sState) => sState.isOpen(); 9580 const isPartOf$1 = (sandbox, sConfig, sState, queryElem) => isOpen$1(sandbox, sConfig, sState) && sState.get().exists((data) => sConfig.isPartOf(sandbox, data, queryElem)); 9581 const getState = (_sandbox, _sConfig, sState) => sState.get(); 9582 const store = (sandbox, cssKey, attr, newValue) => { 9583 getRaw(sandbox.element, cssKey).fold(() => { 9584 remove$8(sandbox.element, attr); 9585 }, (v) => { 9586 set$9(sandbox.element, attr, v); 9587 }); 9588 set$7(sandbox.element, cssKey, newValue); 9589 }; 9590 const restore = (sandbox, cssKey, attr) => { 9591 getOpt(sandbox.element, attr).fold(() => remove$6(sandbox.element, cssKey), (oldValue) => set$7(sandbox.element, cssKey, oldValue)); 9592 }; 9593 const cloak = (sandbox, sConfig, _sState) => { 9594 const sink = sConfig.getAttachPoint(sandbox); 9595 // Use the positioning mode of the sink, so that it does not interfere with the sink's positioning 9596 // We add it here to stop it causing layout problems. 9597 set$7(sandbox.element, 'position', Positioning.getMode(sink)); 9598 store(sandbox, 'visibility', sConfig.cloakVisibilityAttr, 'hidden'); 9599 }; 9600 const hasPosition = (element) => exists(['top', 'left', 'right', 'bottom'], (pos) => getRaw(element, pos).isSome()); 9601 const decloak = (sandbox, sConfig, _sState) => { 9602 if (!hasPosition(sandbox.element)) { 9603 // If a position value was not added to the sandbox during cloaking, remove it 9604 // otherwise certain position values (absolute, relative) will impact the child that _was_ positioned 9605 remove$6(sandbox.element, 'position'); 9606 } 9607 restore(sandbox, 'visibility', sConfig.cloakVisibilityAttr); 9608 }; 9609 9610 var SandboxApis = /*#__PURE__*/Object.freeze({ 9611 __proto__: null, 9612 cloak: cloak, 9613 decloak: decloak, 9614 open: open$1, 9615 openWhileCloaked: openWhileCloaked, 9616 close: close$1, 9617 isOpen: isOpen$1, 9618 isPartOf: isPartOf$1, 9619 getState: getState, 9620 setContent: setContent 9621 }); 9622 9623 const events$6 = (sandboxConfig, sandboxState) => derive$2([ 9624 run$1(sandboxClose(), (sandbox, _simulatedEvent) => { 9625 close$1(sandbox, sandboxConfig, sandboxState); 9626 }) 9627 ]); 9628 9629 var ActiveSandbox = /*#__PURE__*/Object.freeze({ 9630 __proto__: null, 9631 events: events$6 9632 }); 9633 9634 var SandboxSchema = [ 9635 onHandler('onOpen'), 9636 onHandler('onClose'), 9637 // Maybe this should be optional 9638 required$1('isPartOf'), 9639 required$1('getAttachPoint'), 9640 defaulted('cloakVisibilityAttr', 'data-precloak-visibility') 9641 ]; 9642 9643 const init$6 = () => { 9644 const contents = value$2(); 9645 const readState = constant$1('not-implemented'); 9646 return nu$4({ 9647 readState, 9648 isOpen: contents.isSet, 9649 clear: contents.clear, 9650 set: contents.set, 9651 get: contents.get 9652 }); 9653 }; 9654 9655 var SandboxState = /*#__PURE__*/Object.freeze({ 9656 __proto__: null, 9657 init: init$6 9658 }); 9659 9660 const Sandboxing = create$3({ 9661 fields: SandboxSchema, 9662 name: 'sandboxing', 9663 active: ActiveSandbox, 9664 apis: SandboxApis, 9665 state: SandboxState 9666 }); 9667 9668 const getAnimationRoot = (component, slideConfig) => slideConfig.getAnimationRoot.fold(() => component.element, (get) => get(component)); 9669 9670 const getDimensionProperty = (slideConfig) => slideConfig.dimension.property; 9671 const getDimension = (slideConfig, elem) => slideConfig.dimension.getDimension(elem); 9672 const disableTransitions = (component, slideConfig) => { 9673 const root = getAnimationRoot(component, slideConfig); 9674 remove$2(root, [slideConfig.shrinkingClass, slideConfig.growingClass]); 9675 }; 9676 const setShrunk = (component, slideConfig) => { 9677 remove$3(component.element, slideConfig.openClass); 9678 add$2(component.element, slideConfig.closedClass); 9679 set$7(component.element, getDimensionProperty(slideConfig), '0px'); 9680 reflow(component.element); 9681 }; 9682 const setGrown = (component, slideConfig) => { 9683 remove$3(component.element, slideConfig.closedClass); 9684 add$2(component.element, slideConfig.openClass); 9685 remove$6(component.element, getDimensionProperty(slideConfig)); 9686 }; 9687 const doImmediateShrink = (component, slideConfig, slideState, _calculatedSize) => { 9688 slideState.setCollapsed(); 9689 // Force current dimension to begin transition 9690 set$7(component.element, getDimensionProperty(slideConfig), getDimension(slideConfig, component.element)); 9691 // TINY-8710: we don't think reflow is required (as has been done elsewhere) as the animation is not needed 9692 disableTransitions(component, slideConfig); 9693 setShrunk(component, slideConfig); 9694 slideConfig.onStartShrink(component); 9695 slideConfig.onShrunk(component); 9696 }; 9697 const doStartShrink = (component, slideConfig, slideState, calculatedSize) => { 9698 const size = calculatedSize.getOrThunk(() => getDimension(slideConfig, component.element)); 9699 slideState.setCollapsed(); 9700 // Force current dimension to begin transition 9701 set$7(component.element, getDimensionProperty(slideConfig), size); 9702 reflow(component.element); 9703 const root = getAnimationRoot(component, slideConfig); 9704 remove$3(root, slideConfig.growingClass); 9705 add$2(root, slideConfig.shrinkingClass); // enable transitions 9706 setShrunk(component, slideConfig); 9707 slideConfig.onStartShrink(component); 9708 }; 9709 // A "smartShrink" will do an immediate shrink if no shrinking is scheduled to happen 9710 const doStartSmartShrink = (component, slideConfig, slideState) => { 9711 const size = getDimension(slideConfig, component.element); 9712 const shrinker = size === '0px' ? doImmediateShrink : doStartShrink; 9713 shrinker(component, slideConfig, slideState, Optional.some(size)); 9714 }; 9715 // Showing is complex due to the inability to transition to "auto". 9716 // We also can't cache the dimension as the parents may have resized since it was last shown. 9717 const doStartGrow = (component, slideConfig, slideState) => { 9718 // Start the growing animation styles 9719 const root = getAnimationRoot(component, slideConfig); 9720 // Record whether this is interrupting a shrink and its current size 9721 const wasShrinking = has(root, slideConfig.shrinkingClass); 9722 const beforeSize = getDimension(slideConfig, component.element); 9723 setGrown(component, slideConfig); 9724 const fullSize = getDimension(slideConfig, component.element); 9725 // If the grow is interrupting a shrink, use the size from before the grow as the start size 9726 // And reflow so that the animation works. 9727 const startPartialGrow = () => { 9728 set$7(component.element, getDimensionProperty(slideConfig), beforeSize); 9729 reflow(component.element); 9730 }; 9731 // If the grow is not interrupting a shrink, start from 0 (shrunk) 9732 const startCompleteGrow = () => { 9733 setShrunk(component, slideConfig); 9734 }; 9735 // Determine what the initial size for the grow operation should be. 9736 const setStartSize = wasShrinking ? startPartialGrow : startCompleteGrow; 9737 setStartSize(); 9738 remove$3(root, slideConfig.shrinkingClass); 9739 add$2(root, slideConfig.growingClass); 9740 setGrown(component, slideConfig); 9741 set$7(component.element, getDimensionProperty(slideConfig), fullSize); 9742 slideState.setExpanded(); 9743 slideConfig.onStartGrow(component); 9744 }; 9745 const refresh$3 = (component, slideConfig, slideState) => { 9746 if (slideState.isExpanded()) { 9747 remove$6(component.element, getDimensionProperty(slideConfig)); 9748 const fullSize = getDimension(slideConfig, component.element); 9749 set$7(component.element, getDimensionProperty(slideConfig), fullSize); 9750 } 9751 }; 9752 const grow = (component, slideConfig, slideState) => { 9753 if (!slideState.isExpanded()) { 9754 doStartGrow(component, slideConfig, slideState); 9755 } 9756 }; 9757 const shrink = (component, slideConfig, slideState) => { 9758 if (slideState.isExpanded()) { 9759 doStartSmartShrink(component, slideConfig, slideState); 9760 } 9761 }; 9762 const immediateShrink = (component, slideConfig, slideState) => { 9763 if (slideState.isExpanded()) { 9764 doImmediateShrink(component, slideConfig, slideState); 9765 } 9766 }; 9767 const hasGrown = (component, slideConfig, slideState) => slideState.isExpanded(); 9768 const hasShrunk = (component, slideConfig, slideState) => slideState.isCollapsed(); 9769 const isGrowing = (component, slideConfig, _slideState) => { 9770 const root = getAnimationRoot(component, slideConfig); 9771 return has(root, slideConfig.growingClass) === true; 9772 }; 9773 const isShrinking = (component, slideConfig, _slideState) => { 9774 const root = getAnimationRoot(component, slideConfig); 9775 return has(root, slideConfig.shrinkingClass) === true; 9776 }; 9777 const isTransitioning = (component, slideConfig, slideState) => isGrowing(component, slideConfig) || isShrinking(component, slideConfig); 9778 const toggleGrow = (component, slideConfig, slideState) => { 9779 const f = slideState.isExpanded() ? doStartSmartShrink : doStartGrow; 9780 f(component, slideConfig, slideState); 9781 }; 9782 const immediateGrow = (component, slideConfig, slideState) => { 9783 if (!slideState.isExpanded()) { 9784 setGrown(component, slideConfig); 9785 set$7(component.element, getDimensionProperty(slideConfig), getDimension(slideConfig, component.element)); 9786 // TINY-8710: we don't think reflow is required (as has been done elsewhere) as the animation is not needed 9787 // Keep disableTransition to handle the case where it's part way through transitioning 9788 disableTransitions(component, slideConfig); 9789 slideState.setExpanded(); 9790 slideConfig.onStartGrow(component); 9791 slideConfig.onGrown(component); 9792 } 9793 }; 9794 9795 var SlidingApis = /*#__PURE__*/Object.freeze({ 9796 __proto__: null, 9797 refresh: refresh$3, 9798 grow: grow, 9799 shrink: shrink, 9800 immediateShrink: immediateShrink, 9801 hasGrown: hasGrown, 9802 hasShrunk: hasShrunk, 9803 isGrowing: isGrowing, 9804 isShrinking: isShrinking, 9805 isTransitioning: isTransitioning, 9806 toggleGrow: toggleGrow, 9807 disableTransitions: disableTransitions, 9808 immediateGrow: immediateGrow 9809 }); 9810 9811 const exhibit$3 = (base, slideConfig, _slideState) => { 9812 const expanded = slideConfig.expanded; 9813 return expanded ? nu$2({ 9814 classes: [slideConfig.openClass], 9815 styles: {} 9816 }) : nu$2({ 9817 classes: [slideConfig.closedClass], 9818 styles: wrap(slideConfig.dimension.property, '0px') 9819 }); 9820 }; 9821 const events$5 = (slideConfig, slideState) => derive$2([ 9822 runOnSource(transitionend(), (component, simulatedEvent) => { 9823 const raw = simulatedEvent.event.raw; 9824 // This will fire for all transitions, we're only interested in the dimension completion on source 9825 if (raw.propertyName === slideConfig.dimension.property) { 9826 disableTransitions(component, slideConfig); // disable transitions immediately (Safari animates the dimension removal below) 9827 if (slideState.isExpanded()) { 9828 remove$6(component.element, slideConfig.dimension.property); 9829 } // when showing, remove the dimension so it is responsive 9830 const notify = slideState.isExpanded() ? slideConfig.onGrown : slideConfig.onShrunk; 9831 notify(component); 9832 } 9833 }) 9834 ]); 9835 9836 var ActiveSliding = /*#__PURE__*/Object.freeze({ 9837 __proto__: null, 9838 exhibit: exhibit$3, 9839 events: events$5 9840 }); 9841 9842 var SlidingSchema = [ 9843 required$1('closedClass'), 9844 required$1('openClass'), 9845 required$1('shrinkingClass'), 9846 required$1('growingClass'), 9847 // Element which shrinking and growing animations 9848 option$3('getAnimationRoot'), 9849 onHandler('onShrunk'), 9850 onHandler('onStartShrink'), 9851 onHandler('onGrown'), 9852 onHandler('onStartGrow'), 9853 defaulted('expanded', false), 9854 requiredOf('dimension', choose$1('property', { 9855 width: [ 9856 output$1('property', 'width'), 9857 output$1('getDimension', (elem) => get$c(elem) + 'px') 9858 ], 9859 height: [ 9860 output$1('property', 'height'), 9861 output$1('getDimension', (elem) => get$d(elem) + 'px') 9862 ] 9863 })) 9864 ]; 9865 9866 const init$5 = (spec) => { 9867 const state = Cell(spec.expanded); 9868 const readState = () => 'expanded: ' + state.get(); 9869 return nu$4({ 9870 isExpanded: () => state.get() === true, 9871 isCollapsed: () => state.get() === false, 9872 setCollapsed: curry(state.set, false), 9873 setExpanded: curry(state.set, true), 9874 readState 9875 }); 9876 }; 9877 9878 var SlidingState = /*#__PURE__*/Object.freeze({ 9879 __proto__: null, 9880 init: init$5 9881 }); 9882 9883 const Sliding = create$3({ 9884 fields: SlidingSchema, 9885 name: 'sliding', 9886 active: ActiveSliding, 9887 apis: SlidingApis, 9888 state: SlidingState 9889 }); 9890 9891 const events$4 = (streamConfig, streamState) => { 9892 const streams = streamConfig.stream.streams; 9893 const processor = streams.setup(streamConfig, streamState); 9894 return derive$2([ 9895 run$1(streamConfig.event, processor), 9896 runOnDetached(() => streamState.cancel()) 9897 ].concat(streamConfig.cancelEvent.map((e) => [ 9898 run$1(e, () => streamState.cancel()) 9899 ]).getOr([]))); 9900 }; 9901 9902 var ActiveStreaming = /*#__PURE__*/Object.freeze({ 9903 __proto__: null, 9904 events: events$4 9905 }); 9906 9907 const throttle = (_config) => { 9908 const state = Cell(null); 9909 const readState = () => ({ 9910 timer: state.get() !== null ? 'set' : 'unset' 9911 }); 9912 const setTimer = (t) => { 9913 state.set(t); 9914 }; 9915 const cancel = () => { 9916 const t = state.get(); 9917 if (t !== null) { 9918 t.cancel(); 9919 } 9920 }; 9921 return nu$4({ 9922 readState, 9923 setTimer, 9924 cancel 9925 }); 9926 }; 9927 const init$4 = (spec) => spec.stream.streams.state(spec); 9928 9929 var StreamingState = /*#__PURE__*/Object.freeze({ 9930 __proto__: null, 9931 throttle: throttle, 9932 init: init$4 9933 }); 9934 9935 const setup$e = (streamInfo, streamState) => { 9936 const sInfo = streamInfo.stream; 9937 const throttler = last(streamInfo.onStream, sInfo.delay); 9938 streamState.setTimer(throttler); 9939 return (component, simulatedEvent) => { 9940 throttler.throttle(component, simulatedEvent); 9941 if (sInfo.stopEvent) { 9942 simulatedEvent.stop(); 9943 } 9944 }; 9945 }; 9946 var StreamingSchema = [ 9947 requiredOf('stream', choose$1('mode', { 9948 throttle: [ 9949 required$1('delay'), 9950 defaulted('stopEvent', true), 9951 output$1('streams', { 9952 setup: setup$e, 9953 state: throttle 9954 }) 9955 ] 9956 })), 9957 defaulted('event', 'input'), 9958 option$3('cancelEvent'), 9959 onStrictHandler('onStream') 9960 ]; 9961 9962 const Streaming = create$3({ 9963 fields: StreamingSchema, 9964 name: 'streaming', 9965 active: ActiveStreaming, 9966 state: StreamingState 9967 }); 9968 9969 const exhibit$2 = (base, tabConfig) => nu$2({ 9970 attributes: wrapAll([ 9971 { key: tabConfig.tabAttr, value: 'true' } 9972 ]) 9973 }); 9974 9975 var ActiveTabstopping = /*#__PURE__*/Object.freeze({ 9976 __proto__: null, 9977 exhibit: exhibit$2 9978 }); 9979 9980 var TabstopSchema = [ 9981 defaulted('tabAttr', 'data-alloy-tabstop') 9982 ]; 9983 9984 const Tabstopping = create$3({ 9985 fields: TabstopSchema, 9986 name: 'tabstopping', 9987 active: ActiveTabstopping 9988 }); 9989 9990 const updateAriaState = (component, toggleConfig, toggleState) => { 9991 const ariaInfo = toggleConfig.aria; 9992 ariaInfo.update(component, ariaInfo, toggleState.get()); 9993 }; 9994 const updateClass = (component, toggleConfig, toggleState) => { 9995 toggleConfig.toggleClass.each((toggleClass) => { 9996 if (toggleState.get()) { 9997 add$2(component.element, toggleClass); 9998 } 9999 else { 10000 remove$3(component.element, toggleClass); 10001 } 10002 }); 10003 }; 10004 const set = (component, toggleConfig, toggleState, state) => { 10005 const initialState = toggleState.get(); 10006 toggleState.set(state); 10007 updateClass(component, toggleConfig, toggleState); 10008 updateAriaState(component, toggleConfig, toggleState); 10009 if (initialState !== state) { 10010 toggleConfig.onToggled(component, state); 10011 } 10012 }; 10013 const toggle$1 = (component, toggleConfig, toggleState) => { 10014 set(component, toggleConfig, toggleState, !toggleState.get()); 10015 }; 10016 const on = (component, toggleConfig, toggleState) => { 10017 set(component, toggleConfig, toggleState, true); 10018 }; 10019 const off = (component, toggleConfig, toggleState) => { 10020 set(component, toggleConfig, toggleState, false); 10021 }; 10022 const isOn = (component, toggleConfig, toggleState) => toggleState.get(); 10023 const onLoad = (component, toggleConfig, toggleState) => { 10024 // There used to be a bit of code in here that would only overwrite 10025 // the attribute if it didn't have a current value. I can't remember 10026 // what case that was for, so I'm removing it until it is required. 10027 set(component, toggleConfig, toggleState, toggleConfig.selected); 10028 }; 10029 10030 var ToggleApis = /*#__PURE__*/Object.freeze({ 10031 __proto__: null, 10032 onLoad: onLoad, 10033 toggle: toggle$1, 10034 isOn: isOn, 10035 on: on, 10036 off: off, 10037 set: set 10038 }); 10039 10040 const exhibit$1 = () => nu$2({}); 10041 const events$3 = (toggleConfig, toggleState) => { 10042 const execute = executeEvent(toggleConfig, toggleState, toggle$1); 10043 const load = loadEvent(toggleConfig, toggleState, onLoad); 10044 return derive$2(flatten([ 10045 toggleConfig.toggleOnExecute ? [execute] : [], 10046 [load] 10047 ])); 10048 }; 10049 10050 var ActiveToggle = /*#__PURE__*/Object.freeze({ 10051 __proto__: null, 10052 exhibit: exhibit$1, 10053 events: events$3 10054 }); 10055 10056 const updatePressed = (component, ariaInfo, status) => { 10057 set$9(component.element, 'aria-pressed', status); 10058 if (ariaInfo.syncWithExpanded) { 10059 updateExpanded(component, ariaInfo, status); 10060 } 10061 }; 10062 const updateSelected = (component, ariaInfo, status) => { 10063 set$9(component.element, 'aria-selected', status); 10064 }; 10065 const updateChecked = (component, ariaInfo, status) => { 10066 set$9(component.element, 'aria-checked', status); 10067 }; 10068 const updateExpanded = (component, ariaInfo, status) => { 10069 set$9(component.element, 'aria-expanded', status); 10070 }; 10071 10072 var ToggleSchema = [ 10073 defaulted('selected', false), 10074 option$3('toggleClass'), 10075 defaulted('toggleOnExecute', true), 10076 onHandler('onToggled'), 10077 defaultedOf('aria', { 10078 mode: 'none' 10079 }, choose$1('mode', { 10080 pressed: [ 10081 defaulted('syncWithExpanded', false), 10082 output$1('update', updatePressed) 10083 ], 10084 checked: [ 10085 output$1('update', updateChecked) 10086 ], 10087 expanded: [ 10088 output$1('update', updateExpanded) 10089 ], 10090 selected: [ 10091 output$1('update', updateSelected) 10092 ], 10093 none: [ 10094 output$1('update', noop) 10095 ] 10096 })) 10097 ]; 10098 10099 const Toggling = create$3({ 10100 fields: ToggleSchema, 10101 name: 'toggling', 10102 active: ActiveToggle, 10103 apis: ToggleApis, 10104 state: SetupBehaviourCellState(false) 10105 }); 10106 10107 const ExclusivityChannel = generate$6('tooltip.exclusive'); 10108 const ShowTooltipEvent = generate$6('tooltip.show'); 10109 const HideTooltipEvent = generate$6('tooltip.hide'); 10110 const ImmediateHideTooltipEvent = generate$6('tooltip.immediateHide'); 10111 const ImmediateShowTooltipEvent = generate$6('tooltip.immediateShow'); 10112 10113 const hideAllExclusive = (component, _tConfig, _tState) => { 10114 component.getSystem().broadcastOn([ExclusivityChannel], {}); 10115 }; 10116 const setComponents = (_component, _tConfig, tState, specs) => { 10117 tState.getTooltip().each((tooltip) => { 10118 if (tooltip.getSystem().isConnected()) { 10119 Replacing.set(tooltip, specs); 10120 } 10121 }); 10122 }; 10123 const isEnabled = (_component, _tConfig, tState) => tState.isEnabled(); 10124 const setEnabled = (_component, _tConfig, tState, enabled) => tState.setEnabled(enabled); 10125 const immediateOpenClose = (component, _tConfig, _tState, open) => emit(component, open ? ImmediateShowTooltipEvent : ImmediateHideTooltipEvent); 10126 10127 var TooltippingApis = /*#__PURE__*/Object.freeze({ 10128 __proto__: null, 10129 hideAllExclusive: hideAllExclusive, 10130 immediateOpenClose: immediateOpenClose, 10131 isEnabled: isEnabled, 10132 setComponents: setComponents, 10133 setEnabled: setEnabled 10134 }); 10135 10136 const events$2 = (tooltipConfig, state) => { 10137 const hide = (comp) => { 10138 state.getTooltip().each((p) => { 10139 if (p.getSystem().isConnected()) { 10140 detach(p); 10141 tooltipConfig.onHide(comp, p); 10142 state.clearTooltip(); 10143 } 10144 }); 10145 state.clearTimer(); 10146 }; 10147 const show = (comp) => { 10148 if (!state.isShowing() && state.isEnabled()) { 10149 hideAllExclusive(comp); 10150 const sink = tooltipConfig.lazySink(comp).getOrDie(); 10151 const popup = comp.getSystem().build({ 10152 dom: tooltipConfig.tooltipDom, 10153 components: tooltipConfig.tooltipComponents, 10154 events: derive$2(tooltipConfig.mode === 'normal' 10155 ? [ 10156 run$1(mouseover(), (_) => { 10157 emit(comp, ShowTooltipEvent); 10158 }), 10159 run$1(mouseout(), (_) => { 10160 emit(comp, HideTooltipEvent); 10161 }) 10162 ] 10163 : []), 10164 behaviours: derive$1([ 10165 Replacing.config({}) 10166 ]) 10167 }); 10168 state.setTooltip(popup); 10169 attach(sink, popup); 10170 tooltipConfig.onShow(comp, popup); 10171 Positioning.position(sink, popup, { anchor: tooltipConfig.anchor(comp) }); 10172 } 10173 }; 10174 const reposition = (comp) => { 10175 state.getTooltip().each((tooltip) => { 10176 const sink = tooltipConfig.lazySink(comp).getOrDie(); 10177 Positioning.position(sink, tooltip, { anchor: tooltipConfig.anchor(comp) }); 10178 }); 10179 }; 10180 const getEvents = () => { 10181 switch (tooltipConfig.mode) { 10182 case 'normal': 10183 return [ 10184 run$1(focusin(), (comp) => { 10185 emit(comp, ImmediateShowTooltipEvent); 10186 }), 10187 run$1(postBlur(), (comp) => { 10188 emit(comp, ImmediateHideTooltipEvent); 10189 }), 10190 run$1(mouseover(), (comp) => { 10191 emit(comp, ShowTooltipEvent); 10192 }), 10193 run$1(mouseout(), (comp) => { 10194 emit(comp, HideTooltipEvent); 10195 }) 10196 ]; 10197 case 'follow-highlight': 10198 return [ 10199 run$1(highlight$1(), (comp, _se) => { 10200 emit(comp, ShowTooltipEvent); 10201 }), 10202 run$1(dehighlight$1(), (comp) => { 10203 emit(comp, HideTooltipEvent); 10204 }) 10205 ]; 10206 case 'children-normal': 10207 return [ 10208 run$1(focusin(), (comp, se) => { 10209 search(comp.element).each((_) => { 10210 if (is(se.event.target, '[data-mce-tooltip]')) { 10211 state.getTooltip().fold(() => { 10212 emit(comp, ImmediateShowTooltipEvent); 10213 }, (tooltip) => { 10214 if (state.isShowing()) { 10215 tooltipConfig.onShow(comp, tooltip); 10216 reposition(comp); 10217 } 10218 }); 10219 } 10220 }); 10221 }), 10222 run$1(postBlur(), (comp) => { 10223 search(comp.element).fold(() => { 10224 emit(comp, ImmediateHideTooltipEvent); 10225 }, noop); 10226 }), 10227 run$1(mouseover(), (comp) => { 10228 descendant(comp.element, '[data-mce-tooltip]:hover').each((_) => { 10229 state.getTooltip().fold(() => { 10230 emit(comp, ShowTooltipEvent); 10231 }, (tooltip) => { 10232 if (state.isShowing()) { 10233 tooltipConfig.onShow(comp, tooltip); 10234 reposition(comp); 10235 } 10236 }); 10237 }); 10238 }), 10239 run$1(mouseout(), (comp) => { 10240 descendant(comp.element, '[data-mce-tooltip]:hover').fold(() => { 10241 emit(comp, HideTooltipEvent); 10242 }, noop); 10243 }), 10244 ]; 10245 default: 10246 return [ 10247 run$1(focusin(), (comp, se) => { 10248 search(comp.element).each((_) => { 10249 if (is(se.event.target, '[data-mce-tooltip]')) { 10250 state.getTooltip().fold(() => { 10251 emit(comp, ImmediateShowTooltipEvent); 10252 }, (tooltip) => { 10253 if (state.isShowing()) { 10254 tooltipConfig.onShow(comp, tooltip); 10255 reposition(comp); 10256
10256 } 10257 }); 10258 } 10259 }); 10260 }), 10261 run$1(postBlur(), (comp) => { 10262 search(comp.element).fold(() => { 10263 emit(comp, ImmediateHideTooltipEvent); 10264 }, noop); 10265 }), 10266 ]; 10267 } 10268 }; 10269 return derive$2(flatten([ 10270 [ 10271 runOnInit((component) => { 10272 tooltipConfig.onSetup(component); 10273 }), 10274 run$1(ShowTooltipEvent, (comp) => { 10275 state.resetTimer(() => { 10276 show(comp); 10277 }, tooltipConfig.delayForShow()); 10278 }), 10279 run$1(HideTooltipEvent, (comp) => { 10280 state.resetTimer(() => { 10281 hide(comp); 10282 }, tooltipConfig.delayForHide()); 10283 }), 10284 run$1(ImmediateShowTooltipEvent, (comp) => { 10285 state.resetTimer(() => { 10286 show(comp); 10287 }, 0); 10288 }), 10289 run$1(ImmediateHideTooltipEvent, (comp) => { 10290 state.resetTimer(() => { 10291 hide(comp); 10292 }, 0); 10293 }), 10294 run$1(receive(), (comp, message) => { 10295 // TODO: Think about the types for this, or find a better way for this 10296 // to rely on receiving. 10297 const receivingData = message; 10298 if (!receivingData.universal) { 10299 if (contains$2(receivingData.channels, ExclusivityChannel) || contains$2(receivingData.channels, closeTooltips())) { 10300 if (receivingData.data.closedTooltip && state.isShowing()) { 10301 receivingData.data.closedTooltip(); 10302 } 10303 hide(comp); 10304 } 10305 } 10306 }), 10307 runOnDetached((comp) => { 10308 hide(comp); 10309 }) 10310 ], 10311 (getEvents()) 10312 ])); 10313 }; 10314 10315 var ActiveTooltipping = /*#__PURE__*/Object.freeze({ 10316 __proto__: null, 10317 events: events$2 10318 }); 10319 10320 var TooltippingSchema = [ 10321 required$1('lazySink'), 10322 required$1('tooltipDom'), 10323 defaulted('exclusive', true), 10324 defaulted('tooltipComponents', []), 10325 defaultedFunction('delayForShow', constant$1(300)), 10326 defaultedFunction('delayForHide', constant$1(100)), 10327 defaultedFunction('onSetup', noop), 10328 defaultedStringEnum('mode', 'normal', ['normal', 'follow-highlight', 'children-keyboard-focus', 'children-normal']), 10329 defaulted('anchor', (comp) => ({ 10330 type: 'hotspot', 10331 hotspot: comp, 10332 layouts: { 10333 onLtr: constant$1([south$2, north$2, southeast$2, northeast$2, southwest$2, northwest$2]), 10334 onRtl: constant$1([south$2, north$2, southeast$2, northeast$2, southwest$2, northwest$2]) 10335 }, 10336 bubble: nu$6(0, -2, {}), 10337 })), 10338 onHandler('onHide'), 10339 onHandler('onShow'), 10340 ]; 10341 10342 const init$3 = () => { 10343 const enabled = Cell(true); 10344 const timer = value$2(); 10345 const popup = value$2(); 10346 const clearTimer = () => { 10347 timer.on(clearTimeout); 10348 }; 10349 const resetTimer = (f, delay) => { 10350 clearTimer(); 10351 timer.set(setTimeout(f, delay)); 10352 }; 10353 const readState = constant$1('not-implemented'); 10354 return nu$4({ 10355 getTooltip: popup.get, 10356 isShowing: popup.isSet, 10357 setTooltip: popup.set, 10358 clearTooltip: popup.clear, 10359 clearTimer, 10360 resetTimer, 10361 readState, 10362 isEnabled: () => enabled.get(), 10363 setEnabled: (setToEnabled) => enabled.set(setToEnabled) 10364 }); 10365 }; 10366 10367 var TooltippingState = /*#__PURE__*/Object.freeze({ 10368 __proto__: null, 10369 init: init$3 10370 }); 10371 10372 const Tooltipping = create$3({ 10373 fields: TooltippingSchema, 10374 name: 'tooltipping', 10375 active: ActiveTooltipping, 10376 state: TooltippingState, 10377 apis: TooltippingApis 10378 }); 10379 10380 const exhibit = () => nu$2({ 10381 styles: {
vendor: 11,719 bytes, lines 10382-10702
10382 '-webkit-user-select': 'none', 10383 'user-select': 'none', 10384 '-ms-user-select': 'none', 10385 '-moz-user-select': '-moz-none' 10386 }, 10387 attributes: { 10388 unselectable: 'on' 10389 } 10390 }); 10391 const events$1 = () => derive$2([ 10392 abort(selectstart(), always) 10393 ]); 10394 10395 var ActiveUnselecting = /*#__PURE__*/Object.freeze({ 10396 __proto__: null, 10397 events: events$1, 10398 exhibit: exhibit 10399 }); 10400 10401 const Unselecting = create$3({ 10402 fields: [], 10403 name: 'unselecting', 10404 active: ActiveUnselecting 10405 }); 10406 10407 const getAttrs = (elem) => { 10408 const attributes = elem.dom.attributes !== undefined ? elem.dom.attributes : []; 10409 return foldl(attributes, (b, attr) => { 10410 // Make class go through the class path. Do not list it as an attribute. 10411 if (attr.name === 'class') { 10412 return b; 10413 } 10414 else { 10415 return { ...b, [attr.name]: attr.value }; 10416 } 10417 }, {}); 10418 }; 10419 const getClasses = (elem) => Array.prototype.slice.call(elem.dom.classList, 0); 10420 const fromHtml = (html) => { 10421 const elem = SugarElement.fromHtml(html); 10422 const children$1 = children(elem); 10423 const attrs = getAttrs(elem); 10424 const classes = getClasses(elem); 10425 const contents = children$1.length === 0 ? {} : { innerHtml: get$f(elem) }; 10426 return { 10427 tag: name$3(elem), 10428 classes, 10429 attributes: attrs, 10430 ...contents 10431 }; 10432 }; 10433 10434 const record = (spec) => { 10435 const uid = isSketchSpec(spec) && hasNonNullableKey(spec, 'uid') ? spec.uid : generate$4('memento'); 10436 const get = (anyInSystem) => anyInSystem.getSystem().getByUid(uid).getOrDie(); 10437 const getOpt = (anyInSystem) => anyInSystem.getSystem().getByUid(uid).toOptional(); 10438 const asSpec = () => ({ 10439 ...spec, 10440 uid 10441 }); 10442 return { 10443 get, 10444 getOpt, 10445 asSpec 10446 }; 10447 }; 10448 10449 // TODO: ^ rename the parts/ api to composites, it will break mobile alloy now if we do 10450 const parts$g = AlloyParts; 10451 const partType$1 = PartType; 10452 10453 const fromSource = (event, source) => { 10454 const stopper = Cell(false); 10455 const cutter = Cell(false); 10456 const stop = () => { 10457 stopper.set(true); 10458 }; 10459 const cut = () => { 10460 cutter.set(true); 10461 }; 10462 return { 10463 stop, 10464 cut, 10465 isStopped: stopper.get, 10466 isCut: cutter.get, 10467 event, 10468 // Used only for tiered menu at the moment. It is an element, not a component 10469 setSource: source.set, 10470 getSource: source.get 10471 }; 10472 }; 10473 // Events that come from outside of the alloy root (e.g. window scroll) 10474 const fromExternal = (event) => { 10475 const stopper = Cell(false); 10476 const stop = () => { 10477 stopper.set(true); 10478 }; 10479 return { 10480 stop, 10481 cut: noop, // cutting has no meaning for a broadcasted event 10482 isStopped: stopper.get, 10483 isCut: never, 10484 event, 10485 // Nor do targets really 10486 setSource: die('Cannot set source of a broadcasted event'), 10487 getSource: die('Cannot get source of a broadcasted event') 10488 }; 10489 }; 10490 10491 const isDangerous = (event) => { 10492 // Will trigger the Back button in the browser 10493 const keyEv = event.raw; 10494 return keyEv.which === BACKSPACE[0] && !contains$2(['input', 'textarea'], name$3(event.target)) && !closest$1(event.target, '[contenteditable="true"]'); 10495 }; 10496 const setup$d = (container, rawSettings) => { 10497 const settings = { 10498 stopBackspace: true, 10499 ...rawSettings 10500 }; 10501 const pointerEvents = [ 10502 'touchstart', 10503 'touchmove', 10504 'touchend', 10505 'touchcancel', 10506 'gesturestart', 10507 'mousedown', 10508 'mouseup', 10509 'mouseover', 10510 'mousemove', 10511 'mouseout', 10512 'click', 10513 'pointerdown', 10514 'pointermove', 10515 'pointerup', 10516 'lostpointercapture' 10517 ]; 10518 const tapEvent = monitor(settings); 10519 // These events are just passed through ... no additional processing 10520 const simpleEvents = map$2(pointerEvents.concat([ 10521 'selectstart', 10522 'input', 10523 'contextmenu', 10524 'change', 10525 'transitionend', 10526 'transitioncancel', 10527 // Test the drag events 10528 'drag', 10529 'dragstart', 10530 'dragend', 10531 'dragenter', 10532 'dragleave', 10533 'dragover', 10534 'drop', 10535 'keyup' 10536 ]), (type) => bind$1(container, type, (event) => { 10537 tapEvent.fireIfReady(event, type).each((tapStopped) => { 10538 if (tapStopped) { 10539 event.kill(); 10540 } 10541 }); 10542 const stopped = settings.triggerEvent(type, event); 10543 if (stopped) { 10544 event.kill(); 10545 } 10546 })); 10547 const pasteTimeout = value$2(); 10548 const onPaste = bind$1(container, 'paste', (event) => { 10549 tapEvent.fireIfReady(event, 'paste').each((tapStopped) => { 10550 if (tapStopped) { 10551 event.kill(); 10552 } 10553 }); 10554 const stopped = settings.triggerEvent('paste', event); 10555 if (stopped) { 10556 event.kill(); 10557 } 10558 pasteTimeout.set(setTimeout(() => { 10559 settings.triggerEvent(postPaste(), event); 10560 }, 0)); 10561 }); 10562 const onKeydown = bind$1(container, 'keydown', (event) => { 10563 // Prevent default of backspace when not in input fields. 10564 const stopped = settings.triggerEvent('keydown', event); 10565 if (stopped) { 10566 event.kill(); 10567 } 10568 else if (settings.stopBackspace && isDangerous(event)) { 10569 event.prevent(); 10570 } 10571 }); 10572 const onFocusIn = bind$1(container, 'focusin', (event) => { 10573 const stopped = settings.triggerEvent('focusin', event); 10574 if (stopped) { 10575 event.kill(); 10576 } 10577 }); 10578 const focusoutTimeout = value$2(); 10579 const onFocusOut = bind$1(container, 'focusout', (event) => { 10580 const stopped = settings.triggerEvent('focusout', event); 10581 if (stopped) { 10582 event.kill(); 10583 } 10584 // INVESTIGATE: Come up with a better way of doing this. Related target can be used, but not on FF. 10585 // It allows the active element to change before firing the blur that we will listen to 10586 // for things like closing popups 10587 focusoutTimeout.set(setTimeout(() => { 10588 settings.triggerEvent(postBlur(), event); 10589 }, 0)); 10590 }); 10591 const unbind = () => { 10592 each$1(simpleEvents, (e) => { 10593 e.unbind(); 10594 }); 10595 onKeydown.unbind(); 10596 onFocusIn.unbind(); 10597 onFocusOut.unbind(); 10598 onPaste.unbind(); 10599 pasteTimeout.on(clearTimeout); 10600 focusoutTimeout.on(clearTimeout); 10601 }; 10602 return { 10603 unbind 10604 }; 10605 }; 10606 10607 const derive = (rawEvent, rawTarget) => { 10608 const source = get$h(rawEvent, 'target').getOr(rawTarget); 10609 return Cell(source); 10610 }; 10611 10612 const adt = Adt.generate([ 10613 { stopped: [] }, 10614 { resume: ['element'] }, 10615 { complete: [] } 10616 ]); 10617 const doTriggerHandler = (lookup, eventType, rawEvent, target, source, logger) => { 10618 const handler = lookup(eventType, target); 10619 const simulatedEvent = fromSource(rawEvent, source); 10620 return handler.fold(() => { 10621 // No handler, so complete. 10622 logger.logEventNoHandlers(eventType, target); 10623 return adt.complete(); 10624 }, (handlerInfo) => { 10625 const descHandler = handlerInfo.descHandler; 10626 const eventHandler = getCurried(descHandler); 10627 eventHandler(simulatedEvent); 10628 // Now, check if the event was stopped. 10629 if (simulatedEvent.isStopped()) { 10630 logger.logEventStopped(eventType, handlerInfo.element, descHandler.purpose); 10631 return adt.stopped(); 10632 } 10633 else if (simulatedEvent.isCut()) { 10634 logger.logEventCut(eventType, handlerInfo.element, descHandler.purpose); 10635 return adt.complete(); 10636 } 10637 else { 10638 return parent(handlerInfo.element).fold(() => { 10639 logger.logNoParent(eventType, handlerInfo.element, descHandler.purpose); 10640 // No parent, so complete. 10641 return adt.complete(); 10642 }, (parent) => { 10643 logger.logEventResponse(eventType, handlerInfo.element, descHandler.purpose); 10644 // Resume at parent 10645 return adt.resume(parent); 10646 }); 10647 } 10648 }); 10649 }; 10650 const doTriggerOnUntilStopped = (lookup, eventType, rawEvent, rawTarget, source, logger) => doTriggerHandler(lookup, eventType, rawEvent, rawTarget, source, logger).fold( 10651 // stopped. 10652 always, 10653 // Go again. 10654 (parent) => doTriggerOnUntilStopped(lookup, eventType, rawEvent, parent, source, logger), 10655 // completed 10656 never); 10657 const triggerHandler = (lookup, eventType, rawEvent, target, logger) => { 10658 const source = derive(rawEvent, target); 10659 return doTriggerHandler(lookup, eventType, rawEvent, target, source, logger); 10660 }; 10661 const broadcast = (listeners, rawEvent, _logger) => { 10662 const simulatedEvent = fromExternal(rawEvent); 10663 each$1(listeners, (listener) => { 10664 const descHandler = listener.descHandler; 10665 const handler = getCurried(descHandler); 10666 handler(simulatedEvent); 10667 }); 10668 return simulatedEvent.isStopped(); 10669 }; 10670 const triggerUntilStopped = (lookup, eventType, rawEvent, logger) => triggerOnUntilStopped(lookup, eventType, rawEvent, rawEvent.target, logger); 10671 const triggerOnUntilStopped = (lookup, eventType, rawEvent, rawTarget, logger) => { 10672 const source = derive(rawEvent, rawTarget); 10673 return doTriggerOnUntilStopped(lookup, eventType, rawEvent, rawTarget, source, logger); 10674 }; 10675 10676 const eventHandler = (element, descHandler) => ({ 10677 element, 10678 descHandler 10679 }); 10680 const broadcastHandler = (id, handler) => ({ 10681 id, 10682 descHandler: handler 10683 }); 10684 const EventRegistry = () => { 10685 const registry = {}; 10686 const registerId = (extraArgs, id, events) => { 10687 each(events, (v, k) => { 10688 const handlers = registry[k] !== undefined ? registry[k] : {}; 10689 handlers[id] = curryArgs(v, extraArgs); 10690 registry[k] = handlers; 10691 }); 10692 }; 10693 const findHandler = (handlers, elem) => read(elem) 10694 .bind((id) => get$h(handlers, id)) 10695 .map((descHandler) => eventHandler(elem, descHandler)); 10696 // Given just the event type, find all handlers regardless of element 10697 const filterByType = (type) => get$h(registry, type) 10698 .map((handlers) => mapToArray(handlers, (f, id) => broadcastHandler(id, f))) 10699 .getOr([]); 10700 // Given event type, and element, find the handler. 10701 const find = (isAboveRoot, type, target) => get$h(registry, type) 10702 .bind((handlers) => closest(target, (elem) =>
10702 findHandler(handlers, elem), isAboveRoot)); 10703 const unregisterId = (id) => { 10704 // INVESTIGATE: Find a better way than mutation if we can. 10705 each(registry, (handlersById, _eventName) => { 10706 if (has$2(handlersById, id)) { 10707 delete handlersById[id]; 10708 } 10709 }); 10710 }; 10711 return { 10712 registerId, 10713 unregisterId, 10714 filterByType, 10715 find 10716 }; 10717 }; 10718 10719 const Registry = () => { 10720 const events = EventRegistry(); 10721 // An index of uid -> built components 10722 const components = {}; 10723 const readOrTag = (component) => { 10724 const elem = component.element; 10725 return read(elem).getOrThunk(() => 10726 // No existing tag, so add one. 10727 write('uid-', component.element)); 10728 }; 10729 const failOnDuplicate = (component, tagId) => { 10730 const conflict = components[tagId]; 10731 if (conflict === component) { 10732 unregister(component); 10733 } 10734 else { 10735 throw new Error('The tagId "' + tagId + '" is already used by: ' + element(conflict.element) + '\nCannot use it for: ' + element(component.element) + '\n' + 10736 'The conflicting element is' + (inBody(conflict.element) ? ' ' : ' not ') + 'already in the DOM'); 10737 } 10738 }; 10739 const register = (component) => { 10740 const tagId = readOrTag(component); 10741 if (hasNonNullableKey(components, tagId)) { 10742 failOnDuplicate(component, tagId); 10743 } 10744 // Component is passed through an an extra argument to all events 10745 const extraArgs = [component]; 10746 events.registerId(extraArgs, tagId, component.events); 10747 components[tagId] = component; 10748 }; 10749 const unregister = (component) => { 10750 read(component.element).each((tagId) => { 10751 delete components[tagId]; 10752 events.unregisterId(tagId); 10753 }); 10754 }; 10755 const filter = (type) => events.filterByType(type); 10756 const find = (isAboveRoot, type, target) => events.find(isAboveRoot, type, target); 10757 const getById = (id) => get$h(components, id); 10758 return { 10759 find, 10760 filter, 10761 register, 10762 unregister, 10763 getById 10764 }; 10765 }; 10766 10767 const takeover = (root) => { 10768 const isAboveRoot = (el) => parent(root.element).fold(always, (parent) => eq(el, parent)); 10769 const registry = Registry(); 10770 const lookup = (eventName, target) => registry.find(isAboveRoot, eventName, target); 10771 const domEvents = setup$d(root.element, { 10772 triggerEvent: (eventName, event) => { 10773 return monitorEvent(eventName, event.target, (logger) => triggerUntilStopped(lookup, eventName, event, logger)); 10774 } 10775 }); 10776 const systemApi = { 10777 // This is a real system 10778 debugInfo: constant$1('real'), 10779 triggerEvent: (eventName, target, data) => { 10780 monitorEvent(eventName, target, (logger) => 10781 // The return value is not used because this is a fake event. 10782 triggerOnUntilStopped(lookup, eventName, data, target, logger)); 10783 }, 10784 triggerFocus: (target, originator) => { 10785 read(target).fold(() => { 10786 // When the target is not within the alloy system, dispatch a normal focus event. 10787 focus$4(target); 10788 }, (_alloyId) => { 10789 monitorEvent(focus$3(), target, (logger) => { 10790 // NOTE: This will stop at first handler. 10791 triggerHandler(lookup, focus$3(), { 10792 // originator is used by the default events to ensure that focus doesn't 10793 // get called infinitely 10794 originator, 10795 kill: noop, 10796 prevent: noop, 10797 target 10798 }, target, logger); 10799 return false;
10800 }); 10801 }); 10802 }, 10803 triggerEscape: (comp, simulatedEvent) => { 10804 systemApi.triggerEvent('keydown', comp.element, simulatedEvent.event); 10805 }, 10806 getByUid: (uid) => { 10807 return getByUid(uid); 10808 }, 10809 getByDom: (elem) => { 10810 return getByDom(elem); 10811 }, 10812 build: build$1, 10813 buildOrPatch: buildOrPatch, 10814 addToGui: (c) => { 10815 add(c); 10816 }, 10817 removeFromGui: (c) => { 10818 remove(c); 10819 }, 10820 addToWorld: (c) => { 10821 addToWorld(c); 10822 }, 10823 removeFromWorld: (c) => { 10824 removeFromWorld(c); 10825 }, 10826 broadcast: (message) => { 10827 broadcast$1(message); 10828 }, 10829 broadcastOn: (channels, message) => { 10830 broadcastOn(channels, message); 10831 }, 10832 broadcastEvent: (eventName, event) => { 10833 broadcastEvent(eventName, event); 10834 }, 10835 isConnected: always 10836 }; 10837 const addToWorld = (component) => { 10838 component.connect(systemApi); 10839 if (!isText(component.element)) { 10840 registry.register(component); 10841 each$1(component.components(), addToWorld); 10842 systemApi.triggerEvent(systemInit(), component.element, { target: component.element }); 10843 } 10844 }; 10845 const removeFromWorld = (component) => { 10846 if (!isText(component.element)) { 10847 each$1(component.components(), removeFromWorld); 10848 registry.unregister(component); 10849 } 10850 component.disconnect(); 10851 }; 10852 const add = (component) => { 10853 attach(root, component); 10854 }; 10855 const remove = (component) => { 10856 detach(component); 10857 }; 10858 const destroy = () => { 10859 // INVESTIGATE: something with registry? 10860 domEvents.unbind(); 10861 remove$7(root.element); 10862 }; 10863 const broadcastData = (data) => { 10864 const receivers = registry.filter(receive()); 10865 each$1(receivers, (receiver) => { 10866 const descHandler = receiver.descHandler; 10867 const handler = getCurried(descHandler); 10868 handler(data); 10869 }); 10870 }; 10871 const broadcast$1 = (message) => { 10872 broadcastData({ 10873 universal: true, 10874 data: message 10875 }); 10876 }; 10877 const broadcastOn = (channels, message) => { 10878 broadcastData({ 10879 universal: false, 10880 channels, 10881 data: message 10882 }); 10883 }; 10884 // This doesn't follow usual DOM bubbling. It will just dispatch on all 10885 // targets that have the event. It is the general case of the more specialised 10886 // "message". "messages" may actually just go away. This is used for things 10887 // like window scroll. 10888 const broadcastEvent = (eventName, event) => { 10889 const listeners = registry.filter(eventName); 10890 return broadcast(listeners, event); 10891 }; 10892 const getByUid = (uid) => registry.getById(uid).fold(() => Result.error(new Error('Could not find component with uid: "' + uid + '" in system.')), Result.value); 10893 const getByDom = (elem) => { 10894 const uid = read(elem).getOr('not found'); 10895 return getByUid(uid); 10896 }; 10897 addToWorld(root); 10898 return { 10899 root, 10900 element: root.element, 10901 destroy, 10902 add, 10903 remove, 10904 getByUid, 10905 getByDom, 10906 addToWorld, 10907 removeFromWorld, 10908 broadcast: broadcast$1, 10909 broadcastOn, 10910 broadcastEvent 10911 }; 10912 }; 10913 10914 const pointerEvents = () => { 10915 const onClick = (component, simulatedEvent) => { 10916 simulatedEvent.stop(); 10917 emitExecute(component); 10918 }; 10919 return [ 10920 // Trigger execute when clicked 10921 run$1(click(), onClick), 10922 run$1(tap(), onClick), 10923 // Other mouse down listeners above this one should not get mousedown behaviour (like dragging) 10924 cutter(touchstart()), 10925 cutter(mousedown()) 10926 ]; 10927 }; 10928 const events = (optAction) => { 10929 const executeHandler = (action) => runOnExecute$1((component, simulatedEvent) => { 10930 action(component); 10931 simulatedEvent.stop(); 10932 }); 10933 return derive$2(flatten([ 10934 // Only listen to execute if it is supplied 10935 optAction.map(executeHandler).toArray(), 10936 pointerEvents() 10937 ])); 10938 }; 10939
vendor: 5,576 bytes, lines 10940-11074
10940 const factory$m = (detail) => { 10941 const events$1 = events(detail.action); 10942 const tag = detail.dom.tag; 10943 const lookupAttr = (attr) => get$h(detail.dom, 'attributes').bind((attrs) => get$h(attrs, attr)); 10944 // Button tags should not have a default role of button, and only buttons should 10945 // get a type of button. 10946 const getModAttributes = () => { 10947 if (tag === 'button') { 10948 // Default to type button, unless specified otherwise 10949 const type = lookupAttr('type').getOr('button'); 10950 // Only use a role if it is specified 10951 const roleAttrs = lookupAttr('role').map((role) => ({ role })).getOr({}); 10952 return { 10953 type, 10954 ...roleAttrs 10955 }; 10956 } 10957 else { 10958 // We are not a button, so type is irrelevant (unless specified) 10959 // Default role to button 10960 const role = detail.role.getOr(lookupAttr('role').getOr('button')); 10961 return { role }; 10962 } 10963 }; 10964 return { 10965 uid: detail.uid, 10966 dom: detail.dom, 10967 components: detail.components, 10968 events: events$1, 10969 behaviours: SketchBehaviours.augment(detail.buttonBehaviours, [ 10970 Focusing.config({}), 10971 Keying.config({ 10972 mode: 'execution', 10973 // Note execution will capture keyup when the focus is on the button 10974 // on Firefox, because otherwise it will fire a click event and double 10975 // up on the action 10976 useSpace: true, 10977 useEnter: true 10978 }) 10979 ]), 10980 domModification: { 10981 attributes: getModAttributes() 10982 }, 10983 eventOrder: detail.eventOrder 10984 }; 10985 }; 10986 const Button = single({ 10987 name: 'Button', 10988 factory: factory$m, 10989 configFields: [ 10990 defaulted('uid', undefined), 10991 required$1('dom'), 10992 defaulted('components', []), 10993 SketchBehaviours.field('buttonBehaviours', [Focusing, Keying]), 10994 option$3('action'), 10995 option$3('role'), 10996 defaulted('eventOrder', {}) 10997 ] 10998 }); 10999 11000 const schema$m = constant$1([ 11001 defaulted('shell', false), 11002 required$1('makeItem'), 11003 defaulted('setupItem', noop), 11004 SketchBehaviours.field('listBehaviours', [Replacing]) 11005 ]); 11006 const customListDetail = () => ({ 11007 behaviours: derive$1([ 11008 Replacing.config({}) 11009 ]) 11010 }); 11011 const itemsPart = optional({ 11012 name: 'items', 11013 overrides: customListDetail 11014 }); 11015 const parts$f = constant$1([ 11016 itemsPart 11017 ]); 11018 const name$1 = constant$1('CustomList'); 11019 11020 const factory$l = (detail, components, _spec, _external) => { 11021 const setItems = (list, items) => { 11022 getListContainer(list).fold(() => { 11023 // check that the group container existed. It may not have if the components 11024 // did not list anything, and shell was false. 11025 // eslint-disable-next-line no-console 11026 console.error('Custom List was defined to not be a shell, but no item container was specified in components'); 11027 throw new Error('Custom List was defined to not be a shell, but no item container was specified in components'); 11028 }, (container) => { 11029 // Get all the children of container, because they will be items. 11030 // And then use the item setGroup api 11031 const itemComps = Replacing.contents(container); 11032 const numListsRequired = items.length; 11033 const numListsToAdd = numListsRequired - itemComps.length; 11034 const itemsToAdd = numListsToAdd > 0 ? 11035 range$2(numListsToAdd, () => detail.makeItem()) : []; 11036 const itemsToRemove = itemComps.slice(numListsRequired); 11037 each$1(itemsToRemove, (item) => Replacing.remove(container, item)); 11038 each$1(itemsToAdd, (item) => Replacing.append(container, item)); 11039 const builtLists = Replacing.contents(container); 11040 each$1(builtLists, (item, i) => { 11041 detail.setupItem(list, item, items[i], i); 11042 }); 11043 }); 11044 }; 11045 // In shell mode, the group overrides need to be added to the main container, and there can be no children 11046 const extra = detail.shell ? { behaviours: [Replacing.config({})], components: [] } : { behaviours: [], components }; 11047 const getListContainer = (component) => detail.shell ? Optional.some(component) : getPart(component, detail, 'items'); 11048 return { 11049 uid: detail.uid, 11050 dom: detail.dom, 11051 components: extra.components, 11052 behaviours: augment(detail.listBehaviours, extra.behaviours), 11053 apis: { 11054 setItems 11055 } 11056 }; 11057 }; 11058 const CustomList = composite({ 11059 name: name$1(), 11060 configFields: schema$m(), 11061 partFields: parts$f(), 11062 factory: factory$l, 11063 apis: { 11064 setItems: (apis, list, items) => { 11065 apis.setItems(list, items); 11066 } 11067 } 11068 }); 11069 11070 const attribute = 'aria-controls'; 11071 const find$1 = (queryElem) => { 11072 const dependent = closest$4(queryElem, (elem) => { 11073 if (!isElement$1(elem)) { 11074 return false;
11075 } 11076 const id = get$g(elem, 'id'); 11077 return id !== undefined && id.indexOf(attribute) > -1; 11078 }); 11079 return dependent.bind((dep) => { 11080 const id = get$g(dep, 'id'); 11081 const dos = getRootNode(dep); 11082 return descendant(dos, `[${attribute}="${id}"]`); 11083 }); 11084 }; 11085 const manager = () => { 11086 const ariaId = generate$6(attribute); 11087 const link = (elem) => { 11088 set$9(elem, attribute, ariaId); 11089 }; 11090 const unlink = (elem) => { 11091 remove$8(elem, attribute); 11092 }; 11093 return { 11094 id: ariaId, 11095 link, 11096 unlink, 11097 }; 11098 }; 11099 11100 const isAriaPartOf = (component, queryElem) => find$1(queryElem).exists((owner) => isPartOf(component, owner)); 11101 const isPartOf = (component, queryElem) => closest$2(queryElem, (el) => eq(el, component.element), never) || isAriaPartOf(component, queryElem); 11102 11103 const hoverEvent = 'alloy.item-hover'; 11104 const focusEvent = 'alloy.item-focus'; 11105 const toggledEvent = 'alloy.item-toggled'; 11106 const onHover = (item) => { 11107 // Firstly, check that the focus isn't already inside the item. This 11108 // is to handle situations like widgets where the widget is inside the item 11109 // and it has the focus, so as you slightly adjust the mouse, you don't 11110 // want to lose focus on the widget. Note, that because this isn't API based 11111 // (i.e. we are manually searching for focus), it may not be that flexible. 11112 if (search(item.element).isNone() || Focusing.isFocused(item)) { 11113 if (!Focusing.isFocused(item)) { 11114 Focusing.focus(item); 11115 } 11116 emitWith(item, hoverEvent, { item }); 11117 } 11118 }; 11119 const onFocus$1 = (item) => { 11120 emitWith(item, focusEvent, { item }); 11121 }; 11122 const onToggled = (item, state) => { 11123 emitWith(item, toggledEvent, { item, state }); 11124 }; 11125 const hover = constant$1(hoverEvent); 11126 const focus$1 = constant$1(focusEvent); 11127 const toggled = constant$1(toggledEvent); 11128 11129 // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents 11130 const getItemRole = (detail) => detail.role.fold(() => detail.toggling 11131 .map((toggling) => toggling.exclusive ? 'menuitemradio' : 'menuitemcheckbox') 11132 .getOr('menuitem'), identity); 11133 const getTogglingSpec = (tConfig, isOption) => ({ 11134 aria: { 11135 mode: isOption ? 'selected' : 'checked' 11136 }, 11137 // Filter out the additional properties that are not in Toggling Behaviour's configuration (e.g. exclusive) 11138 ...filter$1(tConfig, (_value, name) => name !== 'exclusive'), 11139 onToggled: (component, state) => { 11140 if (isFunction(tConfig.onToggled)) { 11141 tConfig.onToggled(component, state); 11142 } 11143 onToggled(component, state); 11144 } 11145 }); 11146 const builder$2 = (detail) => ({ 11147 dom: detail.dom, 11148 domModification: { 11149 // INVESTIGATE: If more efficient, destructure attributes out 11150 ...detail.domModification, 11151 attributes: { 11152 'role': getItemRole(detail), 11153 ...detail.domModification.attributes, 11154 'aria-haspopup': detail.hasSubmenu, 11155 ...(detail.hasSubmenu ? { 'aria-expanded': false } : {}) 11156 } 11157 }, 11158 behaviours: SketchBehaviours.augment(detail.itemBehaviours, [ 11159 // Investigate, is the Toggling.revoke still necessary here? 11160 detail.toggling.fold(Toggling.revoke, (tConfig) => Toggling.config(getTogglingSpec(tConfig, detail.role.exists((role) => role === 'option')))), 11161 Focusing.config({ 11162 ignore: detail.ignoreFocus, 11163 // Rationale: because nothing is focusable, when you click 11164 // on the items to choose them, the focus jumps to the first 11165 // focusable outer container ... often the body. If we prevent 11166 // mouseDown ... that doesn't happen. But only tested on Chrome/FF. 11167 stopMousedown: detail.ignoreFocus, 11168 onFocus: (component) => { 11169 onFocus$1(component); 11170 } 11171 }), 11172 Keying.config({ 11173 mode: 'execution' 11174 }), 11175 Representing.config({ 11176 store: { 11177 mode: 'memory', 11178 initialValue: detail.data 11179 } 11180 }), 11181 config('item-type-events', [ 11182 // Treat clicks the same as a button 11183 ...pointerEvents(), 11184 run$1(mouseover(), onHover), 11185 run$1(focusItem(), Focusing.focus) 11186 ]) 11187 ]), 11188 components: detail.components, 11189 eventOrder: detail.eventOrder 11190 }); 11191 const schema$l = [ 11192 required$1('data'), 11193 required$1('components'), 11194 required$1('dom'), 11195 defaulted('hasSubmenu', false), 11196 option$3('toggling'), 11197 option$3('role'), 11198 // Maybe this needs to have fewer behaviours 11199 SketchBehaviours.field('itemBehaviours', [Toggling, Focusing, Ke
11199ying, Representing]), 11200 defaulted('ignoreFocus', false), 11201 defaulted('domModification', {}), 11202 output$1('builder', builder$2), 11203 defaulted('eventOrder', {}) 11204 ]; 11205 var ItemType = schema$l; 11206 11207 const builder$1 = (detail) => ({ 11208 dom: detail.dom, 11209 components: detail.components, 11210 events: derive$2([ 11211 stopper(focusItem()) 11212 ]) 11213 }); 11214 const schema$k = [ 11215 required$1('dom'), 11216 required$1('components'), 11217 output$1('builder', builder$1) 11218 ]; 11219 var SeparatorType = schema$k; 11220 11221 const owner$2 = constant$1('item-widget'); 11222 const parts$e = constant$1([ 11223 required({ 11224 name: 'widget', 11225 overrides: (detail) => { 11226 return { 11227 behaviours: derive$1([ 11228 Representing.config({ 11229 store: { 11230 mode: 'manual', 11231 getValue: (_component) => { 11232 return detail.data; 11233 }, 11234 setValue: noop 11235 } 11236 }) 11237 ]) 11238 }; 11239 } 11240 }) 11241 ]); 11242 11243 const builder = (detail) => { 11244 const subs = substitutes(owner$2(), detail, parts$e()); 11245 const components = components$1(owner$2(), detail, subs.internals()); 11246 const focusWidget = (component) => getPart(component, detail, 'widget').map((widget) => { 11247 Keying.focusIn(widget); 11248 return widget; 11249 }); 11250 const onHorizontalArrow = (component, simulatedEvent) => inside(simulatedEvent.event.target) ? Optional.none() : (() => { 11251 if (detail.autofocus) { 11252 simulatedEvent.setSource(component.element); 11253 return Optional.none(); 11254 } 11255 else { 11256 return Optional.none(); 11257 } 11258 })(); 11259 return { 11260 dom: detail.dom, 11261 components, 11262 domModification: detail.domModification, 11263 events: derive$2([ 11264 runOnExecute$1((component, simulatedEvent) => { 11265 focusWidget(component).each((_widget) => { 11266 simulatedEvent.stop(); 11267 }); 11268 }), 11269 run$1(mouseover(), onHover), 11270 run$1(focusItem(), (component, _simulatedEvent) => { 11271 if (detail.autofocus) { 11272 focusWidget(component); 11273 } 11274 else { 11275 Focusing.focus(component); 11276 } 11277 }) 11278 ]), 11279 behaviours: SketchBehaviours.augment(detail.widgetBehaviours, [ 11280 Representing.config({ 11281 store: { 11282 mode: 'memory', 11283 initialValue: detail.data 11284 } 11285 }), 11286 Focusing.config({ 11287 ignore: detail.ignoreFocus, 11288 // What about stopMousedown from ItemType? 11289 onFocus: (component) => { 11290 onFocus$1(component); 11291 } 11292 }), 11293 Keying.config({ 11294 mode: 'special', 11295 // This is required as long as Highlighting tries to focus the first thing (after focusItem fires) 11296 focusIn: detail.autofocus ? (component) => { 11297 focusWidget(component); 11298 } : revoke(), 11299 onLeft: onHorizontalArrow, 11300 onRight: onHorizontalArrow, 11301 onEscape: (component, simulatedEvent) => { 11302 // If the outer list item didn't have focus, 11303 // then focus it (i.e. escape the inner widget). Only do if not autofocusing 11304 // Autofocusing should treat the widget like it is the only item, so it should 11305 // let its outer menu handle escape 11306 if (!Focusing.isFocused(component) && !detail.autofocus) { 11307 Focusing.focus(component); 11308 return Optional.some(true); 11309 } 11310 else if (detail.autofocus) { 11311 simulatedEvent.setSource(component.element); 11312 return Optional.none(); 11313 } 11314 else { 11315 return Optional.none(); 11316 } 11317 } 11318 }) 11319 ]) 11320 }; 11321 }; 11322 const schema$j = [ 11323 required$1('uid'), 11324 required$1('data'), 11325 required$1('components'), 11326 required$1('dom'), 11327 defaulted('autofocus', false), 11328 defaulted('ignoreFocus', false), 11329 SketchBehaviours.field('widgetBehaviours', [Representing, Focusing, Ke
vendor: 14,054 bytes, lines 11329-11654
11329ying]), 11330 defaulted('domModification', {}), 11331 // We don't have the uid at this point 11332 defaultUidsSchema(parts$e()), 11333 output$1('builder', builder) 11334 ]; 11335 var WidgetType = schema$j; 11336 11337 const itemSchema$2 = choose$1('type', { 11338 widget: WidgetType, 11339 item: ItemType, 11340 separator: SeparatorType 11341 }); 11342 const configureGrid = (detail, movementInfo) => ({ 11343 mode: 'flatgrid', 11344 selector: '.' + detail.markers.item, 11345 initSize: { 11346 numColumns: movementInfo.initSize.numColumns, 11347 numRows: movementInfo.initSize.numRows 11348 }, 11349 focusManager: detail.focusManager 11350 }); 11351 const configureMatrix = (detail, movementInfo) => ({ 11352 mode: 'matrix', 11353 selectors: { 11354 row: movementInfo.rowSelector, 11355 cell: '.' + detail.markers.item 11356 }, 11357 previousSelector: movementInfo.previousSelector, 11358 focusManager: detail.focusManager 11359 }); 11360 const configureMenu = (detail, _movementInfo) => ({ 11361 mode: 'menu', 11362 selector: '.' + detail.markers.item, 11363 focusManager: detail.focusManager 11364 }); 11365 const parts$d = constant$1([ 11366 group({ 11367 factory: { 11368 sketch: (spec) => { 11369 const itemInfo = asRawOrDie$1('menu.spec item', itemSchema$2, spec); 11370 return itemInfo.builder(itemInfo); 11371 } 11372 }, 11373 name: 'items', 11374 unit: 'item', 11375 defaults: (detail, u) => { 11376 // Switch this to a common library 11377 // The WidgetItemSpec is just because it has uid, and the others don't 11378 // for some reason. So there is nothing guaranteeing that `u` is a WidgetItemSpec, 11379 // so we should probably rework this code. 11380 return has$2(u, 'uid') ? u : { 11381 ...u, 11382 uid: generate$4('item') 11383 }; 11384 }, 11385 overrides: (detail, u) => { 11386 return { 11387 type: u.type, 11388 ignoreFocus: detail.fakeFocus, 11389 domModification: { 11390 classes: [detail.markers.item] 11391 } 11392 }; 11393 } 11394 }) 11395 ]); 11396 const schema$i = constant$1([ 11397 optionString('role'), 11398 required$1('value'), 11399 required$1('items'), 11400 required$1('dom'), 11401 required$1('components'), 11402 defaulted('eventOrder', {}), 11403 field('menuBehaviours', [Highlighting, Representing, Composing, Keying]), 11404 defaultedOf('movement', { 11405 mode: 'menu' 11406 }, choose$1('mode', { 11407 grid: [ 11408 initSize(), 11409 output$1('config', configureGrid) 11410 ], 11411 matrix: [ 11412 output$1('config', configureMatrix), 11413 required$1('rowSelector'), 11414 defaulted('previousSelector', Optional.none), 11415 ], 11416 menu: [ 11417 output$1('config', configureMenu) 11418 ] 11419 })), 11420 itemMarkers(), 11421 defaulted('fakeFocus', false), 11422 defaulted('focusManager', dom$2()), 11423 onHandler('onHighlight'), 11424 onHandler('onDehighlight'), 11425 defaulted('showMenuRole', true), 11426 ]); 11427 11428 const focus = constant$1('alloy.menu-focus'); 11429 11430 const deselectOtherRadioItems = (menu, item) => { 11431 // TODO: TINY-8812 - This ideally should be done in a way such that a menu can have multiple radio groups. 11432 const checkedRadioItems = descendants(menu.element, '[role="menuitemradio"][aria-checked="true"]'); 11433 each$1(checkedRadioItems, (ele) => { 11434 if (!eq(ele, item.element)) { 11435 menu.getSystem().getByDom(ele).each((c) => { 11436 Toggling.off(c); 11437 }); 11438 } 11439 }); 11440 }; 11441 const make$6 = (detail, components, _spec, _externals) => ({ 11442 uid: detail.uid, 11443 dom: detail.dom, 11444 markers: detail.markers, 11445 behaviours: augment(detail.menuBehaviours, [ 11446 Highlighting.config({ 11447 // Highlighting for a menu is selecting items inside the menu 11448 highlightClass: detail.markers.selectedItem, 11449 itemClass: detail.markers.item, 11450 onHighlight: detail.onHighlight, 11451 onDehighlight: detail.onDehighlight 11452 }), 11453 Representing.config({ 11454 store: { 11455 mode: 'memory', 11456 initialValue: detail.value 11457 } 11458 }), 11459 Composing.config({ 11460 find: Optional.some 11461 }), 11462 Keying.config(detail.movement.config(detail, detail.movement)) 11463 ]), 11464 events: derive$2([ 11465 // This is dispatched from a menu to tell an item to be highlighted. 11466 run$1(focus$1(), (menu, simulatedEvent) => { 11467 // Highlight the item 11468 const event = simulatedEvent.event; 11469 menu.getSystem().getByDom(event.target).each((item) => { 11470 Highlighting.highlight(menu, item); 11471 simulatedEvent.stop(); 11472 // Trigger the focus event on the menu. 11473 emitWith(menu, focus(), { menu, item }); 11474 }); 11475 }), 11476 // Highlight the item that the cursor is over. The onHighlight 11477 // code needs to handle updating focus if required 11478 run$1(hover(), (menu, simulatedEvent) => { 11479 const item = simulatedEvent.event.item; 11480 Highlighting.highlight(menu, item); 11481 }), 11482 // Enforce only a single radio menu item is toggled by finding any other toggled 11483 // radio menu items and untoggling them when a certain item is toggled 11484 run$1(toggled(), (menu, simulatedEvent) => { 11485 const { item, state } = simulatedEvent.event; 11486 if (state && get$g(item.element, 'role') === 'menuitemradio') { 11487 deselectOtherRadioItems(menu, item); 11488 } 11489 }) 11490 ]), 11491 components, 11492 eventOrder: detail.eventOrder, 11493 ...detail.showMenuRole ? { 11494 domModification: { 11495 attributes: { 11496 role: detail.role.getOr('menu') 11497 } 11498 } 11499 } : {} 11500 }); 11501 11502 const Menu = composite({ 11503 name: 'Menu', 11504 configFields: schema$i(), 11505 partFields: parts$d(), 11506 factory: make$6 11507 }); 11508 11509 const transpose$1 = (obj) => 11510 // Assumes no duplicate fields. 11511 tupleMap(obj, (v, k) => ({ k: v, v: k })); 11512 const trace = (items, byItem, byMenu, finish) => 11513 // Given a finishing submenu (which will be the value of expansions), 11514 // find the triggering item, find its menu, and repeat the process. If there 11515 // is no triggering item, we are done. 11516 get$h(byMenu, finish).bind((triggerItem) => get$h(items, triggerItem).bind((triggerMenu) => { 11517 const rest = trace(items, byItem, byMenu, triggerMenu); 11518 return Optional.some([triggerMenu].concat(rest)); 11519 })).getOr([]); 11520 const generate$2 = (menus, expansions) => { 11521 const items = {}; 11522 each(menus, (menuItems, menu) => { 11523 each$1(menuItems, (item) => { 11524 items[item] = menu; 11525 }); 11526 }); 11527 const byItem = expansions; 11528 const byMenu = transpose$1(expansions); 11529 // For each menu, calculate the backlog of submenus to get to it. 11530 const menuPaths = map$1(byMenu, (_triggerItem, submenu) => [submenu].concat(trace(items, byItem, byMenu, submenu))); 11531 return map$1(items, (menu) => get$h(menuPaths, menu).getOr([menu])); 11532 }; 11533 11534 const init$2 = () => { 11535 const expansions = Cell({}); 11536 const menus = Cell({}); 11537 const paths = Cell({}); 11538 const primary = value$2(); 11539 // Probably think of a better way to store this information. 11540 const directory = Cell({}); 11541 const clear = () => { 11542 expansions.set({}); 11543 menus.set({}); 11544 paths.set({}); 11545 primary.clear(); 11546 }; 11547 const isClear = () => primary.get().isNone(); 11548 const setMenuBuilt = (menuName, built) => { 11549 menus.set({ 11550 ...menus.get(), 11551 [menuName]: { 11552 type: 'prepared', 11553 menu: built 11554 } 11555 }); 11556 }; 11557 const setContents = (sPrimary, sMenus, sExpansions, dir) => { 11558 primary.set(sPrimary); 11559 expansions.set(sExpansions); 11560 menus.set(sMenus); 11561 directory.set(dir); 11562 const sPaths = generate$2(dir, sExpansions); 11563 paths.set(sPaths); 11564 }; 11565 const getTriggeringItem = (menuValue) => find$4(expansions.get(), (v, _k) => v === menuValue); 11566 const getTriggerData = (menuValue, getItemByValue, path) => getPreparedMenu(menuValue).bind((menu) => getTriggeringItem(menuValue).bind((triggeringItemValue) => getItemByValue(triggeringItemValue).map((triggeredItem) => ({ 11567 triggeredMenu: menu, 11568 triggeringItem: triggeredItem, 11569 triggeringPath: path 11570 })))); 11571 const getTriggeringPath = (itemValue, getItemByValue) => { 11572 // Get the path up to the last item 11573 const extraPath = filter$2(lookupItem(itemValue).toArray(), (menuValue) => getPreparedMenu(menuValue).isSome()); 11574 return get$h(paths.get(), itemValue).bind((path) => { 11575 // remember the path is [ most-recent-menu, next-most-recent-menu ] 11576 // convert each menu identifier into { triggeringItem: comp, menu: comp } 11577 // could combine into a fold ... probably a left to reverse ... but we'll take the 11578 // straightforward version when prototyping 11579 const revPath = reverse(extraPath.concat(path)); 11580 const triggers = bind$3(revPath, (menuValue, menuIndex) => 11581 // finding menuValue, it should match the trigger 11582 getTriggerData(menuValue, getItemByValue, revPath.slice(0, menuIndex + 1)).fold(() => is$1(primary.get(), menuValue) ? [] : [Optional.none()], (data) => [Optional.some(data)])); 11583 // Convert List<Optional<X>> to Optional<List<X>> if ALL are Some 11584 return sequence(triggers); 11585 }); 11586 }; 11587 // Given an item, return a list of all menus including the one that it triggered (if there is one) 11588 const expand = (itemValue) => get$h(expansions.get(), itemValue).map((menu) => { 11589 const current = get$h(paths.get(), itemValue).getOr([]); 11590 return [menu].concat(current); 11591 }); 11592 const collapse = (itemValue) => 11593 // Look up which key has the itemValue 11594 get$h(paths.get(), itemValue).bind((path) => path.length > 1 ? Optional.some(path.slice(1)) : Optional.none()); 11595 const refresh = (itemValue) => get$h(paths.get(), itemValue); 11596 const getPreparedMenu = (menuValue) => lookupMenu(menuValue).bind(extractPreparedMenu); 11597 const lookupMenu = (menuValue) => get$h(menus.get(), menuValue); 11598 const lookupItem = (itemValue) => get$h(expansions.get(), itemValue); 11599 const otherMenus = (path) => { 11600 const menuValues = directory.get(); 11601 return difference(keys(menuValues), path); 11602 }; 11603 const getPrimary = () => primary.get().bind(getPreparedMenu); 11604 const getMenus = () => menus.get(); 11605 return { 11606 setMenuBuilt, 11607 setContents, 11608 expand, 11609 refresh, 11610 collapse, 11611 lookupMenu, 11612 lookupItem, 11613 otherMenus, 11614 getPrimary, 11615 getMenus, 11616 clear, 11617 isClear, 11618 getTriggeringPath 11619 }; 11620 }; 11621 const extractPreparedMenu = (prep) => prep.type === 'prepared' ? Optional.some(prep.menu) : Optional.none(); 11622 const LayeredState = { 11623 init: init$2, 11624 extractPreparedMenu 11625 }; 11626 11627 const onMenuItemHighlightedEvent = generate$6('tiered-menu-item-highlight'); 11628 const onMenuItemDehighlightedEvent = generate$6('tiered-menu-item-dehighlight'); 11629 11630 const make$5 = (detail, _rawUiSpec) => { 11631 const submenuParentItems = value$2(); 11632 // So the way to provide extra configuration for the menus that tiered menus create is just 11633 // to provide different menu specs when building up the TieredData. The TieredMenu itself 11634 // does not control it, except to set: markers, fakeFocus, onHighlight, and focusManager 11635 const buildMenus = (container, primaryName, menus) => map$1(menus, (spec, name) => { 11636 const makeSketch = () => Menu.sketch({ 11637 ...spec, 11638 value: name, 11639 // The TieredMenu markers should be inherited by the Menu. "Markers" are things like 11640 // what is the class for the currently selected item 11641 markers: detail.markers, 11642 // If the TieredMenu has been configured with FakeFocus, it needs the menus that it generates 11643 // to preserve that configuration. Generally, FakeFocus is used for situations where the user 11644 // wants to keep focus inside some editable element (like an input, or editor content) 11645 fakeFocus: detail.fakeFocus, 11646 // The TieredMenu detail.onHighlight function only relates to selecting an item, 11647 // not a menu, and the menuComp it is passed is the menu, not the tiered menu. 11648 // This makes it a difficult handler to use for a tieredmenu, so we are 11649 // deprecating it. 11650 onHighlight: (menuComp, itemComp) => { 11651 // Trigger an internal event so that we can listen to it at the tieredmenu 11652 // level, and call detail.onHighlightItem handler with tmenu, menu, and item. 11653 const highlightData = { 11654 menuComp,
11655 itemComp 11656 }; 11657 emitWith(menuComp, onMenuItemHighlightedEvent, highlightData); 11658 }, 11659 onDehighlight: (menuComp, itemComp) => { 11660 const dehighlightData = { 11661 menuComp, 11662 itemComp 11663 }; 11664 // Trigger an internal event so that we can listen to it at the tieredmenu 11665 // level, and call detail.onDehighlightItem handler with tmenu, menu, and item. 11666 emitWith(menuComp, onMenuItemDehighlightedEvent, dehighlightData); 11667 }, 11668 // The Menu itself doesn't set the focusManager based on the value of fakeFocus. It only uses 11669 // its fakeFocus configuration for creating items that ignore focus, but it still needs to be 11670 // told which focusManager to use. Perhaps we should change this, though it does allow for more 11671 // complex focusManagers in single menus. 11672 focusManager: detail.fakeFocus ? highlights() : dom$2() 11673 }); 11674 // Only build the primary at first. Build the others as needed. 11675 return name === primaryName ? { 11676 type: 'prepared', 11677 menu: container.getSystem().build(makeSketch()) 11678 } : { 11679 type: 'notbuilt', 11680 nbMenu: makeSketch 11681 }; 11682 }); 11683 const layeredState = LayeredState.init(); 11684 const setup = (container) => { 11685 const componentMap = buildMenus(container, detail.data.primary, detail.data.menus); 11686 const directory = toDirectory(); 11687 layeredState.setContents(detail.data.primary, componentMap, detail.data.expansions, directory); 11688 return layeredState.getPrimary(); 11689 }; 11690 const getItemValue = (item) => Representing.getValue(item).value; 11691 // Find the first item with value `itemValue` in any of the menus inside this tiered menu structure 11692 const getItemByValue = (_container, menus, itemValue) => 11693 // Can *greatly* improve the performance of this by calculating things up front. 11694 findMap(menus, (menu) => { 11695 if (!menu.getSystem().isConnected()) { 11696 return Optional.none(); 11697 } 11698 const candidates = Highlighting.getCandidates(menu); 11699 return find$5(candidates, (c) => getItemValue(c) === itemValue); 11700 }); 11701 const toDirectory = (_container) => map$1(detail.data.menus, (data, _menuName) => bind$3(data.items, (item) => item.type === 'separator' ? [] : [item.data.value])); 11702 // This just sets the active menu. It will not set any active items. 11703 const setActiveMenu = Highlighting.highlight; 11704 // The item highlighted as active is either the currently active item in the menu, 11705 // or the first one. 11706 const setActiveMenuAndItem = (container, menu) => { 11707 // Firstly, choose the active menu
11708 setActiveMenu(container, menu); 11709 // Then, choose the active item inside the active menu 11710 Highlighting.getHighlighted(menu).orThunk(() => Highlighting.getFirst(menu)).each((item) => { 11711 if (detail.fakeFocus) { 11712 // When using fakeFocus, the items won't have a tab-index, so calling focusItem on them 11713 // won't do anything. So we need to manually call highlighting, which is what fakeFocus 11714 // uses. It would probably be better to use the focusManager specified. 11715 Highlighting.highlight(menu, item); 11716 } 11717 else { 11718 // We don't just use Focusing.focus here, because some items can have slightly different 11719 // handling when they respond to a focusItem event. Widgets with autofocus, for example, 11720 // will trigger a Keying.focusIn instead of Focusing.focus call, because they want to move 11721 // the focus _inside_ the widget, not just to its outer level. The focusItem event 11722 // performs a similar purpose to SystemEvents.focus() and potentially, could be consolidated. 11723 dispatch(container, item.element, focusItem()); 11724 } 11725 }); 11726 }; 11727 const getMenus = (state, menuValues) => cat(map$2(menuValues, (mv) => state.lookupMenu(mv).bind((prep) => prep.type === 'prepared' ? Optional.some(prep.menu) : Optional.none()))); 11728 const closeOthers = (container, state, path) => { 11729 const others = getMenus(state, state.otherMenus(path)); 11730 each$1(others, (o) => { 11731 // May not need to do the active menu thing. 11732 remove$2(o.element, [detail.markers.backgroundMenu]); 11733 if (!detail.stayInDom) { 11734 Replacing.remove(container, o); 11735 } 11736 }); 11737 }; 11738 const getSubmenuParents = (container) => submenuParentItems.get().getOrThunk(() => { 11739 const r = {}; 11740 const items = descendants(container.element, `.${detail.markers.item}`); 11741 const parentItems = filter$2(items, (i) => get$g(i, 'aria-haspopup') === 'true'); 11742 each$1(parentItems, (i) => { 11743 container.getSystem().getByDom(i).each((itemComp) => {
vendor: 5,932 bytes, lines 11744-11852
11744 const key = getItemValue(itemComp); 11745 r[key] = itemComp; 11746 }); 11747 }); 11748 submenuParentItems.set(r); 11749 return r; 11750 }); 11751 // Not ideal. Ideally, we would like a map of item keys to components. 11752 const updateAriaExpansions = (container, path) => { 11753 const parentItems = getSubmenuParents(container); 11754 each(parentItems, (v, k) => { 11755 // Really should turn path into a Set 11756 const expanded = contains$2(path, k); 11757 set$9(v.element, 'aria-expanded', expanded); 11758 }); 11759 }; 11760 const updateMenuPath = (container, state, path) => Optional.from(path[0]).bind((latestMenuName) => state.lookupMenu(latestMenuName).bind((menuPrep) => { 11761 if (menuPrep.type === 'notbuilt') { 11762 return Optional.none(); 11763 } 11764 else { 11765 const activeMenu = menuPrep.menu; 11766 const rest = getMenus(state, path.slice(1)); 11767 each$1(rest, (r) => { 11768 add$2(r.element, detail.markers.backgroundMenu); 11769 }); 11770 if (!inBody(activeMenu.element)) { 11771 Replacing.append(container, premade(activeMenu)); 11772 } 11773 // Remove the background-menu class from the active menu 11774 remove$2(activeMenu.element, [detail.markers.backgroundMenu]); 11775 setActiveMenuAndItem(container, activeMenu); 11776 closeOthers(container, state, path); 11777 return Optional.some(activeMenu); 11778 } 11779 })); 11780 let ExpandHighlightDecision; 11781 (function (ExpandHighlightDecision) { 11782 ExpandHighlightDecision[ExpandHighlightDecision["HighlightSubmenu"] = 0] = "HighlightSubmenu"; 11783 ExpandHighlightDecision[ExpandHighlightDecision["HighlightParent"] = 1] = "HighlightParent"; 11784 })(ExpandHighlightDecision || (ExpandHighlightDecision = {})); 11785 const buildIfRequired = (container, menuName, menuPrep) => { 11786 if (menuPrep.type === 'notbuilt') { 11787 const menu = container.getSystem().build(menuPrep.nbMenu()); 11788 layeredState.setMenuBuilt(menuName, menu); 11789 return menu; 11790 } 11791 else { 11792 return menuPrep.menu; 11793 } 11794 }; 11795 const expandRight = (container, item, decision = ExpandHighlightDecision.HighlightSubmenu) => { 11796 if (item.hasConfigured(Disabling) && Disabling.isDisabled(item)) { 11797 return Optional.some(item); 11798 } 11799 else { 11800 const value = getItemValue(item); 11801 return layeredState.expand(value).bind((path) => { 11802 // Called when submenus are opened by keyboard AND hovering navigation 11803 updateAriaExpansions(container, path); 11804 // When expanding, always select the first. 11805 return Optional.from(path[0]).bind((menuName) => layeredState.lookupMenu(menuName).bind((activeMenuPrep) => { 11806 const activeMenu = buildIfRequired(container, menuName, activeMenuPrep); 11807 // DUPE with above. Fix later. 11808 if (!inBody(activeMenu.element)) { 11809 Replacing.append(container, premade(activeMenu)); 11810 } 11811 // updateMenuPath is the code which changes the active menu. We don't always 11812 // want to change the active menu. Sometimes, we just want to show it (e.g. hover) 11813 detail.onOpenSubmenu(container, item, activeMenu, reverse(path)); 11814 if (decision === ExpandHighlightDecision.HighlightSubmenu) { 11815 Highlighting.highlightFirst(activeMenu); 11816 return updateMenuPath(container, layeredState, path); 11817 } 11818 else { 11819 Highlighting.dehighlightAll(activeMenu); 11820 return Optional.some(item); 11821 } 11822 })); 11823 }); 11824 } 11825 }; 11826 const collapseLeft = (container, item) => { 11827 const value = getItemValue(item); 11828 return layeredState.collapse(value).bind((path) => { 11829 // Called when submenus are closed because of KEYBOARD navigation 11830 updateAriaExpansions(container, path); 11831 return updateMenuPath(container, layeredState, path).map((activeMenu) => { 11832 detail.onCollapseMenu(container, item, activeMenu); 11833 return activeMenu; 11834 }); 11835 }); 11836 }; 11837 const updateView = (container, item) => { 11838 const value = getItemValue(item); 11839 return layeredState.refresh(value).bind((path) => { 11840 // Only this function collapses irrelevant submenus when navigating by HOVERING. 11841 // Does mean this is called twice when navigating by hovering, since both 11842 // updateView and expandRight are called by the ItemEvents.hover() handler 11843 updateAriaExpansions(container, path); 11844 return updateMenuPath(container, layeredState, path); 11845 }); 11846 }; 11847 const onRight = (container, item) => inside(item.element) ? Optional.none() : expandRight(container, item, ExpandHighlightDecision.HighlightSubmenu); 11848 const onLeft = (container, item) => 11849 // Exclude inputs, textareas etc. 11850 inside(item.element) ? Optional.none() : collapseLeft(container, item); 11851 const onEscape = (container, item) => collapseLeft(container, item).orThunk(() => detail.onEscape(container, item).map(() => container) // This should only fire when the user presses ESC ... not any other close. 11852 );
11853 const keyOnItem = (f) => (container, simulatedEvent) => { 11854 // 2022-08-16 This seems to be the only code in alloy that actually uses 11855 // the getSource aspect of an event. Remember, that this code is firing 11856 // when an event bubbles up the tiered menu, e.g. left arrow key. 11857 // The only current code that sets the source manually is in the Widget item 11858 // type, and it only sets the source when it is using autofocus. Autofocus 11859 // is used to essentially treat the widget like it is the top-level item, so 11860 // when events originate from *within* the widget, their source is changed to 11861 // the top-level item. Consider removing EventSource from alloy altogether. 11862 return closest$3(simulatedEvent.getSource(), `.${detail.markers.item}`) 11863 .bind((target) => container.getSystem().getByDom(target).toOptional().bind((item) => f(container, item).map(always))); 11864 }; 11865 // NOTE: Many of these events rely on identifying the current item by information 11866 // sent with the event. However, in situations where you are using fakeFocus, but 11867 // the real focus is still somewhere in the menu (e.g. search bar), this will lead to 11868 // an incorrect identification of the active item. Ideally, instead of pulling the 11869 // item from the event, we should just use Highlighting to identify the active item, 11870 // and operate on it. However, not all events will necessarily have to happen on the 11871 // active item, so we need to consider all the cases before making this change. For now, 11872 // there will be a known limitation that if the real focus is still inside the TieredMenu, 11873 // but the menu is using fakeFocus, then the actions will operate on the wrong targets. 11874 // A workaround for that is to stop or cut or redispatch the events in whichever 11875 // component has the real focus. 11876 // TODO: TINY-9011 Introduce proper handling of fakeFocus in TieredMenu 11877 const events = derive$2([ 11878 // Set "active-menu" for the menu with focus 11879 run$1(focus(), (tmenu, simulatedEvent) => { 11880 // Ensure the item is actually part of this menu structure, and not part of another menu structure that's bubbling. 11881 const item = simulatedEvent.event.item; 11882 layeredState.lookupItem(getItemValue(item)).each(() => { 11883 const menu = simulatedEvent.event.menu; 11884 Highlighting.highlight(tmenu, menu); 11885 const value = getItemValue(simulatedEvent.event.item); 11886 layeredState.refresh(value).each((path) => closeOthers(tmenu, layeredState, path)); 11887 }); 11888 }), 11889 runOnExecute$1((component, simulatedEvent) => { 11890 // Trigger on execute on the targeted element 11891 // I.e. clicking on menu item 11892 const target = simulatedEvent.event.target; 11893 component.getSystem().getByDom(target).each((item) => { 11894 const itemValue = getItemValue(item); 11895 // INVESTIGATE: I don't know if this is doing anything any more. Check. 11896 if (itemValue.indexOf('collapse-item') === 0) { 11897 collapseLeft(component, item); 11898 } 11899 expandRight(component, item, ExpandHighlightDecision.HighlightSubmenu).fold(() => { 11900 detail.onExecute(component, item); 11901 }, noop); 11902 }); 11903 }), 11904 // Open the menu as soon as it is added to the DOM 11905 runOnAttached((container, _simulatedEvent) => { 11906 setup(container).each((primary) => { 11907 Replacing.append(container, premade(primary)); 11908 detail.onOpenMenu(container, primary); 11909 if (detail.highlightOnOpen === HighlightOnOpen.HighlightMenuAndItem) {
11910 setActiveMenuAndItem(container, primary); 11911 } 11912 else if (detail.highlightOnOpen === HighlightOnOpen.HighlightJustMenu) { 11913 setActiveMenu(container, primary); 11914 } 11915 }); 11916 }), 11917 // Listen to the events bubbling up from menu about highlighting, and trigger 11918 // our handlers with tmenu, menu and item 11919 run$1(onMenuItemHighlightedEvent, (tmenuComp, se) => { 11920 detail.onHighlightItem(tmenuComp, se.event.menuComp, se.event.itemComp); 11921 }), 11922 run$1(onMenuItemDehighlightedEvent, (tmenuComp, se) => { 11923 detail.onDehighlightItem(tmenuComp, se.event.menuComp, se.event.itemComp); 11924 }), 11925 ...(detail.navigateOnHover ? [ 11926 // Hide any irrelevant submenus and expand any submenus based 11927 // on hovered item 11928 run$1(hover(), (tmenu, simulatedEvent) => { 11929 const item = simulatedEvent.event.item; 11930 updateView(tmenu, item); 11931 expandRight(tmenu, item, ExpandHighlightDecision.HighlightParent); 11932 detail.onHover(tmenu, item); 11933 }) 11934 ] : []) 11935 ]); 11936 const getActiveItem = (container) => Highlighting.getHighlighted(container).bind(Highlighting.getHighlighted); 11937 const collapseMenuApi = (container) => { 11938 getActiveItem(container).each((currentItem) => { 11939 collapseLeft(container, currentItem); 11940 }); 11941 }; 11942 const highlightPrimary = (container) => { 11943 layeredState.getPrimary().each((primary) => {
11944 setActiveMenuAndItem(container, primary); 11945 }); 11946 }; 11947 const extractMenuFromContainer = (container) => Optional.from(container.components()[0]).filter((comp) => get$g(comp.element, 'role') === 'menu'); 11948 const repositionMenus = (container) => { 11949 // Get the primary menu 11950 const maybeActivePrimary = layeredState.getPrimary().bind((primary) => 11951 // Get the triggering path (item, menu) up to the active item 11952 getActiveItem(container).bind((currentItem) => { 11953 const itemValue = getItemValue(currentItem); 11954 const allMenus = values(layeredState.getMenus()); 11955 const preparedMenus = cat(map$2(allMenus, LayeredState.extractPreparedMenu)); 11956 return layeredState.getTriggeringPath(itemValue, (v) => getItemByValue(container, preparedMenus, v)); 11957 }).map((triggeringPath) => ({ primary, triggeringPath }))); 11958 maybeActivePrimary.fold(() => { 11959 // When a menu is open but there is no activeItem, we get the menu from the container. 11960 extractMenuFromContainer(container).each((primaryMenu) => { 11961 detail.onRepositionMenu(container, primaryMenu, []); 11962 }); 11963 }, ({ primary, triggeringPath }) => { 11964 // Refresh all the menus up to the active item 11965 detail.onRepositionMenu(container, primary, triggeringPath); 11966 }); 11967 }; 11968 const apis = { 11969 collapseMenu: collapseMenuApi, 11970 highlightPrimary, 11971 repositionMenus 11972 }; 11973 return { 11974 uid: detail.uid, 11975 dom: detail.dom, 11976 markers: detail.markers, 11977 behaviours: augment(detail.tmenuBehaviours, [ 11978 Keying.config({ 11979 mode: 'special', 11980 onRight: keyOnItem(onRight), 11981 onLeft: keyOnItem(onLeft), 11982 onEscape: keyOnItem(onEscape), 11983 onTab: detail.onTab, 11984 onShiftTab: detail.onShiftTab, 11985 focusIn: (container, _keyInfo) => { 11986 layeredState.getPrimary().each((primary) => { 11987 dispatch(container, primary.element, focusItem()); 11988 }); 11989 } 11990 }), 11991 // Highlighting is used for highlighting the active menu 11992 Highlighting.config({ 11993 highlightClass: detail.markers.selectedMenu, 11994 itemClass: detail.markers.menu 11995 }), 11996 Composing.config({ 11997 find: (container) => { 11998 return Highlighting.getHighlighted(container); 11999 } 12000 }), 12001 Replacing.config({}) 12002 ]), 12003 eventOrder: detail.eventOrder, 12004 apis, 12005 events 12006 }; 12007 }; 12008 const collapseItem$1 = constant$1('collapse-item'); 12009 12010 const tieredData = (primary, menus, expansions) => ({ 12011 primary, 12012 menus, 12013 expansions 12014 }); 12015 const singleData = (name, menu) => ({ 12016 primary: name, 12017 menus: wrap(name, menu), 12018 expansions: {} 12019 }); 12020 const collapseItem = (text) => ({ 12021 value: generate$6(collapseItem$1()), 12022 meta: { 12023 text 12024 } 12025 }); 12026 const tieredMenu = single({ 12027 name: 'TieredMenu', 12028 configFields: [ 12029 onStrictKeyboardHandler('onExecute'), 12030 onStrictKeyboardHandler('onEscape'), 12031 onKeyboardHandler('onTab'), 12032 onKeyboardHandler('onShiftTab'), 12033 onStrictHandler('onOpenMenu'), 12034 onStrictHandler('onOpenSubmenu'), 12035 onHandler('onRepositionMenu'), 12036 onHandler('onCollapseMenu'), 12037 // Ideally, we should validate that this is a valid value, but 12038 // this is an number-based enum, so it would just be a number. 12039 defaulted('highlightOnOpen', HighlightOnOpen.HighlightMenuAndItem), 12040 requiredObjOf('data', [ 12041 required$1('primary'), 12042 required$1('menus'), 12043 required$1('expansions') 12044 ]), 12045 defaulted('fakeFocus', false), 12046 onHandler('onHighlightItem'), 12047 onHandler('onDehighlightItem'), 12048 onHandler('onHover'), 12049 tieredMenuMarkers(), 12050 required$1('dom'), 12051 defaulted('navigateOnHover', true), 12052 defaulted('stayInDom', false), 12053 field('tmenuBehaviours', [Keying, Highlighting, Composing, Replacing]), 12054 defaulted('eventOrder', {}) 12055 ], 12056 apis: { 12057 collapseMenu: (apis, tmenu) => { 12058 apis.collapseMenu(tmenu); 12059 },
12060 // This will highlight the primary menu AND an item in the primary menu 12061 // Do not use just to set the active menu. 12062 highlightPrimary: (apis, tmenu) => { 12063 apis.highlightPrimary(tmenu); 12064 }, 12065 repositionMenus: (apis, tmenu) => { 12066 apis.repositionMenus(tmenu); 12067 } 12068 }, 12069 factory: make$5, 12070 extraApis: { 12071 tieredData, 12072 singleData, 12073 collapseItem 12074 } 12075 }); 12076 12077 const suffix = constant$1('sink'); 12078 const partType = constant$1(optional({ 12079 name: suffix(), 12080 overrides: constant$1({ 12081 dom: { 12082 tag: 'div' 12083 }, 12084 behaviours: derive$1([ 12085 Positioning.config({ 12086 // TODO: Make an internal sink also be able to be used with relative layouts 12087 useFixed: always 12088 }) 12089 ]), 12090 events: derive$2([ 12091 // Sinks should not let keydown or click propagate 12092 cutter(keydown()), 12093 cutter(mousedown()), 12094 cutter(click()) 12095 ]) 12096 }) 12097 })); 12098 12099 const schema$h = objOfOnly([ 12100 defaulted('isExtraPart', never), 12101 optionObjOf('fireEventInstead', [ 12102 defaulted('event', dismissRequested()) 12103 ]) 12104 ]); 12105 const receivingChannel$1 = (rawSpec) => { 12106 const detail = asRawOrDie$1('Dismissal', schema$h, rawSpec); 12107 return { 12108 [dismissPopups()]: { 12109 schema: objOfOnly([ 12110 required$1('target') 12111 ]), 12112 onReceive: (sandbox, data) => { 12113 if (Sandboxing.isOpen(sandbox)) { 12114 const isPart = Sandboxing.isPartOf(sandbox, data.target) || detail.isExtraPart(sandbox, data.target); 12115 if (!isPart) { 12116 detail.fireEventInstead.fold(() => Sandboxing.close(sandbox), (fe) => emit(sandbox, fe.event)); 12117 } 12118 } 12119 } 12120 } 12121 }; 12122 }; 12123 12124 const schema$g = objOfOnly([ 12125 optionObjOf('fireEventInstead', [ 12126 defaulted('event', repositionRequested()) 12127 ]), 12128 requiredFunction('doReposition') 12129 ]); 12130 const receivingChannel = (rawSpec) => { 12131 const detail = asRawOrDie$1('Reposition', schema$g, rawSpec); 12132 return { 12133 [repositionPopups()]: { 12134 onReceive: (sandbox) => { 12135 if (Sandboxing.isOpen(sandbox)) { 12136 detail.fireEventInstead.fold(() => detail.doReposition(sandbox), (fe) => emit(sandbox, fe.event)); 12137 } 12138 } 12139 } 12140 }; 12141 }; 12142 12143 const getAnchor = (detail, component) => { 12144 const hotspot = detail.getHotspot(component).getOr(component); 12145 const type = 'hotspot'; 12146 const overrides = detail.getAnchorOverrides(); 12147 return detail.layouts.fold(() => ({ type, hotspot, overrides }), (layouts) => ({ type, hotspot, overrides, layouts })); 12148 }; 12149 const fetch$1 = (detail, mapFetch, component) => { 12150 const fetcher = detail.fetch; 12151 return fetcher(component).map(mapFetch); 12152 }; 12153 const openF = (detail, mapFetch, anchor, component, sandbox, externals, highlightOnOpen) => { 12154 const futureData = fetch$1(detail, mapFetch, component); 12155 const getLazySink = getSink(component, detail); 12156 // When Tab or Shift+Tab bubbles up from inside an open menu, close the menu, 12157 // refocus the trigger, and re-emit the keydown so the trigger's parent 12158 // (toolbar, menubar, etc.) handles it via its own Keying config. 12159 const onTabOutOfMenu = (_tmenu, se) => { 12160 Focusing.focus(component); 12161 emitWith(component, 'keydown', { raw: se.event.raw }); 12162 Sandboxing.close(sandbox); 12163 return Optional.some(true); 12164 }; 12165 // TODO: Make this potentially a single menu also 12166 return futureData.map((tdata) => tdata.bind((data) => { 12167 const primaryMenu = data.menus[data.primary];
12168 Optional.from(primaryMenu).each((menu) => { 12169 detail.listRole.each((listRole) => { 12170 menu.role = listRole; 12171 }); 12172 }); 12173 return Optional.from(tieredMenu.sketch({ 12174 // Externals are configured by the "menu" part. It's called external because it isn't contained 12175 // within the DOM descendants of the dropdown. You can configure things like `fakeFocus` here. 12176 ...externals.menu(), 12177 uid: generate$4(''), 12178 data, 12179 highlightOnOpen, 12180 onOpenMenu: (tmenu, menu) => { 12181 const sink = getLazySink().getOrDie(); 12182 Positioning.position(sink, menu, { anchor }); 12183 Sandboxing.decloak(sandbox); 12184 }, 12185 onOpenSubmenu: (tmenu, item, submenu) => { 12186 const sink = getLazySink().getOrDie(); 12187 Positioning.position(sink, submenu, { 12188 anchor: { 12189 type: 'submenu', 12190 item 12191 } 12192 }); 12193 Sandboxing.decloak(sandbox); 12194 }, 12195 onRepositionMenu: (tmenu, primaryMenu, submenuTriggers) => { 12196 const sink = getLazySink().getOrDie(); 12197 Positioning.position(sink, primaryMenu, { anchor }); 12198 each$1(submenuTriggers, (st) => { 12199 Positioning.position(sink, st.triggeredMenu, { 12200 anchor: { type: 'submenu', item: st.triggeringItem } 12201 }); 12202 }); 12203 }, 12204 onEscape: () => { 12205 // Focus the triggering component after escaping the menu 12206 Focusing.focus(component); 12207 Sandboxing.close(sandbox); 12208 return Optional.some(true); 12209 }, 12210 onTab: onTabOutOfMenu, 12211 onShiftTab: onTabOutOfMenu 12212 })); 12213 })); 12214 }; 12215 // onOpenSync is because some operations need to be applied immediately, not wrapped in a future 12216 // It can avoid things like flickering due to asynchronous bouncing 12217 const open = (detail, mapFetch, hotspot, sandbox, externals, onOpenSync, highlightOnOpen) => { 12218 const anchor = getAnchor(detail, hotspot); 12219 const processed = openF(detail, mapFetch, anchor, hotspot, sandbox, externals, highlightOnOpen); 12220 return processed.map((tdata) => { 12221 // If we have data, display a menu. Else, close the menu if it was open 12222 tdata.fold(() => { 12223 if (Sandboxing.isOpen(sandbox)) { 12224 Sandboxing.close(sandbox); 12225 } 12226 }, (data) => { 12227 Sandboxing.cloak(sandbox); 12228 Sandboxing.open(sandbox, data); 12229 onOpenSync(sandbox); 12230 }); 12231 return sandbox; 12232 }); 12233 }; 12234 const close = (detail, mapFetch, component, sandbox, _externals, _onOpenSync, _highlightOnOpen) => { 12235 Sandboxing.close(sandbox); 12236 return Future.pure(sandbox); 12237 }; 12238 const togglePopup = (detail, mapFetch, hotspot, externals, onOpenSync, highlightOnOpen) => { 12239 const sandbox = Coupling.getCoupled(hotspot, 'sandbox'); 12240 const showing = Sandboxing.isOpen(sandbox); 12241 const action = showing ? close : open; 12242 return action(detail, mapFetch, hotspot, sandbox, externals, onOpenSync, highlightOnOpen); 12243 }; 12244 const matchWidth = (hotspot, container, useMinWidth) => { 12245 const menu = Composing.getCurrent(container).getOr(container); 12246 const buttonWidth = get$c(hotspot.element); 12247 if (useMinWidth) { 12248 set$7(menu.element, 'min-width', buttonWidth + 'px'); 12249 } 12250 else { 12251 set$6(menu.element, buttonWidth); 12252 } 12253 }; 12254 const getSink = (anyInSystem, sinkDetail) => anyInSystem 12255 .getSystem() 12256 .getByUid(sinkDetail.uid + '-' + suffix()) 12257 .map((internalSink) => () => Result.value(internalSink)) 12258 .getOrThunk(() => sinkDetail.lazySink.fold(() => () => Result.error(new Error('No internal sink is specified, nor could an external sink be found')), (lazySinkFn) => () => lazySinkFn(anyInSystem))); 12259 const doRepositionMenus = (sandbox) => { 12260 Sandboxing.getState(sandbox).each((tmenu) => { 12261 tieredMenu.repositionMenus(tmenu); 12262 }); 12263 }; 12264 const makeSandbox$1 = (detail, hotspot, extras) => {
vendor: 4,405 bytes, lines 12265-12372
12265 const ariaControls = manager(); 12266 const onOpen = (component, menu) => { 12267 const anchor = getAnchor(detail, hotspot); 12268 ariaControls.link(hotspot.element); 12269 if (detail.matchWidth) { 12270 matchWidth(anchor.hotspot, menu, detail.useMinWidth); 12271 } 12272 detail.onOpen(anchor, component, menu); 12273 if (extras !== undefined && extras.onOpen !== undefined) { 12274 extras.onOpen(component, menu); 12275 } 12276 tieredMenu.repositionMenus(menu); 12277 }; 12278 const onClose = (component, menu) => { 12279 ariaControls.unlink(hotspot.element); 12280 lazySink().getOr(menu).element.dom.dispatchEvent(new window.FocusEvent('focusout')); 12281 if (extras !== undefined && extras.onClose !== undefined) { 12282 extras.onClose(component, menu); 12283 } 12284 }; 12285 const lazySink = getSink(hotspot, detail); 12286 return { 12287 dom: { 12288 tag: 'div', 12289 classes: detail.sandboxClasses, 12290 // TODO: Add aria-selected attribute 12291 attributes: { 12292 id: ariaControls.id, 12293 } 12294 }, 12295 behaviours: SketchBehaviours.augment(detail.sandboxBehaviours, [ 12296 Representing.config({ 12297 store: { 12298 mode: 'memory', 12299 initialValue: hotspot 12300 } 12301 }), 12302 Sandboxing.config({ 12303 onOpen, 12304 onClose, 12305 isPartOf: (container, data, queryElem) => { 12306 return isPartOf(data, queryElem) || isPartOf(hotspot, queryElem); 12307 }, 12308 getAttachPoint: () => { 12309 return lazySink().getOrDie(); 12310 } 12311 }), 12312 // The Composing of the dropdown here is the the active menu of the TieredMenu 12313 // inside the sandbox. 12314 Composing.config({ 12315 find: (sandbox) => { 12316 return Sandboxing.getState(sandbox).bind((menu) => Composing.getCurrent(menu)); 12317 } 12318 }), 12319 Receiving.config({ 12320 channels: { 12321 ...receivingChannel$1({ 12322 isExtraPart: never 12323 }), 12324 ...receivingChannel({ 12325 doReposition: doRepositionMenus 12326 }) 12327 } 12328 }) 12329 ]) 12330 }; 12331 }; 12332 const repositionMenus = (comp) => { 12333 const sandbox = Coupling.getCoupled(comp, 'sandbox'); 12334 doRepositionMenus(sandbox); 12335 }; 12336 12337 // TODO: Roll this back into Fields at some point 12338 // Unfortunately there appears to be a cyclical dependency or something that's preventing it, but for now this will do as it's home 12339 const sandboxFields = () => [ 12340 defaulted('sandboxClasses', []), 12341 SketchBehaviours.field('sandboxBehaviours', [Composing, Receiving, Sandboxing, Representing]) 12342 ]; 12343 12344 const schema$f = constant$1([ 12345 required$1('dom'), 12346 required$1('fetch'), 12347 onHandler('onOpen'), 12348 onKeyboardHandler('onExecute'), 12349 defaulted('getHotspot', Optional.some), 12350 defaulted('getAnchorOverrides', constant$1({})), 12351 schema$n(), 12352 field('dropdownBehaviours', [Toggling, Coupling, Keying, Focusing]), 12353 required$1('toggleClass'), 12354 defaulted('eventOrder', {}), 12355 option$3('lazySink'), 12356 defaulted('matchWidth', false), 12357 defaulted('useMinWidth', false), 12358 option$3('role'), 12359 option$3('listRole'), 12360 ].concat(sandboxFields())); 12361 const parts$c = constant$1([ 12362 external$1({ 12363 schema: [ 12364 tieredMenuMarkers(), 12365 // Defining a defaulted field isn't necessary when dealing with 12366 // external parts, because the post-boulder part spec is not passed 12367 // through to any of these functions (defaults, overrides etc.). So all 12368 // this does is make it a bit clearer what you should expect, but remember 12369 // that the default value here is irrelevant! 12370 defaulted('fakeFocus', false) 12371 ], 12372 name: 'menu',
12373 defaults: (detail) => { 12374 return { 12375 onExecute: detail.onExecute 12376 }; 12377 } 12378 }), 12379 partType() 12380 ]); 12381 12382 const factory$k = (detail, components, _spec, externals) => { 12383 const lookupAttr = (attr) => get$h(detail.dom, 'attributes').bind((attrs) => get$h(attrs, attr)); 12384 const switchToMenu = (sandbox) => { 12385 Sandboxing.getState(sandbox).each((tmenu) => { 12386 // This will highlight the menu AND the item 12387 tieredMenu.highlightPrimary(tmenu); 12388 }); 12389 }; 12390 const togglePopup$1 = (dropdownComp, onOpenSync, highlightOnOpen) => { 12391 return togglePopup(detail, identity, dropdownComp, externals, onOpenSync, highlightOnOpen); 12392 }; 12393 const action = (component) => { 12394 const onOpenSync = switchToMenu; 12395 togglePopup$1(component, onOpenSync, HighlightOnOpen.HighlightMenuAndItem).get(noop); 12396 }; 12397 const apis = { 12398 expand: (comp) => { 12399 if (!Toggling.isOn(comp)) { 12400 togglePopup$1(comp, noop, HighlightOnOpen.HighlightNone).get(noop); 12401 } 12402 }, 12403 open: (comp) => { 12404 if (!Toggling.isOn(comp)) { 12405 togglePopup$1(comp, noop, HighlightOnOpen.HighlightMenuAndItem).get(noop); 12406 } 12407 }, 12408 refetch: (comp) => { 12409 // Generally, the triggers for a refetch should make it so that the 12410 // sandbox has been created, but it's not guaranteed, so we still handle the 12411 // case where there isn't yet a sandbox. 12412 const optSandbox = Coupling.getExistingCoupled(comp, 'sandbox'); 12413 return optSandbox.fold(() => { 12414 // If we don't have a sandbox, refetch is the same as open, 12415 // except we return when it is completed. 12416 return togglePopup$1(comp, noop, HighlightOnOpen.HighlightMenuAndItem) 12417 .map(noop); 12418 }, (sandboxComp) => { 12419 // We are intentionally not preserving the selected items when 12420 // triggering a refetch, and will just highlight the first item. 12421 // Note: this will mean that submenus will close. If we want to start 12422 // preserving the selected items, we can't rely on the components themselves, 12423 // so we'd need to use the item and menu values through Representing. 12424 // However, be aware that alloy menus and items often have randomised values, 12425 // so these might not be reliable either. 12426 // NOTE: We use DropdownUtils.open directly, because we want it to 'open', 12427 // even if it's already open. If we just used apis.open, it wouldn't do 12428 // anything if it was already open, which means we wouldn't see the new 12429 // refetched data. 12430 return open(detail, identity, comp, 12431 // NOTE: The TieredMenu is inside the sandbox. They aren't the same component. 12432 sandboxComp, externals, noop, HighlightOnOpen.HighlightMenuAndItem).map(noop); 12433 }); 12434 }, 12435 isOpen: Toggling.isOn, 12436 close: (comp) => { 12437 if (Toggling.isOn(comp)) { 12438 togglePopup$1(comp, noop, HighlightOnOpen.HighlightMenuAndItem).get(noop); 12439 } 12440 }, 12441 // If we are open, refresh the menus in the tiered menu system 12442 repositionMenus: (comp) => { 12443 if (Toggling.isOn(comp)) { 12444 repositionMenus(comp); 12445 } 12446 } 12447 }; 12448 const triggerExecute = (comp, _se) => { 12449 emitExecute(comp); 12450 return Optional.some(true); 12451 }; 12452 return { 12453 uid: detail.uid, 12454 dom: detail.dom, 12455 components, 12456 behaviours: augment(detail.dropdownBehaviours, [ 12457 Toggling.config({ 12458 toggleClass: detail.toggleClass, 12459 aria: { 12460 mode: 'expanded' 12461 } 12462 }), 12463 Coupling.config({ 12464 others: { 12465 sandbox: (hotspot) => { 12466 return makeSandbox$1(detail, hotspot, { 12467 onOpen: () =>
vendor: 34,477 bytes, lines 12467-13326
12467 Toggling.on(hotspot), 12468 onClose: () => Toggling.off(hotspot) 12469 }); 12470 } 12471 } 12472 }), 12473 Keying.config({ 12474 mode: 'special', 12475 onSpace: triggerExecute, 12476 onEnter: triggerExecute, 12477 onDown: (comp, _se) => { 12478 if (Dropdown.isOpen(comp)) { 12479 const sandbox = Coupling.getCoupled(comp, 'sandbox'); 12480 switchToMenu(sandbox); 12481 } 12482 else { 12483 Dropdown.open(comp); 12484 } 12485 return Optional.some(true); 12486 }, 12487 onEscape: (comp, _se) => { 12488 if (Dropdown.isOpen(comp)) { 12489 Dropdown.close(comp); 12490 return Optional.some(true); 12491 } 12492 else { 12493 return Optional.none(); 12494 } 12495 } 12496 }), 12497 Focusing.config({}) 12498 ]), 12499 events: events(Optional.some(action)), 12500 eventOrder: { 12501 ...detail.eventOrder, 12502 // Order, the button state is toggled first, so assumed !selected means close. 12503 [execute$5()]: ['disabling', 'toggling', 'alloy.base.behaviour'] 12504 }, 12505 apis, 12506 domModification: { 12507 attributes: { 12508 'aria-haspopup': detail.listRole.getOr('true'), 12509 ...detail.role.fold(() => ({}), (role) => ({ role })), 12510 ...detail.dom.tag === 'button' ? { type: lookupAttr('type').getOr('button') } : {} 12511 } 12512 } 12513 }; 12514 }; 12515 const Dropdown = composite({ 12516 name: 'Dropdown', 12517 configFields: schema$f(), 12518 partFields: parts$c(), 12519 factory: factory$k, 12520 apis: { 12521 open: (apis, comp) => apis.open(comp), 12522 refetch: (apis, comp) => apis.refetch(comp), 12523 expand: (apis, comp) => apis.expand(comp), 12524 close: (apis, comp) => apis.close(comp), 12525 isOpen: (apis, comp) => apis.isOpen(comp), 12526 repositionMenus: (apis, comp) => apis.repositionMenus(comp) 12527 } 12528 }); 12529 12530 const owner$1 = 'form'; 12531 const schema$e = [ 12532 field('formBehaviours', [Representing]) 12533 ]; 12534 const getPartName$1 = (name) => '<alloy.field.' + name + '>'; 12535 const sketch$2 = (fSpec) => { 12536 const parts = (() => { 12537 const record = []; 12538 const field = (name, config) => { 12539 record.push(name); 12540 return generateOne$1(owner$1, getPartName$1(name), config); 12541 }; 12542 return { 12543 field, 12544 record: constant$1(record) 12545 }; 12546 })(); 12547 const spec = fSpec(parts); 12548 const partNames = parts.record(); 12549 // Unlike other sketches, a form does not know its parts in advance (as they represent each field 12550 // in a particular form). Therefore, it needs to calculate the part names on the fly 12551 const fieldParts = map$2(partNames, (n) => required({ name: n, pname: getPartName$1(n) })); 12552 return composite$1(owner$1, schema$e, fieldParts, make$4, spec); 12553 }; 12554 const toResult = (o, e) => o.fold(() => Result.error(e), Result.value); 12555 const make$4 = (detail, components) => ({ 12556 uid: detail.uid, 12557 dom: detail.dom, 12558 components, 12559 // Form has an assumption that every field must have composing, and that the composed element has representing. 12560 behaviours: augment(detail.formBehaviours, [ 12561 Representing.config({ 12562 store: { 12563 mode: 'manual', 12564 getValue: (form) => { 12565 const resPs = getAllParts(form, detail); 12566 return map$1(resPs, (resPThunk, pName) => resPThunk().bind((v) => { 12567 const opt = Composing.getCurrent(v); 12568 return toResult(opt, new Error(`Cannot find a current component to extract the value from for form part '${pName}': ` + element(v.element))); 12569 }).map(Representing.getValue)); 12570 }, 12571 setValue: (form, values) => { 12572 each(values, (newValue, key) => { 12573 getPart(form, detail, key).each((wrapper) => { 12574 Composing.getCurrent(wrapper).each((field) => { 12575 Representing.setValue(field, newValue); 12576 }); 12577 }); 12578 }); 12579 } 12580 } 12581 }) 12582 ]), 12583 apis: { 12584 getField: (form, key) => { 12585 // Returns an Optional (not a result); 12586 return getPart(form, detail, key).bind(Composing.getCurrent); 12587 } 12588 } 12589 }); 12590 const Form = { 12591 getField: makeApi((apis, component, key) => apis.getField(component, key)), 12592 sketch: sketch$2 12593 }; 12594 12595 const schema$d = constant$1([ 12596 required$1('dom'), 12597 defaulted('shell', true), 12598 field('toolbarBehaviours', [Replacing]) 12599 ]); 12600 // TODO: Dupe with Toolbar 12601 const enhanceGroups = () => ({ 12602 behaviours: derive$1([ 12603 Replacing.config({}) 12604 ]) 12605 }); 12606 const parts$b = constant$1([ 12607 // Note, is the container for putting all the groups in, not a group itself. 12608 optional({ 12609 name: 'groups', 12610 overrides: enhanceGroups 12611 }) 12612 ]); 12613 12614 const factory$j = (detail, components, _spec, _externals) => { 12615 const setGroups = (toolbar, groups) => { 12616 getGroupContainer(toolbar).fold(() => { 12617 // check that the group container existed. It may not have if the components 12618 // did not list anything, and shell was false. 12619 // eslint-disable-next-line no-console 12620 console.error('Toolbar was defined to not be a shell, but no groups container was specified in components'); 12621 throw new Error('Toolbar was defined to not be a shell, but no groups container was specified in components'); 12622 }, (container) => { 12623 Replacing.set(container, groups); 12624 }); 12625 }; 12626 const getGroupContainer = (component) => detail.shell ? Optional.some(component) : getPart(component, detail, 'groups'); 12627 // In shell mode, the group overrides need to be added to the main container, and there can be no children 12628 const extra = detail.shell ? { behaviours: [Replacing.config({})], components: [] } : { behaviours: [], components }; 12629 return { 12630 uid: detail.uid, 12631 dom: detail.dom, 12632 components: extra.components, 12633 behaviours: augment(detail.toolbarBehaviours, extra.behaviours), 12634 apis: { 12635 setGroups, 12636 refresh: noop 12637 }, 12638 domModification: { 12639 attributes: { 12640 role: 'toolbar' 12641 } 12642 } 12643 }; 12644 }; 12645 const Toolbar = composite({ 12646 name: 'Toolbar', 12647 configFields: schema$d(), 12648 partFields: parts$b(), 12649 factory: factory$j, 12650 apis: { 12651 setGroups: (apis, toolbar, groups) => { 12652 apis.setGroups(toolbar, groups); 12653 } 12654 } 12655 }); 12656 12657 const schema$c = constant$1([ 12658 markers$1(['toggledClass']), 12659 required$1('lazySink'), 12660 requiredFunction('fetch'), 12661 optionFunction('getBounds'), 12662 optionObjOf('fireDismissalEventInstead', [ 12663 defaulted('event', dismissRequested()) 12664 ]), 12665 schema$n(), 12666 onHandler('onToggled'), 12667 ]); 12668 const parts$a = constant$1([ 12669 external$1({ 12670 name: 'button', 12671 overrides: (detail) => ({ 12672 dom: { 12673 attributes: { 12674 'aria-haspopup': 'true' 12675 } 12676 }, 12677 buttonBehaviours: derive$1([ 12678 Toggling.config({ 12679 toggleClass: detail.markers.toggledClass, 12680 aria: { 12681 mode: 'expanded' 12682 }, 12683 toggleOnExecute: false, 12684 /** 12685 * For FloatingToolbars, we can hook up our `onToggled` handler directly to the Toggling 12686 * because we don't have to worry about any animations. 12687 * 12688 * Unfortunately, for SlidingToolbars, Toggling is more directly hooked into the animation for growing, 12689 * so to have an event `onToggled` that doesn't care about the animation, we can't just hook into the Toggling config. 12690 */ 12691 onToggled: detail.onToggled 12692 }) 12693 ]) 12694 }) 12695 }), 12696 external$1({ 12697 factory: Toolbar, 12698 schema: schema$d(), 12699 name: 'toolbar', 12700 overrides: (detail) => { 12701 return { 12702 toolbarBehaviours: derive$1([ 12703 Keying.config({ 12704 mode: 'cyclic', 12705 onEscape: (comp) => { 12706 getPart(comp, detail, 'button').each(Focusing.focus); 12707 // Don't return true here, as we need to allow the sandbox to handle the escape to close the overflow 12708 return Optional.none(); 12709 } 12710 }) 12711 ]) 12712 }; 12713 } 12714 }) 12715 ]); 12716 12717 const shouldSkipFocus = value$2(); 12718 const toggleWithoutFocusing = (button, externals) => { 12719 shouldSkipFocus.set(true); 12720 toggle(button, externals); 12721 shouldSkipFocus.clear(); 12722 }; 12723 const toggle = (button, externals) => { 12724 const toolbarSandbox = Coupling.getCoupled(button, 'toolbarSandbox'); 12725 if (Sandboxing.isOpen(toolbarSandbox)) { 12726 Sandboxing.close(toolbarSandbox); 12727 } 12728 else { 12729 Sandboxing.open(toolbarSandbox, externals.toolbar()); 12730 } 12731 }; 12732 const position = (button, toolbar, detail, layouts) => { 12733 const bounds = detail.getBounds.map((bounder) => bounder()); 12734 const sink = detail.lazySink(button).getOrDie(); 12735 Positioning.positionWithinBounds(sink, toolbar, { 12736 anchor: { 12737 type: 'hotspot', 12738 hotspot: button, 12739 layouts, 12740 overrides: { 12741 maxWidthFunction: expandable() 12742 } 12743 } 12744 }, bounds); 12745 }; 12746 const setGroups$1 = (button, toolbar, detail, layouts, groups) => { 12747 Toolbar.setGroups(toolbar, groups); 12748 position(button, toolbar, detail, layouts); 12749 Toggling.on(button); 12750 }; 12751 const makeSandbox = (button, spec, detail) => { 12752 const ariaControls = manager(); 12753 const onOpen = (sandbox, toolbar) => { 12754 const skipFocus = shouldSkipFocus.get().getOr(false); 12755 detail.fetch().get((groups) => { 12756 setGroups$1(button, toolbar, detail, spec.layouts, groups); 12757 ariaControls.link(button.element); 12758 if (!skipFocus) { 12759 Keying.focusIn(toolbar); 12760 } 12761 }); 12762 }; 12763 const onClose = () => { 12764 // Toggle and focus the button 12765 Toggling.off(button); 12766 if (!shouldSkipFocus.get().getOr(false)) { 12767 Focusing.focus(button); 12768 } 12769 ariaControls.unlink(button.element); 12770 }; 12771 return { 12772 dom: { 12773 tag: 'div', 12774 attributes: { 12775 id: ariaControls.id 12776 } 12777 }, 12778 behaviours: derive$1([ 12779 Keying.config({ 12780 mode: 'special', 12781 onEscape: (comp) => { 12782 Sandboxing.close(comp); 12783 return Optional.some(true); 12784 } 12785 }), 12786 Sandboxing.config({ 12787 onOpen, 12788 onClose, 12789 isPartOf: (container, data, queryElem) => { 12790 return isPartOf(data, queryElem) || isPartOf(button, queryElem); 12791 }, 12792 getAttachPoint: () => { 12793 return detail.lazySink(button).getOrDie(); 12794 } 12795 }), 12796 Receiving.config({ 12797 channels: { 12798 ...receivingChannel$1({ 12799 isExtraPart: never, 12800 ...detail.fireDismissalEventInstead.map((fe) => ({ fireEventInstead: { event: fe.event } })).getOr({}) 12801 }), 12802 ...receivingChannel({ 12803 doReposition: () => { 12804 Sandboxing.getState(Coupling.getCoupled(button, 'toolbarSandbox')).each((toolbar) => { 12805 position(button, toolbar, detail, spec.layouts); 12806 }); 12807 } 12808 }) 12809 } 12810 }) 12811 ]) 12812 }; 12813 }; 12814 const factory$i = (detail, components, spec, externals) => ({ 12815 ...Button.sketch({ 12816 ...externals.button(), 12817 action: (button) => { 12818 toggle(button, externals); 12819 }, 12820 buttonBehaviours: SketchBehaviours.augment({ dump: externals.button().buttonBehaviours }, [ 12821 Coupling.config({ 12822 others: { 12823 toolbarSandbox: (button) => { 12824 return makeSandbox(button, spec, detail); 12825 } 12826 } 12827 }) 12828 ]) 12829 }), 12830 apis: { 12831 setGroups: (button, groups) => { 12832 Sandboxing.getState(Coupling.getCoupled(button, 'toolbarSandbox')).each((toolbar) => { 12833 setGroups$1(button, toolbar, detail, spec.layouts, groups); 12834 }); 12835 }, 12836 reposition: (button) => { 12837 Sandboxing.getState(Coupling.getCoupled(button, 'toolbarSandbox')).each((toolbar) => { 12838 position(button, toolbar, detail, spec.layouts); 12839 }); 12840 }, 12841 toggle: (button) => { 12842 toggle(button, externals); 12843 }, 12844 toggleWithoutFocusing: (button) => { 12845 toggleWithoutFocusing(button, externals); 12846 }, 12847 getToolbar: (button) => { 12848 return Sandboxing.getState(Coupling.getCoupled(button, 'toolbarSandbox')); 12849 }, 12850 isOpen: (button) => { 12851 return Sandboxing.isOpen(Coupling.getCoupled(button, 'toolbarSandbox')); 12852 } 12853 } 12854 }); 12855 const FloatingToolbarButton = composite({ 12856 name: 'FloatingToolbarButton', 12857 factory: factory$i, 12858 configFields: schema$c(), 12859 partFields: parts$a(), 12860 apis: { 12861 setGroups: (apis, button, groups) => { 12862 apis.setGroups(button, groups); 12863 }, 12864 reposition: (apis, button) => { 12865 apis.reposition(button); 12866 }, 12867 toggle: (apis, button) => { 12868 apis.toggle(button); 12869 }, 12870 toggleWithoutFocusing: (apis, button) => { 12871 apis.toggleWithoutFocusing(button); 12872 }, 12873 getToolbar: (apis, button) => apis.getToolbar(button), 12874 isOpen: (apis, button) => apis.isOpen(button) 12875 } 12876 }); 12877 12878 const schema$b = constant$1([ 12879 defaulted('prefix', 'form-field'), 12880 field('fieldBehaviours', [Composing, Representing]) 12881 ]); 12882 const parts$9 = constant$1([ 12883 optional({ 12884 schema: [required$1('dom')], 12885 name: 'label' 12886 }), 12887 optional({ 12888 factory: { 12889 sketch: (spec) => { 12890 return { 12891 uid: spec.uid, 12892 dom: { 12893 tag: 'span', 12894 styles: { 12895 display: 'none' 12896 }, 12897 attributes: { 12898 'aria-hidden': 'true' 12899 }, 12900 innerHtml: spec.text 12901 } 12902 }; 12903 } 12904 }, 12905 schema: [required$1('text')], 12906 name: 'aria-descriptor' 12907 }), 12908 required({ 12909 factory: { 12910 sketch: (spec) => { 12911 const excludeFactory = exclude(spec, ['factory']); 12912 return spec.factory.sketch(excludeFactory); 12913 } 12914 }, 12915 schema: [required$1('factory')], 12916 name: 'field' 12917 }) 12918 ]); 12919 12920 const factory$h = (detail, components, _spec, _externals) => { 12921 const behaviours = augment(detail.fieldBehaviours, [ 12922 Composing.config({ 12923 find: (container) => { 12924 return getPart(container, detail, 'field'); 12925 } 12926 }), 12927 Representing.config({ 12928 store: { 12929 mode: 'manual', 12930 getValue: (field) => { 12931 return Composing.getCurrent(field).bind(Representing.getValue); 12932 }, 12933 setValue: (field, value) => { 12934 Composing.getCurrent(field).each((current) => { 12935 Representing.setValue(current, value); 12936 }); 12937 } 12938 } 12939 }) 12940 ]); 12941 const events = derive$2([ 12942 // Used to be systemInit 12943 runOnAttached((component, _simulatedEvent) => { 12944 const ps = getParts(component, detail, ['label', 'field', 'aria-descriptor']); 12945 ps.field().each((field) => { 12946 const id = generate$6(detail.prefix); 12947 ps.label().each((label) => { 12948 // TODO: Find a nicer way of doing this. 12949 set$9(label.element, 'for', id); 12950 set$9(field.element, 'id', id); 12951 }); 12952 ps['aria-descriptor']().each((descriptor) => { 12953 const descriptorId = generate$6(detail.prefix); 12954 set$9(descriptor.element, 'id', descriptorId); 12955 set$9(field.element, 'aria-describedby', descriptorId); 12956 }); 12957 }); 12958 }) 12959 ]); 12960 const apis = { 12961 getField: (container) => getPart(container, detail, 'field'), 12962 getLabel: (container) => 12963 // TODO: Use constants for part names 12964 getPart(container, detail, 'label') 12965 }; 12966 return { 12967 uid: detail.uid, 12968 dom: detail.dom, 12969 components, 12970 behaviours, 12971 events, 12972 apis 12973 }; 12974 }; 12975 const FormField = composite({ 12976 name: 'FormField', 12977 configFields: schema$b(), 12978 partFields: parts$9(), 12979 factory: factory$h, 12980 apis: { 12981 getField: (apis, comp) => apis.getField(comp), 12982 getLabel: (apis, comp) => apis.getLabel(comp) 12983 } 12984 }); 12985 12986 const schema$a = constant$1([ 12987 defaulted('field1Name', 'field1'), 12988 defaulted('field2Name', 'field2'), 12989 onStrictHandler('onLockedChange'), 12990 markers$1(['lockClass']), 12991 defaulted('locked', false), 12992 SketchBehaviours.field('coupledFieldBehaviours', [Composing, Representing]), 12993 defaultedFunction('onInput', noop) 12994 ]); 12995 const getField = (comp, detail, partName) => getPart(comp, detail, partName).bind(Composing.getCurrent); 12996 const coupledPart = (selfName, otherName) => required({ 12997 factory: FormField, 12998 name: selfName, 12999 overrides: (detail) => { 13000 return { 13001 fieldBehaviours: derive$1([ 13002 config('coupled-input-behaviour', [ 13003 run$1(input(), (me) => { 13004 getField(me, detail, otherName).each((other) => { 13005 getPart(me, detail, 'lock').each((lock) => { 13006 // TODO IMPROVEMENT: Allow locker to fire onLockedChange if it is turned on after being off. 13007 if (Toggling.isOn(lock)) { 13008 detail.onLockedChange(me, other, lock); 13009 } 13010 detail.onInput(me); 13011 }); 13012 }); 13013 }) 13014 ]) 13015 ]) 13016 }; 13017 } 13018 }); 13019 const parts$8 = constant$1([ 13020 coupledPart('field1', 'field2'), 13021 coupledPart('field2', 'field1'), 13022 required({ 13023 factory: Button, 13024 schema: [ 13025 required$1('dom') 13026 ], 13027 name: 'lock', 13028 overrides: (detail) => { 13029 return { 13030 buttonBehaviours: derive$1([ 13031 Toggling.config({ 13032 selected: detail.locked, 13033 toggleClass: detail.markers.lockClass, 13034 aria: { 13035 mode: 'pressed' 13036 } 13037 }) 13038 ]) 13039 }; 13040 } 13041 }) 13042 ]); 13043 13044 const factory$g = (detail, components, _spec, _externals) => ({ 13045 uid: detail.uid, 13046 dom: detail.dom, 13047 components, 13048 behaviours: SketchBehaviours.augment(detail.coupledFieldBehaviours, [ 13049 Composing.config({ find: Optional.some }), 13050 Representing.config({ 13051 store: { 13052 mode: 'manual', 13053 getValue: (comp) => { 13054 const parts = getPartsOrDie(comp, detail, ['field1', 'field2']); 13055 return { 13056 [detail.field1Name]: Representing.getValue(parts.field1()), 13057 [detail.field2Name]: Representing.getValue(parts.field2()) 13058 }; 13059 }, 13060 setValue: (comp, value) => { 13061 const parts = getPartsOrDie(comp, detail, ['field1', 'field2']); 13062 if (hasNonNullableKey(value, detail.field1Name)) { 13063 Representing.setValue(parts.field1(), value[detail.field1Name]); 13064 } 13065 if (hasNonNullableKey(value, detail.field2Name)) { 13066 Representing.setValue(parts.field2(), value[detail.field2Name]); 13067 } 13068 } 13069 } 13070 }) 13071 ]), 13072 apis: { 13073 getField1: (component) => getPart(component, detail, 'field1'), 13074 getField2: (component) => getPart(component, detail, 'field2'), 13075 getLock: (component) => getPart(component, detail, 'lock') 13076 } 13077 }); 13078 const FormCoupledInputs = composite({ 13079 name: 'FormCoupledInputs', 13080 configFields: schema$a(), 13081 partFields: parts$8(), 13082 factory: factory$g, 13083 apis: { 13084 getField1: (apis, component) => apis.getField1(component), 13085 getField2: (apis, component) => apis.getField2(component), 13086 getLock: (apis, component) => apis.getLock(component) 13087 } 13088 }); 13089 13090 const factory$f = (detail, _spec) => { 13091 const options = map$2(detail.options, (option) => ({ 13092 dom: { 13093 tag: 'option', 13094 value: option.value, 13095 innerHtml: option.text 13096 } 13097 })); 13098 const initialValues = detail.data.map((v) => wrap('initialValue', v)).getOr({}); 13099 return { 13100 uid: detail.uid, 13101 dom: { 13102 tag: 'select', 13103 classes: detail.selectClasses, 13104 attributes: detail.selectAttributes 13105 }, 13106 components: options, 13107 behaviours: augment(detail.selectBehaviours, [ 13108 Focusing.config({}), 13109 Representing.config({ 13110 store: { 13111 mode: 'manual', 13112 getValue: (select) => { 13113 return get$5(select.element); 13114 }, 13115 setValue: (select, newValue) => { 13116 const firstOption = head(detail.options); 13117 // This is probably generically useful ... may become a part of Representing. 13118 const found = find$5(detail.options, (opt) => opt.value === newValue); 13119 if (found.isSome()) { 13120 set$4(select.element, newValue); 13121 } 13122 else if (select.element.dom.selectedIndex === -1 && newValue === '') { 13123 /* 13124 Sometimes after a redial alloy tries to set a new value, but if no value has been set in the data this used to fail. Now we set the value to the first option in the list if: 13125 The index is out of range, indicating that the list of options have changed, or was never set. 13126 The user is not trying to set a specific value (which would be user error) 13127 */ 13128 firstOption.each((value) => set$4(select.element, value.value)); 13129 } 13130 }, 13131 ...initialValues 13132 } 13133 }) 13134 ]) 13135 }; 13136 }; 13137 const HtmlSelect = single({ 13138 name: 'HtmlSelect', 13139 configFields: [ 13140 required$1('options'), 13141 field('selectBehaviours', [Focusing, Representing]), 13142 defaulted('selectClasses', []), 13143 defaulted('selectAttributes', {}), 13144 option$3('data') 13145 ], 13146 factory: factory$f 13147 }); 13148 13149 const makeMenu = (detail, menuSandbox, placementSpec, menuSpec, getBounds) => { 13150 const lazySink = () => detail.lazySink(menuSandbox); 13151 const layouts = menuSpec.type === 'horizontal' ? { layouts: { 13152 onLtr: () => belowOrAbove(), 13153 onRtl: () => belowOrAboveRtl() 13154 } } : {}; 13155 const isFirstTierSubmenu = (triggeringPaths) => triggeringPaths.length === 2; // primary and first tier menu === 2 items 13156 const getSubmenuLayouts = (triggeringPaths) => isFirstTierSubmenu(triggeringPaths) ? layouts : {}; 13157 return tieredMenu.sketch({ 13158 dom: { 13159 tag: 'div' 13160 }, 13161 data: menuSpec.data, 13162 markers: menuSpec.menu.markers, 13163 highlightOnOpen: menuSpec.menu.highlightOnOpen, 13164 fakeFocus: menuSpec.menu.fakeFocus, 13165 onEscape: () => { 13166 // Note for the future: this should possibly also call detail.onHide 13167 Sandboxing.close(menuSandbox); 13168 detail.onEscape.map((handler) => handler(menuSandbox)); 13169 return Optional.some(true); 13170 }, 13171 onExecute: () => { 13172 return Optional.some(true); 13173 }, 13174 onOpenMenu: (tmenu, menu) => { 13175 Positioning.positionWithinBounds(lazySink().getOrDie(), menu, placementSpec, getBounds()); 13176 }, 13177 onOpenSubmenu: (tmenu, item, submenu, triggeringPaths) => { 13178 const sink = lazySink().getOrDie(); 13179 Positioning.position(sink, submenu, { 13180 anchor: { 13181 type: 'submenu', 13182 item, 13183 ...getSubmenuLayouts(triggeringPaths) 13184 } 13185 }); 13186 }, 13187 onRepositionMenu: (tmenu, primaryMenu, submenuTriggers) => { 13188 const sink = lazySink().getOrDie(); 13189 Positioning.positionWithinBounds(sink, primaryMenu, placementSpec, getBounds()); 13190 each$1(submenuTriggers, (st) => { 13191 const submenuLayouts = getSubmenuLayouts(st.triggeringPath); 13192 Positioning.position(sink, st.triggeredMenu, { 13193 anchor: { type: 'submenu', item: st.triggeringItem, ...submenuLayouts } 13194 }); 13195 }); 13196 } 13197 }); 13198 }; 13199 const factory$e = (detail, spec) => { 13200 const isPartOfRelated = (sandbox, queryElem) => { 13201 const related = detail.getRelated(sandbox); 13202 return related.exists((rel) => isPartOf(rel, queryElem)); 13203 }; 13204 const setContent = (sandbox, thing) => { 13205 // Keep the same location, and just change the content. 13206 Sandboxing.setContent(sandbox, thing); 13207 }; 13208 const showAt = (sandbox, thing, placementSpec) => { 13209 const getBounds = Optional.none; 13210 showWithinBounds(sandbox, thing, placementSpec, getBounds); 13211 }; 13212 const showWithinBounds = (sandbox, thing, placementSpec, getBounds) => { 13213 const sink = detail.lazySink(sandbox).getOrDie(); 13214 Sandboxing.openWhileCloaked(sandbox, thing, () => Positioning.positionWithinBounds(sink, sandbox, placementSpec, getBounds())); 13215 Representing.setValue(sandbox, Optional.some({ 13216 mode: 'position', 13217 config: placementSpec, 13218 getBounds 13219 })); 13220 }; 13221 // TODO AP-191 write a test for showMenuAt 13222 const showMenuAt = (sandbox, placementSpec, menuSpec) => { 13223 showMenuWithinBounds(sandbox, placementSpec, menuSpec, Optional.none); 13224 }; 13225 const showMenuWithinBounds = (sandbox, placementSpec, menuSpec, getBounds) => { 13226 const menu = makeMenu(detail, sandbox, placementSpec, menuSpec, getBounds); 13227 Sandboxing.open(sandbox, menu); 13228 Representing.setValue(sandbox, Optional.some({ 13229 mode: 'menu', 13230 menu 13231 })); 13232 }; 13233 const hide = (sandbox) => { 13234 if (Sandboxing.isOpen(sandbox)) { 13235 Representing.setValue(sandbox, Optional.none()); 13236 Sandboxing.close(sandbox); 13237 } 13238 }; 13239 const getContent = (sandbox) => Sandboxing.getState(sandbox); 13240 const reposition = (sandbox) => { 13241 if (Sandboxing.isOpen(sandbox)) { 13242 Representing.getValue(sandbox).each((state) => { 13243 switch (state.mode) { 13244 case 'menu': 13245 Sandboxing.getState(sandbox).each(tieredMenu.repositionMenus); 13246 break; 13247 case 'position': 13248 const sink = detail.lazySink(sandbox).getOrDie(); 13249 Positioning.positionWithinBounds(sink, sandbox, state.config, state.getBounds()); 13250 break; 13251 } 13252 }); 13253 } 13254 }; 13255 const apis = { 13256 setContent, 13257 showAt, 13258 showWithinBounds, 13259 showMenuAt, 13260 showMenuWithinBounds, 13261 hide, 13262 getContent, 13263 reposition, 13264 isOpen: Sandboxing.isOpen 13265 }; 13266 return { 13267 uid: detail.uid, 13268 dom: detail.dom, 13269 behaviours: augment(detail.inlineBehaviours, [ 13270 Sandboxing.config({ 13271 isPartOf: (sandbox, data, queryElem) => { 13272 return isPartOf(data, queryElem) || isPartOfRelated(sandbox, queryElem); 13273 }, 13274 getAttachPoint: (sandbox) => { 13275 return detail.lazySink(sandbox).getOrDie(); 13276 }, 13277 onOpen: (sandbox) => { 13278 detail.onShow(sandbox); 13279 }, 13280 onClose: (sandbox) => { 13281 detail.onHide(sandbox); 13282 } 13283 }), 13284 Representing.config({ 13285 store: { 13286 mode: 'memory', 13287 initialValue: Optional.none() 13288 } 13289 }), 13290 Receiving.config({ 13291 channels: { 13292 ...receivingChannel$1({ 13293 isExtraPart: spec.isExtraPart, 13294 ...detail.fireDismissalEventInstead.map((fe) => ({ fireEventInstead: { event: fe.event } })).getOr({}) 13295 }), 13296 ...receivingChannel({ 13297 ...detail.fireRepositionEventInstead.map((fe) => ({ fireEventInstead: { event: fe.event } })).getOr({}), 13298 doReposition: reposition 13299 }) 13300 } 13301 }) 13302 ]), 13303 eventOrder: detail.eventOrder, 13304 apis 13305 }; 13306 }; 13307 const InlineView = single({ 13308 name: 'InlineView', 13309 configFields: [ 13310 required$1('lazySink'), 13311 onHandler('onShow'), 13312 onHandler('onHide'), 13313 optionFunction('onEscape'), 13314 field('inlineBehaviours', [Sandboxing, Representing, Receiving]), 13315 optionObjOf('fireDismissalEventInstead', [ 13316 defaulted('event', dismissRequested()) 13317 ]), 13318 optionObjOf('fireRepositionEventInstead', [ 13319 defaulted('event', repositionRequested()) 13320 ]), 13321 defaulted('getRelated', Optional.none), 13322 defaulted('isExtraPart', never), 13323 defaulted('eventOrder', Optional.none) 13324 ], 13325 factory: factory$e, 13326 apis: {
13327 showAt: (apis, component, anchor, thing) => { 13328 apis.showAt(component, anchor, thing); 13329 }, 13330 showWithinBounds: (apis, component, anchor, thing, bounds) => { 13331 apis.showWithinBounds(component, anchor, thing, bounds); 13332 }, 13333 showMenuAt: (apis, component, anchor, menuSpec) => { 13334 apis.showMenuAt(component, anchor, menuSpec); 13335 }, 13336 showMenuWithinBounds: (apis, component, anchor, menuSpec, bounds) => { 13337 apis.showMenuWithinBounds(component, anchor, menuSpec, bounds); 13338 }, 13339 hide: (apis, component) => { 13340 apis.hide(component); 13341 }, 13342 isOpen: (apis, component) => apis.isOpen(component), 13343 getContent: (apis, component) => apis.getContent(component), 13344 setContent: (apis, component, thing) => { 13345 apis.setContent(component, thing); 13346 }, 13347 reposition: (apis, component) => { 13348 apis.reposition(component); 13349 } 13350 } 13351 }); 13352 13353 const schema$9 = constant$1([ 13354 defaultedString('type', 'text'), 13355 option$3('data'), 13356 defaulted('inputAttributes', {}), 13357 defaulted('inputStyles', {}), 13358 defaulted('tag', 'input'), 13359 defaulted('inputClasses', []), 13360 onHandler('onSetValue'), 13361 defaultedFunction('fromInputValue', identity), 13362 defaultedFunction('toInputValue', identity), 13363 defaulted('styles', {}), 13364 defaulted('eventOrder', {}), 13365 field('inputBehaviours', [Representing, Focusing]), 13366 defaulted('selectOnFocus', true) 13367 ]); 13368 const focusBehaviours = (detail) => derive$1([ 13369 Focusing.config({ 13370 onFocus: !detail.selectOnFocus ? noop : (component) => { 13371 const input = component.element; 13372 const value = get$5(input); 13373 // TODO: There are probably more types that can't handle setSelectionRange 13374 if (detail.type !== 'range') { 13375 input.dom.setSelectionRange(0, value.length); 13376 } 13377 } 13378 }) 13379 ]); 13380 const behaviours = (detail) => ({ 13381 ...focusBehaviours(detail), 13382 ...augment(detail.inputBehaviours, [ 13383 Representing.config({ 13384 store: { 13385 mode: 'manual', 13386 // Propagating its Optional 13387 ...detail.data.map((data) => ({ initialValue: data })).getOr({}), 13388 getValue: (input) => { 13389 return detail.fromInputValue(get$5(input.element)); 13390 }, 13391 setValue: (input, data) => { 13392 const current = get$5(input.element); 13393 // Only set it if it has changed ... otherwise the cursor goes to the end. 13394 if (current !== data) { 13395 set$4(input.element, detail.toInputValue(data)); 13396 } 13397 } 13398 }, 13399 onSetValue: detail.onSetValue 13400 }) 13401 ]) 13402 }); 13403 const dom$1 = (detail) => ({ 13404 tag: detail.tag, 13405 attributes: { 13406 type: detail.type, 13407 ...detail.inputAttributes 13408 }, 13409 styles: detail.inputStyles, 13410 classes: detail.inputClasses 13411 }); 13412 13413 const factory$d = (detail, _spec) => ({ 13414 uid: detail.uid, 13415 dom: dom$1(detail), 13416 // No children. 13417 components: [], 13418 behaviours: behaviours(detail), 13419 eventOrder: detail.eventOrder 13420 }); 13421 const Input = single({ 13422 name: 'Input', 13423 configFields: schema$9(), 13424 factory: factory$d 13425 }); 13426 13427 const parts$7 = generate$5(owner$2(), parts$e()); 13428 13429 const labelledBy = (labelledElement, labelElement) => { 13430 const labelId = getOpt(labelledElement, 'id') 13431 .fold(() => { 13432 const id = generate$6('dialog-label'); 13433 set$9(labelElement, 'id', id); 13434 return id; 13435 }, identity); 13436 set$9(labelledElement, 'aria-labelledby', labelId); 13437 }; 13438 13439 const schema$8 = constant$1([ 13440 required$1('lazySink'), 13441 option$3('dragBlockClass'),
vendor: 5,204 bytes, lines 13442-13581
13442 defaultedFunction('getBounds', win), 13443 defaulted('useTabstopAt', always), 13444 defaulted('firstTabstop', 0), 13445 defaulted('eventOrder', {}), 13446 defaultedStringEnum('role', 'dialog', ['dialog', 'alertdialog']), 13447 field('modalBehaviours', [Keying]), 13448 onKeyboardHandler('onExecute'), 13449 onStrictKeyboardHandler('onEscape') 13450 ]); 13451 const basic = { sketch: identity }; 13452 const parts$6 = constant$1([ 13453 optional({ 13454 name: 'draghandle', 13455 overrides: (detail, spec) => { 13456 return { 13457 behaviours: derive$1([ 13458 Dragging.config({ 13459 mode: 'mouse', 13460 getTarget: (handle) => { 13461 return ancestor$1(handle, '[role="dialog"]').getOr(handle); 13462 }, 13463 blockerClass: detail.dragBlockClass.getOrDie( 13464 // TODO: Support errors in Optional getOrDie. 13465 new Error('The drag blocker class was not specified for a dialog with a drag handle: \n' + 13466 JSON.stringify(spec, null, 2)).message), 13467 getBounds: detail.getDragBounds 13468 }) 13469 ]) 13470 }; 13471 } 13472 }), 13473 required({ 13474 schema: [required$1('dom')], 13475 name: 'title' 13476 }), 13477 required({ 13478 factory: basic, 13479 schema: [required$1('dom')], 13480 name: 'close' 13481 }), 13482 required({ 13483 factory: basic, 13484 schema: [required$1('dom')], 13485 name: 'body' 13486 }), 13487 optional({ 13488 factory: basic, 13489 schema: [required$1('dom')], 13490 name: 'footer' 13491 }), 13492 external$1({ 13493 factory: { 13494 sketch: (spec, detail) => 13495 // Merging should take care of the uid 13496 ({ 13497 ...spec, 13498 dom: detail.dom, 13499 components: detail.components 13500 }) 13501 }, 13502 schema: [ 13503 defaulted('dom', { 13504 tag: 'div', 13505 styles: { 13506 position: 'fixed', 13507 left: '0px', 13508 top: '0px', 13509 right: '0px', 13510 bottom: '0px' 13511 } 13512 }), 13513 defaulted('components', []) 13514 ], 13515 name: 'blocker' 13516 }) 13517 ]); 13518 13519 const factory$c = (detail, components, spec, externals) => { 13520 const dialogComp = value$2(); 13521 // TODO IMPROVEMENT: Make close actually close the dialog by default! 13522 const showDialog = (dialog) => { 13523 dialogComp.set(dialog); 13524 const sink = detail.lazySink(dialog).getOrDie(); 13525 const externalBlocker = externals.blocker(); 13526 const blocker = sink.getSystem().build({ 13527 ...externalBlocker, 13528 components: externalBlocker.components.concat([ 13529 premade(dialog) 13530 ]), 13531 behaviours: derive$1([ 13532 Focusing.config({}), 13533 config('dialog-blocker-events', [ 13534 // Ensure we use runOnSource otherwise this would cause an infinite loop, as `focusIn` would fire a `focusin` which would then get responded to and so forth 13535 runOnSource(focusin(), () => { 13536 Blocking.isBlocked(dialog) ? noop() : Keying.focusIn(dialog); 13537 }) 13538 ]) 13539 ]) 13540 }); 13541 attach(sink, blocker); 13542 Keying.focusIn(dialog); 13543 }; 13544 const hideDialog = (dialog) => { 13545 dialogComp.clear(); 13546 parent(dialog.element).each((blockerDom) => { 13547 dialog.getSystem().getByDom(blockerDom).each((blocker) => { 13548 detach(blocker); 13549 }); 13550 }); 13551 }; 13552 const getDialogBody = (dialog) => getPartOrDie(dialog, detail, 'body'); 13553 const getDialogFooter = (dialog) => getPart(dialog, detail, 'footer'); 13554 const setBusy = (dialog, getBusySpec) => { 13555 Blocking.block(dialog, getBusySpec); 13556 }; 13557 const setIdle = (dialog) => { 13558 Blocking.unblock(dialog); 13559 }; 13560 const modalEventsId = generate$6('modal-events'); 13561 const eventOrder = { 13562 ...detail.eventOrder, 13563 [attachedToDom()]: [modalEventsId].concat(detail.eventOrder['alloy.system.attached'] || []) 13564 }; 13565 const browser = detect$1(); 13566 return { 13567 uid: detail.uid, 13568 dom: detail.dom, 13569 components, 13570 apis: { 13571 show: showDialog, 13572 hide: hideDialog, 13573 getBody: getDialogBody, 13574 getFooter: getDialogFooter, 13575 setIdle, 13576 setBusy 13577 }, 13578 eventOrder, 13579 domModification: { 13580 attributes: { 13581 'role': detail.role,
13582 'aria-modal': 'true' 13583 } 13584 }, 13585 behaviours: augment(detail.modalBehaviours, [ 13586 Replacing.config({}), 13587 Keying.config({ 13588 mode: 'cyclic', 13589 onEnter: detail.onExecute, 13590 onEscape: detail.onEscape, 13591 useTabstopAt: detail.useTabstopAt, 13592 firstTabstop: detail.firstTabstop 13593 }), 13594 Blocking.config({ 13595 getRoot: dialogComp.get 13596 }), 13597 config(modalEventsId, [ 13598 runOnAttached((c) => { 13599 // TINY-10808 - Workaround to address the dialog header not being announced on VoiceOver with aria-labelledby, ideally we should use the aria-labelledby 13600 const titleElm = getPartOrDie(c, detail, 'title').element; 13601 const title = get$6(titleElm); 13602 if (isNonNullable(title) && title !== '') { 13603 if (browser.os.isMacOS()) { 13604 set$9(c.element, 'aria-label', title); 13605 } 13606 else { 13607 labelledBy(c.element, titleElm); 13608 } 13609 } 13610 }) 13611 ]) 13612 ]) 13613 }; 13614 }; 13615 const ModalDialog = composite({ 13616 name: 'ModalDialog', 13617 configFields: schema$8(), 13618 partFields: parts$6(), 13619 factory: factory$c, 13620 apis: { 13621 show: (apis, dialog) => { 13622 apis.show(dialog); 13623 }, 13624 hide: (apis, dialog) => { 13625 apis.hide(dialog); 13626 }, 13627 getBody: (apis, dialog) => apis.getBody(dialog), 13628 getFooter: (apis, dialog) => apis.getFooter(dialog), 13629 setBusy: (apis, dialog, getBusySpec) => { 13630 apis.setBusy(dialog, getBusySpec); 13631 }, 13632 setIdle: (apis, dialog) => { 13633 apis.setIdle(dialog); 13634 } 13635 } 13636 }); 13637 13638 const labelPart = optional({ 13639 schema: [required$1('dom')], 13640 name: 'label' 13641 }); 13642 const edgePart = (name) => optional({ 13643 name: '' + name + '-edge', 13644 overrides: (detail) => { 13645 const action = detail.model.manager.edgeActions[name]; 13646 // Not all edges have actions for all sliders. 13647 // A horizontal slider will only have left and right, for instance, 13648 // ignoring top, bottom and diagonal edges as they don't make sense in context of those sliders. 13649 return action.fold(() => ({}), (a) => ({ 13650 events: derive$2([ 13651 runActionExtra(touchstart(), (comp, se, d) => a(comp, d), [detail]), 13652 runActionExtra(mousedown(), (comp, se, d) => a(comp, d), [detail]), 13653 runActionExtra(mousemove(), (comp, se, det) => { 13654 if (det.mouseIsDown.get()) { 13655 a(comp, det); 13656 } 13657 }, [detail]) 13658 ]) 13659 })); 13660 } 13661 }); 13662 // When the user touches the top left edge, it should move the thumb 13663 const tlEdgePart = edgePart('top-left'); 13664 // When the user touches the top edge, it should move the thumb 13665 const tedgePart = edgePart('top'); 13666 // When the user touches the top right edge, it should move the thumb 13667 const trEdgePart = edgePart('top-right'); 13668 // When the user touches the right edge, it should move the thumb 13669 const redgePart = edgePart('right'); 13670 // When the user touches the bottom right edge, it should move the thumb 13671 const brEdgePart = edgePart('bottom-right'); 13672 // When the user touches the bottom edge, it should move the thumb 13673 const bedgePart = edgePart('bottom'); 13674 // When the user touches the bottom left edge, it should move the thumb 13675 const blEdgePart = edgePart('bottom-left'); 13676 // When the user touches the left edge, it should move the thumb 13677 const ledgePart = edgePart('left'); 13678 // The thumb part needs to have position absolute to be positioned correctly 13679 const thumbPart = required({ 13680 name: 'thumb', 13681 defaults: constant$1({ 13682 dom: { 13683 styles: { position: 'absolute' } 13684 } 13685 }),
13686 overrides: (detail) => { 13687 return { 13688 events: derive$2([ 13689 // If the user touches the thumb itself, pretend they touched the spectrum instead. This 13690 // allows sliding even when they touchstart the current value 13691 redirectToPart(touchstart(), detail, 'spectrum'), 13692 redirectToPart(touchmove(), detail, 'spectrum'), 13693 redirectToPart(touchend(), detail, 'spectrum'), 13694 redirectToPart(mousedown(), detail, 'spectrum'), 13695 redirectToPart(mousemove(), detail, 'spectrum'), 13696 redirectToPart(mouseup(), detail, 'spectrum') 13697 ]) 13698 }; 13699 } 13700 }); 13701 const isShift = (event) => isShift$1(event.event); 13702 const spectrumPart = required({ 13703 schema: [ 13704 customField('mouseIsDown', () => Cell(false)) 13705 ], 13706 name: 'spectrum', 13707 overrides: (detail) => { 13708 const modelDetail = detail.model; 13709 const model = modelDetail.manager; 13710 const setValueFrom = (component, simulatedEvent) => model.getValueFromEvent(simulatedEvent).map((value) => model.setValueFrom(component, detail, value)); 13711 return { 13712 behaviours: derive$1([ 13713 // Move left and right along the spectrum 13714 Keying.config({ 13715 mode: 'special', 13716 onLeft: (spectrum, event) => model.onLeft(spectrum, detail, isShift(event)), 13717 onRight: (spectrum, event) => model.onRight(spectrum, detail, isShift(event)), 13718 onUp: (spectrum, event) => model.onUp(spectrum, detail, isShift(event)), 13719 onDown: (spectrum, event) => model.onDown(spectrum, detail, isShift(event)) 13720 }), 13721 Tabstopping.config({}), 13722 Focusing.config({}) 13723 ]), 13724 events: derive$2([ 13725 run$1(touchstart(), setValueFrom), 13726 run$1(touchmove(), setValueFrom), 13727 run$1(mousedown(), setValueFrom), 13728 run$1(mousemove(), (spectrum, se) => { 13729 if (detail.mouseIsDown.get()) { 13730 setValueFrom(spectrum, se); 13731 } 13732 }) 13733 ]) 13734 }; 13735 } 13736 }); 13737 var SliderParts = [ 13738 labelPart, 13739 ledgePart, 13740 redgePart, 13741 tedgePart, 13742 bedgePart, 13743 tlEdgePart, 13744 trEdgePart, 13745 blEdgePart, 13746 brEdgePart, 13747 thumbPart, 13748 spectrumPart 13749 ]; 13750 13751 const _sliderChangeEvent = 'slider.change.value'; 13752 const sliderChangeEvent = constant$1(_sliderChangeEvent); 13753 const isTouchEvent$2 = (evt) => evt.type.indexOf('touch') !== -1; 13754 const getEventSource = (simulatedEvent) => { 13755 const evt = simulatedEvent.event.raw; 13756 if (isTouchEvent$2(evt)) { 13757 const touchEvent = evt; 13758 return touchEvent.touches !== undefined && touchEvent.touches.length === 1 ? 13759 Optional.some(touchEvent.touches[0]).map((t) => SugarPosition(t.clientX, t.clientY)) : Optional.none(); 13760 } 13761 else { 13762 const mouseEvent = evt; 13763 return mouseEvent.clientX !== undefined ? Optional.some(mouseEvent).map((me) => SugarPosition(me.clientX, me.clientY)) : Optional.none(); 13764 } 13765 }; 13766 13767 const t = 'top', r = 'right', b = 'bottom', l = 'left'; 13768 // Values 13769 const minX = (detail) => detail.model.minX; 13770 const minY = (detail) => detail.model.minY; 13771 const min1X = (detail) => detail.model.minX - 1; 13772 const min1Y = (detail) => detail.model.minY - 1; 13773 const maxX = (detail) => detail.model.maxX; 13774 const maxY = (detail) => detail.model.maxY; 13775 const max1X = (detail) => detail.model.maxX + 1; 13776 const max1Y = (detail) => detail.model.maxY + 1; 13777 const range = (detail, max, min) => max(detail) - min(detail); 13778 const xRange = (detail) => range(detail, maxX, minX);
13779 const yRange = (detail) => range(detail, maxY, minY); 13780 const halfX = (detail) => xRange(detail) / 2; 13781 const halfY = (detail) => yRange(detail) / 2; 13782 const step = (detail, useMultiplier) => useMultiplier ? detail.stepSize * detail.speedMultiplier : detail.stepSize; 13783 const snap = (detail) => detail.snapToGrid; 13784 const snapStart = (detail) => detail.snapStart; 13785 const rounded = (detail) => detail.rounded; 13786 // Not great but... /shrug 13787 const hasEdge = (detail, edgeName) => detail[edgeName + '-edge'] !== undefined; 13788 const hasLEdge = (detail) => hasEdge(detail, l); 13789 const hasREdge = (detail) => hasEdge(detail, r); 13790 const hasTEdge = (detail) => hasEdge(detail, t); 13791 const hasBEdge = (detail) => hasEdge(detail, b); 13792 // Ew, any 13793 const currentValue = (detail) => detail.model.value.get(); 13794 13795 const xyValue = (x, y) => ({ 13796 x, 13797 y 13798 }); 13799 const fireSliderChange$3 = (component, value) => { 13800 emitWith(component, sliderChangeEvent(), { value }); 13801 }; 13802 // North West XY 13803 const setToTLEdgeXY = (edge, detail) => { 13804 fireSliderChange$3(edge, xyValue(min1X(detail), min1Y(detail))); 13805 }; 13806 // North 13807 const setToTEdge = (edge, detail) => { 13808 fireSliderChange$3(edge, min1Y(detail)); 13809 }; 13810 // North XY 13811 const setToTEdgeXY = (edge, detail) => { 13812 fireSliderChange$3(edge, xyValue(halfX(detail), min1Y(detail))); 13813 }; 13814 // North East XY 13815 const setToTREdgeXY = (edge, detail) => { 13816 fireSliderChange$3(edge, xyValue(max1X(detail), min1Y(detail))); 13817 }; 13818 // East 13819 const setToREdge = (edge, detail) => { 13820 fireSliderChange$3(edge, max1X(detail)); 13821 }; 13822 // East XY 13823 const setToREdgeXY = (edge, detail) => { 13824 fireSliderChange$3(edge, xyValue(max1X(detail), halfY(detail))); 13825 }; 13826 // South East XY 13827 const setToBREdgeXY = (edge, detail) => { 13828 fireSliderChange$3(edge, xyValue(max1X(detail), max1Y(detail))); 13829 }; 13830 // South 13831 const setToBEdge = (edge, detail) => { 13832 fireSliderChange$3(edge, max1Y(detail)); 13833 }; 13834 // South XY 13835 const setToBEdgeXY = (edge, detail) => { 13836 fireSliderChange$3(edge, xyValue(halfX(detail), max1Y(detail))); 13837 }; 13838 // South West XY 13839 const setToBLEdgeXY = (edge, detail) => { 13840 fireSliderChange$3(edge, xyValue(min1X(detail), max1Y(detail))); 13841 }; 13842 // West 13843 const setToLEdge = (edge, detail) => { 13844 fireSliderChange$3(edge, min1X(detail)); 13845 }; 13846 // West XY 13847 const setToLEdgeXY = (edge, detail) => { 13848 fireSliderChange$3(edge, xyValue(min1X(detail), halfY(detail))); 13849 }; 13850 13851 const reduceBy = (value, min, max, step) => { 13852 if (value < min) { 13853 return value; 13854 } 13855 else if (value > max) { 13856 return max; 13857 } 13858 else if (value === min) { 13859 return min - 1; 13860 } 13861 else { 13862 return Math.max(min, value - step); 13863 } 13864 }; 13865 const increaseBy = (value, min, max, step) => { 13866 if (value > max) { 13867 return value; 13868 } 13869 else if (value < min) { 13870 return min; 13871 } 13872 else if (value === max) { 13873 return max + 1; 13874 } 13875 else { 13876 return Math.min(max, value + step); 13877 } 13878 }; 13879 const capValue = (value, min, max) => Math.max(min, Math.min(max, value)); 13880 const snapValueOf = (value, min, max, step, snapStart) => 13881 // We are snapping by the step size. Therefore, find the nearest multiple of 13882 // the step 13883 snapStart.fold(() => { 13884 // There is no initial snapping start, so just go from the minimum 13885 const initValue = value - min; 13886 const extraValue = Math.round(initValue / step) * step; 13887 return capValue(min + extraValue, min - 1, max + 1); 13888 }, (start) => { 13889 // There is an initial snapping start, so using that as the starting point, 13890 // calculate the nearest snap position based on the value 13891 const remainder = (value - start) % step; 13892 const adjustment = Math.round(remainder / step); 13893 const rawSteps = Math.floor((value - start) / step); 13894 const maxSteps = Math.floor((max - start) / step); 13895 const numSteps = Math.min(maxSteps, rawSteps + adjustment); 13896 const r = start + (numSteps * step); 13897 return Math.max(start, r); 13898 }); 13899 const findOffsetOf = (value, min, max) => Math.min(max, Math.max(value, min)) - min; 13900 const findValueOf = (args) => { 13901 const { min, max, range, value, step, snap, snapStart, rounded, hasMinEdge, hasMaxEdge, minBound, maxBound, screenRange } = args; 13902 const capMin = hasMinEdge ? min - 1 : min; 13903 const capMax = hasMaxEdge ? max + 1 : max; 13904 if (value < minBound) { 13905 return capMin; 13906 } 13907 else if (value > maxBound) { 13908 return capMax; 13909 } 13910 else { 13911 const offset = findOffsetOf(value, minBound, maxBound); 13912 const newValue = capValue(((offset / screenRange) * range) + min, capMin, capMax); 13913 if (snap && newValue >= min && newValue <= max) { 13914 return snapValueOf(newValue, min, max, step, snapStart); 13915 } 13916 else if (rounded) { 13917 return Math.round(newValue); 13918 } 13919 else { 13920 return newValue; 13921 } 13922 } 13923 }; 13924 const findOffsetOfValue$2 = (args) => { 13925 const { min, max, range, value, hasMinEdge, hasMaxEdge, maxBound, maxOffset, centerMinEdge, centerMaxEdge } = args; 13926 if (value < min) { 13927 return hasMinEdge ? 0 : centerMinEdge; 13928 } 13929 else if (value > max) { 13930 return hasMaxEdge ? maxBound : centerMaxEdge; 13931 } 13932 else { 13933 // position along the slider 13934 return (value - min) / range * maxOffset; 13935 } 13936 }; 13937 13938 const top = 'top', right = 'right', bottom = 'bottom', left = 'left', width = 'width', height = 'height';
13939 // Screen offsets from bounding client rect 13940 const getBounds = (component) => component.element.dom.getBoundingClientRect(); 13941 const getBoundsProperty = (bounds, property) => bounds[property]; 13942 const getMinXBounds = (component) => { 13943 const bounds = getBounds(component); 13944 return getBoundsProperty(bounds, left); 13945 }; 13946 const getMaxXBounds = (component) => { 13947 const bounds = getBounds(component); 13948 return getBoundsProperty(bounds, right); 13949 }; 13950 const getMinYBounds = (component) => { 13951 const bounds = getBounds(component); 13952 return getBoundsProperty(bounds, top); 13953 }; 13954 const getMaxYBounds = (component) => { 13955 const bounds = getBounds(component); 13956 return getBoundsProperty(bounds, bottom); 13957 }; 13958 const getXScreenRange = (component) => { 13959 const bounds = getBounds(component); 13960 return getBoundsProperty(bounds, width); 13961 }; 13962 const getYScreenRange = (component) => { 13963 const bounds = getBounds(component); 13964 return getBoundsProperty(bounds, height); 13965 }; 13966 const getCenterOffsetOf = (componentMinEdge, componentMaxEdge, spectrumMinEdge) => (componentMinEdge + componentMaxEdge) / 2 - spectrumMinEdge; 13967 const getXCenterOffSetOf = (component, spectrum) => { 13968 const componentBounds = getBounds(component); 13969 const spectrumBounds = getBounds(spectrum); 13970 const componentMinEdge = getBoundsProperty(componentBounds, left); 13971 const componentMaxEdge = getBoundsProperty(componentBounds, right); 13972 const spectrumMinEdge = getBoundsProperty(spectrumBounds, left); 13973 return getCenterOffsetOf(componentMinEdge, componentMaxEdge, spectrumMinEdge); 13974 }; 13975 const getYCenterOffSetOf = (component, spectrum) => { 13976 const componentBounds = getBounds(component); 13977 const spectrumBounds = getBounds(spectrum); 13978 const componentMinEdge = getBoundsProperty(componentBounds, top); 13979 const componentMaxEdge = getBoundsProperty(componentBounds, bottom); 13980 const spectrumMinEdge = getBoundsProperty(spectrumBounds, top); 13981 return getCenterOffsetOf(componentMinEdge, componentMaxEdge, spectrumMinEdge); 13982 }; 13983 13984 // fire slider change event with x value 13985 const fireSliderChange$2 = (spectrum, value) => { 13986 emitWith(spectrum, sliderChangeEvent(), { value }); 13987 }; 13988 // find the value of the x offset of where the mouse was clicked from the model. 13989 const findValueOfOffset$1 = (spectrum, detail, left) => { 13990 const args = { 13991 min: minX(detail), 13992 max: maxX(detail), 13993 range: xRange(detail), 13994 value: left, 13995 step: step(detail), 13996 snap: snap(detail), 13997 snapStart: snapStart(detail), 13998 rounded: rounded(detail), 13999 hasMinEdge: hasLEdge(detail), 14000 hasMaxEdge: hasREdge(detail), 14001 minBound: getMinXBounds(spectrum), 14002 maxBound: getMaxXBounds(spectrum), 14003 screenRange: getXScreenRange(spectrum) 14004 }; 14005 return findValueOf(args); 14006 }; 14007 // find the value and fire a slider change event, returning the value 14008 const setValueFrom$2 = (spectrum, detail, value) => { 14009 const xValue = findValueOfOffset$1(spectrum, detail, value); 14010 const sliderVal = xValue; 14011 fireSliderChange$2(spectrum, sliderVal); 14012 return xValue; 14013 }; 14014 // fire a slider change event with the minimum value 14015 const setToMin$2 = (spectrum, detail) => { 14016 const min = minX(detail); 14017 fireSliderChange$2(spectrum, min); 14018 }; 14019 // fire a slider change event with the maximum value 14020 const setToMax$2 = (spectrum, detail) => { 14021 const max = maxX(detail); 14022 fireSliderChange$2(spectrum, max); 14023 }; 14024 // move in a direction by step size. Fire change at the end 14025 const moveBy$2 = (direction, spectrum, detail, useMultiplier) => { 14026 const f = (direction > 0) ? increaseBy : reduceBy; 14027 const xValue = f(currentValue(detail), minX(detail), maxX(detail), step(detail, useMultiplier)); 14028 fireSliderChange$2(spectrum, xValue); 14029 return Optional.some(xValue); 14030 }; 14031 const handleMovement$2 = (direction) => (spectrum, detail, useMultiplier) => moveBy$2(direction, spectrum, detail, useMultiplier).map(always); 14032 // get x offset from event 14033 const getValueFromEvent$2 = (simulatedEvent) => { 14034 const pos = getEventSource(simulatedEvent); 14035 return pos.map((p) => p.left); 14036 }; 14037 // find the x offset of a given value from the model 14038 const findOffsetOfValue$1 = (spectrum, detail, value, minEdge, maxEdge) => { 14039 const minOffset = 0; 14040 const maxOffset = getXScreenRange(spectrum); 14041 const centerMinEdge = minEdge.bind((edge) => Optional.some(getXCenterOffSetOf(edge, spectrum))).getOr(minOffset); 14042 const centerMaxEdge = maxEdge.bind((edge) => Optional.some(getXCenterOffSetOf(edge, spectrum))).getOr(maxOffset); 14043 const args = { 14044 min: minX(detail), 14045 max: maxX(detail), 14046 range: xRange(detail), 14047 value, 14048 hasMinEdge: hasLEdge(detail), 14049 hasMaxEdge: hasREdge(detail), 14050 minBound: getMinXBounds(spectrum), 14051 minOffset, 14052 maxBound: getMaxXBounds(spectrum), 14053 maxOffset, 14054 centerMinEdge, 14055 centerMaxEdge 14056 }; 14057 return findOffsetOfValue$2(args); 14058 }; 14059 // find left offset for absolute positioning from a given value 14060 const findPositionOfValue$1 = (slider, spectrum, value, minEdge, maxEdge, detail) => { 14061 const offset = findOffsetOfValue$1(spectrum, detail, value, minEdge, maxEdge); 14062 return (getMinXBounds(spectrum) - getMinXBounds(slider)) + offset; 14063 }; 14064 // update the position of the thumb from the slider's current value 14065 const setPositionFromValue$2 = (slider, thumb, detail, edges) => { 14066 const value = currentValue(detail); 14067 const pos = findPositionOfValue$1(slider, edges.getSpectrum(slider), value, edges.getLeftEdge(slider), edges.getRightEdge(slider), detail); 14068 const thumbRadius = get$c(thumb.element) / 2; 14069 set$7(thumb.element, 'left', (pos - thumbRadius) + 'px'); 14070 }; 14071 // Key Events 14072 const onLeft$2 = handleMovement$2(-1); 14073 const onRight$2 = handleMovement$2(1); 14074 const onUp$2 = Optional.none; 14075 const onDown$2 = Optional.none; 14076 // Edge Click Actions 14077 const edgeActions$2 = { 14078 'top-left': Optional.none(), 14079 'top': Optional.none(), 14080 'top-right': Optional.none(), 14081 'right': Optional.some(setToREdge),
14082 'bottom-right': Optional.none(), 14083 'bottom': Optional.none(), 14084 'bottom-left': Optional.none(), 14085 'left': Optional.some(setToLEdge) 14086 }; 14087 14088 var HorizontalModel = /*#__PURE__*/Object.freeze({ 14089 __proto__: null, 14090 setValueFrom: setValueFrom$2, 14091 setToMin: setToMin$2, 14092 setToMax: setToMax$2, 14093 findValueOfOffset: findValueOfOffset$1, 14094 getValueFromEvent: getValueFromEvent$2, 14095 findPositionOfValue: findPositionOfValue$1, 14096 setPositionFromValue: setPositionFromValue$2, 14097 onLeft: onLeft$2, 14098 onRight: onRight$2, 14099 onUp: onUp$2, 14100 onDown: onDown$2, 14101 edgeActions: edgeActions$2 14102 }); 14103 14104 // fire slider change event with y value 14105 const fireSliderChange$1 = (spectrum, value) => { 14106 emitWith(spectrum, sliderChangeEvent(), { value }); 14107 }; 14108 // find the value of the y offset of where the mouse was clicked from the model. 14109 const findValueOfOffset = (spectrum, detail, top) => { 14110 const args = { 14111 min: minY(detail), 14112 max: maxY(detail), 14113 range: yRange(detail), 14114 value: top, 14115 step: step(detail), 14116 snap: snap(detail), 14117 snapStart: snapStart(detail), 14118 rounded: rounded(detail), 14119 hasMinEdge: hasTEdge(detail), 14120 hasMaxEdge: hasBEdge(detail), 14121 minBound: getMinYBounds(spectrum), 14122 maxBound: getMaxYBounds(spectrum), 14123 screenRange: getYScreenRange(spectrum) 14124 }; 14125 return findValueOf(args); 14126 }; 14127 // find the value and fire a slider change event, returning the value 14128 const setValueFrom$1 = (spectrum, detail, value) => { 14129 const yValue = findValueOfOffset(spectrum, detail, value); 14130 const sliderVal = yValue; 14131 fireSliderChange$1(spectrum, sliderVal); 14132 return yValue; 14133 }; 14134 // fire a slider change event with the minimum value 14135 const setToMin$1 = (spectrum, detail) => { 14136 const min = minY(detail); 14137 fireSliderChange$1(spectrum, min); 14138 }; 14139 // fire a slider change event with the maximum value 14140 const setToMax$1 = (spectrum, detail) => { 14141 const max = maxY(detail); 14142 fireSliderChange$1(spectrum, max); 14143 }; 14144 // move in a direction by step size. Fire change at the end 14145 const moveBy$1 = (direction, spectrum, detail, useMultiplier) => { 14146 const f = (direction > 0) ? increaseBy : reduceBy; 14147 const yValue = f(currentValue(detail), minY(detail), maxY(detail), step(detail, useMultiplier)); 14148 fireSliderChange$1(spectrum, yValue); 14149 return Optional.some(yValue); 14150 }; 14151 const handleMovement$1 = (direction) => (spectrum, detail, useMultiplier) => moveBy$1(direction, spectrum, detail, useMultiplier).map(always); 14152 // get y offset from event 14153 const getValueFromEvent$1 = (simulatedEvent) => { 14154 const pos = getEventSource(simulatedEvent); 14155 return pos.map((p) => { 14156 return p.top; 14157 }); 14158 }; 14159 // find the y offset of a given value from the model 14160 const findOffsetOfValue = (spectrum, detail, value, minEdge, maxEdge) => { 14161 const minOffset = 0; 14162 const maxOffset = getYScreenRange(spectrum); 14163 const centerMinEdge = minEdge.bind((edge) => Optional.some(getYCenterOffSetOf(edge, spectrum))).getOr(minOffset); 14164 const centerMaxEdge = maxEdge.bind((edge) => Optional.some(getYCenterOffSetOf(edge, spectrum))).getOr(maxOffset); 14165 const args = { 14166 min: minY(detail), 14167 max: maxY(detail), 14168 range: yRange(detail), 14169 value, 14170 hasMinEdge: hasTEdge(detail), 14171 hasMaxEdge: hasBEdge(detail), 14172 minBound: getMinYBounds(spectrum), 14173 minOffset, 14174 maxBound: getMaxYBounds(spectrum), 14175 maxOffset, 14176 centerMinEdge, 14177 centerMaxEdge 14178 }; 14179 return findOffsetOfValue$2(args); 14180 }; 14181 // find left offset for absolute positioning from a given value 14182 const findPositionOfValue = (slider, spectrum, value, minEdge, maxEdge, detail) => { 14183 const offset = findOffsetOfValue(spectrum, detail, value, minEdge, maxEdge); 14184 return (getMinYBounds(spectrum) - getMinYBounds(slider)) + offset; 14185 }; 14186 // update the position of the thumb from the slider's current value 14187 const setPositionFromValue$1 = (slider, thumb, detail, edges) => { 14188 const value = currentValue(detail); 14189 const pos = findPositionOfValue(slider, edges.getSpectrum(slider), value, edges.getTopEdge(slider), edges.getBottomEdge(slider), detail); 14190 const thumbRadius = get$d(thumb.element) / 2; 14191 set$7(thumb.element, 'top', (pos - thumbRadius) + 'px'); 14192 }; 14193 // Key Events 14194 const onLeft$1 = Optional.none; 14195 const onRight$1 = Optional.none; 14196 const onUp$1 = handleMovement$1(-1); 14197 const onDown$1 = handleMovement$1(1); 14198 // Edge Click Actions 14199 const edgeActions$1 = { 14200 'top-left': Optional.none(), 14201 'top': Optional.some(setToTEdge), 14202 'top-right': Optional.none(), 14203 'right': Optional.none(), 14204 'bottom-right': Optional.none(), 14205 'bottom': Optional.some(setToBEdge),
vendor: 4,234 bytes, lines 14206-14297
14206 'bottom-left': Optional.none(), 14207 'left': Optional.none() 14208 }; 14209 14210 var VerticalModel = /*#__PURE__*/Object.freeze({ 14211 __proto__: null, 14212 setValueFrom: setValueFrom$1, 14213 setToMin: setToMin$1, 14214 setToMax: setToMax$1, 14215 findValueOfOffset: findValueOfOffset, 14216 getValueFromEvent: getValueFromEvent$1, 14217 findPositionOfValue: findPositionOfValue, 14218 setPositionFromValue: setPositionFromValue$1, 14219 onLeft: onLeft$1, 14220 onRight: onRight$1, 14221 onUp: onUp$1, 14222 onDown: onDown$1, 14223 edgeActions: edgeActions$1 14224 }); 14225 14226 // fire slider change event with xy value 14227 const fireSliderChange = (spectrum, value) => { 14228 emitWith(spectrum, sliderChangeEvent(), { value }); 14229 }; 14230 const sliderValue = (x, y) => ({ 14231 x, 14232 y 14233 }); 14234 // find both values of x and y offsets of where the mouse was clicked from the model. 14235 // then fire a slider change event with those values, returning the values 14236 const setValueFrom = (spectrum, detail, value) => { 14237 const xValue = findValueOfOffset$1(spectrum, detail, value.left); 14238 const yValue = findValueOfOffset(spectrum, detail, value.top); 14239 const val = sliderValue(xValue, yValue); 14240 fireSliderChange(spectrum, val); 14241 return val; 14242 }; 14243 // move in a direction by step size. Fire change at the end 14244 const moveBy = (direction, isVerticalMovement, spectrum, detail, useMultiplier) => { 14245 const f = (direction > 0) ? increaseBy : reduceBy; 14246 const xValue = isVerticalMovement ? currentValue(detail).x : 14247 f(currentValue(detail).x, minX(detail), maxX(detail), step(detail, useMultiplier)); 14248 const yValue = !isVerticalMovement ? currentValue(detail).y : 14249 f(currentValue(detail).y, minY(detail), maxY(detail), step(detail, useMultiplier)); 14250 fireSliderChange(spectrum, sliderValue(xValue, yValue)); 14251 return Optional.some(xValue); 14252 }; 14253 const handleMovement = (direction, isVerticalMovement) => (spectrum, detail, useMultiplier) => moveBy(direction, isVerticalMovement, spectrum, detail, useMultiplier).map(always); 14254 // fire a slider change event with the minimum value 14255 const setToMin = (spectrum, detail) => { 14256 const mX = minX(detail); 14257 const mY = minY(detail); 14258 fireSliderChange(spectrum, sliderValue(mX, mY)); 14259 }; 14260 // fire a slider change event with the maximum value 14261 const setToMax = (spectrum, detail) => { 14262 const mX = maxX(detail); 14263 const mY = maxY(detail); 14264 fireSliderChange(spectrum, sliderValue(mX, mY)); 14265 }; 14266 // get event data as a SugarPosition 14267 const getValueFromEvent = (simulatedEvent) => getEventSource(simulatedEvent); 14268 // update the position of the thumb from the slider's current value 14269 const setPositionFromValue = (slider, thumb, detail, edges) => { 14270 const value = currentValue(detail); 14271 const xPos = findPositionOfValue$1(slider, edges.getSpectrum(slider), value.x, edges.getLeftEdge(slider), edges.getRightEdge(slider), detail); 14272 const yPos = findPositionOfValue(slider, edges.getSpectrum(slider), value.y, edges.getTopEdge(slider), edges.getBottomEdge(slider), detail); 14273 const thumbXRadius = get$c(thumb.element) / 2; 14274 const thumbYRadius = get$d(thumb.element) / 2; 14275 set$7(thumb.element, 'left', (xPos - thumbXRadius) + 'px'); 14276 set$7(thumb.element, 'top', (yPos - thumbYRadius) + 'px'); 14277 }; 14278 // Key Events 14279 const onLeft = handleMovement(-1, false); 14280 const onRight = handleMovement(1, false); 14281 const onUp = handleMovement(-1, true); 14282 const onDown = handleMovement(1, true); 14283 // Edge Click Actions 14284 const edgeActions = { 14285 'top-left': Optional.some(setToTLEdgeXY), 14286 'top': Optional.some(setToTEdgeXY), 14287 'top-right': Optional.some(setToTREdgeXY), 14288 'right': Optional.some(setToREdgeXY), 14289 'bottom-right': Optional.some(setToBREdgeXY), 14290 'bottom': Optional.some(setToBEdgeXY), 14291 'bottom-left': Optional.some(setToBLEdgeXY), 14292 'left': Optional.some(setToLEdgeXY) 14293 }; 14294 14295 var TwoDModel = /*#__PURE__*/Object.freeze({ 14296 __proto__: null, 14297 setValueFrom: setValueFrom,
14298 setToMin: setToMin, 14299 setToMax: setToMax, 14300 getValueFromEvent: getValueFromEvent, 14301 setPositionFromValue: setPositionFromValue, 14302 onLeft: onLeft, 14303 onRight: onRight, 14304 onUp: onUp, 14305 onDown: onDown, 14306 edgeActions: edgeActions 14307 }); 14308 14309 const SliderSchema = [ 14310 defaulted('stepSize', 1), 14311 defaulted('speedMultiplier', 10), 14312 defaulted('onChange', noop), 14313 defaulted('onChoose', noop), 14314 defaulted('onInit', noop), 14315 defaulted('onDragStart', noop), 14316 defaulted('onDragEnd', noop), 14317 defaulted('snapToGrid', false), 14318 defaulted('rounded', true), 14319 option$3('snapStart'), 14320 requiredOf('model', choose$1('mode', { 14321 x: [ 14322 defaulted('minX', 0), 14323 defaulted('maxX', 100), 14324 customField('value', (spec) => Cell(spec.mode.minX)), 14325 required$1('getInitialValue'), 14326 output$1('manager', HorizontalModel) 14327 ], 14328 y: [ 14329 defaulted('minY', 0), 14330 defaulted('maxY', 100), 14331 customField('value', (spec) => Cell(spec.mode.minY)), 14332 required$1('getInitialValue'), 14333 output$1('manager', VerticalModel) 14334 ], 14335 xy: [ 14336 defaulted('minX', 0), 14337 defaulted('maxX', 100), 14338 defaulted('minY', 0), 14339 defaulted('maxY', 100), 14340 customField('value', (spec) => Cell({ 14341 x: spec.mode.minX, 14342 y: spec.mode.minY 14343 })), 14344 required$1('getInitialValue'), 14345 output$1('manager', TwoDModel) 14346 ] 14347 })), 14348 field('sliderBehaviours', [Keying, Representing]), 14349 customField('mouseIsDown', () => Cell(false)) 14350 ]; 14351 14352 const sketch$1 = (detail, components, _spec, _externals) => { 14353 const getThumb = (component) => getPartOrDie(component, detail, 'thumb'); 14354 const getSpectrum = (component) => getPartOrDie(component, detail, 'spectrum'); 14355 const getLeftEdge = (component) => getPart(component, detail, 'left-edge'); 14356 const getRightEdge = (component) => getPart(component, detail, 'right-edge'); 14357 const getTopEdge = (component) => getPart(component, detail, 'top-edge'); 14358 const getBottomEdge = (component) => getPart(component, detail, 'bottom-edge'); 14359 const modelDetail = detail.model; 14360 const model = modelDetail.manager; 14361 const refresh = (slider, thumb) => { 14362 model.setPositionFromValue(slider, thumb, detail, { 14363 getLeftEdge, 14364 getRightEdge, 14365 getTopEdge, 14366 getBottomEdge, 14367 getSpectrum 14368 }); 14369 }; 14370 const setValue = (slider, newValue) => { 14371 modelDetail.value.set(newValue); 14372 const thumb = getThumb(slider); 14373 refresh(slider, thumb); 14374 }; 14375 const changeValue = (slider, newValue) => { 14376 setValue(slider, newValue); 14377 const thumb = getThumb(slider); 14378 detail.onChange(slider, thumb, newValue); 14379 return Optional.some(true); 14380 }; 14381 const resetToMin = (slider) => { 14382 model.setToMin(slider, detail); 14383 }; 14384 const resetToMax = (slider) => { 14385 model.setToMax(slider, detail); 14386 }; 14387 const choose = (slider) => { 14388 const fireOnChoose = () => { 14389 getPart(slider, detail, 'thumb').each((thumb) => { 14390 const value = modelDetail.value.get(); 14391 detail.onChoose(slider, thumb, value); 14392 }); 14393 }; 14394 const wasDown = detail.mouseIsDown.get(); 14395 detail.mouseIsDown.set(false); 14396 // We don't want this to fire if the mouse wasn't pressed down over anything other than the slider. 14397 if (wasDown) { 14398 fireOnChoose(); 14399 } 14400 }; 14401 const onDragStart = (slider, simulatedEvent) => { 14402 simulatedEvent.stop(); 14403 detail.mouseIsDown.set(true);
vendor: 17,979 bytes, lines 14404-14881
14404 detail.onDragStart(slider, getThumb(slider)); 14405 }; 14406 const onDragEnd = (slider, simulatedEvent) => { 14407 simulatedEvent.stop(); 14408 detail.onDragEnd(slider, getThumb(slider)); 14409 choose(slider); 14410 }; 14411 const focusWidget = (component) => { 14412 getPart(component, detail, 'spectrum').map(Keying.focusIn); 14413 }; 14414 return { 14415 uid: detail.uid, 14416 dom: detail.dom, 14417 components, 14418 behaviours: augment(detail.sliderBehaviours, [ 14419 Keying.config({ 14420 mode: 'special', 14421 focusIn: focusWidget 14422 }), 14423 Representing.config({ 14424 store: { 14425 mode: 'manual', 14426 getValue: (_) => { 14427 return modelDetail.value.get(); 14428 }, 14429 setValue 14430 } 14431 }), 14432 Receiving.config({ 14433 channels: { 14434 [mouseReleased()]: { 14435 onReceive: choose 14436 } 14437 } 14438 }) 14439 ]), 14440 events: derive$2([ 14441 run$1(sliderChangeEvent(), (slider, simulatedEvent) => { 14442 changeValue(slider, simulatedEvent.event.value); 14443 }), 14444 runOnAttached((slider, _simulatedEvent) => { 14445 // Set the initial value 14446 const getInitial = modelDetail.getInitialValue(); 14447 modelDetail.value.set(getInitial); 14448 const thumb = getThumb(slider); 14449 refresh(slider, thumb); 14450 const spectrum = getSpectrum(slider); 14451 // Call onInit instead of onChange for the first value. 14452 detail.onInit(slider, thumb, spectrum, modelDetail.value.get()); 14453 }), 14454 run$1(touchstart(), onDragStart), 14455 run$1(touchend(), onDragEnd), 14456 run$1(mousedown(), (component, event) => { 14457 focusWidget(component); 14458 onDragStart(component, event); 14459 }), 14460 run$1(mouseup(), onDragEnd), 14461 ]), 14462 apis: { 14463 resetToMin, 14464 resetToMax, 14465 setValue, 14466 refresh 14467 }, 14468 domModification: { 14469 styles: { 14470 position: 'relative' 14471 } 14472 } 14473 }; 14474 }; 14475 14476 const Slider = composite({ 14477 name: 'Slider', 14478 configFields: SliderSchema, 14479 partFields: SliderParts, 14480 factory: sketch$1, 14481 apis: { 14482 setValue: (apis, slider, value) => { 14483 apis.setValue(slider, value); 14484 }, 14485 resetToMin: (apis, slider) => { 14486 apis.resetToMin(slider); 14487 }, 14488 resetToMax: (apis, slider) => { 14489 apis.resetToMax(slider); 14490 }, 14491 refresh: (apis, slider) => { 14492 apis.refresh(slider); 14493 } 14494 } 14495 }); 14496 14497 const owner = 'container'; 14498 const schema$7 = [ 14499 field('slotBehaviours', []) 14500 ]; 14501 const getPartName = (name) => '<alloy.field.' + name + '>'; 14502 const sketch = (sSpec) => { 14503 // As parts.slot is called, record all of the parts that are registered 14504 // as part of this SlotContainer. 14505 const parts = (() => { 14506 const record = []; 14507 const slot = (name, config) => { 14508 record.push(name); 14509 return generateOne$1(owner, getPartName(name), config); 14510 }; 14511 return { 14512 slot, 14513 record: constant$1(record) 14514 }; 14515 })(); 14516 const spec = sSpec(parts); 14517 const partNames = parts.record(); 14518 // Like a Form, a SlotContainer does not know its parts in advance. So the 14519 // record lists the names of the parts to put in the schema. 14520 // TODO: Find a nice way to remove dupe with Form 14521 const fieldParts = map$2(partNames, (n) => required({ name: n, pname: getPartName(n) })); 14522 return composite$1(owner, schema$7, fieldParts, make$3, spec); 14523 }; 14524 const make$3 = (detail, components) => { 14525 const getSlotNames = (_) => getAllPartNames(detail); 14526 const getSlot = (container, key) => getPart(container, detail, key); 14527 const onSlot = (f, def) => (container, key) => getPart(container, detail, key).map((slot) => f(slot, key)).getOr(def); 14528 const onSlots = (f) => (container, keys) => { 14529 each$1(keys, (key) => f(container, key)); 14530 }; 14531 const doShowing = (comp, _key) => get$g(comp.element, 'aria-hidden') !== 'true'; 14532 const doShow = (comp, key) => { 14533 // NOTE: May need to restore old values. 14534 if (!doShowing(comp)) { 14535 const element = comp.element; 14536 remove$6(element, 'display'); 14537 remove$8(element, 'aria-hidden'); 14538 emitWith(comp, slotVisibility(), { name: key, visible: true }); 14539 } 14540 }; 14541 const doHide = (comp, key) => { 14542 // NOTE: May need to save old values. 14543 if (doShowing(comp)) { 14544 const element = comp.element; 14545 set$7(element, 'display', 'none'); 14546 set$9(element, 'aria-hidden', 'true'); 14547 emitWith(comp, slotVisibility(), { name: key, visible: false }); 14548 } 14549 }; 14550 const isShowing = onSlot(doShowing, false); 14551 const hideSlot = onSlot(doHide); 14552 const hideSlots = onSlots(hideSlot); 14553 const hideAllSlots = (container) => hideSlots(container, getSlotNames()); 14554 const showSlot = onSlot(doShow); 14555 const apis = { 14556 getSlotNames, 14557 getSlot, 14558 isShowing, 14559 hideSlot, 14560 hideAllSlots, 14561 showSlot 14562 }; 14563 return { 14564 uid: detail.uid, 14565 dom: detail.dom, 14566 components, 14567 behaviours: get$2(detail.slotBehaviours), 14568 apis 14569 }; 14570 }; 14571 // No type safety doing it this way. But removes dupe. 14572 // We could probably use spread operator to help here. 14573 const slotApis = map$1({ 14574 getSlotNames: (apis, c) => apis.getSlotNames(c), 14575 getSlot: (apis, c, key) => apis.getSlot(c, key), 14576 isShowing: (apis, c, key) => apis.isShowing(c, key), 14577 hideSlot: (apis, c, key) => apis.hideSlot(c, key), 14578 hideAllSlots: (apis, c) => apis.hideAllSlots(c), 14579 showSlot: (apis, c, key) => apis.showSlot(c, key) 14580 }, (value) => makeApi(value)); 14581 const SlotContainer = { 14582 ...slotApis, 14583 ...{ sketch } 14584 }; 14585 14586 const generate$1 = (xs, f) => { 14587 const init = { 14588 len: 0, 14589 list: [] 14590 }; 14591 const r = foldl(xs, (b, a) => { 14592 const value = f(a, b.len); 14593 return value.fold(constant$1(b), (v) => ({ 14594 len: v.finish, 14595 list: b.list.concat([v]) 14596 })); 14597 }, init); 14598 return r.list; 14599 }; 14600 14601 const output = (within, extra, withinWidth) => ({ 14602 within, 14603 extra, 14604 withinWidth 14605 }); 14606 const apportion = (units, total, len) => { 14607 const parray = generate$1(units, (unit, current) => { 14608 const width = len(unit); 14609 return Optional.some({ 14610 element: unit, 14611 start: current, 14612 finish: current + width, 14613 width 14614 }); 14615 }); 14616 const within = filter$2(parray, (unit) => unit.finish <= total); 14617 const withinWidth = foldr(within, (acc, el) => acc + el.width, 0); 14618 const extra = parray.slice(within.length); 14619 return { 14620 within, 14621 extra, 14622 withinWidth 14623 }; 14624 }; 14625 const toUnit = (parray) => map$2(parray, (unit) => unit.element); 14626 const fitLast = (within, extra, withinWidth) => { 14627 const fits = toUnit(within.concat(extra)); 14628 return output(fits, [], withinWidth); 14629 }; 14630 const overflow = (within, extra, overflower, withinWidth) => { 14631 const fits = toUnit(within).concat([overflower]); 14632 return output(fits, toUnit(extra), withinWidth); 14633 }; 14634 const fitAll = (within, extra, withinWidth) => output(toUnit(within), [], withinWidth); 14635 const tryFit = (total, units, len) => { 14636 const divide = apportion(units, total, len); 14637 return divide.extra.length === 0 ? Optional.some(divide) : Optional.none(); 14638 }; 14639 const partition = (total, units, len, overflower) => { 14640 // Firstly, we try without the overflower. 14641 const divide = tryFit(total, units, len).getOrThunk(() => 14642 // If that doesn't work, overflow 14643 apportion(units, total - len(overflower), len)); 14644 const within = divide.within; 14645 const extra = divide.extra; 14646 const withinWidth = divide.withinWidth; 14647 if (extra.length === 1 && extra[0].width <= len(overflower)) { 14648 return fitLast(within, extra, withinWidth); 14649 } 14650 else if (extra.length >= 1) { 14651 return overflow(within, extra, overflower, withinWidth); 14652 } 14653 else { 14654 return fitAll(within, extra, withinWidth); 14655 } 14656 }; 14657 14658 const setGroups = (toolbar, storedGroups) => { 14659 const bGroups = map$2(storedGroups, (g) => premade(g)); 14660 Toolbar.setGroups(toolbar, bGroups); 14661 }; 14662 const findFocusedComp = (comps) => findMap(comps, (comp) => search(comp.element).bind((focusedElm) => comp.getSystem().getByDom(focusedElm).toOptional())); 14663 const refresh$2 = (toolbar, detail, setOverflow) => { 14664 // Ensure we have toolbar groups to render 14665 const builtGroups = detail.builtGroups.get(); 14666 if (builtGroups.length === 0) { 14667 return; 14668 } 14669 const primary = getPartOrDie(toolbar, detail, 'primary'); 14670 const overflowGroup = Coupling.getCoupled(toolbar, 'overflowGroup'); 14671 // Set the primary toolbar to have visibility hidden; 14672 set$7(primary.element, 'visibility', 'hidden'); 14673 const groups = builtGroups.concat([overflowGroup]); 14674 // Store the current focus state 14675 const focusedComp = findFocusedComp(groups); 14676 // Clear the overflow toolbar 14677 setOverflow([]); 14678 // Put all the groups inside the primary toolbar 14679 setGroups(primary, groups); 14680 const availableWidth = get$c(primary.element); 14681 const overflows = partition(availableWidth, detail.builtGroups.get(), (comp) => Math.ceil(comp.element.dom.getBoundingClientRect().width), overflowGroup); 14682 if (overflows.extra.length === 0) { 14683 // Not ideal. Breaking abstraction somewhat, though remove is better than insert 14684 // Can just reset the toolbar groups also ... but may be a bit slower. 14685 Replacing.remove(primary, overflowGroup); 14686 setOverflow([]); 14687 } 14688 else { 14689 setGroups(primary, overflows.within); 14690 setOverflow(overflows.extra); 14691 } 14692 remove$6(primary.element, 'visibility'); 14693 reflow(primary.element); 14694 // Restore the focus 14695 focusedComp.each(Focusing.focus); 14696 }; 14697 14698 const schema$6 = constant$1([ 14699 field('splitToolbarBehaviours', [Coupling]), 14700 customField('builtGroups', () => Cell([])) 14701 ]); 14702 14703 const schema$5 = constant$1([ 14704 markers$1(['overflowToggledClass']), 14705 optionFunction('getOverflowBounds'), 14706 required$1('lazySink'), 14707 customField('overflowGroups', () => Cell([])), 14708 onHandler('onOpened'), 14709 onHandler('onClosed') 14710 ].concat(schema$6())); 14711 const parts$5 = constant$1([ 14712 required({ 14713 factory: Toolbar, 14714 schema: schema$d(), 14715 name: 'primary' 14716 }), 14717 external$1({ 14718 schema: schema$d(), 14719 name: 'overflow' 14720 }), 14721 external$1({ 14722 name: 'overflow-button' 14723 }), 14724 external$1({ 14725 name: 'overflow-group' 14726 }) 14727 ]); 14728 14729 const schema$4 = constant$1([ 14730 required$1('items'), 14731 markers$1(['itemSelector']), 14732 field('tgroupBehaviours', [Keying]) 14733 ]); 14734 const parts$4 = constant$1([ 14735 group({ 14736 name: 'items', 14737 unit: 'item' 14738 }) 14739 ]); 14740 14741 const factory$b = (detail, components, _spec, _externals) => ({ 14742 uid: detail.uid, 14743 dom: detail.dom, 14744 components, 14745 behaviours: augment(detail.tgroupBehaviours, [ 14746 Keying.config({ 14747 mode: 'flow', 14748 selector: detail.markers.itemSelector 14749 }) 14750 ]), 14751 domModification: { 14752 attributes: { 14753 role: 'group' 14754 } 14755 } 14756 }); 14757 const ToolbarGroup = composite({ 14758 name: 'ToolbarGroup', 14759 configFields: schema$4(), 14760 partFields: parts$4(), 14761 factory: factory$b 14762 }); 14763 14764 const buildGroups = (comps) => map$2(comps, (g) => premade(g)); 14765 const refresh$1 = (toolbar, memFloatingToolbarButton, detail) => { 14766 refresh$2(toolbar, detail, (overflowGroups) => { 14767 detail.overflowGroups.set(overflowGroups); 14768 memFloatingToolbarButton.getOpt(toolbar).each((floatingToolbarButton) => { 14769 FloatingToolbarButton.setGroups(floatingToolbarButton, buildGroups(overflowGroups)); 14770 }); 14771 }); 14772 }; 14773 const factory$a = (detail, components, spec, externals) => { 14774 const memFloatingToolbarButton = record(FloatingToolbarButton.sketch({ 14775 fetch: () => Future.nu((resolve) => { 14776 resolve(buildGroups(detail.overflowGroups.get())); 14777 }), 14778 layouts: { 14779 onLtr: () => [southwest$2, southeast$2], 14780 onRtl: () => [southeast$2, southwest$2], 14781 onBottomLtr: () => [northwest$2, northeast$2], 14782 onBottomRtl: () => [northeast$2, northwest$2] 14783 }, 14784 getBounds: spec.getOverflowBounds, 14785 lazySink: detail.lazySink, 14786 fireDismissalEventInstead: {}, 14787 markers: { 14788 toggledClass: detail.markers.overflowToggledClass 14789 }, 14790 parts: { 14791 button: externals['overflow-button'](), 14792 toolbar: externals.overflow() 14793 }, 14794 onToggled: (comp, state) => detail[state ? 'onOpened' : 'onClosed'](comp) 14795 })); 14796 return { 14797 uid: detail.uid, 14798 dom: detail.dom, 14799 components, 14800 behaviours: augment(detail.splitToolbarBehaviours, [ 14801 Coupling.config({ 14802 others: { 14803 overflowGroup: () => { 14804 return ToolbarGroup.sketch({ 14805 ...externals['overflow-group'](), 14806 items: [ 14807 memFloatingToolbarButton.asSpec() 14808 ] 14809 }); 14810 } 14811 } 14812 }) 14813 ]), 14814 apis: { 14815 setGroups: (toolbar, groups) => { 14816 detail.builtGroups.set(map$2(groups, toolbar.getSystem().build)); 14817 refresh$1(toolbar, memFloatingToolbarButton, detail); 14818 }, 14819 refresh: (toolbar) => refresh$1(toolbar, memFloatingToolbarButton, detail), 14820 toggle: (toolbar) => { 14821 memFloatingToolbarButton.getOpt(toolbar).each((floatingToolbarButton) => { 14822 FloatingToolbarButton.toggle(floatingToolbarButton); 14823 }); 14824 }, 14825 toggleWithoutFocusing: (toolbar) => { 14826 memFloatingToolbarButton.getOpt(toolbar).each(FloatingToolbarButton.toggleWithoutFocusing); 14827 }, 14828 isOpen: (toolbar) => memFloatingToolbarButton.getOpt(toolbar).map(FloatingToolbarButton.isOpen).getOr(false), 14829 reposition: (toolbar) => { 14830 memFloatingToolbarButton.getOpt(toolbar).each((floatingToolbarButton) => { 14831 FloatingToolbarButton.reposition(floatingToolbarButton); 14832 }); 14833 }, 14834 getOverflow: (toolbar) => memFloatingToolbarButton.getOpt(toolbar).bind(FloatingToolbarButton.getToolbar) 14835 }, 14836 domModification: { 14837 attributes: { role: 'group' } 14838 } 14839 }; 14840 }; 14841 const SplitFloatingToolbar = composite({ 14842 name: 'SplitFloatingToolbar', 14843 configFields: schema$5(), 14844 partFields: parts$5(), 14845 factory: factory$a, 14846 apis: { 14847 setGroups: (apis, toolbar, groups) => { 14848 apis.setGroups(toolbar, groups); 14849 }, 14850 refresh: (apis, toolbar) => { 14851 apis.refresh(toolbar); 14852 }, 14853 reposition: (apis, toolbar) => { 14854 apis.reposition(toolbar); 14855 }, 14856 toggle: (apis, toolbar) => { 14857 apis.toggle(toolbar); 14858 }, 14859 toggleWithoutFocusing: (apis, toolbar) => { 14860 apis.toggle(toolbar); 14861 }, 14862 isOpen: (apis, toolbar) => apis.isOpen(toolbar), 14863 getOverflow: (apis, toolbar) => apis.getOverflow(toolbar) 14864 } 14865 }); 14866 14867 const schema$3 = constant$1([ 14868 markers$1(['closedClass', 'openClass', 'shrinkingClass', 'growingClass', 'overflowToggledClass']), 14869 onHandler('onOpened'), 14870 onHandler('onClosed') 14871 ].concat(schema$6())); 14872 const parts$3 = constant$1([ 14873 required({ 14874 factory: Toolbar, 14875 schema: schema$d(), 14876 name: 'primary' 14877 }), 14878 required({ 14879 factory: Toolbar, 14880 schema: schema$d(), 14881 name: 'overflow',
14882 overrides: (detail) => { 14883 return { 14884 toolbarBehaviours: derive$1([ 14885 Sliding.config({ 14886 dimension: { 14887 property: 'height' 14888 }, 14889 closedClass: detail.markers.closedClass, 14890 openClass: detail.markers.openClass, 14891 shrinkingClass: detail.markers.shrinkingClass, 14892 growingClass: detail.markers.growingClass, 14893 onShrunk: (comp) => { 14894 getPart(comp, detail, 'overflow-button').each((button) => { 14895 Toggling.off(button); 14896 }); 14897 detail.onClosed(comp); 14898 }, 14899 onGrown: (comp) => { 14900 detail.onOpened(comp); 14901 }, 14902 onStartGrow: (comp) => { 14903 getPart(comp, detail, 'overflow-button').each(Toggling.on); 14904 } 14905 }), 14906 Keying.config({ 14907 mode: 'acyclic', 14908 onEscape: (comp) => { 14909 getPart(comp, detail, 'overflow-button').each(Focusing.focus); 14910 return Optional.some(true); 14911 } 14912 }) 14913 ]) 14914 }; 14915 } 14916 }), 14917 external$1({ 14918 name: 'overflow-button', 14919 overrides: (detail) => ({ 14920 buttonBehaviours: derive$1([ 14921 Toggling.config({ 14922 toggleClass: detail.markers.overflowToggledClass, 14923 aria: { 14924 mode: 'expanded' 14925 }, 14926 toggleOnExecute: false 14927 }) 14928 ]) 14929 }) 14930 }), 14931 external$1({ 14932 name: 'overflow-group' 14933 }) 14934 ]); 14935 14936 const isOpen = (toolbar, detail) => getPart(toolbar, detail, 'overflow').map(Sliding.hasGrown).getOr(false); 14937 const toggleToolbar = (toolbar, detail, skipFocus) => { 14938 // Make sure that the toolbar needs to toggled by checking for overflow button presence 14939 getPart(toolbar, detail, 'overflow-button') 14940 .each((oveflowButton) => { 14941 getPart(toolbar, detail, 'overflow').each((overf) => { 14942 refresh(toolbar, detail); 14943 if (Sliding.hasShrunk(overf)) { 14944 const fn = detail.onOpened; 14945 detail.onOpened = (comp) => { 14946 if (!skipFocus) { 14947 Keying.focusIn(overf); 14948 } 14949 fn(comp); 14950 detail.onOpened = fn; 14951 }; 14952 } 14953 else { 14954 const fn = detail.onClosed; 14955 detail.onClosed = (comp) => { 14956 if (!skipFocus) { 14957 Focusing.focus(oveflowButton); 14958 } 14959 fn(comp); 14960 detail.onClosed = fn; 14961 }; 14962 } 14963 Sliding.toggleGrow(overf); 14964 }); 14965 }); 14966 }; 14967 const refresh = (toolbar, detail) => { 14968 getPart(toolbar, detail, 'overflow').each((overflow) => { 14969 refresh$2(toolbar, detail, (groups) => { 14970 const builtGroups = map$2(groups, (g) => premade(g)); 14971 Toolbar.setGroups(overflow, builtGroups); 14972 }); 14973 getPart(toolbar, detail, 'overflow-button').each((button) => { 14974 if (Sliding.hasGrown(overflow)) { 14975 Toggling.on(button); 14976 } 14977 }); 14978 Sliding.refresh(overflow); 14979 }); 14980 }; 14981 const factory$9 = (detail, components, spec, externals) => { 14982 const toolbarToggleEvent = 'alloy.toolbar.toggle'; 14983 const doSetGroups = (toolbar, groups) => { 14984 const built = map$2(groups, toolbar.getSystem().build); 14985 detail.builtGroups.set(built); 14986 }; 14987 return { 14988 uid: detail.uid, 14989 dom: detail.dom, 14990 components, 14991 behaviours: augment(detail.splitToolbarBehaviours, [ 14992 Coupling.config({ 14993 others: { 14994 overflowGroup: (toolbar) => { 14995 return ToolbarGroup.sketch({ 14996 ...externals['overflow-group'](), 14997 items: [ 14998 Button.sketch({ 14999 ...externals['overflow-button'](), 15000 action: (_button) => { 15001 emit(toolbar, toolbarToggleEvent); 15002 } 15003 }) 15004 ] 15005 }); 15006 } 15007 } 15008 }), 15009 config('toolbar-toggle-events', [ 15010 run$1(toolbarToggleEvent, (toolbar) => { 15011 toggleToolbar(toolbar, detail, false); 15012 }) 15013 ]) 15014 ]), 15015 apis: { 15016 setGroups: (toolbar, groups) => { 15017 doSetGroups(toolbar, groups); 15018 refresh(toolbar, detail); 15019 }, 15020 refresh: (toolbar) => refresh(toolbar, detail), 15021 toggle: (toolbar) => { 15022 toggleToolbar(toolbar, detail, false); 15023 }, 15024 toggleWithoutFocusing: (toolbar) => { 15025 toggleToolbar(toolbar, detail, true); 15026 }, 15027 isOpen: (toolbar) => isOpen(toolbar, detail) 15028 }, 15029 domModification: { 15030 attributes: { role: 'group' } 15031 } 15032 }; 15033 }; 15034 const SplitSlidingToolbar = composite({ 15035 name: 'SplitSlidingToolbar', 15036 configFields: schema$3(), 15037 partFields: parts$3(), 15038 factory: factory$9, 15039 apis: { 15040 setGroups: (apis, toolbar, groups) => { 15041 apis.setGroups(toolbar, groups); 15042 }, 15043 refresh: (apis, toolbar) => { 15044 apis.refresh(toolbar); 15045 }, 15046 toggle: (apis, toolbar) => { 15047 apis.toggle(toolbar); 15048 }, 15049 isOpen: (apis, toolbar) => apis.isOpen(toolbar) 15050 } 15051 }); 15052 15053 const factory$8 = (detail, _spec) => ({ 15054 uid: detail.uid, 15055 dom: detail.dom, 15056 components: detail.components, 15057 events: events(detail.action), 15058 behaviours: augment(detail.tabButtonBehaviours, [ 15059 Focusing.config({}), 15060 Keying.config({ 15061 mode: 'execution',
vendor: 12,279 bytes, lines 15062-15389
15062 useSpace: true, 15063 useEnter: true 15064 }), 15065 Representing.config({ 15066 store: { 15067 mode: 'memory', 15068 initialValue: detail.value 15069 } 15070 }) 15071 ]), 15072 domModification: detail.domModification 15073 }); 15074 const TabButton = single({ 15075 name: 'TabButton', 15076 configFields: [ 15077 defaulted('uid', undefined), 15078 required$1('value'), 15079 field$1('dom', 'dom', mergeWithThunk(() => ({ 15080 attributes: { 15081 'role': 'tab', 15082 // NOTE: This is used in TabSection to connect "labelledby" 15083 'id': generate$6('aria'), 15084 'aria-selected': 'false' 15085 } 15086 })), anyValue()), 15087 option$3('action'), 15088 defaulted('domModification', {}), 15089 field('tabButtonBehaviours', [Focusing, Keying, Representing]), 15090 required$1('view') 15091 ], 15092 factory: factory$8 15093 }); 15094 15095 const schema$2 = constant$1([ 15096 required$1('tabs'), 15097 required$1('dom'), 15098 defaulted('clickToDismiss', false), 15099 field('tabbarBehaviours', [Highlighting, Keying]), 15100 markers$1(['tabClass', 'selectedClass']) 15101 ]); 15102 const tabsPart = group({ 15103 factory: TabButton, 15104 name: 'tabs', 15105 unit: 'tab', 15106 overrides: (barDetail) => { 15107 const dismissTab$1 = (tabbar, button) => { 15108 Highlighting.dehighlight(tabbar, button); 15109 emitWith(tabbar, dismissTab(), { 15110 tabbar, 15111 button 15112 }); 15113 }; 15114 const changeTab$1 = (tabbar, button) => { 15115 Highlighting.highlight(tabbar, button); 15116 emitWith(tabbar, changeTab(), { 15117 tabbar, 15118 button 15119 }); 15120 }; 15121 return { 15122 action: (button) => { 15123 const tabbar = button.getSystem().getByUid(barDetail.uid).getOrDie(); 15124 const activeButton = Highlighting.isHighlighted(tabbar, button); 15125 const response = (() => { 15126 if (activeButton && barDetail.clickToDismiss) { 15127 return dismissTab$1; 15128 } 15129 else if (!activeButton) { 15130 return changeTab$1; 15131 } 15132 else { 15133 return noop; 15134 } 15135 })(); 15136 response(tabbar, button); 15137 }, 15138 domModification: { 15139 classes: [barDetail.markers.tabClass] 15140 } 15141 }; 15142 } 15143 }); 15144 const parts$2 = constant$1([ 15145 tabsPart 15146 ]); 15147 15148 const factory$7 = (detail, components, _spec, _externals) => ({ 15149 'uid': detail.uid, 15150 'dom': detail.dom, 15151 components, 15152 'debug.sketcher': 'Tabbar', 15153 'domModification': { 15154 attributes: { 15155 role: 'tablist' 15156 } 15157 }, 15158 'behaviours': augment(detail.tabbarBehaviours, [ 15159 Highlighting.config({ 15160 highlightClass: detail.markers.selectedClass, 15161 itemClass: detail.markers.tabClass, 15162 // https://www.w3.org/TR/2010/WD-wai-aria-practices-20100916/#tabpanel 15163 // Consider a more seam-less way of combining highlighting and toggling 15164 onHighlight: (tabbar, tab) => { 15165 // TODO: Integrate highlighting and toggling in a nice way 15166 set$9(tab.element, 'aria-selected', 'true'); 15167 }, 15168 onDehighlight: (tabbar, tab) => { 15169 set$9(tab.element, 'aria-selected', 'false'); 15170 } 15171 }), 15172 Keying.config({ 15173 mode: 'flow', 15174 getInitial: (tabbar) => { 15175 // Restore focus to the previously highlighted tab. 15176 return Highlighting.getHighlighted(tabbar).map((tab) => tab.element); 15177 }, 15178 selector: '.' + detail.markers.tabClass, 15179 executeOnMove: true 15180 }) 15181 ]) 15182 }); 15183 const Tabbar = composite({ 15184 name: 'Tabbar', 15185 configFields: schema$2(), 15186 partFields: parts$2(), 15187 factory: factory$7 15188 }); 15189 15190 const factory$6 = (detail, _spec) => ({ 15191 uid: detail.uid, 15192 dom: detail.dom, 15193 behaviours: augment(detail.tabviewBehaviours, [ 15194 Replacing.config({}) 15195 ]), 15196 domModification: { 15197 attributes: { role: 'tabpanel' } 15198 } 15199 }); 15200 const Tabview = single({ 15201 name: 'Tabview', 15202 configFields: [ 15203 field('tabviewBehaviours', [Replacing]) 15204 ], 15205 factory: factory$6 15206 }); 15207 15208 const schema$1 = constant$1([ 15209 defaulted('selectFirst', true), 15210 onHandler('onChangeTab'), 15211 onHandler('onDismissTab'), 15212 defaulted('tabs', []), 15213 field('tabSectionBehaviours', []) 15214 ]); 15215 const barPart = required({ 15216 factory: Tabbar, 15217 schema: [ 15218 required$1('dom'), 15219 requiredObjOf('markers', [ 15220 required$1('tabClass'), 15221 required$1('selectedClass') 15222 ]) 15223 ], 15224 name: 'tabbar', 15225 defaults: (detail) => { 15226 return { 15227 tabs: detail.tabs 15228 }; 15229 } 15230 }); 15231 const viewPart = required({ 15232 factory: Tabview, 15233 name: 'tabview' 15234 }); 15235 const parts$1 = constant$1([ 15236 barPart, 15237 viewPart 15238 ]); 15239 15240 const factory$5 = (detail, components, _spec, _externals) => { 15241 const changeTab$1 = (button) => { 15242 const tabValue = Representing.getValue(button); 15243 getPart(button, detail, 'tabview').each((tabview) => { 15244 const tabWithValue = find$5(detail.tabs, (t) => t.value === tabValue); 15245 tabWithValue.each((tabData) => { 15246 const panel = tabData.view(); 15247 // Update the tabview to refer to the current tab. 15248 getOpt(button.element, 'id').each((id) => { 15249 set$9(tabview.element, 'aria-labelledby', id); 15250 }); 15251 Replacing.set(tabview, panel); 15252 detail.onChangeTab(tabview, button, panel); 15253 }); 15254 }); 15255 }; 15256 const changeTabBy = (section, byPred) => { 15257 getPart(section, detail, 'tabbar').each((tabbar) => { 15258 byPred(tabbar).each(emitExecute); 15259 }); 15260 }; 15261 return { 15262 uid: detail.uid, 15263 dom: detail.dom, 15264 components, 15265 behaviours: get$2(detail.tabSectionBehaviours), 15266 events: derive$2(flatten([ 15267 detail.selectFirst ? [ 15268 runOnAttached((section, _simulatedEvent) => { 15269 changeTabBy(section, Highlighting.getFirst); 15270 }) 15271 ] : [], 15272 [ 15273 run$1(changeTab(), (section, simulatedEvent) => { 15274 const button = simulatedEvent.event.button; 15275 changeTab$1(button); 15276 }), 15277 run$1(dismissTab(), (section, simulatedEvent) => { 15278 const button = simulatedEvent.event.button; 15279 detail.onDismissTab(section, button); 15280 }) 15281 ] 15282 ])), 15283 apis: { 15284 getViewItems: (section) => { 15285 return getPart(section, detail, 'tabview').map((tabview) => Replacing.contents(tabview)).getOr([]); 15286 }, 15287 // How should "clickToDismiss" interact with this? At the moment, it will never dismiss 15288 showTab: (section, tabKey) => { 15289 // We only change the tab if it isn't currently active because that takes 15290 // the whole "dismiss" issue out of the equation. 15291 const getTabIfNotActive = (tabbar) => { 15292 const candidates = Highlighting.getCandidates(tabbar); 15293 const optTab = find$5(candidates, (c) => Representing.getValue(c) === tabKey); 15294 return optTab.filter((tab) => !Highlighting.isHighlighted(tabbar, tab)); 15295 }; 15296 changeTabBy(section, getTabIfNotActive); 15297 } 15298 } 15299 }; 15300 }; 15301 const TabSection = composite({ 15302 name: 'TabSection', 15303 configFields: schema$1(), 15304 partFields: parts$1(), 15305 factory: factory$5, 15306 apis: { 15307 getViewItems: (apis, component) => apis.getViewItems(component), 15308 showTab: (apis, component, tabKey) => { 15309 apis.showTab(component, tabKey); 15310 } 15311 } 15312 }); 15313 15314 // When showing a value in an input field, which part of the item do we use? 15315 const setValueFromItem = (model, input, item) => { 15316 const itemData = Representing.getValue(item); 15317 Representing.setValue(input, itemData); 15318 setCursorAtEnd(input); 15319 }; 15320 const setSelectionOn = (input, f) => { 15321 const el = input.element; 15322 const value = get$5(el); 15323 const node = el.dom; 15324 // Only do for valid input types. 15325 if (get$g(el, 'type') !== 'number') { 15326 f(node, value); 15327 } 15328 }; 15329 const setCursorAtEnd = (input) => { 15330 setSelectionOn(input, (node, value) => node.setSelectionRange(value.length, value.length)); 15331 }; 15332 const setSelectionToEnd = (input, startOffset) => { 15333 setSelectionOn(input, (node, value) => node.setSelectionRange(startOffset, value.length)); 15334 }; 15335 const attemptSelectOver = (model, input, item) => { 15336 if (!model.selectsOver) { 15337 return Optional.none(); 15338 } 15339 else { 15340 const currentValue = Representing.getValue(input); 15341 const inputDisplay = model.getDisplayText(currentValue); 15342 const itemValue = Representing.getValue(item); 15343 const itemDisplay = model.getDisplayText(itemValue); 15344 return itemDisplay.indexOf(inputDisplay) === 0 ? 15345 Optional.some(() => { 15346 setValueFromItem(model, input, item); 15347 setSelectionToEnd(input, inputDisplay.length); 15348 }) 15349 : Optional.none(); 15350 } 15351 }; 15352 15353 const itemExecute = constant$1('alloy.typeahead.itemexecute'); 15354 15355 // TODO: Fix this. 15356 const make$2 = (detail, components, spec, externals) => { 15357 const navigateList = (comp, simulatedEvent, highlighter) => { 15358 /* 15359 * If we have an open Sandbox with an active menu, 15360 * but no highlighted item, then highlight the menu 15361 * 15362 * If we have an open Sandbox with an active menu, 15363 * and there is a highlighted item, simulated a keydown 15364 * on the menu 15365 * 15366 * If we have a closed sandbox, open the sandbox 15367 * 15368 * Regardless, this is a user initiated action. End previewing. 15369 */ 15370 detail.previewing.set(false); 15371 const sandbox = Coupling.getCoupled(comp, 'sandbox'); 15372 if (Sandboxing.isOpen(sandbox)) { 15373 Composing.getCurrent(sandbox).each((menu) => { 15374 Highlighting.getHighlighted(menu).fold(() => { 15375 highlighter(menu); 15376 }, () => { 15377 dispatchEvent(sandbox, menu.element, 'keydown', simulatedEvent); 15378 }); 15379 }); 15380 } 15381 else { 15382 const onOpenSync = (sandbox) => { 15383 Composing.getCurrent(sandbox).each(highlighter); 15384 }; 15385 open(detail, mapFetch(comp), comp, sandbox, externals, onOpenSync, HighlightOnOpen.HighlightMenuAndItem).get(noop); 15386 } 15387 }; 15388 // Due to the fact that typeahead probably need to separate value from text, they can't reuse 15389 // (easily) the same representing logic as input fields.
15390 const focusBehaviours$1 = focusBehaviours(detail); 15391 const mapFetch = (comp) => (tdata) => tdata.map((data) => { 15392 const menus = values(data.menus); 15393 const items = bind$3(menus, (menu) => filter$2(menu.items, (item) => item.type === 'item')); 15394 const repState = Representing.getState(comp); 15395 repState.update(map$2(items, (item) => item.data)); 15396 return data; 15397 }); 15398 // This function (getActiveMenu) is intended to make it easier to read what is happening 15399 // without having to decipher the Highlighting and Composing calls. 15400 const getActiveMenu = (sandboxComp) => Composing.getCurrent(sandboxComp); 15401 const typeaheadCustomEvents = 'typeaheadevents'; 15402 const behaviours = [ 15403 Focusing.config({}), 15404 Representing.config({ 15405 onSetValue: detail.onSetValue, 15406 store: { 15407 mode: 'dataset', 15408 getDataKey: (comp) => get$5(comp.element), 15409 // This really needs to be configurable 15410 getFallbackEntry: (itemString) => ({ 15411 value: itemString, 15412 meta: {} 15413 }), 15414 setValue: (comp, data) => { 15415 set$4(comp.element, detail.model.getDisplayText(data)); 15416 }, 15417 ...detail.initialData.map((d) => wrap('initialValue', d)).getOr({}) 15418 } 15419 }), 15420 Streaming.config({ 15421 stream: { 15422 mode: 'throttle', 15423 delay: detail.responseTime, 15424 stopEvent: false 15425 }, 15426 onStream: (component, _simulatedEvent) => { 15427 const sandbox = Coupling.getCoupled(component, 'sandbox'); 15428 const focusInInput = Focusing.isFocused(component); 15429 // You don't want it to change when something else has triggered the change. 15430 if (focusInInput) { 15431 if (get$5(component.element).length >= detail.minChars) { 15432 // Get the value of the previously active (selected/highlighted) item. We 15433 // are going to try to preserve this. 15434 const previousValue = getActiveMenu(sandbox).bind((activeMenu) => Highlighting.getHighlighted(activeMenu).map(Representing.getValue)); 15435 // Turning previewing ON here every keystroke is unnecessary, but relies 15436 // on the fact that it will be turned off if required by highlighting events. 15437 // So even if previewing was supposed to be off, turning it on here is 15438 // just temporary, because the onOpenSync below will trigger a highlight 15439 // if there was meant to be one, which will turn it off if required. 15440 detail.previewing.set(true); 15441 const onOpenSync = (_sandbox) => { 15442 // This getActiveMenu relies on a menu being highlighted / active 15443 getActiveMenu(sandbox).each((activeMenu) => { 15444 // The folds can make this hard to follow, but the basic gist of it is 15445 // that we want to see if we need to highlight one of the items in the 15446 // menu that we just opened. If we do highlight an item, then that 15447 // highlighting action will clear previewing (handled by the TieredMenu 15448 // part configuration for onHighlight). Note: that onOpenSync runs 15449 // *after* the highlightOnOpen setting. 15450 // 15451 // 1. If in "selectsOver" mode and we don't have a previous item, 15452 // then highlight the first one. This one will be used as the basis 15453 // for the "selectsOver" text selection. The act of highlighting the 15454 // first item will take us out of previewing mode. If the "selectsOver" 15455 // operation fails, it should clear the highlight, and restore previewing 15456 // 2. If not in "selectsOver" mode, and we don't have a previous item, 15457 // then we don't highlight anything. This will keep us in previewing 15458 // mode until the menu is interacted with (hover, navigation etc.) 15459 // 3. If we have a previous item, then try and rehighlight it. But if 15460 // we can't, the just highlight the first. Either action will take us 15461 // out of previewing mode. 15462 previousValue.fold(() => { 15463 // We are using "selectOver", so we need *something* to highlight 15464 if (detail.model.selectsOver) { 15465 Highlighting.highlightFirst(activeMenu); 15466 } 15467 // We aren't using "selectOver", so don't highlight anything 15468 // to preserve our "previewing" mode. 15469 }, (pv) => { 15470 // We have a previous item, so if we can't rehighlight it, then 15471 // we'll change to the first item. We want to keep some selection. 15472 Highlighting.highlightBy(activeMenu, (item) => { 15473 const itemData = Representing.getValue(item); 15474 return itemData.value === pv.value; 15475 }); 15476 // Highlight first if could not find it? 15477 Highlighting.getHighlighted(activeMenu).orThunk(() => { 15478 Highlighting.highlightFirst(activeMenu); 15479 return Optional.none(); 15480 }); 15481 }); 15482 }); 15483 }; 15484 open(detail, mapFetch(component), component, sandbox, externals, onOpenSync, 15485 // The onOpenSync takes care of what should be given the highlights, but 15486 // we want to highlight just the menu so that the onOpenSync can find the 15487 // activeMenu. 15488 HighlightOnOpen.HighlightJustMenu).get(noop); 15489 } 15490 } 15491 }, 15492 cancelEvent: typeaheadCancel() 15493 }), 15494 Keying.config({
vendor: 12,560 bytes, lines 15495-15713
15495 mode: 'special', 15496 onDown: (comp, simulatedEvent) => { 15497 // The navigation here will stop the "previewing" mode, because 15498 // now the menu will get focus (fake focus, but focus nevertheless) 15499 navigateList(comp, simulatedEvent, Highlighting.highlightFirst); 15500 return Optional.some(true); 15501 }, 15502 onEscape: (comp) => { 15503 // Escape only has handling if the sandbox is visible. It has no meaning 15504 // to the input itself. 15505 const sandbox = Coupling.getCoupled(comp, 'sandbox'); 15506 if (Sandboxing.isOpen(sandbox)) { 15507 Sandboxing.close(sandbox); 15508 return Optional.some(true); 15509 } 15510 return Optional.none(); 15511 }, 15512 onUp: (comp, simulatedEvent) => { 15513 // The navigation here will stop the "previewing" mode, because 15514 // now the menu will get focus (fake focus, but focus nevertheless) 15515 navigateList(comp, simulatedEvent, Highlighting.highlightLast); 15516 return Optional.some(true); 15517 }, 15518 onEnter: (comp) => { 15519 const sandbox = Coupling.getCoupled(comp, 'sandbox'); 15520 const sandboxIsOpen = Sandboxing.isOpen(sandbox); 15521 // 'Previewing' means that items are shown but none has been actively selected by the user. 15522 // When previewing, all keyboard input should still be processed by the 15523 // input itself, not the menu. The menu is not considered to have focus. 15524 // 'Previewing' is turned on by (streaming) keystrokes, and turned off by 15525 // successful interaction with the menu (navigation, highlighting, hovering). 15526 // So if we aren't previewing, and the dropdown sandbox is open, then 15527 // we process <enter> keys on the items in the menu. All this will do 15528 // is trigger an itemExecute event. The typeahead events (in the spec below) 15529 // are responsible for doing something with that event. 15530 if (sandboxIsOpen && !detail.previewing.get()) { 15531 return getActiveMenu(sandbox).bind((activeMenu) => Highlighting.getHighlighted(activeMenu)).map((item) => { 15532 // And item was selected, so trigger execute and consider the 15533 // <enter> key 'handled' 15534 emitWith(comp, itemExecute(), { item }); 15535 return true; 15536 }); 15537 } 15538 else { 15539 // We are either previewing, or the sandbox isn't open, so we should 15540 // process the <enter> key inside the input itself. This should cancel 15541 // any attempt to fetch data (the typeaheadCancel), and trigger the execute. 15542 // We also close the sandbox if it's open. 15543 const currentValue = Representing.getValue(comp); 15544 emit(comp, typeaheadCancel()); 15545 detail.onExecute(sandbox, comp, currentValue); 15546 // If we're open and previewing, close the sandbox after firing execute. 15547 if (sandboxIsOpen) { 15548 Sandboxing.close(sandbox); 15549 } 15550 return Optional.some(true); 15551 } 15552 } 15553 }), 15554 Toggling.config({ 15555 toggleClass: detail.markers.openClass, 15556 aria: { 15557 mode: 'expanded' 15558 } 15559 }), 15560 Coupling.config({ 15561 others: { 15562 sandbox: (hotspot) => { 15563 return makeSandbox$1(detail, hotspot, { 15564 onOpen: () => Toggling.on(hotspot), 15565 onClose: () => { 15566 // TINY-9280: Remove aria-activedescendant that is set when menu item is highlighted 15567 detail.lazyTypeaheadComp.get().each((input) => remove$8(input.element, 'aria-activedescendant')); 15568 Toggling.off(hotspot); 15569 } 15570 }); 15571 } 15572 } 15573 }), 15574 config(typeaheadCustomEvents, [ 15575 runOnAttached((typeaheadComp) => { 15576 // Set up the reference to the typeahead, so that it can retrieved from 15577 // the tiered menu part, even if the tieredmenu is in a different 15578 // system / alloy root / mothership. 15579 detail.lazyTypeaheadComp.set(Optional.some(typeaheadComp)); 15580 }), 15581 runOnDetached((_typeaheadComp) => { 15582 detail.lazyTypeaheadComp.set(Optional.none()); 15583 }), 15584 runOnExecute$1((comp) => { 15585 const onOpenSync = noop; 15586 togglePopup(detail, mapFetch(comp), comp, externals, onOpenSync, HighlightOnOpen.HighlightMenuAndItem).get(noop); 15587 }), 15588 run$1(itemExecute(), (comp, se) => { 15589 const sandbox = Coupling.getCoupled(comp, 'sandbox'); 15590 // Copy the value from the executed item into the input, because it was "chosen" 15591 setValueFromItem(detail.model, comp, se.event.item); 15592 emit(comp, typeaheadCancel()); 15593 detail.onItemExecute(comp, sandbox, se.event.item, Representing.getValue(comp)); 15594 Sandboxing.close(sandbox); 15595 setCursorAtEnd(comp); 15596 }) 15597 ].concat(detail.dismissOnBlur ? [ 15598 run$1(postBlur(), (typeahead) => { 15599 const sandbox = Coupling.getCoupled(typeahead, 'sandbox'); 15600 // Only close the sandbox if the focus isn't inside it! 15601 if (search(sandbox.element).isNone()) { 15602 Sandboxing.close(sandbox); 15603 } 15604 }) 15605 ] : [])) 15606 ]; 15607 // The order specified here isn't important. Alloy just requires a 15608 // deterministic order for the configured behaviours. 15609 const eventOrder = { 15610 [detachedFromDom()]: [ 15611 Representing.name(), 15612 Streaming.name(), 15613 typeaheadCustomEvents 15614 ], 15615 ...detail.eventOrder, 15616 }; 15617 return { 15618 uid: detail.uid, 15619 dom: dom$1(deepMerge(detail, { 15620 // TODO: Add aria-activedescendant attribute 15621 inputAttributes: { 15622 'role': 'combobox', 15623 'aria-autocomplete': 'list', 15624 'aria-haspopup': 'true' 15625 } 15626 })), 15627 behaviours: { 15628 ...focusBehaviours$1, 15629 ...augment(detail.typeaheadBehaviours, behaviours) 15630 }, 15631 eventOrder 15632 }; 15633 }; 15634 15635 const schema = constant$1([ 15636 option$3('lazySink'), 15637 required$1('fetch'), 15638 defaulted('minChars', 5), 15639 defaulted('responseTime', 1000), 15640 onHandler('onOpen'), 15641 // TODO: Remove dupe with Dropdown 15642 defaulted('getHotspot', Optional.some), 15643 defaulted('getAnchorOverrides', constant$1({})), 15644 defaulted('layouts', Optional.none()), 15645 defaulted('eventOrder', {}), 15646 // Information about what these model settings do can be found in TypeaheadTypes 15647 defaultedObjOf('model', {}, [ 15648 defaulted('getDisplayText', (itemData) => itemData.meta !== undefined && itemData.meta.text !== undefined ? itemData.meta.text : itemData.value), 15649 defaulted('selectsOver', true), 15650 defaulted('populateFromBrowse', true) 15651 ]), 15652 onHandler('onSetValue'), 15653 onKeyboardHandler('onExecute'), 15654 onHandler('onItemExecute'), 15655 defaulted('inputClasses', []), 15656 defaulted('inputAttributes', {}), 15657 defaulted('inputStyles', {}), 15658 defaulted('matchWidth', true), 15659 defaulted('useMinWidth', false), 15660 defaulted('dismissOnBlur', true), 15661 markers$1(['openClass']), 15662 option$3('initialData'), 15663 option$3('listRole'), 15664 field('typeaheadBehaviours', [ 15665 Focusing, Representing, Streaming, Keying, Toggling, Coupling 15666 ]), 15667 customField('lazyTypeaheadComp', () => Cell(Optional.none)), 15668 customField('previewing', () => Cell(true)) 15669 ].concat(schema$9()).concat(sandboxFields())); 15670 const parts = constant$1([ 15671 external$1({ 15672 schema: [ 15673 tieredMenuMarkers() 15674 ], 15675 name: 'menu', 15676 overrides: (detail) => { 15677 return { 15678 fakeFocus: true, 15679 onHighlightItem: (_tmenu, menu, item) => { 15680 if (!detail.previewing.get()) { 15681 // We need to use this type of reference, rather than just looking 15682 // it up from the system by uid, because the input and the tieredmenu 15683 // might be in different systems. 15684 detail.lazyTypeaheadComp.get().each((input) => { 15685 if (detail.model.populateFromBrowse) { 15686 setValueFromItem(detail.model, input, item); 15687 } 15688 // The focus is retained on the input element when the menu is shown, unlike the combobox, in which the focus is passed to the menu. 15689 // This results in screen readers not being able to announce the menu or highlighted item. 15690 // The solution is to tell screen readers which menu item is highlighted using the `aria-activedescendant` attribute. 15691 // TINY-9280: The aria attribute is removed when the menu is closed. 15692 // Since `onDehighlight` is called only when highlighting a new menu item, this will be handled in 15693 // https://github.com/tinymce/tinymce/blob/2d8c1c034e8aa484b868a0c44605489ee0ca9cd4/modules/alloy/src/main/ts/ephox/alloy/ui/composite/TypeaheadSpec.ts#L282 15694 getOpt(item.element, 'id').each((id) => set$9(input.element, 'aria-activedescendant', id)); 15695 }); 15696 } 15697 else { 15698 // ASSUMPTION: Currently, any interaction with the menu via the keyboard or the mouse 15699 // will firstly clear previewing mode before triggering any highlights 15700 // so if we are still in previewing mode by the time we get to the highlight call, 15701 // that means that the highlight was triggered NOT by the user interacting 15702 // with the menu, but instead by the Highlighting API call that happens automatically 15703 // when a streamed keyboard input event is updating its results. That call will 15704 // try to keep any active highlight if there already was one (defaulting to first 15705 // if it can't find the original), but if there wasn't an active highlight, but 15706 // it is using "selectsOver", it will just highlight the first item. In this 15707 // latter case, it is only doing that so that selectsOver has something to copy. 15708 // So all of the complex code below is trying to handle whether we should stay 15709 // in previewing mode after this highlight, and the ONLY case where we should stay 15710 // in previewing mode is that we were in previewing mode, we are using selectsOver, 15711 // and the selectsOver failed to succeed. In that case, to stay in previewing mode, 15712 // we want to cancel the highlight that we just made via the highlighting API 15713 // and reset previewing to true. Otherwise, all codepaths should set previewing
15714 // to false, because now we have a valid highlight. 15715 // 15716 // As of 2022-08-18, the selectsOver model is not in use by TinyMCE, so 15717 // this subtle interaction is unfortunately largely untested. Also, if we can't 15718 // get a reference to the typeahead input by lazyTypeaheadComp, then we don't 15719 // change previewing, either. Note also, that it is likely that if we checked 15720 // if selectsOver would succeed before setting the highlight in the streaming 15721 // response, this could might be a lot easier to follow. 15722 detail.lazyTypeaheadComp.get().each((input) => { 15723 attemptSelectOver(detail.model, input, item).fold( 15724 // If we are in "previewing" mode and we can't select over the 15725 // thing that is first, then clear the highlight. 15726 // Hopefully, this doesn't cause a flicker. Find a better 15727 // way to do this. 15728 () => { 15729 // If using "selectOver", we essentially want to cancel the highlight 15730 // that was only invoked just so that we'd have something to selectOver, 15731 // so we dehighlight, and then, importantly, *DON'T* clear previewing. 15732 // We'll set it to be true to be explicit, although it should 15733 // always be true if it reached here (unless an above function changed 15734 // it) 15735 if (detail.model.selectsOver) { 15736 Highlighting.dehighlight(menu, item); 15737 detail.previewing.set(true); 15738 } 15739 else { 15740 // Because we aren't using selectsOver mode, we now want to keep 15741 // whatever highlight we just made, and because we have a highlighted 15742 // item in the menu, we are no longer previewing. 15743 detail.previewing.set(false); 15744 } 15745 }, ((selectOverTextInInput) => { 15746 // We have made a selection in the menu, and have selected over text 15747 // in the input, so clear previewing. 15748 selectOverTextInInput(); 15749 detail.previewing.set(false); 15750 })); 15751 }); 15752 } 15753 }, 15754 // Because the focus stays inside the input, this onExecute is fired when the 15755 // user "clicks" on an item. The focusing behaviour should be configured 15756 // so that items don't get focus, but they prevent a mousedown event from 15757 // firing so that the typeahead doesn't lose focus. This is the handler 15758 // for clicking on an item. We need to close the sandbox, update the typeahead 15759 // to show the item clicked on, and fire an execute. 15760 onExecute: (_menu, item) => { 15761 // Note: This will only work when the typeahead and menu are in the same system. 15762 return detail.lazyTypeaheadComp.get().map((typeahead) => { 15763 emitWith(typeahead, itemExecute(), { item }); 15764 return true; 15765 }); 15766 }, 15767 onHover: (menu, item) => { 15768 // Hovering is also a user-initiated action, so previewing mode is over. 15769 // TODO: Have a better API for managing state in between parts. 15770 detail.previewing.set(false); 15771 detail.lazyTypeaheadComp.get().each((input) => { 15772 if (detail.model.populateFromBrowse) { 15773 setValueFromItem(detail.model, input, item); 15774 } 15775 }); 15776 } 15777 }; 15778 } 15779 }) 15780 ]); 15781 15782 const Typeahead = composite({ 15783 name: 'Typeahead', 15784 configFields: schema(), 15785 partFields: parts(), 15786 factory: make$2 15787 }); 15788 15789 var global$b = tinymce.util.Tools.resolve('tinymce.ThemeManager'); 15790 15791 var global$a = tinymce.util.Tools.resolve('tinymce.util.Delay'); 15792 15793 var global$9 = tinymce.util.Tools.resolve('tinymce.dom.DOMUtils'); 15794 15795 var global$8 = tinymce.util.Tools.resolve('tinymce.EditorManager'); 15796 15797 var global$7 = tinymce.util.Tools.resolve('tinymce.Env'); 15798 15799 const ToolbarMode$1 = { 15800 default: 'wrap', 15801 floating: 'floating', 15802 sliding: 'sliding', 15803 scrolling: 'scrolling' 15804 }; 15805 const ToolbarLocation$1 = { 15806 auto: 'auto', 15807 top: 'top', 15808 bottom: 'bottom' 15809 }; 15810 const option$2 = (name) => (editor) => editor.options.get(name); 15811 const wrapOptional = (fn) => (editor) => Optional.from(fn(editor)); 15812 const register$f = (editor) => { 15813 const isPhone = global$7.deviceType.isPhone(); 15814 const isMobile = global$7.deviceType.isTablet() || isPhone; 15815 const registerOption = editor.options.register; 15816 const stringOrFalseProcessor = (value) => isString(value) || value === false; 15817 const stringOrNumberProcessor = (value) => isString(value) || isNumber(value); 15818 registerOption('skin', { 15819 processor: (value) => isString(value) || value === false, 15820 default: 'oxide' 15821 }); 15822 registerOption('skin_url', { 15823 processor: 'string' 15824 }); 15825 registerOption('height', { 15826 processor: stringOrNumberProcessor, 15827 default: Math.max(editor.getElement().offsetHeight, 400) 15828 }); 15829 registerOption('width', { 15830 processor: stringOrNumberProcessor, 15831 default: global$9.DOM.getStyle(editor.getElement(), 'width') 15832 }); 15833 registerOption('min_height', { 15834 processor: 'number', 15835 default: 100 15836 }); 15837 registerOption('min_width', { 15838 processor: 'number' 15839 }); 15840 registerOption('max_height', { 15841 processor: 'number' 15842 }); 15843 registerOption('max_width', { 15844 processor: 'number' 15845 }); 15846 registerOption('style_formats', { 15847 processor: 'object[]' 15848 }); 15849 registerOption('style_formats_merge', { 15850 processor: 'boolean', 15851 default: false 15852 }); 15853 registerOption('style_formats_autohide', { 15854 processor: 'boolean', 15855 default: false 15856 }); 15857 registerOption('line_height_formats', { 15858 processor: 'string', 15859 default: '1 1.1 1.2 1.3 1.4 1.5 2' 15860 }); 15861 registerOption('font_family_formats', { 15862 processor: 'string', 15863 default: 'Andale Mono=andale mono,monospace;' + 15864 'Arial=arial,helvetica,sans-serif;' + 15865 'Arial Black=arial black,sans-serif;' + 15866 'Book Antiqua=book antiqua,palatino,serif;' + 15867 'Comic Sans MS=comic sans ms,sans-serif;' + 15868 'Courier New=courier new,courier,monospace;' + 15869 'Georgia=georgia,palatino,serif;' + 15870 'Helvetica=helvetica,arial,sans-serif;' + 15871 'Impact=impact,sans-serif;' + 15872 'Symbol=symbol;' + 15873 'Tahoma=tahoma,arial,helvetica,sans-serif;' + 15874 'Terminal=terminal,monaco,monospace;' + 15875 'Times New Roman=times new roman,times,serif;' + 15876 'Trebuchet MS=trebuchet ms,geneva,sans-serif;' + 15877 'Verdana=verdana,geneva,sans-serif;' + 15878 'Webdings=webdings;' + 15879 'Wingdings=wingdings,zapf dingbats' 15880 }); 15881 registerOption('font_size_formats', { 15882 processor: 'string', 15883 default: '8pt 10pt 12pt 14pt 18pt 24pt 36pt' 15884 }); 15885 registerOption('font_size_input_default_unit', { 15886 processor: 'string', 15887 default: 'pt' 15888 }); 15889 registerOption('block_formats', { 15890 processor: 'string', 15891 default: 'Paragraph=p;' + 15892 'Heading 1=h1;' + 15893 'Heading 2=h2;' + 15894 'Heading 3=h3;' + 15895 'Heading 4=h4;' + 15896 'Heading 5=h5;' + 15897 'Heading 6=h6;' + 15898 'Preformatted=pre' 15899 }); 15900 registerOption('content_langs', { 15901 processor: 'object[]' 15902 }); 15903 registerOption('removed_menuitems', { 15904 processor: 'string', 15905 default: '' 15906 }); 15907 registerOption('menubar', { 15908 processor: (value) => isString(value) || isBoolean(value),
15909 // Phones don't have a lot of screen space so disable the menubar 15910 default: !isPhone 15911 }); 15912 registerOption('menu', { 15913 processor: 'object', 15914 default: {} 15915 }); 15916 registerOption('toolbar', { 15917 processor: (value) => { 15918 if (isBoolean(value) || isString(value) || isArray(value)) { 15919 return { value, valid: true }; 15920 } 15921 else { 15922 return { valid: false, message: 'Must be a boolean, string or array.' }; 15923 } 15924 }, 15925 default: true 15926 }); 15927 // Register the toolbarN variations: toolbar1 -> toolbar9 15928 range$2(9, (num) => { 15929 registerOption('toolbar' + (num + 1), { 15930 processor: 'string' 15931 }); 15932 }); 15933 registerOption('toolbar_mode', { 15934 processor: 'string', 15935 // Use the default side-scrolling toolbar for tablets/phones 15936 default: isMobile ? 'scrolling' : 'floating' 15937 }); 15938 registerOption('toolbar_groups', { 15939 processor: 'object', 15940 default: {} 15941 }); 15942 registerOption('toolbar_location', { 15943 processor: 'string', 15944 default: ToolbarLocation$1.auto 15945 }); 15946 registerOption('toolbar_persist', { 15947 processor: 'boolean', 15948 default: false 15949 }); 15950 registerOption('toolbar_sticky', { 15951 processor: 'boolean', 15952 default: editor.inline 15953 }); 15954 registerOption('toolbar_sticky_offset', { 15955 processor: 'number', 15956 default: 0 15957 }); 15958 registerOption('fixed_toolbar_container', { 15959 processor: 'string', 15960 default: '' 15961 }); 15962 registerOption('fixed_toolbar_container_target', { 15963 processor: 'object' 15964 }); 15965 registerOption('ui_mode', { 15966 processor: 'string', 15967 default: 'combined' 15968 }); 15969 registerOption('file_picker_callback', { 15970 processor: 'function' 15971 }); 15972 registerOption('file_picker_validator_handler', { 15973 processor: 'function' 15974 }); 15975 registerOption('file_picker_types', { 15976 processor: 'string' 15977 }); 15978 registerOption('typeahead_urls', { 15979 processor: 'boolean', 15980 default: true 15981 }); 15982 registerOption('anchor_top', { 15983 processor: stringOrFalseProcessor, 15984 default: '#top' 15985 }); 15986 registerOption('anchor_bottom', { 15987 processor: stringOrFalseProcessor, 15988 default: '#bottom' 15989 }); 15990 registerOption('draggable_modal', { 15991 processor: 'boolean', 15992 default: false 15993 }); 15994 registerOption('statusbar', { 15995 processor: 'boolean', 15996 default: true 15997 }); 15998 registerOption('elementpath', { 15999 processor: 'boolean', 16000 default: true 16001 }); 16002 registerOption('branding', { 16003 processor: 'boolean', 16004 default: true 16005 }); 16006 registerOption('promotion', { 16007 processor: 'boolean', 16008 default: true 16009 }); 16010 registerOption('resize', { 16011 processor: (value) => value === 'both' || isBoolean(value), 16012 // Editor resize doesn't work on touch devices at this stage 16013 default: !global$7.deviceType.isTouch() 16014 }); 16015 registerOption('sidebar_show', { 16016 processor: 'string' 16017 }); 16018 registerOption('view_show', { 16019 processor: 'string' 16020 }); 16021 // This option is being registered in the theme instead of the help plugin as it cannot be accessed from the theme when registered there 16022 registerOption('help_accessibility', { 16023 processor: 'boolean', 16024 default: editor.hasPlugin('help') 16025 }); 16026 registerOption('default_font_stack', { 16027 processor: 'string[]', 16028 default: [] 16029 }); 16030 }; 16031 const isReadOnly = option$2('readonly'); 16032 const isDisabled = option$2('disabled'); 16033 const getHeightOption = option$2('height'); 16034 const getWidthOption = option$2('width'); 16035 const getMinWidthOption = wrapOptional(option$2('min_width')); 16036 const getMinHeightOption = wrapOptional(option$2('min_height')); 16037 const getMaxWidthOption = wrapOptional(option$2('max_width')); 16038 const getMaxHeightOption = wrapOptional(option$2('max_height')); 16039 const getUserStyleFormats = wrapOptional(option$2('style_formats')); 16040 const shouldMergeStyleFormats = option$2('style_formats_merge'); 16041 const shouldAutoHideStyleFormats = option$2('style_formats_autohide'); 16042 const getContentLanguages = option$2('content_langs'); 16043 const getRemovedMenuItems = option$2('removed_menuitems'); 16044 const getToolbarMode = option$2('toolbar_mode'); 16045 const getToolbarGroups = option$2('toolbar_groups'); 16046 const getToolbarLocation = option$2('toolbar_location'); 16047 const fixedContainerSelector = option$2('fixed_toolbar_container'); 16048 const fixedToolbarContainerTarget = option$2('fixed_toolbar_container_target'); 16049 const isToolbarPersist = option$2('toolbar_persist'); 16050 const getStickyToolbarOffset = option$2('toolbar_sticky_offset'); 16051 const getMenubar = option$2('menubar'); 16052 const getToolbar = option$2('toolbar'); 16053 const getFilePickerCallback = option$2('file_picker_callback'); 16054 const getFilePickerValidatorHandler = option$2('file_picker_validator_handler'); 16055 const getFontSizeInputDefaultUnit = option$2('font_size_input_default_unit'); 16056 const getFilePickerTypes = option$2('file_picker_types'); 16057 const useTypeaheadUrls = option$2('typeahead_urls'); 16058 const getAnchorTop = option$2('anchor_top'); 16059 const getAnchorBottom = option$2('anchor_bottom'); 16060 const isDraggableModal$1 = option$2('draggable_modal'); 16061 const useStatusBar = option$2('statusbar'); 16062 const useElementPath = option$2('elementpath'); 16063 const useBranding = option$2('branding'); 16064 const getResize = option$2('resize'); 16065 const getPasteAsText = option$2('paste_as_text'); 16066 const getSidebarShow = option$2('sidebar_show'); 16067 const getViewShow = option$2('view_show'); 16068 const promotionEnabled = option$2('promotion'); 16069 const useHelpAccessibility = option$2('help_accessibility'); 16070 const getDefaultFontStack = option$2('default_font_stack'); 16071 const getSkin = option$2('skin'); 16072 const isSkinDisabled = (editor) => editor.options.get('skin') === false; 16073 const isMenubarEnabled = (editor) => editor.options.get('menubar') !== false; 16074 const getSkinUrl = (editor) => { 16075 const skinUrl = editor.options.get('skin_url'); 16076 if (isSkinDisabled(editor)) { 16077 return skinUrl; 16078 } 16079 else { 16080 if (skinUrl) { 16081 return editor.documentBaseURI.toAbsolute(skinUrl); 16082 } 16083 else { 16084 const skin = editor.options.get('skin'); 16085 return global$8.baseURL + '/skins/ui/' + skin; 16086 } 16087 } 16088 }; 16089 const getSkinUrlOption = (editor) => Optional.from(editor.options.get('skin_url')); 16090 const getLineHeightFormats = (editor) => editor.options.get('line_height_formats').split(' '); 16091 const isToolbarEnabled = (editor) => { 16092 const toolbar = getToolbar(editor); 16093 const isToolbarString = isString(toolbar); 16094 const isToolbarObjectArray = isArray(toolbar) && toolbar.length > 0; 16095 // Toolbar is enabled if its value is true, a string or non-empty object array, but not string array 16096 return !isMultipleToolbars(editor) && (isToolbarObjectArray || isToolbarString || toolbar === true); 16097 }; 16098 // Convert toolbar<n> into toolbars array 16099 const getMultipleToolbarsOption = (editor) => { 16100 const toolbars = range$2(9, (num) => editor.options.get('toolbar' + (num + 1))); 16101 const toolbarArray = filter$2(toolbars, isString); 16102 return someIf(toolbarArray.length > 0, toolbarArray); 16103 }; 16104 // Check if multiple toolbars is enabled 16105 // Multiple toolbars is enabled if toolbar value is a string array or if toolbar<n> is present 16106 const isMultipleToolbars = (editor) => getMultipleToolbarsOption(editor).fold(() => { 16107 const toolbar = getToolbar(editor); 16108 return isArrayOf(toolbar, isString) && toolbar.length > 0; 16109 }, always); 16110 const isToolbarLocationBottom = (editor) => getToolbarLocation(editor) === ToolbarLocation$1.bottom; 16111 const fixedContainerTarget = (editor) => { 16112 if (!editor.inline) { 16113 // fixed_toolbar_container(_target) is only available in inline mode 16114 return Optional.none(); 16115 } 16116 const selector = fixedContainerSelector(editor) ?? ''; 16117 if (selector.length > 0) { 16118 // If we have a valid selector 16119 return descendant(body(), selector); 16120 } 16121 const element = fixedToolbarContainerTarget(editor); 16122 if (isNonNullable(element)) { 16123 // If we have a valid target 16124 return Optional.some(SugarElement.fromDom(element)); 16125 } 16126 return Optional.none(); 16127 }; 16128 const useFixedContainer = (editor) => editor.inline && fixedContainerTarget(editor).isSome(); 16129 const getUiContainer = (editor) => { 16130 const fixedContainer = fixedContainerTarget(editor); 16131 return fixedContainer.getOrThunk(() => getContentContainer(getRootNode(SugarElement.fromDom(editor.getElement())))); 16132 }; 16133 const isDistractionFree = (editor) => editor.inline && !isMenubarEnabled(editor) && !isToolbarEnabled(editor) && !isMultipleToolbars(editor); 16134 const isStickyToolbar = (editor) => { 16135 const isStickyToolbar = editor.options.get('toolbar_sticky'); 16136 return (isStickyToolbar || editor.inline) && !useFixedContainer(editor) && !isDistractionFree(editor); 16137 }; 16138 const isSplitUiMode = (editor) => !useFixedContainer(editor) && editor.options.get('ui_mode') === 'split'; 16139 const getMenus = (editor) => { 16140 const menu = editor.options.get('menu'); 16141 return map$1(menu, (menu) => ({ ...menu, items: menu.items })); 16142 }; 16143 16144 var Options = /*#__PURE__*/Object.freeze({ 16145 __proto__: null, 16146 ToolbarMode: ToolbarMode$1, 16147 ToolbarLocation: ToolbarLocation$1, 16148 getAnchorBottom: getAnchorBottom, 16149 getAnchorTop: getAnchorTop, 16150 getContentLanguages: getContentLanguages, 16151 getDefaultFontStack: getDefaultFontStack, 16152 getFilePickerCallback: getFilePickerCallback, 16153 getFilePickerTypes: getFilePickerTypes, 16154 getFilePickerValidatorHandler: getFilePickerValidatorHandler, 16155 getFontSizeInputDefaultUnit: getFontSizeInputDefaultUnit, 16156 getHeightOption: getHeightOption, 16157 getLineHeightFormats: getLineHeightFormats, 16158 getMaxHeightOption: getMaxHeightOption, 16159 getMaxWidthOption: getMaxWidthOption, 16160 getMenubar: getMenubar, 16161 getMenus: getMenus, 16162 getMinHeightOption: getMinHeightOption, 16163 getMinWidthOption: getMinWidthOption, 16164 getMultipleToolbarsOption: getMultipleToolbarsOption, 16165 getPasteAsText: getPasteAsText, 16166 getRemovedMenuItems: getRemovedMenuItems, 16167 getResize: getResize, 16168 getSidebarShow: getSidebarShow, 16169 getSkin: getSkin, 16170 getSkinUrl: getSkinUrl, 16171 getSkinUrlOption: getSkinUrlOption, 16172 getStickyToolbarOffset: getStickyToolbarOffset, 16173 getToolbar: getToolbar, 16174 getToolbarGroups: getToolbarGroups, 16175 getToolbarLocation: getToolbarLocation, 16176 getToolbarMode: getToolbarMode, 16177 getUiContainer: getUiContainer, 16178 getUserStyleFormats: getUserStyleFormats, 16179 getViewShow: getViewShow,
16180 getWidthOption: getWidthOption, 16181 isDisabled: isDisabled, 16182 isDistractionFree: isDistractionFree, 16183 isDraggableModal: isDraggableModal$1, 16184 isMenubarEnabled: isMenubarEnabled, 16185 isMultipleToolbars: isMultipleToolbars, 16186 isReadOnly: isReadOnly, 16187 isSkinDisabled: isSkinDisabled, 16188 isSplitUiMode: isSplitUiMode, 16189 isStickyToolbar: isStickyToolbar, 16190 isToolbarEnabled: isToolbarEnabled, 16191 isToolbarLocationBottom: isToolbarLocationBottom, 16192 isToolbarPersist: isToolbarPersist, 16193 promotionEnabled: promotionEnabled, 16194 register: register$f, 16195 shouldAutoHideStyleFormats: shouldAutoHideStyleFormats, 16196 shouldMergeStyleFormats: shouldMergeStyleFormats, 16197 useBranding: useBranding, 16198 useElementPath: useElementPath, 16199 useFixedContainer: useFixedContainer, 16200 useHelpAccessibility: useHelpAccessibility, 16201 useStatusBar: useStatusBar, 16202 useTypeaheadUrls: useTypeaheadUrls 16203 }); 16204 16205 // See https://developer.mozilla.org/en-US/docs/Glossary/Scroll_container for what makes an element scrollable 16206 const nonScrollingOverflows = ['visible', 'hidden', 'clip']; 16207 const isScrollingOverflowValue = (value) => trim$1(value).length > 0 && !contains$2(nonScrollingOverflows, value); 16208 const isScroller = (elem) => { 16209 if (isHTMLElement(elem)) { 16210 const overflowX = get$e(elem, 'overflow-x'); 16211 const overflowY = get$e(elem, 'overflow-y'); 16212 return isScrollingOverflowValue(overflowX) || isScrollingOverflowValue(overflowY); 16213 } 16214 else { 16215 return false; 16216 } 16217 }; 16218 const isFullscreen = (editor) => editor.plugins.fullscreen && editor.plugins.fullscreen.isFullscreen(); 16219 // NOTE: Calculating the list of scrolling ancestors each time this function is called might 16220 // be unnecessary. It will depend on its usage. 16221 const detect = (editor, popupSinkElem) => { 16222 const ancestorsScrollers = ancestors(popupSinkElem, isScroller); 16223 // If there is no scrollable container, we try to see if it's in a shadow root, and try to traverse beyond the host of shadow root to retrieve the scrollable container 16224 // If it is not within a ShadowRoot, since if there's a scrollable container as the ancestors, then it would not execute the code below, or return an empty array if it's not in a ShadowRoot 16225 const scrollers = ancestorsScrollers.length === 0 16226 ? getShadowRoot(popupSinkElem).map(getShadowHost).map((x) => ancestors(x, isScroller)).getOr([]) 16227 : ancestorsScrollers; 16228 return head(scrollers) 16229 .map((element) => ({ 16230 element, 16231 // A list of all scrolling elements above the nearest scroller, 16232 // ordered from closest to popup -> closest to top of document 16233 others: scrollers.slice(1), 16234 isFullscreen: () => isFullscreen(editor) 16235 })); 16236 }; 16237 const detectWhenSplitUiMode = (editor, popupSinkElem) => isSplitUiMode(editor) ? detect(editor, popupSinkElem) : Optional.none(); 16238 // Using all the scrolling viewports in the ancestry, limit the absolute 16239 // coordinates of window so that the bounds are limited by all the scrolling 16240 // viewports. 16241 const getBoundsFrom = (sc) => { 16242 const scrollableBoxes = [ 16243 // sc.element is the main scroller, others are *additional* scrollers above that 16244 // we need to combine all of them to constrain the bounds 16245 ...map$2(sc.others, box$1), 16246 win() 16247 ]; 16248 return sc.isFullscreen() ? win() : constrainByMany(box$1(sc.element), scrollableBoxes); 16249 }; 16250 16251 /*! @license DOMPurify 3.4.12 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.12/LICENSE */ 16252 16253 function _arrayLikeToArray(r, a) { 16254 (null == a || a > r.length) && (a = r.length); 16255 for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; 16256 return n; 16257 } 16258 function _arrayWithHoles(r) { 16259 if (Array.isArray(r)) return r; 16260 } 16261 function _iterableToArrayLimit(r, l) { 16262 var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; 16263 if (null != t) { 16264 var e, 16265 n, 16266 i, 16267 u, 16268 a = [], 16269 f = true, 16270 o = false;
vendor: 9,367 bytes, lines 16271-16518
16271 try { 16272 if (i = (t = t.call(r)).next, 0 === l) ; else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); 16273 } catch (r) { 16274 o = true, n = r; 16275 } finally { 16276 try { 16277 if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; 16278 } finally { 16279 if (o) throw n; 16280 } 16281 } 16282 return a; 16283 } 16284 } 16285 function _nonIterableRest() { 16286 throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); 16287 } 16288 function _slicedToArray(r, e) { 16289 return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); 16290 } 16291 function _unsupportedIterableToArray(r, a) { 16292 if (r) { 16293 if ("string" == typeof r) return _arrayLikeToArray(r, a); 16294 var t = {}.toString.call(r).slice(8, -1); 16295 return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; 16296 } 16297 } 16298 16299 const entries = Object.entries, 16300 setPrototypeOf = Object.setPrototypeOf, 16301 isFrozen = Object.isFrozen, 16302 getPrototypeOf = Object.getPrototypeOf, 16303 getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; 16304 let freeze = Object.freeze, 16305 seal = Object.seal, 16306 create$1 = Object.create; // eslint-disable-line import/no-mutable-exports 16307 let _ref = typeof Reflect !== 'undefined' && Reflect, 16308 apply = _ref.apply, 16309 construct = _ref.construct; 16310 if (!freeze) { 16311 freeze = function freeze(x) { 16312 return x; 16313 }; 16314 } 16315 if (!seal) { 16316 seal = function seal(x) { 16317 return x; 16318 }; 16319 } 16320 if (!apply) { 16321 apply = function apply(func, thisArg) { 16322 for (var _len = arguments.length, args = new Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) { 16323 args[_key - 2] = arguments[_key]; 16324 } 16325 return func.apply(thisArg, args); 16326 }; 16327 } 16328 if (!construct) { 16329 construct = function construct(Func) { 16330 for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { 16331 args[_key2 - 1] = arguments[_key2]; 16332 } 16333 return new Func(...args); 16334 }; 16335 } 16336 const arrayForEach = unapply(Array.prototype.forEach); 16337 const arrayLastIndexOf = unapply(Array.prototype.lastIndexOf); 16338 const arrayPop = unapply(Array.prototype.pop); 16339 const arrayPush = unapply(Array.prototype.push); 16340 const arraySplice = unapply(Array.prototype.splice); 16341 const arrayIsArray = Array.isArray; 16342 const stringToLowerCase = unapply(String.prototype.toLowerCase); 16343 const stringToString = unapply(String.prototype.toString); 16344 const stringMatch = unapply(String.prototype.match); 16345 const stringReplace = unapply(String.prototype.replace); 16346 const stringIndexOf = unapply(String.prototype.indexOf); 16347 const stringTrim = unapply(String.prototype.trim); 16348 const numberToString = unapply(Number.prototype.toString); 16349 const booleanToString = unapply(Boolean.prototype.toString); 16350 const bigintToString = typeof BigInt === 'undefined' ? null : unapply(BigInt.prototype.toString); 16351 const symbolToString = typeof Symbol === 'undefined' ? null : unapply(Symbol.prototype.toString); 16352 const objectHasOwnProperty = unapply(Object.prototype.hasOwnProperty); 16353 const objectToString = unapply(Object.prototype.toString); 16354 const regExpTest = unapply(RegExp.prototype.test); 16355 const typeErrorCreate = unconstruct(TypeError); 16356 /** 16357 * Creates a new function that calls the given function with a specified thisArg and arguments. 16358 * 16359 * @param func - The function to be wrapped and called. 16360 * @returns A new function that calls the given function with a specified thisArg and arguments. 16361 */ 16362 function unapply(func) { 16363 return function (thisArg) { 16364 if (thisArg instanceof RegExp) { 16365 thisArg.lastIndex = 0; 16366 } 16367 for (var _len3 = arguments.length, args = new Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) { 16368 args[_key3 - 1] = arguments[_key3]; 16369 } 16370 return apply(func, thisArg, args); 16371 }; 16372 } 16373 /** 16374 * Creates a new function that constructs an instance of the given constructor function with the provided arguments. 16375 * 16376 * @param func - The constructor function to be wrapped and called. 16377 * @returns A new function that constructs an instance of the given constructor function with the provided arguments. 16378 */ 16379 function unconstruct(Func) { 16380 return function () { 16381 for (var _len4 = arguments.length, args = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++) { 16382 args[_key4] = arguments[_key4]; 16383 } 16384 return construct(Func, args); 16385 }; 16386 } 16387 /** 16388 * Add properties to a lookup table 16389 * 16390 * @param set - The set to which elements will be added. 16391 * @param array - The array containing elements to be added to the set. 16392 * @param transformCaseFunc - An optional function to transform the case of each element before adding to the set. 16393 * @returns The modified set with added elements. 16394 */ 16395 function addToSet(set, array) { 16396 let transformCaseFunc = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : stringToLowerCase; 16397 if (setPrototypeOf) { 16398 // Make 'in' and truthy checks like Boolean(set.constructor) 16399 // independent of any properties defined on Object.prototype. 16400 // Prevent prototype setters from intercepting set as a this value. 16401 setPrototypeOf(set, null); 16402 } 16403 if (!arrayIsArray(array)) { 16404 return set; 16405 } 16406 let l = array.length; 16407 while (l--) { 16408 let element = array[l]; 16409 if (typeof element === 'string') { 16410 const lcElement = transformCaseFunc(element); 16411 if (lcElement !== element) { 16412 // Config presets (e.g. tags.js, attrs.js) are immutable. 16413 if (!isFrozen(array)) { 16414 array[l] = lcElement; 16415 } 16416 element = lcElement; 16417 } 16418 } 16419 set[element] = true; 16420 } 16421 return set; 16422 } 16423 /** 16424 * Clean up an array to harden against CSPP 16425 * 16426 * @param array - The array to be cleaned. 16427 * @returns The cleaned version of the array 16428 */ 16429 function cleanArray(array) { 16430 for (let index = 0; index < array.length; index++) { 16431 const isPropertyExist = objectHasOwnProperty(array, index); 16432 if (!isPropertyExist) { 16433 array[index] = null; 16434 } 16435 } 16436 return array; 16437 } 16438 /** 16439 * Shallow clone an object 16440 * 16441 * @param object - The object to be cloned. 16442 * @returns A new object that copies the original. 16443 */ 16444 function clone(object) { 16445 const newObject = create$1(null); 16446 for (const _ref2 of entries(object)) { 16447 var _ref3 = _slicedToArray(_ref2, 2); 16448 const property = _ref3[0]; 16449 const value = _ref3[1]; 16450 const isPropertyExist = objectHasOwnProperty(object, property); 16451 if (isPropertyExist) { 16452 if (arrayIsArray(value)) { 16453 newObject[property] = cleanArray(value); 16454 } else if (value && typeof value === 'object' && value.constructor === Object) { 16455 newObject[property] = clone(value); 16456 } else { 16457 newObject[property] = value; 16458 } 16459 } 16460 } 16461 return newObject; 16462 } 16463 /** 16464 * Convert non-node values into strings without depending on direct property access. 16465 * 16466 * @param value - The value to stringify. 16467 * @returns A string representation of the provided value. 16468 */ 16469 function stringifyValue(value) { 16470 switch (typeof value) { 16471 case 'string': 16472 { 16473 return value; 16474 } 16475 case 'number': 16476 { 16477 return numberToString(value); 16478 } 16479 case 'boolean': 16480 { 16481 return booleanToString(value); 16482 } 16483 case 'bigint': 16484 { 16485 return bigintToString ? bigintToString(value) : '0'; 16486 } 16487 case 'symbol': 16488 { 16489 return symbolToString ? symbolToString(value) : 'Symbol()'; 16490 } 16491 case 'undefined': 16492 { 16493 return objectToString(value); 16494 } 16495 case 'function': 16496 case 'object': 16497 { 16498 if (value === null) { 16499 return objectToString(value); 16500 } 16501 const valueAsRecord = value; 16502 const valueToString = lookupGetter(valueAsRecord, 'toString'); 16503 if (typeof valueToString === 'function') { 16504 const stringified = valueToString(valueAsRecord); 16505 return typeof stringified === 'string' ? stringified : objectToString(stringified); 16506 } 16507 return objectToString(value); 16508 } 16509 default: 16510 { 16511 return objectToString(value); 16512 } 16513 } 16514 } 16515 /** 16516 * This method automatically checks if the prop is function or getter and behaves accordingly. 16517 * 16518 * @param object - The object to look up the getter function in its prototype chain.
16519 * @param prop - The property name for which to find the getter function. 16520 * @returns The getter function found in the prototype chain or a fallback function. 16521 */ 16522 function lookupGetter(object, prop) { 16523 while (object !== null) { 16524 const desc = getOwnPropertyDescriptor(object, prop); 16525 if (desc) { 16526 if (desc.get) { 16527 return unapply(desc.get); 16528 } 16529 if (typeof desc.value === 'function') { 16530 return unapply(desc.value); 16531 } 16532 } 16533 object = getPrototypeOf(object); 16534 } 16535 function fallbackValue() { 16536 return null; 16537 } 16538 return fallbackValue; 16539 } 16540 function isRegex(value) { 16541 try { 16542 regExpTest(value, ''); 16543 return true; 16544 } catch (_unused) { 16545 return false; 16546 } 16547 } 16548 16549 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']); 16550 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']); 16551 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']); 16552 // List of SVG elements that are disallowed by default. 16553 // We still need to know them so that we can do namespace 16554 // checks properly in case one wants to add them to 16555 // allow-list. 16556 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']); 16557 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']); 16558 // Similarly to SVG, we want to know all MathML elements, 16559 // even those that we disallow by default. 16560 const mathMlDisallowed = freeze(['maction', 'maligngroup', 'malignmark', 'mlongdiv', 'mscarries', 'mscarry', 'msgroup', 'mstack', 'msline', 'msrow', 'semantics', 'annotation', 'annotation-xml', 'mprescripts', 'none']); 16561 const text$1
vendor: 15,995 bytes, lines 16561-16796
16561 = freeze(['#text']); 16562 16563 const html = freeze(['accept', 'action', 'align', 'alt', 'autocapitalize', 'autocomplete', 'autopictureinpicture', 'autoplay', 'background', 'bgcolor', 'border', 'capture', 'cellpadding', 'cellspacing', 'checked', 'cite', 'class', 'clear', 'color', 'cols', 'colspan', 'command', 'commandfor', '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']); 16564 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', 'dominant-baseline', '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-orientation', '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']); 16565 const mathMl = freeze(['accent', 'accentunder', 'align', 'bevelled', 'close', 'columnalign', 'columnlines', 'columnspacing', 'columnspan', 'denomalign', 'depth', 'dir', 'display', 'displaystyle', 'encoding', 'fence', 'frame', 'height', 'href', 'id', 'largeop', 'length', 'linethickness', 'lquote', 'lspace', 'mathbackground', 'mathcolor', 'mathsize', 'mathvariant', 'maxsize', 'minsize', 'movablelimits', 'notation', 'numalign', 'open', 'rowalign', 'rowlines', 'rowspacing', 'rowspan', 'rspace', 'rquote', 'scriptlevel', 'scriptminsize', 'scriptsizemultiplier', 'selection', 'separator', 'separators', 'stretchy', 'subscriptshift', 'supscriptshift', 'symmetric', 'voffset', 'width', 'xmlns']); 16566 const xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']); 16567 16568 const MUSTACHE_EXPR = seal(/{{[\w\W]*|^[\w\W]*}}/g); 16569 const ERB_EXPR = seal(/<%[\w\W]*|^[\w\W]*%>/g); 16570 const TMPLIT_EXPR = seal(/\${[\w\W]*/g); 16571 const DATA_ATTR = seal(/^data-[\-\w.\u00B7-\uFFFF]+$/); // eslint-disable-line no-useless-escape 16572 const ARIA_ATTR = seal(/^aria-[\-\w]+$/); // eslint-disable-line no-useless-escape 16573 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 16574 ); 16575 const IS_SCRIPT_OR_DATA = seal(/^(?:\w+script|data):/i); 16576 const ATTR_WHITESPACE = seal(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g // eslint-disable-line no-control-regex 16577 ); 16578 const DOCTYPE_NAME = seal(/^html$/i); 16579 const CUSTOM_ELEMENT = seal(/^[a-z][.\w]*(-[.\w]+)+$/i); 16580 // Markup-significant character probes used by _sanitizeElements. 16581 // Shared module-level instances are safe despite the sticky /g flags: 16582 // unapply() resets lastIndex for RegExp receivers before every call. 16583 const ELEMENT_MARKUP_PROBE = seal(/<[/\w!]/g); 16584 const COMMENT_MARKUP_PROBE = seal(/<[/\w]/g); 16585 const FALLBACK_TAG_CLOSE = seal(/<\/no(script|embed|frames)/i); 16586 const SELF_CLOSING_TAG = seal(/\/>/i); 16587 16588 // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType 16589 const NODE_TYPE = { 16590 element: 1, 16591 attribute: 2, 16592 text: 3, 16593 cdataSection: 4, 16594 entityReference: 5, 16595 // Deprecated 16596 entityNode: 6, 16597 // Deprecated 16598 processingInstruction: 7, 16599 comment: 8, 16600 document: 9, 16601 documentType: 10, 16602 documentFragment: 11, 16603 notation: 12 // Deprecated 16604 }; 16605 const getGlobal = function getGlobal() { 16606 return typeof window === 'undefined' ? null : window; 16607 }; 16608 /** 16609 * Creates a no-op policy for internal use only. 16610 * Don't export this function outside this module! 16611 * @param trustedTypes The policy factory. 16612 * @param purifyHostElement The Script element used to load DOMPurify (to determine policy name suffix). 16613 * @return The policy created (or null, if Trusted Types 16614 * are not supported or creating the policy failed). 16615 */ 16616 const _createTrustedTypesPolicy = function _createTrustedTypesPolicy(trustedTypes, purifyHostElement) { 16617 if (typeof trustedTypes !== 'object' || typeof trustedTypes.createPolicy !== 'function') { 16618 return null; 16619 } 16620 // Allow the callers to control the unique policy name 16621 // by adding a data-tt-policy-suffix to the script element with the DOMPurify. 16622 // Policy creation with duplicate names throws in Trusted Types. 16623 let suffix = null; 16624 const ATTR_NAME = 'data-tt-policy-suffix'; 16625 if (purifyHostElement && purifyHostElement.hasAttribute(ATTR_NAME)) { 16626 suffix = purifyHostElement.getAttribute(ATTR_NAME); 16627 } 16628 const policyName = 'dompurify' + (suffix ? '#' + suffix : ''); 16629 try { 16630 return trustedTypes.createPolicy(policyName, { 16631 createHTML(html) { 16632 return html; 16633 }, 16634 createScriptURL(scriptUrl) { 16635 return scriptUrl; 16636 } 16637 }); 16638 } catch (_) { 16639 // Policy creation failed (most likely another DOMPurify script has 16640 // already run). Skip creating the policy, as this will only cause errors 16641 // if TT are enforced. 16642 console.warn('TrustedTypes policy ' + policyName + ' could not be created.'); 16643 return null; 16644 } 16645 }; 16646 const _createHooksMap = function _createHooksMap() { 16647 return { 16648 afterSanitizeAttributes: [], 16649 afterSanitizeElements: [], 16650 afterSanitizeShadowDOM: [], 16651 beforeSanitizeAttributes: [], 16652 beforeSanitizeElements: [], 16653 beforeSanitizeShadowDOM: [], 16654 uponSanitizeAttribute: [], 16655 uponSanitizeElement: [], 16656 uponSanitizeShadowNode: [] 16657 }; 16658 }; 16659 /** 16660 * Resolve a set-valued configuration option: a fresh set built from 16661 * cfg[key] when it is an own array property (seeded with a clone of 16662 * options.base when given, case-normalized via options.transform), 16663 * the fallback set otherwise. 16664 * 16665 * @param cfg the cloned, prototype-free configuration object 16666 * @param key the configuration property to read 16667 * @param fallback the set to use when the option is absent or not an array 16668 * @param options transform and optional base set to merge into 16669 * @returns the resolved set 16670 */ 16671 const _resolveSetOption = function _resolveSetOption(cfg, key, fallback, options) { 16672 return objectHasOwnProperty(cfg, key) && arrayIsArray(cfg[key]) ? addToSet(options.base ? clone(options.base) : {}, cfg[key], options.transform) : fallback; 16673 }; 16674 function createDOMPurify() { 16675 let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal(); 16676 const DOMPurify = root => createDOMPurify(root); 16677 DOMPurify.version = '3.4.12'; 16678 DOMPurify.removed = []; 16679 if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) { 16680 // Not running in a browser, provide a factory function 16681 // so that you can pass your own Window 16682 DOMPurify.isSupported = false; 16683 return DOMPurify; 16684 } 16685 let document = window.document; 16686 const originalDocument = document; 16687 const currentScript = originalDocument.currentScript; 16688 window.DocumentFragment; 16689 const HTMLTemplateElement = window.HTMLTemplateElement, 16690 Node = window.Node, 16691 Element = window.Element, 16692 NodeFilter = window.NodeFilter, 16693 _window$NamedNodeMap = window.NamedNodeMap; 16694 _window$NamedNodeMap === void 0 ? window.NamedNodeMap || window.MozNamedAttrMap : _window$NamedNodeMap; 16695 window.HTMLFormElement; 16696 const DOMParser = window.DOMParser, 16697 trustedTypes = window.trustedTypes; 16698 const ElementPrototype = Element.prototype; 16699 const cloneNode = lookupGetter(ElementPrototype, 'cloneNode'); 16700 const remove = lookupGetter(ElementPrototype, 'remove'); 16701 const getNextSibling = lookupGetter(ElementPrototype, 'nextSibling'); 16702 const getChildNodes = lookupGetter(ElementPrototype, 'childNodes'); 16703 const getParentNode = lookupGetter(ElementPrototype, 'parentNode'); 16704 const getShadowRoot = lookupGetter(ElementPrototype, 'shadowRoot'); 16705 const getAttributes = lookupGetter(ElementPrototype, 'attributes'); 16706 const getNodeType = Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeType') : null; 16707 const getNodeName = Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeName') : null; 16708 // As per issue #47, the web-components registry is inherited by a 16709 // new document created via createHTMLDocument. As per the spec 16710 // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries) 16711 // a new empty registry is used when creating a template contents owner 16712 // document, so we use that as our parent document to ensure nothing 16713 // is inherited. 16714 if (typeof HTMLTemplateElement === 'function') { 16715 const template = document.createElement('template'); 16716 if (template.content && template.content.ownerDocument) { 16717 document = template.content.ownerDocument; 16718 } 16719 } 16720 let trustedTypesPolicy; 16721 let emptyHTML = ''; 16722 // The instance's own internal Trusted Types policy. Unlike a caller-supplied 16723 // `TRUSTED_TYPES_POLICY`, this is created at most once â Trusted Types throws 16724 // on duplicate policy names â and is the only policy allowed to persist 16725 // across configurations and survive `clearConfig()`. 16726 let defaultTrustedTypesPolicy; 16727 let defaultTrustedTypesPolicyResolved = false; 16728 // Tracks whether we are already inside a call to the configured Trusted Types 16729 // policy (`createHTML` or `createScriptURL`). If a supplied policy callback 16730 // itself calls `DOMPurify.sanitize` (the cause of #1422), `sanitize` would 16731 // re-enter the policy and recurse until the stack overflows. We detect that 16732 // re-entry and throw a clear, actionable error instead. The guard is shared 16733 // across both callbacks, because either one re-entering `sanitize` triggers 16734 // the same unbounded recursion. 16735 let IN_TRUSTED_TYPES_POLICY = 0; 16736 const _assertNotInTrustedTypesPolicy = function _assertNotInTrustedTypesPolicy() { 16737 if (IN_TRUSTED_TYPES_POLICY > 0) { 16738 throw typeErrorCreate('A configured TRUSTED_TYPES_POLICY callback (createHTML or ' + 'createScriptURL) must not call DOMPurify.sanitize, as that causes ' + 'infinite recursion. Do not pass a policy whose callbacks wrap ' + 'DOMPurify as TRUSTED_TYPES_POLICY; see the "DOMPurify and Trusted ' + 'Types" section of the README.'); 16739 } 16740 }; 16741 const _createTrustedHTML = function _createTrustedHTML(html) { 16742 _assertNotInTrustedTypesPolicy(); 16743 IN_TRUSTED_TYPES_POLICY++; 16744 try { 16745 return trustedTypesPolicy.createHTML(html); 16746 } finally { 16747 IN_TRUSTED_TYPES_POLICY--; 16748 } 16749 }; 16750 const _createTrustedScriptURL = function _createTrustedScriptURL(scriptUrl) { 16751 _assertNotInTrustedTypesPolicy(); 16752 IN_TRUSTED_TYPES_POLICY++; 16753 try { 16754 return trustedTypesPolicy.createScriptURL(scriptUrl); 16755 } finally { 16756 IN_TRUSTED_TYPES_POLICY--; 16757 } 16758 }; 16759 // Lazily resolve (and cache) the instance's internal default policy. 16760 // Resolution is attempted at most once: a successful `createPolicy` cannot be 16761 // repeated (Trusted Types throws on duplicate names), and a failed or 16762 // unsupported attempt must not be retried on every parse. 16763 const _getDefaultTrustedTypesPolicy = function _getDefaultTrustedTypesPolicy() { 16764 if (!defaultTrustedTypesPolicyResolved) { 16765 defaultTrustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript); 16766 defaultTrustedTypesPolicyResolved = true; 16767 } 16768 return defaultTrustedTypesPolicy; 16769 }; 16770 const _document = document, 16771 implementation = _document.implementation, 16772 createNodeIterator = _document.createNodeIterator, 16773 createDocumentFragment = _document.createDocumentFragment, 16774 getElementsByTagName = _document.getElementsByTagName; 16775 const importNode = originalDocument.importNode; 16776 let hooks = _createHooksMap(); 16777 /** 16778 * Expose whether this browser supports running the full DOMPurify. 16779 */ 16780 DOMPurify.isSupported = typeof entries === 'function' && typeof getParentNode === 'function' && implementation && implementation.createHTMLDocument !== undefined; 16781 const MUSTACHE_EXPR$1 = MUSTACHE_EXPR, 16782 ERB_EXPR$1 = ERB_EXPR, 16783 TMPLIT_EXPR$1 = TMPLIT_EXPR, 16784 DATA_ATTR$1 = DATA_ATTR, 16785 ARIA_ATTR$1 = ARIA_ATTR, 16786 IS_SCRIPT_OR_DATA$1 = IS_SCRIPT_OR_DATA, 16787 ATTR_WHITESPACE$1 = ATTR_WHITESPACE, 16788 CUSTOM_ELEMENT$1 = CUSTOM_ELEMENT; 16789 let IS_ALLOWED_URI$1 = IS_ALLOWED_URI; 16790 /** 16791 * We consider the elements and attributes below to be safe. Ideally 16792 * don't add any new ones but feel free to remove unwanted ones. 16793 */ 16794 /* allowed element names */ 16795 let ALLOWED_TAGS = null; 16796 const DEFAULT_ALLOWED_TAGS = addToSet({}, [...html$1, ...svg$1, ...svgFilters, ...mathMl$1, ...text
vendor: 1,666 bytes, lines 16796-16831
16796$1]); 16797 /* Allowed attribute names */ 16798 let ALLOWED_ATTR = null; 16799 const DEFAULT_ALLOWED_ATTR = addToSet({}, [...html, ...svg, ...mathMl, ...xml]); 16800 /* 16801 * Configure how DOMPurify should handle custom elements and their attributes as well as customized built-in elements. 16802 * @property {RegExp|Function|null} tagNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any custom elements) 16803 * @property {RegExp|Function|null} attributeNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any attributes not on the allow list) 16804 * @property {boolean} allowCustomizedBuiltInElements allow custom elements derived from built-ins if they pass CUSTOM_ELEMENT_HANDLING.tagNameCheck. Default: `false`. 16805 */ 16806 let CUSTOM_ELEMENT_HANDLING = Object.seal(create$1(null, { 16807 tagNameCheck: { 16808 writable: true, 16809 configurable: false, 16810 enumerable: true, 16811 value: null 16812 }, 16813 attributeNameCheck: { 16814 writable: true, 16815 configurable: false, 16816 enumerable: true, 16817 value: null 16818 }, 16819 allowCustomizedBuiltInElements: { 16820 writable: true, 16821 configurable: false, 16822 enumerable: true, 16823 value: false 16824 } 16825 })); 16826 /* Explicitly forbidden tags (overrides ALLOWED_TAGS/ADD_TAGS) */ 16827 let FORBID_TAGS = null; 16828 /* Explicitly forbidden attributes (overrides ALLOWED_ATTR/ADD_ATTR) */ 16829 let FORBID_ATTR = null; 16830 /* Config object to store ADD_TAGS/ADD_ATTR functions (when used as functions) */ 16831 const EXTRA_ELEMENT_HANDLING = Object.seal(create$1
vendor: 11,859 bytes, lines 16831-17032
16831(null, { 16832 tagCheck: { 16833 writable: true, 16834 configurable: false, 16835 enumerable: true, 16836 value: null 16837 }, 16838 attributeCheck: { 16839 writable: true, 16840 configurable: false, 16841 enumerable: true, 16842 value: null 16843 } 16844 })); 16845 /* Decide if ARIA attributes are okay */ 16846 let ALLOW_ARIA_ATTR = true; 16847 /* Decide if custom data attributes are okay */ 16848 let ALLOW_DATA_ATTR = true; 16849 /* Decide if unknown protocols are okay */ 16850 let ALLOW_UNKNOWN_PROTOCOLS = false; 16851 /* Decide if self-closing tags in attributes are allowed. 16852 * Usually removed due to a mXSS issue in jQuery 3.0 */ 16853 let ALLOW_SELF_CLOSE_IN_ATTR = true; 16854 /* Output should be safe for common template engines. 16855 * This means, DOMPurify removes data attributes, mustaches and ERB 16856 */ 16857 let SAFE_FOR_TEMPLATES = false; 16858 /* Output should be safe even for XML used within HTML and alike. 16859 * This means, DOMPurify removes comments when containing risky content. 16860 */ 16861 let SAFE_FOR_XML = true; 16862 /* Decide if document with <html>... should be returned */ 16863 let WHOLE_DOCUMENT = false; 16864 /* Track whether config is already set on this instance of DOMPurify. */ 16865 let SET_CONFIG = false; 16866 /* Pristine allowlist bindings captured at setConfig() time. On the 16867 * persistent-config path sanitize() restores the sets from these before 16868 * the per-walk hook clone-guard, so a hook's in-call widening cannot 16869 * carry across calls. Null until setConfig() is called; reset by 16870 * clearConfig(). */ 16871 let SET_CONFIG_ALLOWED_TAGS = null; 16872 let SET_CONFIG_ALLOWED_ATTR = null; 16873 /* Decide if all elements (e.g. style, script) must be children of 16874 * document.body. By default, browsers might move them to document.head */ 16875 let FORCE_BODY = false; 16876 /* Decide if a DOM `HTMLBodyElement` should be returned, instead of a html 16877 * string (or a TrustedHTML object if Trusted Types are supported). 16878 * If `WHOLE_DOCUMENT` is enabled a `HTMLHtmlElement` will be returned instead 16879 */ 16880 let RETURN_DOM = false; 16881 /* Decide if a DOM `DocumentFragment` should be returned, instead of a html 16882 * string (or a TrustedHTML object if Trusted Types are supported) */ 16883 let RETURN_DOM_FRAGMENT = false; 16884 /* Try to return a Trusted Type object instead of a string, return a string in 16885 * case Trusted Types are not supported */ 16886 let RETURN_TRUSTED_TYPE = false; 16887 /* Output should be free from DOM clobbering attacks? 16888 * This sanitizes markups named with colliding, clobberable built-in DOM APIs. 16889 */ 16890 let SANITIZE_DOM = true; 16891 /* Achieve full DOM Clobbering protection by isolating the namespace of named 16892 * properties and JS variables, mitigating attacks that abuse the HTML/DOM spec rules. 16893 * 16894 * HTML/DOM spec rules that enable DOM Clobbering: 16895 * - Named Access on Window (§7.3.3) 16896 * - DOM Tree Accessors (§3.1.5) 16897 * - Form Element Parent-Child Relations (§4.10.3) 16898 * - Iframe srcdoc / Nested WindowProxies (§4.8.5) 16899 * - HTMLCollection (§4.2.10.2) 16900 * 16901 * Namespace isolation is implemented by prefixing `id` and `name` attributes 16902 * with a constant string, i.e., `user-content-` 16903 */ 16904 let SANITIZE_NAMED_PROPS = false; 16905 const SANITIZE_NAMED_PROPS_PREFIX = 'user-content-'; 16906 /* Keep element content when removing element? */ 16907 let KEEP_CONTENT = true; 16908 /* If a `Node` is passed to sanitize(), then performs sanitization in-place instead 16909 * of importing it into a new Document and returning a sanitized copy */ 16910 let IN_PLACE = false; 16911 /* Allow usage of profiles like html, svg and mathMl */ 16912 let USE_PROFILES = {}; 16913 /* Tags to ignore content of when KEEP_CONTENT is true */ 16914 let FORBID_CONTENTS = null; 16915 const DEFAULT_FORBID_CONTENTS = addToSet({}, ['annotation-xml', 'audio', 'colgroup', 'desc', 'foreignobject', 'head', 'iframe', 'math', 'mi', 'mn', 'mo', 'ms', 'mtext', 'noembed', 'noframes', 'noscript', 'plaintext', 'script', 16916 // <selectedcontent> mirrors the selected <option>'s subtree, cloned by 16917 // the UA (customizable <select>) â including any on* handlers â and the 16918 // engine re-mirrors synchronously whenever a removal changes which 16919 // option/selectedcontent is current, even inside DOMPurify's inert 16920 // DOMParser document. Hoisting its children on removal re-inserts a fresh 16921 // mirror target ahead of the walk, which the engine refills, looping 16922 // forever (DoS) and amplifying output. Dropping its content on removal 16923 // (rather than hoisting) breaks that cascade; the content is a duplicate 16924 // of the option, which is sanitized on its own. See campaign-3 F1/F6. 16925 'selectedcontent', 'style', 'svg', 'template', 'thead', 'title', 'video', 'xmp']); 16926 /* Tags that are safe for data: URIs */ 16927 let DATA_URI_TAGS = null; 16928 const DEFAULT_DATA_URI_TAGS = addToSet({}, ['audio', 'video', 'img', 'source', 'image', 'track']); 16929 /* Attributes safe for values like "javascript:" */ 16930 let URI_SAFE_ATTRIBUTES = null; 16931 const DEFAULT_URI_SAFE_ATTRIBUTES = addToSet({}, ['alt', 'class', 'for', 'id', 'label', 'name', 'pattern', 'placeholder', 'role', 'summary', 'title', 'value', 'style', 'xmlns']); 16932 const MATHML_NAMESPACE = 'http://www.w3.org/1998/Math/MathML'; 16933 const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'; 16934 const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml'; 16935 /* Document namespace */ 16936 let NAMESPACE = HTML_NAMESPACE; 16937 let IS_EMPTY_INPUT = false; 16938 /* Allowed XHTML+XML namespaces */ 16939 let ALLOWED_NAMESPACES = null; 16940 const DEFAULT_ALLOWED_NAMESPACES = addToSet({}, [MATHML_NAMESPACE, SVG_NAMESPACE, HTML_NAMESPACE], stringToString); 16941 const DEFAULT_MATHML_TEXT_INTEGRATION_POINTS = freeze(['mi', 'mo', 'mn', 'ms', 'mtext']); 16942 let MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, DEFAULT_MATHML_TEXT_INTEGRATION_POINTS); 16943 const DEFAULT_HTML_INTEGRATION_POINTS = freeze(['annotation-xml']); 16944 let HTML_INTEGRATION_POINTS = addToSet({}, DEFAULT_HTML_INTEGRATION_POINTS); 16945 // Certain elements are allowed in both SVG and HTML 16946 // namespace. We need to specify them explicitly 16947 // so that they don't get erroneously deleted from 16948 // HTML namespace. 16949 const COMMON_SVG_AND_HTML_ELEMENTS = addToSet({}, ['title', 'style', 'font', 'a', 'script']); 16950 /* Parsing of strict XHTML documents */ 16951 let PARSER_MEDIA_TYPE = null; 16952 const SUPPORTED_PARSER_MEDIA_TYPES = ['application/xhtml+xml', 'text/html']; 16953 const DEFAULT_PARSER_MEDIA_TYPE = 'text/html'; 16954 let transformCaseFunc = null; 16955 /* Keep a reference to config to pass to hooks */ 16956 let CONFIG = null; 16957 /* Ideally, do not touch anything below this line */ 16958 /* ______________________________________________ */ 16959 const formElement = document.createElement('form'); 16960 const isRegexOrFunction = function isRegexOrFunction(testValue) { 16961 return testValue instanceof RegExp || testValue instanceof Function; 16962 }; 16963 /** 16964 * _parseConfig 16965 * 16966 * @param cfg optional config literal 16967 */ 16968 // eslint-disable-next-line complexity 16969 const _parseConfig = function _parseConfig() { 16970 let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 16971 if (CONFIG && CONFIG === cfg) { 16972 return; 16973 } 16974 /* Shield configuration object from tampering */ 16975 if (!cfg || typeof cfg !== 'object') { 16976 cfg = {}; 16977 } 16978 /* Shield configuration object from prototype pollution */ 16979 cfg = clone(cfg); 16980 PARSER_MEDIA_TYPE = 16981 // eslint-disable-next-line unicorn/prefer-includes 16982 SUPPORTED_PARSER_MEDIA_TYPES.indexOf(cfg.PARSER_MEDIA_TYPE) === -1 ? DEFAULT_PARSER_MEDIA_TYPE : cfg.PARSER_MEDIA_TYPE; 16983 // HTML tags and attributes are not case-sensitive, converting to lowercase. Keeping XHTML as is. 16984 transformCaseFunc = PARSER_MEDIA_TYPE === 'application/xhtml+xml' ? stringToString : stringToLowerCase; 16985 /* Set configuration parameters */ 16986 ALLOWED_TAGS = _resolveSetOption(cfg, 'ALLOWED_TAGS', DEFAULT_ALLOWED_TAGS, { 16987 transform: transformCaseFunc 16988 }); 16989 ALLOWED_ATTR = _resolveSetOption(cfg, 'ALLOWED_ATTR', DEFAULT_ALLOWED_ATTR, { 16990 transform: transformCaseFunc 16991 }); 16992 ALLOWED_NAMESPACES = _resolveSetOption(cfg, 'ALLOWED_NAMESPACES', DEFAULT_ALLOWED_NAMESPACES, { 16993 transform: stringToString 16994 }); 16995 URI_SAFE_ATTRIBUTES = _resolveSetOption(cfg, 'ADD_URI_SAFE_ATTR', DEFAULT_URI_SAFE_ATTRIBUTES, { 16996 transform: transformCaseFunc, 16997 base: DEFAULT_URI_SAFE_ATTRIBUTES 16998 }); 16999 DATA_URI_TAGS = _resolveSetOption(cfg, 'ADD_DATA_URI_TAGS', DEFAULT_DATA_URI_TAGS, { 17000 transform: transformCaseFunc, 17001 base: DEFAULT_DATA_URI_TAGS 17002 }); 17003 FORBID_CONTENTS = _resolveSetOption(cfg, 'FORBID_CONTENTS', DEFAULT_FORBID_CONTENTS, { 17004 transform: transformCaseFunc 17005 }); 17006 FORBID_TAGS = _resolveSetOption(cfg, 'FORBID_TAGS', clone({}), { 17007 transform: transformCaseFunc 17008 }); 17009 FORBID_ATTR = _resolveSetOption(cfg, 'FORBID_ATTR', clone({}), { 17010 transform: transformCaseFunc 17011 }); 17012 USE_PROFILES = objectHasOwnProperty(cfg, 'USE_PROFILES') ? cfg.USE_PROFILES && typeof cfg.USE_PROFILES === 'object' ? clone(cfg.USE_PROFILES) : cfg.USE_PROFILES : false; 17013 ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; // Default true 17014 ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true 17015 ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; // Default false 17016 ALLOW_SELF_CLOSE_IN_ATTR = cfg.ALLOW_SELF_CLOSE_IN_ATTR !== false; // Default true 17017 SAFE_FOR_TEMPLATES = cfg.SAFE_FOR_TEMPLATES || false; // Default false 17018 SAFE_FOR_XML = cfg.SAFE_FOR_XML !== false; // Default true 17019 WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT || false; // Default false 17020 RETURN_DOM = cfg.RETURN_DOM || false; // Default false 17021 RETURN_DOM_FRAGMENT = cfg.RETURN_DOM_FRAGMENT || false; // Default false 17022 RETURN_TRUSTED_TYPE = cfg.RETURN_TRUSTED_TYPE || false; // Default false 17023 FORCE_BODY = cfg.FORCE_BODY || false; // Default false 17024 SANITIZE_DOM = cfg.SANITIZE_DOM !== false; // Default true 17025 SANITIZE_NAMED_PROPS = cfg.SANITIZE_NAMED_PROPS || false; // Default false 17026 KEEP_CONTENT = cfg.KEEP_CONTENT !== false; // Default true 17027 IN_PLACE = cfg.IN_PLACE || false; // Default false 17028 IS_ALLOWED_URI$1 = isRegex(cfg.ALLOWED_URI_REGEXP) ? cfg.ALLOWED_URI_REGEXP : IS_ALLOWED_URI; // Default regexp 17029 NAMESPACE = typeof cfg.NAMESPACE === 'string' ? cfg.NAMESPACE : HTML_NAMESPACE; // Default HTML namespace 17030 MATHML_TEXT_INTEGRATION_POINTS = objectHasOwnProperty(cfg, 'MATHML_TEXT_INTEGRATION_POINTS') && cfg.MATHML_TEXT_INTEGRATION_POINTS && typeof cfg.MATHML_TEXT_INTEGRATION_POINTS === 'object' ? clone(cfg.MATHML_TEXT_INTEGRATION_POINTS) : addToSet({}, DEFAULT_MATHML_TEXT_INTEGRATION_POINTS); // Default built-in map 17031 HTML_INTEGRATION_POINTS = objectHasOwnProperty(cfg, 'HTML_INTEGRATION_POINTS') && cfg.HTML_INTEGRATION_POINTS && typeof cfg.HTML_INTEGRATION_POINTS === 'object' ? clone(cfg.HTML_INTEGRATION_POINTS) : addToSet({}, DEFAULT_HTML_INTEGRATION_POINTS); // Default built-in map 17032 const customElementHandling = objectHasOwnProperty(cfg, 'CUSTOM_ELEMENT_HANDLING') && cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING === 'object' ? clone(cfg.CUSTOM_ELEMENT_HANDLING) : create$
170321(null); 17033 CUSTOM_ELEMENT_HANDLING = create$1(null); 17034 if (objectHasOwnProperty(customElementHandling, 'tagNameCheck') && isRegexOrFunction(customElementHandling.tagNameCheck)) { 17035 CUSTOM_ELEMENT_HANDLING.tagNameCheck = customElementHandling.tagNameCheck; // Default undefined 17036 } 17037 if (objectHasOwnProperty(customElementHandling, 'attributeNameCheck') && isRegexOrFunction(customElementHandling.attributeNameCheck)) { 17038 CUSTOM_ELEMENT_HANDLING.attributeNameCheck = customElementHandling.attributeNameCheck; // Default undefined 17039 } 17040 if (objectHasOwnProperty(customElementHandling, 'allowCustomizedBuiltInElements') && typeof customElementHandling.allowCustomizedBuiltInElements === 'boolean') { 17041 CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = customElementHandling.allowCustomizedBuiltInElements; // Default undefined 17042 } 17043 seal(CUSTOM_ELEMENT_HANDLING); 17044 if (SAFE_FOR_TEMPLATES) { 17045 ALLOW_DATA_ATTR = false; 17046 } 17047 if (RETURN_DOM_FRAGMENT) { 17048 RETURN_DOM = true; 17049 } 17050 /* Parse profile info */ 17051 if (USE_PROFILES) { 17052 ALLOWED_TAGS = addToSet({}, text$1); 17053 ALLOWED_ATTR = create$1
vendor: 82,657 bytes, lines 17053-18674
17053(null); 17054 if (USE_PROFILES.html === true) { 17055 addToSet(ALLOWED_TAGS, html$1); 17056 addToSet(ALLOWED_ATTR, html); 17057 } 17058 if (USE_PROFILES.svg === true) { 17059 addToSet(ALLOWED_TAGS, svg$1); 17060 addToSet(ALLOWED_ATTR, svg); 17061 addToSet(ALLOWED_ATTR, xml); 17062 } 17063 if (USE_PROFILES.svgFilters === true) { 17064 addToSet(ALLOWED_TAGS, svgFilters); 17065 addToSet(ALLOWED_ATTR, svg); 17066 addToSet(ALLOWED_ATTR, xml); 17067 } 17068 if (USE_PROFILES.mathMl === true) { 17069 addToSet(ALLOWED_TAGS, mathMl$1); 17070 addToSet(ALLOWED_ATTR, mathMl); 17071 addToSet(ALLOWED_ATTR, xml); 17072 } 17073 } 17074 /* Always reset function-based ADD_TAGS / ADD_ATTR checks to prevent 17075 * leaking across calls when switching from function to array config */ 17076 EXTRA_ELEMENT_HANDLING.tagCheck = null; 17077 EXTRA_ELEMENT_HANDLING.attributeCheck = null; 17078 /* Merge configuration parameters */ 17079 if (objectHasOwnProperty(cfg, 'ADD_TAGS')) { 17080 if (typeof cfg.ADD_TAGS === 'function') { 17081 EXTRA_ELEMENT_HANDLING.tagCheck = cfg.ADD_TAGS; 17082 } else if (arrayIsArray(cfg.ADD_TAGS)) { 17083 if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) { 17084 ALLOWED_TAGS = clone(ALLOWED_TAGS); 17085 } 17086 addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc); 17087 } 17088 } 17089 if (objectHasOwnProperty(cfg, 'ADD_ATTR')) { 17090 if (typeof cfg.ADD_ATTR === 'function') { 17091 EXTRA_ELEMENT_HANDLING.attributeCheck = cfg.ADD_ATTR; 17092 } else if (arrayIsArray(cfg.ADD_ATTR)) { 17093 if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) { 17094 ALLOWED_ATTR = clone(ALLOWED_ATTR); 17095 } 17096 addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc); 17097 } 17098 } 17099 if (objectHasOwnProperty(cfg, 'ADD_URI_SAFE_ATTR') && arrayIsArray(cfg.ADD_URI_SAFE_ATTR)) { 17100 addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc); 17101 } 17102 if (objectHasOwnProperty(cfg, 'FORBID_CONTENTS') && arrayIsArray(cfg.FORBID_CONTENTS)) { 17103 if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) { 17104 FORBID_CONTENTS = clone(FORBID_CONTENTS); 17105 } 17106 addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc); 17107 } 17108 if (objectHasOwnProperty(cfg, 'ADD_FORBID_CONTENTS') && arrayIsArray(cfg.ADD_FORBID_CONTENTS)) { 17109 if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) { 17110 FORBID_CONTENTS = clone(FORBID_CONTENTS); 17111 } 17112 addToSet(FORBID_CONTENTS, cfg.ADD_FORBID_CONTENTS, transformCaseFunc); 17113 } 17114 /* Add #text in case KEEP_CONTENT is set to true */ 17115 if (KEEP_CONTENT) { 17116 ALLOWED_TAGS['#text'] = true; 17117 } 17118 /* Add html, head and body to ALLOWED_TAGS in case WHOLE_DOCUMENT is true */ 17119 if (WHOLE_DOCUMENT) { 17120 addToSet(ALLOWED_TAGS, ['html', 'head', 'body']); 17121 } 17122 /* Add tbody to ALLOWED_TAGS in case tables are permitted, see #286, #365 */ 17123 if (ALLOWED_TAGS.table) { 17124 addToSet(ALLOWED_TAGS, ['tbody']); 17125 delete FORBID_TAGS.tbody; 17126 } 17127 // Re-derive the active Trusted Types policy from this configuration on 17128 // every parse. The active policy must never be sticky closure state that 17129 // outlives the config that set it: a caller-supplied policy left in place 17130 // after `clearConfig()` â or after a later call that supplied none, or 17131 // `TRUSTED_TYPES_POLICY: null` â could sign a subsequent "default" 17132 // `RETURN_TRUSTED_TYPE` result with a foreign, possibly unsafe policy. 17133 // See GHSA-vxr8-fq34-vvx9. 17134 if (cfg.TRUSTED_TYPES_POLICY) { 17135 if (typeof cfg.TRUSTED_TYPES_POLICY.createHTML !== 'function') { 17136 throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.'); 17137 } 17138 if (typeof cfg.TRUSTED_TYPES_POLICY.createScriptURL !== 'function') { 17139 throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.'); 17140 } 17141 // A caller-supplied policy applies to this configuration only. 17142 const previousTrustedTypesPolicy = trustedTypesPolicy; 17143 trustedTypesPolicy = cfg.TRUSTED_TYPES_POLICY; 17144 // Sign local variables required by `sanitize`. If the supplied policy's 17145 // `createHTML` is circular (i.e. it calls `DOMPurify.sanitize`), this 17146 // throws via the re-entrancy guard. Restore the previous policy first so 17147 // the instance is not left in a poisoned state. See #1422. 17148 try { 17149 emptyHTML = _createTrustedHTML(''); 17150 } catch (error) { 17151 trustedTypesPolicy = previousTrustedTypesPolicy; 17152 throw error; 17153 } 17154 } else if (cfg.TRUSTED_TYPES_POLICY === null) { 17155 // Explicit opt-out for this call: perform no Trusted Types signing and 17156 // create nothing (so a strict `trusted-types` CSP that disallows a 17157 // `dompurify` policy can still call `sanitize` from inside its own 17158 // policy â see #1422). Resetting to `undefined` rather than a sticky 17159 // `null` also drops any previously retained caller policy, so it cannot 17160 // resurface on a later call, while still allowing the next config-less 17161 // call to restore the internal default policy. See GHSA-vxr8-fq34-vvx9. 17162 trustedTypesPolicy = undefined; 17163 emptyHTML = ''; 17164 } else { 17165 // No policy supplied: keep the currently active policy if one is set â a 17166 // previously supplied policy is intentionally sticky across config-less 17167 // calls â otherwise fall back to the instance's own internal policy, 17168 // created at most once. (A policy supplied for a *single* call still 17169 // lingers by design; what must not linger is a policy whose configuration 17170 // has been torn down via `clearConfig()`, which restores the default.) 17171 if (trustedTypesPolicy === undefined) { 17172 trustedTypesPolicy = _getDefaultTrustedTypesPolicy(); 17173 } 17174 // Sign internal variables only when a policy is active. A falsy policy 17175 // (Trusted Types unsupported, creation failed, or an explicit opt-out) 17176 // leaves `emptyHTML` as a plain string, so we never call `.createHTML` on 17177 // a non-policy and throw. See #1422. 17178 if (trustedTypesPolicy && typeof emptyHTML === 'string') { 17179 emptyHTML = _createTrustedHTML(''); 17180 } 17181 } 17182 // Prevent further manipulation of configuration. 17183 // Not available in IE8, Safari 5, etc. 17184 if (freeze) { 17185 freeze(cfg); 17186 } 17187 CONFIG = cfg; 17188 }; 17189 /* Keep track of all possible SVG and MathML tags 17190 * so that we can perform the namespace checks 17191 * correctly. */ 17192 const ALL_SVG_TAGS = addToSet({}, [...svg$1, ...svgFilters, ...svgDisallowed]); 17193 const ALL_MATHML_TAGS = addToSet({}, [...mathMl$1, ...mathMlDisallowed]); 17194 /** 17195 * Namespace rules for an element in the SVG namespace. 17196 * 17197 * @param tagName the element's lowercase tag name 17198 * @param parent the (possibly simulated) parent node 17199 * @param parentTagName the parent's lowercase tag name 17200 * @returns true if a spec-compliant parser could produce this element 17201 */ 17202 const _checkSvgNamespace = function _checkSvgNamespace(tagName, parent, parentTagName) { 17203 // The only way to switch from HTML namespace to SVG 17204 // is via <svg>. If it happens via any other tag, then 17205 // it should be killed. 17206 if (parent.namespaceURI === HTML_NAMESPACE) { 17207 return tagName === 'svg'; 17208 } 17209 // The only way to switch from MathML to SVG is via <svg> 17210 // if the parent is either <annotation-xml> or a MathML 17211 // text integration point. 17212 if (parent.namespaceURI === MATHML_NAMESPACE) { 17213 return tagName === 'svg' && (parentTagName === 'annotation-xml' || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]); 17214 } 17215 // We only allow elements that are defined in SVG 17216 // spec. All others are disallowed in SVG namespace. 17217 return Boolean(ALL_SVG_TAGS[tagName]); 17218 }; 17219 /** 17220 * Namespace rules for an element in the MathML namespace. 17221 * 17222 * @param tagName the element's lowercase tag name 17223 * @param parent the (possibly simulated) parent node 17224 * @param parentTagName the parent's lowercase tag name 17225 * @returns true if a spec-compliant parser could produce this element 17226 */ 17227 const _checkMathMlNamespace = function _checkMathMlNamespace(tagName, parent, parentTagName) { 17228 // The only way to switch from HTML namespace to MathML 17229 // is via <math>. If it happens via any other tag, then 17230 // it should be killed. 17231 if (parent.namespaceURI === HTML_NAMESPACE) { 17232 return tagName === 'math'; 17233 } 17234 // The only way to switch from SVG to MathML is via 17235 // <math> and HTML integration points 17236 if (parent.namespaceURI === SVG_NAMESPACE) { 17237 return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName]; 17238 } 17239 // We only allow elements that are defined in MathML 17240 // spec. All others are disallowed in MathML namespace. 17241 return Boolean(ALL_MATHML_TAGS[tagName]); 17242 }; 17243 /** 17244 * Namespace rules for an element in the HTML namespace. 17245 * 17246 * @param tagName the element's lowercase tag name 17247 * @param parent the (possibly simulated) parent node 17248 * @param parentTagName the parent's lowercase tag name 17249 * @returns true if a spec-compliant parser could produce this element 17250 */ 17251 const _checkHtmlNamespace = function _checkHtmlNamespace(tagName, parent, parentTagName) { 17252 // The only way to switch from SVG to HTML is via 17253 // HTML integration points, and from MathML to HTML 17254 // is via MathML text integration points 17255 if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) { 17256 return false; 17257 } 17258 if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) { 17259 return false; 17260 } 17261 // We disallow tags that are specific for MathML 17262 // or SVG and should never appear in HTML namespace 17263 return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]); 17264 }; 17265 /** 17266 * @param element a DOM element whose namespace is being checked 17267 * @returns Return false if the element has a 17268 * namespace that a spec-compliant parser would never 17269 * return. Return true otherwise. 17270 */ 17271 const _checkValidNamespace = function _checkValidNamespace(element) { 17272 let parent = getParentNode(element); 17273 // In JSDOM, if we're inside shadow DOM, then parentNode 17274 // can be null. We just simulate parent in this case. 17275 if (!parent || !parent.tagName) { 17276 parent = { 17277 namespaceURI: NAMESPACE, 17278 tagName: 'template' 17279 }; 17280 } 17281 const tagName = stringToLowerCase(element.tagName); 17282 const parentTagName = stringToLowerCase(parent.tagName); 17283 if (!ALLOWED_NAMESPACES[element.namespaceURI]) { 17284 return false; 17285 } 17286 if (element.namespaceURI === SVG_NAMESPACE) { 17287 return _checkSvgNamespace(tagName, parent, parentTagName); 17288 } 17289 if (element.namespaceURI === MATHML_NAMESPACE) { 17290 return _checkMathMlNamespace(tagName, parent, parentTagName); 17291 } 17292 if (element.namespaceURI === HTML_NAMESPACE) { 17293 return _checkHtmlNamespace(tagName, parent, parentTagName); 17294 } 17295 // For XHTML and XML documents that support custom namespaces 17296 if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && ALLOWED_NAMESPACES[element.namespaceURI]) { 17297 return true; 17298 } 17299 // The code should never reach this place (this means 17300 // that the element somehow got namespace that is not 17301 // HTML, SVG, MathML or allowed via ALLOWED_NAMESPACES). 17302 // Return false just in case. 17303 return false; 17304 }; 17305 /** 17306 * _forceRemove 17307 * 17308 * @param node a DOM node 17309 */ 17310 const _forceRemove = function _forceRemove(node) { 17311 arrayPush(DOMPurify.removed, { 17312 element: node 17313 }); 17314 try { 17315 // eslint-disable-next-line unicorn/prefer-dom-node-remove 17316 getParentNode(node).removeChild(node); 17317 } catch (_) { 17318 /* The normal detach failed â this is reached for a parentless node 17319 (getParentNode() is null, so .removeChild throws). Element.prototype 17320 .remove() is itself a spec no-op on a parentless node, so a recorded 17321 "removal" would otherwise hand the caller back an intact, 17322 payload-bearing node (e.g. a detached IN_PLACE root the mXSS canary or 17323 the style-with-element-child rule decided to kill). Fail closed by 17324 throwing â exactly as a clobbered root does at the IN_PLACE entry â 17325 rather than trying to "neutralize" the node via its own methods. 17326 Neutralizing would mean calling getAttributeNames()/removeAttribute() 17327 on the node, both of which a <form> root can clobber via a named child 17328 (and _isClobbered does not even probe getAttributeNames), so the 17329 neutralize step could itself be silently defeated, leaving the payload 17330 intact. A throw touches only the cached, clobber-safe remove() and 17331 getParentNode(). Generalizes GHSA-r47g-fvhr-h676 (clobbered-form root) 17332 to every root-kill reason. REPORT-3. 17333 This lives inside the catch, so it never fires for a normally-removed 17334 in-tree node: those have a parent, removeChild() succeeds, and the 17335 catch is not entered. Only a kept (parentless) root reaches here. */ 17336 remove(node); 17337 if (!getParentNode(node)) { 17338 throw typeErrorCreate('a node selected for removal could not be detached from its tree ' + 'and cannot be safely returned; refusing to sanitize in place'); 17339 } 17340 } 17341 }; 17342 /** 17343 * _neutralizeRoot 17344 * 17345 * Fail-closed teardown of an in-place root after the sanitize walk aborts 17346 * (campaign-3 F2). An internal throw mid-walk â e.g. a page-registered 17347 * custom element's reaction detaches a node so `_forceRemove`'s deliberate 17348 * parentless guard throws, or any other re-entrant engine mutation â would 17349 * otherwise leave the caller's *live* tree half-sanitized, with everything 17350 * after the abort point still carrying its handlers. There is no safe way 17351 * to resume the walk (the tree mutated under us), so we strip the root bare: 17352 * remove every child and every attribute, then let the caller's catch see 17353 * the original error. Clobber-safe (cached `remove`/`childNodes`/`attributes` 17354 * getters; the root was already clobber-pre-flighted at the IN_PLACE entry). 17355 * 17356 * @param root the in-place root to empty 17357 */ 17358 const _neutralizeRoot = function _neutralizeRoot(root) { 17359 /* Strip every disallowed attribute (on* handlers included) off the whole 17360 subtree BEFORE detaching anything. Detaching first would hand back 17361 handler-bearing originals (e.g. an already-loading `<img onerror>`) 17362 whose queued resource event still fires in page scope after we throw. 17363 Clobber-safe reads; a doomed clobbered node's own attributes are 17364 irrelevant while its non-clobbered descendants are reached and scrubbed. */ 17365 _neutralizeSubtree(root); 17366 const childNodes = getChildNodes(root); 17367 if (childNodes) { 17368 const snapshot = []; 17369 arrayForEach(childNodes, child => { 17370 arrayPush(snapshot, child); 17371 }); 17372 arrayForEach(snapshot, child => { 17373 try { 17374 remove(child); 17375 } catch (_) { 17376 /* Best-effort teardown; a still-attached child is handled below */ 17377 } 17378 }); 17379 } 17380 const attributes = getAttributes(root); 17381 if (attributes) { 17382 for (let i = attributes.length - 1; i >= 0; --i) { 17383 const attribute = attributes[i]; 17384 const name = attribute && attribute.name; 17385 if (typeof name === 'string') { 17386 try { 17387 root.removeAttribute(name); 17388 } catch (_) { 17389 /* Clobbered removeAttribute â ignore (fail-closed best effort) */ 17390 } 17391 } 17392 } 17393 } 17394 }; 17395 /** 17396 * _removeAttribute 17397 * 17398 * @param name an Attribute name 17399 * @param element a DOM node 17400 */ 17401 const _removeAttribute = function _removeAttribute(name, element) { 17402 try { 17403 arrayPush(DOMPurify.removed, { 17404 attribute: element.getAttributeNode(name), 17405 from: element 17406 }); 17407 } catch (_) { 17408 arrayPush(DOMPurify.removed, { 17409 attribute: null, 17410 from: element 17411 }); 17412 } 17413 element.removeAttribute(name); 17414 // We void attribute values for unremovable "is" attributes 17415 if (name === 'is') { 17416 if (RETURN_DOM || RETURN_DOM_FRAGMENT) { 17417 try { 17418 _forceRemove(element); 17419 } catch (_) {} 17420 } else { 17421 try { 17422 element.setAttribute(name, ''); 17423 } catch (_) {} 17424 } 17425 } 17426 }; 17427 /** 17428 * _stripDisallowedAttributes 17429 * 17430 * Removes every attribute the active configuration does not allow from a 17431 * single element, using the same allowlist as the main attribute pass (so 17432 * `on*` handlers go, but no `/^on/` blocklist is introduced). Used only to 17433 * neutralise nodes that are being discarded from an in-place tree. 17434 * 17435 * @param element the element to strip 17436 */ 17437 const _stripDisallowedAttributes = function _stripDisallowedAttributes(element) { 17438 const attributes = getAttributes(element); 17439 if (!attributes) { 17440 return; 17441 } 17442 for (let i = attributes.length - 1; i >= 0; --i) { 17443 const attribute = attributes[i]; 17444 const name = attribute && attribute.name; 17445 if (typeof name !== 'string' || ALLOWED_ATTR[transformCaseFunc(name)]) { 17446 continue; 17447 } 17448 try { 17449 element.removeAttribute(name); 17450 } catch (_) { 17451 /* Clobbered removeAttribute on a doomed node â ignore */ 17452 } 17453 } 17454 }; 17455 /** 17456 * _neutralizeSubtree 17457 * 17458 * Completes the audit-5 F1 fix across every removal path. The KEEP_CONTENT 17459 * move-hoist neutralises only disallowed-tag removals; clobber, mXSS-canary, 17460 * namespace, comment, processing-instruction and KEEP_CONTENT:false removals 17461 * all drop their subtree wholesale via `_forceRemove`. On the IN_PLACE path 17462 * those dropped nodes are detached from the caller's LIVE tree but a 17463 * handler-bearing original among them (an `<img onerror>`/`<video>` that was 17464 * loading) keeps its queued resource event, which fires in page scope after 17465 * sanitize returns. This walks a removed subtree and strips every attribute 17466 * the active configuration does not allow â so `on*` handlers are cancelled 17467 * through the SAME allowlist that governs kept nodes, not a separate `/^on/` 17468 * blocklist. Run synchronously before sanitize returns, i.e. before any 17469 * queued event can fire. Hook-free by design: these nodes leave the output, 17470 * so firing attribute hooks for them would be surprising. Clobber-safe reads; 17471 * a doomed clobbered node may shadow `removeAttribute` (its own attributes are 17472 * irrelevant â it is discarded â while its non-clobbered descendants, e.g. 17473 * the `<img>`, are reached and scrubbed). 17474 * 17475 * @param root the root of a removed subtree to neutralise 17476 */ 17477 const _neutralizeSubtree = function _neutralizeSubtree(root) { 17478 const stack = [root]; 17479 while (stack.length > 0) { 17480 const node = stack.pop(); 17481 const nodeType = getNodeType ? getNodeType(node) : node.nodeType; 17482 if (nodeType === NODE_TYPE.element) { 17483 _stripDisallowedAttributes(node); 17484 } 17485 const childNodes = getChildNodes(node); 17486 if (childNodes) { 17487 for (let i = childNodes.length - 1; i >= 0; --i) { 17488 stack.push(childNodes[i]); 17489 } 17490 } 17491 } 17492 }; 17493 /** 17494 * _neutralizePatchLinkage 17495 * 17496 * IN_PLACE entry pre-pass (declarative-partial-updates / streaming 17497 * hardening, https://github.com/WICG/declarative-partial-updates). 17498 * 17499 * The main walk strips patch linkage (`for`/`patchsrc`) and removes range 17500 * markers (PIs / markup comments) node-by-node, in document order, AS it 17501 * reaches each node. On a live in-place root that leaves a window: from the 17502 * moment the root is connected until the walk arrives at a given node, that 17503 * node's linkage is live. A patch applied on connection/stream can fire as 17504 * a microtask during the walk and inject or teleport an unsanitized DOM 17505 * range into a region the iterator has already passed and will not revisit, 17506 * so the post-return "tree is sanitized" contract is violated. Sweep the 17507 * whole tree once up front and sever every linkage before the walk begins, 17508 * closing that window. 17509 * 17510 * This CANNOT undo a patch that already fired before sanitize ran â that is 17511 * the irreducible "do not IN_PLACE a live-connected attacker tree" caveat â 17512 * but it closes everything from sanitize-start onward. Gated on SAFE_FOR_XML 17513 * to group with the rest of the declarative-partial-updates handling and 17514 * stay overridable, consistent with the codebase. 17515 * 17516 * Clobber-safe traversal (cached childNodes getter); per-node try/catch so a 17517 * clobbered root cannot defeat the sweep of its non-clobbered descendants. 17518 * 17519 * NOTE (pending real-Chrome confirmation, see test/declarative-patch-probe 17520 * .html Q1): this mirrors the existing policy of keeping `for` on 17521 * <label>/<output>. If the shipping feature can drive a patch through a 17522 * surviving `for`-on-label/output + `id` pair, this pre-pass and the 17523 * attribute check at _isBasicCustomElement's caller must additionally drop 17524 * that pair on the IN_PLACE path. Left as-is until the taxonomy is verified. 17525 * 17526 * @param root the in-place root to sweep 17527 */ 17528 const _neutralizePatchLinkage = function _neutralizePatchLinkage(root) { 17529 if (!SAFE_FOR_XML) { 17530 return; 17531 } 17532 const stack = [root]; 17533 while (stack.length > 0) { 17534 const node = stack.pop(); 17535 const nodeType = getNodeType ? getNodeType(node) : node.nodeType; 17536 /* Remove range markers (the target side of a patch linkage): every 17537 processing instruction, and any markup-bearing comment. */ 17538 if (nodeType === NODE_TYPE.processingInstruction || nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, node.data)) { 17539 try { 17540 remove(node); 17541 } catch (_) { 17542 /* Best-effort */ 17543 } 17544 continue; 17545 } 17546 /* Strip patch-source attributes (the source side) off elements. */ 17547 if (nodeType === NODE_TYPE.element) { 17548 const element = node; 17549 const lcTag = transformCaseFunc(getNodeName ? getNodeName(node) : node.nodeName); 17550 try { 17551 if (element.hasAttribute && element.hasAttribute('patchsrc')) { 17552 element.removeAttribute('patchsrc'); 17553 } 17554 if (element.hasAttribute && element.hasAttribute('for') && lcTag !== 'label' && lcTag !== 'output') { 17555 element.removeAttribute('for'); 17556 } 17557 } catch (_) { 17558 /* Clobbered removeAttribute/hasAttribute on a doomed node â ignore */ 17559 } 17560 } 17561 const childNodes = getChildNodes(node); 17562 if (childNodes) { 17563 for (let i = childNodes.length - 1; i >= 0; --i) { 17564 stack.push(childNodes[i]); 17565 } 17566 } 17567 } 17568 }; 17569 /** 17570 * _initDocument 17571 * 17572 * @param dirty - a string of dirty markup 17573 * @return a DOM, filled with the dirty markup 17574 */ 17575 const _initDocument = function _initDocument(dirty) { 17576 /* Create a HTML document */ 17577 let doc = null; 17578 let leadingWhitespace = null; 17579 if (FORCE_BODY) { 17580 dirty = '<remove></remove>' + dirty; 17581 } else { 17582 /* If FORCE_BODY isn't used, leading whitespace needs to be preserved manually */ 17583 const matches = stringMatch(dirty, /^[\r\n\t ]+/); 17584 leadingWhitespace = matches && matches[0]; 17585 } 17586 if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && NAMESPACE === HTML_NAMESPACE) { 17587 // Root of XHTML doc must contain xmlns declaration (see https://www.w3.org/TR/xhtml1/normative.html#strict) 17588 dirty = '<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>' + dirty + '</body></html>'; 17589 } 17590 const dirtyPayload = trustedTypesPolicy ? _createTrustedHTML(dirty) : dirty; 17591 /* 17592 * Use the DOMParser API by default, fallback later if needs be 17593 * DOMParser not work for svg when has multiple root element. 17594 */ 17595 if (NAMESPACE === HTML_NAMESPACE) { 17596 try { 17597 doc = new DOMParser().parseFromString(dirtyPayload, PARSER_MEDIA_TYPE); 17598 } catch (_) {} 17599 } 17600 /* Use createHTMLDocument in case DOMParser is not available */ 17601 if (!doc || !doc.documentElement) { 17602 doc = implementation.createDocument(NAMESPACE, 'template', null); 17603 try { 17604 doc.documentElement.innerHTML = IS_EMPTY_INPUT ? emptyHTML : dirtyPayload; 17605 } catch (_) { 17606 // Syntax error if dirtyPayload is invalid xml 17607 } 17608 } 17609 const body = doc.body || doc.documentElement; 17610 if (dirty && leadingWhitespace) { 17611 body.insertBefore(document.createTextNode(leadingWhitespace), body.childNodes[0] || null); 17612 } 17613 /* Work on whole document or just its body */ 17614 if (NAMESPACE === HTML_NAMESPACE) { 17615 return getElementsByTagName.call(doc, WHOLE_DOCUMENT ? 'html' : 'body')[0]; 17616 } 17617 return WHOLE_DOCUMENT ? doc.documentElement : body; 17618 }; 17619 /** 17620 * Creates a NodeIterator object that you can use to traverse filtered lists of nodes or elements in a document. 17621 * 17622 * @param root The root element or node to start traversing on. 17623 * @return The created NodeIterator 17624 */ 17625 const _createNodeIterator = function _createNodeIterator(root) { 17626 return createNodeIterator.call(root.ownerDocument || root, root, 17627 // eslint-disable-next-line no-bitwise 17628 NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_CDATA_SECTION, null); 17629 }; 17630 /** 17631 * Replace template expression syntax (mustache, ERB, template 17632 * literal) with a space; shared by all SAFE_FOR_TEMPLATES scrub 17633 * sites. Order matters: mustache, then ERB, then template literal. 17634 * 17635 * @param value the string to scrub 17636 * @returns the scrubbed string 17637 */ 17638 const _stripTemplateExpressions = function _stripTemplateExpressions(value) { 17639 value = stringReplace(value, MUSTACHE_EXPR$1, ' '); 17640 value = stringReplace(value, ERB_EXPR$1, ' '); 17641 value = stringReplace(value, TMPLIT_EXPR$1, ' '); 17642 return value; 17643 }; 17644 /** 17645 * Strip template-engine expressions ({{...}}, ${...}, <%...%>) from the 17646 * character data of an element subtree. Used as the final safety net for 17647 * SAFE_FOR_TEMPLATES on every DOM-returning code path so that expressions 17648 * which only form after text-node normalization (e.g. fragments split across 17649 * stripped elements) cannot survive into a template-evaluating framework. 17650 * 17651 * Walks text/comment/CDATA/processing-instruction nodes and mutates `.data` 17652 * in place rather than round-tripping through innerHTML. This preserves 17653 * descendant node references (important for IN_PLACE callers), avoids a 17654 * serialize/reparse cycle, and reads literal character data â which means 17655 * `<%...%>` in text content matches the ERB regex against its real bytes 17656 * instead of the HTML-entity-escaped form innerHTML would produce. 17657 * 17658 * Attribute values are not visited here; SAFE_FOR_TEMPLATES handling for 17659 * attributes is performed during the per-node `_sanitizeAttributes` pass. 17660 * 17661 * @param node The root element whose character data should be scrubbed. 17662 */ 17663 const _scrubTemplateExpressions2 = function _scrubTemplateExpressions(node) { 17664 var _node$querySelectorAl; 17665 node.normalize(); 17666 const walker = createNodeIterator.call(node.ownerDocument || node, node, 17667 // eslint-disable-next-line no-bitwise 17668 NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_CDATA_SECTION | NodeFilter.SHOW_PROCESSING_INSTRUCTION, null); 17669 let currentNode = walker.nextNode(); 17670 while (currentNode) { 17671 currentNode.data = _stripTemplateExpressions(currentNode.data); 17672 currentNode = walker.nextNode(); 17673 } 17674 // NodeIterator does not descend into <template>.content per the DOM spec, 17675 // so we must explicitly recurse into each template's content fragment, 17676 // mirroring the approach used by _sanitizeShadowDOM. 17677 const templates = (_node$querySelectorAl = node.querySelectorAll) === null || _node$querySelectorAl === void 0 ? void 0 : _node$querySelectorAl.call(node, 'template'); 17678 if (templates) { 17679 arrayForEach(templates, tmpl => { 17680 if (_isDocumentFragment(tmpl.content)) { 17681 _scrubTemplateExpressions2(tmpl.content); 17682 } 17683 }); 17684 } 17685 }; 17686 /** 17687 * _isClobbered 17688 * 17689 * Detect DOM-clobbering on HTMLFormElement nodes. Form is the only HTML 17690 * interface with [LegacyOverrideBuiltIns]; a descendant element with a 17691 * `name` attribute matching a prototype property shadows that property 17692 * on direct reads. We use this check at the IN_PLACE entry-point and 17693 * during attribute sanitization to refuse clobbered forms. 17694 * 17695 * @param element element to check for clobbering attacks 17696 * @return true if clobbered, false if safe 17697 */ 17698 const _isClobbered = function _isClobbered(element) { 17699 // Realm-independent tag-name probe. If we can't determine the tag 17700 // name at all, we can't reason about clobbering â return false 17701 // (the caller's other defences still apply). 17702 const realTagName = getNodeName ? getNodeName(element) : null; 17703 if (typeof realTagName !== 'string') { 17704 return false; 17705 } 17706 if (transformCaseFunc(realTagName) !== 'form') { 17707 return false; 17708 } 17709 return typeof element.nodeName !== 'string' || typeof element.textContent !== 'string' || typeof element.removeChild !== 'function' || 17710 // Realm-safe NamedNodeMap detection: equality against the cached 17711 // prototype getter. Clobbered .attributes (e.g. <input name="attributes">) 17712 // makes the direct read diverge from the cached read; a clean form 17713 // (same-realm OR foreign-realm) has both reads pointing at the same 17714 // canonical NamedNodeMap. 17715 element.attributes !== getAttributes(element) || typeof element.removeAttribute !== 'function' || typeof element.setAttribute !== 'function' || typeof element.namespaceURI !== 'string' || typeof element.insertBefore !== 'function' || typeof element.hasChildNodes !== 'function' || 17716 // NodeType clobbering probe. Cached Node.prototype.nodeType getter 17717 // returns the integer 1 for any Element regardless of realm; direct 17718 // read on a clobbered form (e.g. <input name="nodeType">) returns 17719 // the named child element. Cheap addition â nodeType is read from 17720 // an internal slot, no serialization cost â and removes a residual 17721 // clobbering surface used by several mXSS / PI / comment branches 17722 // in _sanitizeElements that compare currentNode.nodeType directly. 17723 element.nodeType !== getNodeType(element) || 17724 // HTMLFormElement has [LegacyOverrideBuiltIns]: a descendant named 17725 // "childNodes" shadows the prototype getter. Direct reads of 17726 // form.childNodes from a clobbered form return the named child 17727 // instead of the real NodeList, so any walk that reads it directly 17728 // skips the form's real children. Compare the direct read to the 17729 // cached Node.prototype getter â when the form's named-property 17730 // getter intercepts the read, the two values differ and we flag 17731 // the form. This catches every clobbering child type (input, 17732 // select, etc.) regardless of whether the named child happens to 17733 // carry a numeric .length, which a typeof-based probe would miss 17734 // (e.g. HTMLSelectElement.length is a defined unsigned-long). 17735 element.childNodes !== getChildNodes(element); 17736 }; 17737 /** 17738 * Checks whether the given value is a DocumentFragment from any realm. 17739 * 17740 * The realm-independent replacement reads `nodeType` through the cached 17741 * Node.prototype getter and compares to the DOCUMENT_FRAGMENT_NODE 17742 * constant (11). nodeType is a numeric value resolved from the node's 17743 * internal slot, identical across realms for the same kind of node. 17744 * 17745 * @param value object to check 17746 * @return true if value is a DocumentFragment-shaped node from any realm 17747 */ 17748 const _isDocumentFragment = function _isDocumentFragment(value) { 17749 if (!getNodeType || typeof value !== 'object' || value === null) { 17750 return false; 17751 } 17752 try { 17753 return getNodeType(value) === NODE_TYPE.documentFragment; 17754 } catch (_) { 17755 return false; 17756 } 17757 }; 17758 /** 17759 * Checks whether the given object is a DOM node, including nodes that 17760 * originate from a different window/realm (e.g. an iframe's 17761 * contentDocument). The previous `value instanceof Node` check was 17762 * realm-bound: nodes from a different window failed it, causing 17763 * sanitize() to silently stringify them and reset IN_PLACE to false, 17764 * returning the original node unsanitized. See GHSA-4w3q-35jp-p934. 17765 * 17766 * @param value object to check whether it's a DOM node 17767 * @return true if value is a DOM node from any realm 17768 */ 17769 const _isNode = function _isNode(value) { 17770 if (!getNodeType || typeof value !== 'object' || value === null) { 17771 return false; 17772 } 17773 try { 17774 return typeof getNodeType(value) === 'number'; 17775 } catch (_) { 17776 return false; 17777 } 17778 }; 17779 function _executeHooks(hooks, currentNode, data) { 17780 if (hooks.length === 0) { 17781 return; 17782 } 17783 arrayForEach(hooks, hook => { 17784 hook.call(DOMPurify, currentNode, data, CONFIG); 17785 }); 17786 } 17787 /** 17788 * Structural-threat checks that condemn a node regardless of the 17789 * allowlists: mXSS via namespace confusion, risky CSS construction, 17790 * processing instructions, markup-bearing comments. Pure predicate; 17791 * the caller removes. Check order is load-bearing. 17792 * 17793 * @param currentNode the node to inspect 17794 * @param tagName the node's transformCaseFunc'd tag name 17795 * @return true if the node must be removed 17796 */ 17797 const _isUnsafeNode = function _isUnsafeNode(currentNode, tagName) { 17798 /* Detect mXSS attempts abusing namespace confusion */ 17799 if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.textContent) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.innerHTML)) { 17800 return true; 17801 } 17802 /* Remove risky CSS construction leading to mXSS */ 17803 if (SAFE_FOR_XML && currentNode.namespaceURI === HTML_NAMESPACE && tagName === 'style' && _isNode(currentNode.firstElementChild)) { 17804 return true; 17805 } 17806 /* Remove any occurrence of processing instructions */ 17807 if (currentNode.nodeType === NODE_TYPE.processingInstruction) { 17808 return true; 17809 } 17810 /* Remove any kind of possibly harmful comments */ 17811 if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, currentNode.data)) { 17812 return true; 17813 } 17814 return false; 17815 }; 17816 /** 17817 * Handle a node whose tag is forbidden or not allowlisted: keep 17818 * allowed custom elements (false return exits _sanitizeElements 17819 * early - the namespace and fallback-tag removal checks are 17820 * intentionally skipped for kept custom elements), else hoist 17821 * content per KEEP_CONTENT and remove. 17822 * 17823 * A kept custom element is the ONLY case in which this function 17824 * returns false, so the caller uses that return value to run the 17825 * afterSanitizeElements hook on the kept element and keep the 17826 * element-hook lifecycle consistent with normal allowlisted 17827 * elements (GHSA-c2j3-45gr-mqc4). 17828 * 17829 * @param currentNode the disallowed node 17830 * @param tagName the node's transformCaseFunc'd tag name 17831 * @return true if the node was removed, false if kept 17832 */ 17833 const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName) { 17834 /* Check if we have a custom element to handle */ 17835 if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) { 17836 if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) { 17837 return false; 17838 } 17839 if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) { 17840 return false; 17841 } 17842 } 17843 /* Keep content except for bad-listed elements. 17844 Use the cached prototype getters exclusively â the previous code 17845 had `|| currentNode.parentNode` / `|| currentNode.childNodes` 17846 fallbacks, but the cached getters always return the canonical 17847 value (or null for a real parent-less node), so the fallback 17848 path was dead in safe cases and a clobbering surface in unsafe 17849 ones. Falsy cached results stay falsy; the `if (childNodes && 17850 parentNode)` check already gates correctly. */ 17851 if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) { 17852 const parentNode = getParentNode(currentNode); 17853 const childNodes = getChildNodes(currentNode); 17854 if (childNodes && parentNode) { 17855 const childCount = childNodes.length; 17856 /* In-place: hoist the *original* children so the iterator visits 17857 and sanitises them through the same allowlist pass as every other 17858 node. The caller built the tree in the live document, so the 17859 originals carry already-queued resource events (`<img onerror>`, 17860 `<video>`/`<audio>` error, lazy/`onload`, â¦); cloning would leave 17861 those originals detached but still armed, firing in page scope 17862 while the returned tree looked clean. Moving is safe in-place: the 17863 root is pre-validated as an allowed tag and so is never the node 17864 being removed, which keeps `parentNode` inside the iterator root 17865 and the relocated child inside the serialised tree. 17866 Otherwise (string / DOM-copy paths): clone. The iterator is rooted 17867 at â and the result serialised from â `body`, so a restrictive 17868 ALLOWED_TAGS that removes `body` itself must leave its content in 17869 place, which only cloning does; and those paths parse into an 17870 inert document, so their discarded originals never had a queued 17871 event to neutralise. 17872 `childNodes` is live; a tail-to-head walk keeps `childNodes[i]` 17873 valid whether we move (drops the trailing entry) or clone (leaves 17874 the list intact). */ 17875 for (let i = childCount - 1; i >= 0; --i) { 17876 const hoisted = IN_PLACE ? childNodes[i] : cloneNode(childNodes[i], true); 17877 parentNode.insertBefore(hoisted, getNextSibling(currentNode)); 17878 } 17879 } 17880 } 17881 _forceRemove(currentNode); 17882 return true; 17883 }; 17884 /** 17885 * _sanitizeElements 17886 * 17887 * @protect nodeName 17888 * @protect textContent 17889 * @protect removeChild 17890 * @param currentNode to check for permission to exist 17891 * @return true if node was killed, false if left alive 17892 */ 17893 // eslint-disable-next-line complexity 17894 const _sanitizeElements = function _sanitizeElements(currentNode, root) { 17895 /* Execute a hook if present */ 17896 _executeHooks(hooks.beforeSanitizeElements, currentNode, null); 17897 /* A hook may have detached the node â treat it as removed (see the 17898 detached-node comment after the uponSanitizeElement hook below). */ 17899 if (currentNode !== root && getParentNode(currentNode) === null) { 17900 return true; 17901 } 17902 /* Check if element is clobbered or can clobber */ 17903 if (_isClobbered(currentNode)) { 17904 _forceRemove(currentNode); 17905 return true; 17906 } 17907 /* Now let's check the element's type and name */ 17908 const tagName = transformCaseFunc(getNodeName ? getNodeName(currentNode) : currentNode.nodeName); 17909 /* Execute a hook if present */ 17910 _executeHooks(hooks.uponSanitizeElement, currentNode, { 17911 tagName, 17912 allowedTags: ALLOWED_TAGS 17913 }); 17914 /* A hook may have detached the node from the tree â a long-standing 17915 user pattern (issue #469; draw.io-style foreignObject filtering). 17916 Per the cached, unclobberable parentNode getter the node is 17917 genuinely out of the tree, so it can reach neither the serialized 17918 output nor an IN_PLACE live tree; treat it as removed and stop 17919 processing it. Without this guard, the unsafe-node / namespace 17920 checks below would call _forceRemove on a parentless node and hit 17921 the REPORT-3 fail-closed throw â which exists for nodes DOMPurify 17922 wants gone but *cannot* detach (clobbered / parentless roots), the 17923 opposite of a node that is already safely gone. The walk root is 17924 exempt: a detached IN_PLACE root is legitimate input and must still 17925 be fully sanitized, and a kill-decision on it must keep hitting the 17926 REPORT-3 throw. Nodes detached by hooks are the hook's 17927 responsibility: they are not recorded in DOMPurify.removed and are 17928 not neutralized by the post-walk IN_PLACE pass. */ 17929 if (currentNode !== root && getParentNode(currentNode) === null) { 17930 return true; 17931 } 17932 /* Remove mXSS vectors, processing instructions and risky comments */ 17933 if (_isUnsafeNode(currentNode, tagName)) { 17934 _forceRemove(currentNode); 17935 return true; 17936 } 17937 /* Remove element if anything forbids its presence */ 17938 if (FORBID_TAGS[tagName] || !(EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function && EXTRA_ELEMENT_HANDLING.tagCheck(tagName)) && !ALLOWED_TAGS[tagName]) { 17939 const removed = _sanitizeDisallowedNode(currentNode, tagName); 17940 /* A false return means the node is a custom element kept via 17941 CUSTOM_ELEMENT_HANDLING - the only keep path through 17942 _sanitizeDisallowedNode. Run afterSanitizeElements on it so the 17943 element-hook lifecycle matches normal allowlisted elements: a 17944 security policy applied in this hook (e.g. stripping an attribute 17945 from every surviving element) must not silently skip kept custom 17946 elements (GHSA-c2j3-45gr-mqc4). This mirrors the normal-element 17947 tail below - the hook runs, then the walker's subsequent 17948 _sanitizeAttributes pass sanitizes the element's attributes. The 17949 deliberately skipped namespace and fallback-tag removal checks stay 17950 skipped; they are removal decisions, not the hook contract. */ 17951 if (removed === false) { 17952 _executeHooks(hooks.afterSanitizeElements, currentNode, null); 17953 } 17954 return removed; 17955 } 17956 /* Check whether element has a valid namespace. 17957 Realm-safe check (GHSA-hpcv-96wg-7vj8): use the cached Node.prototype 17958 nodeType getter rather than `instanceof Element`, which is realm- 17959 bound and short-circuits to false for any node minted in a different 17960 realm â letting a foreign-realm element with a forbidden namespace 17961 slip past the namespace check entirely. */ 17962 const nt = getNodeType ? getNodeType(currentNode) : currentNode.nodeType; 17963 if (nt === NODE_TYPE.element && !_checkValidNamespace(currentNode)) { 17964 _forceRemove(currentNode); 17965 return true; 17966 } 17967 /* Make sure that older browsers don't get fallback-tag mXSS */ 17968 if ((tagName === 'noscript' || tagName === 'noembed' || tagName === 'noframes') && regExpTest(FALLBACK_TAG_CLOSE, currentNode.innerHTML)) { 17969 _forceRemove(currentNode); 17970 return true; 17971 } 17972 /* Sanitize element content to be template-safe */ 17973 if (SAFE_FOR_TEMPLATES && currentNode.nodeType === NODE_TYPE.text) { 17974 /* Get the element's text content */ 17975 const content = _stripTemplateExpressions(currentNode.textContent); 17976 if (currentNode.textContent !== content) { 17977 arrayPush(DOMPurify.removed, { 17978 element: currentNode.cloneNode() 17979 }); 17980 currentNode.textContent = content; 17981 } 17982 } 17983 /* Execute a hook if present */ 17984 _executeHooks(hooks.afterSanitizeElements, currentNode, null); 17985 return false; 17986 }; 17987 /** 17988 * _isValidAttribute 17989 * 17990 * @param lcTag Lowercase tag name of containing element. 17991 * @param lcName Lowercase attribute name. 17992 * @param value Attribute value. 17993 * @return Returns true if `value` is valid, otherwise false. 17994 */ 17995 // eslint-disable-next-line complexity 17996 const _isValidAttribute = function _isValidAttribute(lcTag, lcName, value) { 17997 /* FORBID_ATTR must always win, even if ADD_ATTR predicate would allow it */ 17998 if (FORBID_ATTR[lcName]) { 17999 return false; 18000 } 18001 /* Reject declarative-partial-updates patch-linkage attributes 18002 (https://github.com/WICG/declarative-partial-updates). 18003 Empirical note (Chrome 150, verified â see 18004 test/declarative-patch-probe-v3.html): expansion is NOT applied after 18005 sanitization. For the string path it fires during sanitize()'s own 18006 parse, so the walk sees and sanitizes the fully materialized expanded 18007 tree â teleports into MathML/SVG integration points included; a 18008 weaponized `<template for>`->`<img onerror>` comes back with the handler 18009 stripped. For the IN_PLACE path it fires on connection, before the walk. 18010 Either way DOMPurify is NOT blind to the patch. 18011 This removal is therefore defense-in-depth rather than the sole barrier: 18012 it prevents live linkage from surviving into the OUTPUT and re-expanding 18013 in the caller's context, and keeps behaviour deterministic if a future 18014 engine defers expansion. `for` is legitimate only on <label>/<output>; 18015 anywhere else (notably <template for>) it links the element to a patch 18016 target and teleports or removes an arbitrary DOM range by id/marker name. 18017 `patchsrc` fetches remote markup and is treated as a script-loading 18018 mechanism (CSP). Gated on SAFE_FOR_XML so the removal groups with the 18019 other structural-threat checks and stays overridable, consistent with 18020 the rest of the codebase. PI range markers are already removed by 18021 _isUnsafeNode. */ 18022 if (SAFE_FOR_XML && lcName === 'patchsrc') { 18023 return false; 18024 } 18025 if (SAFE_FOR_XML && lcName === 'for' && lcTag !== 'label' && lcTag !== 'output') { 18026 return false; 18027 } 18028 /* Make sure attribute cannot clobber */ 18029 if (SANITIZE_DOM && (lcName === 'id' || lcName === 'name') && (value in document || value in formElement)) { 18030 return false; 18031 } 18032 const nameIsPermitted = ALLOWED_ATTR[lcName] || EXTRA_ELEMENT_HANDLING.attributeCheck instanceof Function && EXTRA_ELEMENT_HANDLING.attributeCheck(lcName, lcTag); 18033 /* Allow valid data-* attributes: At least one character after "-" 18034 (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes) 18035 XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804) 18036 We don't need to check the value; it's always URI safe. */ 18037 if (ALLOW_DATA_ATTR && regExpTest(DATA_ATTR$1, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR$1, lcName)) ; else if (!nameIsPermitted) { 18038 if ( 18039 // First condition does a very basic check if a) it's basically a valid custom element tagname AND 18040 // b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck 18041 // and c) if the attribute name passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.attributeNameCheck 18042 _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)) || 18043 // Alternative, second condition checks if it's an `is`-attribute, AND 18044 // the value passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck 18045 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 { 18046 return false; 18047 } 18048 /* Check value is safe. First, is attr inert? If so, is safe */ 18049 } else if (URI_SAFE_ATTRIBUTES[lcName]) ; else if (regExpTest(IS_ALLOWED_URI$1, stringReplace(value, ATTR_WHITESPACE$1, ''))) ; 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$1, stringReplace(value, ATTR_WHITESPACE$1, ''))) ; else if (value) { 18050 return false; 18051 } else ; 18052 return true; 18053 }; 18054 /* Names the HTML spec reserves from valid-custom-element-name; these must 18055 * never be treated as basic custom elements even when a permissive 18056 * CUSTOM_ELEMENT_HANDLING.tagNameCheck is configured. */ 18057 const RESERVED_CUSTOM_ELEMENT_NAMES = addToSet({}, ['annotation-xml', 'color-profile', 'font-face', 'font-face-format', 'font-face-name', 'font-face-src', 'font-face-uri', 'missing-glyph']); 18058 /** 18059 * _isBasicCustomElement 18060 * checks if at least one dash is included in tagName, and it's not the first char 18061 * for more sophisticated checking see https://github.com/sindresorhus/validate-element-name 18062 * 18063 * @param tagName name of the tag of the node to sanitize 18064 * @returns Returns true if the tag name meets the basic criteria for a custom element, otherwise false. 18065 */ 18066 const _isBasicCustomElement = function _isBasicCustomElement(tagName) { 18067 return !RESERVED_CUSTOM_ELEMENT_NAMES[stringToLowerCase(tagName)] && regExpTest(CUSTOM_ELEMENT$1, tagName); 18068 }; 18069 /** 18070 * Wrap an attribute value in the matching Trusted Types object when 18071 * the active policy requires it. Namespaced attributes pass through 18072 * unchanged (no TT support yet, see 18073 * https://bugs.chromium.org/p/chromium/issues/detail?id=1305293). 18074 * 18075 * @param lcTag lowercase tag name of the containing element 18076 * @param lcName lowercase attribute name 18077 * @param namespaceURI the attribute's namespace, if any 18078 * @param value the attribute value to wrap 18079 * @return the value, wrapped when Trusted Types demand it 18080 */ 18081 const _applyTrustedTypesToAttribute = function _applyTrustedTypesToAttribute(lcTag, lcName, namespaceURI, value) { 18082 if (trustedTypesPolicy && typeof trustedTypes === 'object' && typeof trustedTypes.getAttributeType === 'function' && !namespaceURI) { 18083 switch (trustedTypes.getAttributeType(lcTag, lcName)) { 18084 case 'TrustedHTML': 18085 { 18086 return _createTrustedHTML(value); 18087 } 18088 case 'TrustedScriptURL': 18089 { 18090 return _createTrustedScriptURL(value); 18091 } 18092 } 18093 } 18094 return value; 18095 }; 18096 /** 18097 * Write a modified attribute value back onto the element. On 18098 * success, re-probe for clobbering introduced by the new value and 18099 * remove the element when found; otherwise pop the removal entry 18100 * recorded by the earlier _removeAttribute (long-standing pairing 18101 * with the SANITIZE_NAMED_PROPS path - do not "fix" casually). On 18102 * failure, remove the attribute instead. 18103 * 18104 * @param currentNode the element carrying the attribute 18105 * @param name the attribute name as present on the element 18106 * @param namespaceURI the attribute's namespace, if any 18107 * @param value the new attribute value 18108 */ 18109 const _setAttributeValue = function _setAttributeValue(currentNode, name, namespaceURI, value) { 18110 try { 18111 if (namespaceURI) { 18112 currentNode.setAttributeNS(namespaceURI, name, value); 18113 } else { 18114 /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */ 18115 currentNode.setAttribute(name, value); 18116 } 18117 if (_isClobbered(currentNode)) { 18118 _forceRemove(currentNode); 18119 } else { 18120 arrayPop(DOMPurify.removed); 18121 } 18122 } catch (_) { 18123 _removeAttribute(name, currentNode); 18124 } 18125 }; 18126 /** 18127 * _sanitizeAttributes 18128 * 18129 * @protect attributes 18130 * @protect nodeName 18131 * @protect removeAttribute 18132 * @protect setAttribute 18133 * 18134 * @param currentNode to sanitize 18135 */ 18136 const _sanitizeAttributes = function _sanitizeAttributes(currentNode) { 18137 /* Execute a hook if present */ 18138 _executeHooks(hooks.beforeSanitizeAttributes, currentNode, null); 18139 const attributes = currentNode.attributes; 18140 /* Check if we have attributes; if not we might have a text node */ 18141 if (!attributes || _isClobbered(currentNode)) { 18142 return; 18143 } 18144 const hookEvent = { 18145 attrName: '', 18146 attrValue: '', 18147 keepAttr: true, 18148 allowedAttributes: ALLOWED_ATTR, 18149 forceKeepAttr: undefined 18150 }; 18151 let l = attributes.length; 18152 const lcTag = transformCaseFunc(currentNode.nodeName); 18153 /* Go backwards over all attributes; safely remove bad ones */ 18154 while (l--) { 18155 const attr = attributes[l]; 18156 const name = attr.name, 18157 namespaceURI = attr.namespaceURI, 18158 attrValue = attr.value; 18159 const lcName = transformCaseFunc(name); 18160 const initValue = attrValue; 18161 let value = name === 'value' ? initValue : stringTrim(initValue); 18162 /* Execute a hook if present */ 18163 hookEvent.attrName = lcName; 18164 hookEvent.attrValue = value; 18165 hookEvent.keepAttr = true; 18166 hookEvent.forceKeepAttr = undefined; // Allows developers to see this is a property they can set 18167 _executeHooks(hooks.uponSanitizeAttribute, currentNode, hookEvent); 18168 value = hookEvent.attrValue; 18169 /* Full DOM Clobbering protection via namespace isolation, 18170 * Prefix id and name attributes with `user-content-` 18171 */ 18172 if (SANITIZE_NAMED_PROPS && (lcName === 'id' || lcName === 'name') && stringIndexOf(value, SANITIZE_NAMED_PROPS_PREFIX) !== 0) { 18173 // Remove the attribute with this value 18174 _removeAttribute(name, currentNode); 18175 // Prefix the value and later re-create the attribute with the sanitized value 18176 value = SANITIZE_NAMED_PROPS_PREFIX + value; 18177 } 18178 // Else: already prefixed, leave the attribute alone â the prefix is 18179 // itself the clobbering protection, and re-applying it is incorrect. 18180 /* Work around a security issue with comments inside attributes */ 18181 if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i, value)) { 18182 _removeAttribute(name, currentNode); 18183 continue; 18184 } 18185 /* Make sure we cannot easily use animated hrefs, even if animations are allowed */ 18186 if (lcName === 'attributename' && stringMatch(value, 'href')) { 18187 _removeAttribute(name, currentNode); 18188 continue; 18189 } 18190 /* Did the hooks force-keep the attribute? */ 18191 if (hookEvent.forceKeepAttr) { 18192 continue; 18193 } 18194 /* Did the hooks approve of the attribute? */ 18195 if (!hookEvent.keepAttr) { 18196 _removeAttribute(name, currentNode); 18197 continue; 18198 } 18199 /* Work around a security issue in jQuery 3.0 */ 18200 if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(SELF_CLOSING_TAG, value)) { 18201 _removeAttribute(name, currentNode); 18202 continue; 18203 } 18204 /* Sanitize attribute content to be template-safe */ 18205 if (SAFE_FOR_TEMPLATES) { 18206 value = _stripTemplateExpressions(value); 18207 } 18208 /* Is `value` valid for this attribute? */ 18209 if (!_isValidAttribute(lcTag, lcName, value)) { 18210 _removeAttribute(name, currentNode); 18211 continue; 18212 } 18213 /* Handle attributes that require Trusted Types */ 18214 value = _applyTrustedTypesToAttribute(lcTag, lcName, namespaceURI, value); 18215 /* Handle invalid data-* attribute set by try-catching it */ 18216 if (value !== initValue) { 18217 _setAttributeValue(currentNode, name, namespaceURI, value); 18218 } 18219 } 18220 /* Execute a hook if present */ 18221 _executeHooks(hooks.afterSanitizeAttributes, currentNode, null); 18222 }; 18223 /** 18224 * _sanitizeShadowDOM 18225 * 18226 * @param fragment to iterate over recursively 18227 */ 18228 const _sanitizeShadowDOM2 = function _sanitizeShadowDOM(fragment) { 18229 let shadowNode = null; 18230 const shadowIterator = _createNodeIterator(fragment); 18231 /* Execute a hook if present */ 18232 _executeHooks(hooks.beforeSanitizeShadowDOM, fragment, null); 18233 while (shadowNode = shadowIterator.nextNode()) { 18234 /* Execute a hook if present */ 18235 _executeHooks(hooks.uponSanitizeShadowNode, shadowNode, null); 18236 /* Sanitize tags and elements */ 18237 _sanitizeElements(shadowNode, fragment); 18238 /* Check attributes next */ 18239 _sanitizeAttributes(shadowNode); 18240 /* Deep shadow DOM detected. 18241 Realm-safe check (GHSA-hpcv-96wg-7vj8): use nodeType against the 18242 DOCUMENT_FRAGMENT_NODE constant rather than instanceof, so we 18243 recurse into <template>.content from foreign realms too. */ 18244 if (_isDocumentFragment(shadowNode.content)) { 18245 _sanitizeShadowDOM2(shadowNode.content); 18246 } 18247 /* An element iterated here may itself host an attached 18248 shadow root. The default NodeIterator does not enter shadow 18249 trees, so a shadow root nested inside template.content was 18250 previously reached by no walk at all (the pre-pass at 18251 _sanitizeAttachedShadowRoots descends via childNodes, which 18252 doesn't enter template.content; the template-content recursion 18253 above iterates the content but never inspected shadowRoot). 18254 Walk it explicitly. The nodeType guard avoids reading 18255 shadowRoot off text / comment / CDATA / PI nodes that the 18256 iterator also surfaces. */ 18257 const shadowNodeType = getNodeType ? getNodeType(shadowNode) : shadowNode.nodeType; 18258 if (shadowNodeType === NODE_TYPE.element) { 18259 const innerSr = getShadowRoot(shadowNode); 18260 if (_isDocumentFragment(innerSr)) { 18261 _sanitizeAttachedShadowRoots(innerSr); 18262 _sanitizeShadowDOM2(innerSr); 18263 } 18264 } 18265 } 18266 /* Execute a hook if present */ 18267 _executeHooks(hooks.afterSanitizeShadowDOM, fragment, null); 18268 }; 18269 /** 18270 * _sanitizeAttachedShadowRoots 18271 * 18272 * Walks `root` and feeds every attached shadow root we encounter into 18273 * the existing _sanitizeShadowDOM pipeline. The default node iterator 18274 * does not descend into shadow trees, so nodes inside an attached 18275 * shadow root would otherwise be skipped entirely. 18276 * 18277 * Two real input paths put attached shadow roots in front of us: 18278 * 1. IN_PLACE on a DOM node that already has shadow roots attached. 18279 * 2. DOM-node input where importNode(dirty, true) deep-clones the 18280 * shadow root because it was created with `clonable: true`. 18281 * 18282 * This pass runs once, up front, so the main iteration loop (and the 18283 * existing _sanitizeShadowDOM template-content recursion) stay 18284 * untouched â string-input paths are not affected. 18285 * 18286 * @param root the subtree root to walk for attached shadow roots 18287 */ 18288 const _sanitizeAttachedShadowRoots = function _sanitizeAttachedShadowRoots(root) { 18289 /* Iterative (explicit stack) rather than per-child recursion. DOM APIs 18290 impose no depth cap, so an attacker-shaped tree (JSON/CRDT/editor data 18291 built straight into the DOM â the IN_PLACE surface) deeper than the JS 18292 call-stack budget would otherwise overflow native recursion here and 18293 throw at the IN_PLACE entry pre-pass, before a single node is 18294 sanitized, leaving the caller's live tree untouched (fail-open). See 18295 campaign-3 F4. A heap stack keeps depth off the call stack. 18296 Each work item is either a node to descend into, or a deferred 18297 `_sanitizeShadowDOM` for an already-walked shadow root. The deferred 18298 form preserves the original post-order discipline: a shadow root's 18299 nested shadow roots are discovered before the outer shadow is 18300 sanitized (which may remove hosts). Pushes are in reverse of the 18301 desired processing order (LIFO): template content, then children, then 18302 the shadow-sanitize, then the shadow walk â so the order matches the 18303 previous recursion exactly. */ 18304 const stack = [{ 18305 node: root, 18306 shadow: null 18307 }]; 18308 while (stack.length > 0) { 18309 const item = stack.pop(); 18310 /* Deferred shadow-DOM sanitisation: runs after its subtree was walked. */ 18311 if (item.shadow) { 18312 _sanitizeShadowDOM2(item.shadow); 18313 continue; 18314 } 18315 const node = item.node; 18316 const nodeType = getNodeType ? getNodeType(node) : node.nodeType; 18317 const isElement = nodeType === NODE_TYPE.element; 18318 /* (pushed last â processed first) Children, snapshotted in reverse so 18319 the first child is processed first. Snapshotting matters because a 18320 hook may detach siblings mid-walk. */ 18321 const childNodes = getChildNodes(node); 18322 if (childNodes) { 18323 for (let i = childNodes.length - 1; i >= 0; --i) { 18324 stack.push({ 18325 node: childNodes[i], 18326 shadow: null 18327 }); 18328 } 18329 } 18330 /* (pushed before children â processed after them, matching the old 18331 "template content last" order) When the node is a <template>, 18332 descend into its content. */ 18333 if (isElement) { 18334 const rootName = getNodeName ? getNodeName(node) : null; 18335 if (typeof rootName === 'string' && transformCaseFunc(rootName) === 'template') { 18336 const content = node.content; 18337 if (_isDocumentFragment(content)) { 18338 stack.push({ 18339 node: content, 18340 shadow: null 18341 }); 18342 } 18343 } 18344 } 18345 /* Shadow root (processed first): walk its subtree, then sanitise it. 18346 Realm-safe check (GHSA-hpcv-96wg-7vj8): nodeType-based detection 18347 rather than `instanceof DocumentFragment`, which is realm-bound and 18348 silently skipped foreign-realm shadow roots (e.g. 18349 iframe.contentDocument attachShadow). */ 18350 if (isElement) { 18351 const sr = getShadowRoot(node); 18352 if (_isDocumentFragment(sr)) { 18353 /* Push the deferred sanitise first so it pops after the shadow 18354 walk we push next, i.e. nested shadow roots are discovered 18355 before this one is sanitised. */ 18356 stack.push({ 18357 node: null, 18358 shadow: sr 18359 }, { 18360 node: sr, 18361 shadow: null 18362 }); 18363 } 18364 } 18365 } 18366 }; 18367 // eslint-disable-next-line complexity 18368 DOMPurify.sanitize = function (dirty) { 18369 let cfg = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 18370 let body = null; 18371 let importedNode = null; 18372 let currentNode = null; 18373 let returnNode = null; 18374 /* Make sure we have a string to sanitize. 18375 DO NOT return early, as this will return the wrong type if 18376 the user has requested a DOM object rather than a string */ 18377 IS_EMPTY_INPUT = !dirty; 18378 if (IS_EMPTY_INPUT) { 18379 dirty = '<!-->'; 18380 } 18381 /* Stringify, in case dirty is an object */ 18382 if (typeof dirty !== 'string' && !_isNode(dirty)) { 18383 dirty = stringifyValue(dirty); 18384 if (typeof dirty !== 'string') { 18385 throw typeErrorCreate('dirty is not a string, aborting'); 18386 } 18387 } 18388 /* Return dirty HTML if DOMPurify cannot run */ 18389 if (!DOMPurify.isSupported) { 18390 return dirty; 18391 } 18392 /* Assign config vars */ 18393 if (SET_CONFIG) { 18394 /* Persistent setConfig() path: _parseConfig is skipped, so the sets are 18395 * not re-derived per call. Restore them from the pristine bindings 18396 * captured at setConfig() time so a previous call's hook clone (mutated 18397 * below) does not carry over. */ 18398 ALLOWED_TAGS = SET_CONFIG_ALLOWED_TAGS; 18399 ALLOWED_ATTR = SET_CONFIG_ALLOWED_ATTR; 18400 } else { 18401 _parseConfig(cfg); 18402 } 18403 /* Clone the hook-mutable allowlists before the walk whenever an 18404 * uponSanitize* hook is registered. The hook event exposes ALLOWED_TAGS 18405 * and ALLOWED_ATTR by reference (as allowedTags / allowedAttributes), so 18406 * a hook that widens them would otherwise mutate the shared set 18407 * permanently: across later calls and across every element. Cloning per 18408 * walk keeps documented in-call widening working while scoping it to the 18409 * call. A single guard for both config paths - the per-call path rebinds 18410 * the sets in _parseConfig each call, the persistent path restores them 18411 * from the captured bindings just above - so the two cannot diverge. */ 18412 if (hooks.uponSanitizeElement.length > 0 || hooks.uponSanitizeAttribute.length > 0) { 18413 ALLOWED_TAGS = clone(ALLOWED_TAGS); 18414 } 18415 if (hooks.uponSanitizeAttribute.length > 0) { 18416 ALLOWED_ATTR = clone(ALLOWED_ATTR); 18417 } 18418 /* Clean up removed elements */ 18419 DOMPurify.removed = []; 18420 /* Resolve IN_PLACE for this call without mutating persistent config. 18421 Writing the IN_PLACE closure variable here leaks under setConfig(), 18422 where _parseConfig is skipped on later calls: a single string call would 18423 disable in-place mode for every subsequent node call, returning a 18424 sanitized copy while leaving the caller's node â which in-place callers 18425 keep using and whose return value they ignore â unsanitized. REPORT-2. */ 18426 const inPlace = IN_PLACE && typeof dirty !== 'string' && _isNode(dirty); 18427 if (inPlace) { 18428 /* Declarative-partial-updates / streaming pre-pass: sever every patch 18429 linkage across the live tree BEFORE the walk, so no patch can fire 18430 mid-walk and inject into an already-processed region. Runs first, so 18431 it also covers the forbidden/clobbered roots that throw below. */ 18432 _neutralizePatchLinkage(dirty); 18433 /* Do some early pre-sanitization to avoid unsafe root nodes. 18434 Read nodeName through the cached prototype getter â a clobbering 18435 child named "nodeName" on the form root would otherwise shadow 18436 the property and let this check skip the root-allowlist 18437 validation entirely. */ 18438 const nn = getNodeName ? getNodeName(dirty) : dirty.nodeName; 18439 if (typeof nn === 'string') { 18440 const tagName = transformCaseFunc(nn); 18441 if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) { 18442 /* Fail closed on a live root: neutralize handlers/children before 18443 throwing, exactly as the mid-walk abort path does. */ 18444 _neutralizeRoot(dirty); 18445 throw typeErrorCreate('root node is forbidden and cannot be sanitized in-place'); 18446 } 18447 } 18448 /* Pre-flight the root through _isClobbered. The iterator-driven 18449 removal path can not detach a parent-less root: _forceRemove 18450 falls through to Element.prototype.remove(), which per spec 18451 is a no-op on a node with no parent. A clobbered root would 18452 then survive the main loop with its attributes uninspected, 18453 because _sanitizeAttributes early-returns on _isClobbered. The 18454 result would be an attacker-controlled form, complete with any 18455 event-handler attributes the caller passed in, handed back to 18456 the application unsanitized. Refuse to sanitize such a root 18457 the same way we refuse a forbidden tag. GHSA-r47g-fvhr-h676. */ 18458 if (_isClobbered(dirty)) { 18459 /* Fail closed on a live clobbered root before throwing. 18460 _neutralizeRoot's reads are clobber-safe (cached getters); the 18461 form's non-clobbered descendants, e.g. an armed <img>, are scrubbed. */ 18462 _neutralizeRoot(dirty); 18463 throw typeErrorCreate('root node is clobbered and cannot be sanitized in-place'); 18464 } 18465 /* Sanitize attached shadow roots before the main iterator runs. 18466 The iterator does not descend into shadow trees. Same fail-closed 18467 barrier as the main walk (campaign-3 F2): a custom-element reaction 18468 inside a shadow root could abort this pre-pass before the walk runs, 18469 which would otherwise leave the entire live tree unsanitized. */ 18470 try { 18471 _sanitizeAttachedShadowRoots(dirty); 18472 } catch (error) { 18473 _neutralizeRoot(dirty); 18474 throw error; 18475 } 18476 } else if (_isNode(dirty)) { 18477 /* If dirty is a DOM element, append to an empty document to avoid 18478 elements being stripped by the parser */ 18479 body = _initDocument('<!---->'); 18480 importedNode = body.ownerDocument.importNode(dirty, true); 18481 if (importedNode.nodeType === NODE_TYPE.element && importedNode.nodeName === 'BODY') { 18482 /* Node is already a body, use as is */ 18483 body = importedNode; 18484 } else if (importedNode.nodeName === 'HTML') { 18485 body = importedNode; 18486 } else { 18487 // eslint-disable-next-line unicorn/prefer-dom-node-append 18488 body.appendChild(importedNode); 18489 } 18490 /* Clonable shadow roots are deep-cloned by importNode(); sanitize 18491 them before the main iterator runs, since the iterator does not 18492 descend into shadow trees. The walk routes every read through a 18493 cached prototype getter so clobbering descendants on a form root 18494 cannot hide a shadow host from this pass. */ 18495 _sanitizeAttachedShadowRoots(importedNode); 18496 } else { 18497 /* Exit directly if we have nothing to do */ 18498 if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT && 18499 // eslint-disable-next-line unicorn/prefer-includes 18500 dirty.indexOf('<') === -1) { 18501 return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? _createTrustedHTML(dirty) : dirty; 18502 } 18503 /* Initialize the document to work on */ 18504 body = _initDocument(dirty); 18505 /* Check we have a DOM node from the data */ 18506 if (!body) { 18507 return RETURN_DOM ? null : RETURN_TRUSTED_TYPE ? emptyHTML : ''; 18508 } 18509 } 18510 /* Remove first element node (ours) if FORCE_BODY is set */ 18511 if (body && FORCE_BODY) { 18512 _forceRemove(body.firstChild); 18513 } 18514 /* Get node iterator */ 18515 const walkRoot = inPlace ? dirty : body; 18516 const nodeIterator = _createNodeIterator(walkRoot); 18517 /* Now start iterating over the created document. 18518 The walk runs inside an exception barrier (campaign-3 F2): a re-entrant 18519 engine/custom-element mutation can detach a node mid-walk so 18520 `_forceRemove`'s parentless guard throws, aborting the loop. Without the 18521 barrier the caller's in-place tree would be left half-sanitized with the 18522 unvisited tail still armed. On any throw we fail closed â strip the 18523 in-place root bare â then rethrow so the existing throw contract is 18524 preserved. (String/DOM-copy paths never return the partial body, so the 18525 propagating throw is already fail-closed there.) */ 18526 try { 18527 while (currentNode = nodeIterator.nextNode()) { 18528 /* Sanitize tags and elements */ 18529 _sanitizeElements(currentNode, walkRoot); 18530 /* Check attributes next */ 18531 _sanitizeAttributes(currentNode); 18532 /* Shadow DOM detected, sanitize it. 18533 Realm-safe check (GHSA-hpcv-96wg-7vj8): nodeType-based detection 18534 instead of instanceof, so foreign-realm <template>.content is 18535 walked correctly. */ 18536 if (_isDocumentFragment(currentNode.content)) { 18537 _sanitizeShadowDOM2(currentNode.content); 18538 } 18539 } 18540 } catch (error) { 18541 if (inPlace) { 18542 _neutralizeRoot(dirty); 18543 /* Nodes _forceRemove'd earlier in the aborted walk are already 18544 detached from the root, so _neutralizeRoot's subtree pass does not 18545 reach them. Defuse them too, mirroring the success-path loop below. */ 18546 arrayForEach(DOMPurify.removed, entry => { 18547 if (entry.element) { 18548 _neutralizeSubtree(entry.element); 18549 } 18550 }); 18551 } 18552 throw error; 18553 } 18554 /* If we sanitized `dirty` in-place, return it. */ 18555 if (inPlace) { 18556 /* Fail-closed completion of the audit-5 F1 fix: every node removed from 18557 the caller's live tree is detached but may still hold a queued 18558 resource-event handler that fires in page scope after we return. The 18559 move-hoist covers only disallowed-tag KEEP_CONTENT removals; strip the 18560 non-allow-listed attributes off every other removed subtree (clobber, 18561 mXSS, namespace, comments, KEEP_CONTENT:false, â¦) so those handlers are 18562 cancelled before any event can fire. Runs synchronously, pre-return. */ 18563 arrayForEach(DOMPurify.removed, entry => { 18564 if (entry.element) { 18565 _neutralizeSubtree(entry.element); 18566 } 18567 }); 18568 if (SAFE_FOR_TEMPLATES) { 18569 _scrubTemplateExpressions2(dirty); 18570 } 18571 return dirty; 18572 } 18573 /* Return sanitized string or DOM */ 18574 if (RETURN_DOM) { 18575 if (SAFE_FOR_TEMPLATES) { 18576 _scrubTemplateExpressions2(body); 18577 } 18578 if (RETURN_DOM_FRAGMENT) { 18579 returnNode = createDocumentFragment.call(body.ownerDocument); 18580 while (body.firstChild) { 18581 // eslint-disable-next-line unicorn/prefer-dom-node-append 18582 returnNode.appendChild(body.firstChild); 18583 } 18584 } else { 18585 returnNode = body; 18586 } 18587 if (ALLOWED_ATTR.shadowroot || ALLOWED_ATTR.shadowrootmode) { 18588 /* 18589 AdoptNode() is not used because internal state is not reset 18590 (e.g. the past names map of a HTMLFormElement), this is safe 18591 in theory but we would rather not risk another attack vector. 18592 The state that is cloned by importNode() is explicitly defined 18593 by the specs. 18594 */ 18595 returnNode = importNode.call(originalDocument, returnNode, true); 18596 } 18597 return returnNode; 18598 } 18599 let serializedHTML = WHOLE_DOCUMENT ? body.outerHTML : body.innerHTML; 18600 /* Serialize doctype if allowed */ 18601 if (WHOLE_DOCUMENT && ALLOWED_TAGS['!doctype'] && body.ownerDocument && body.ownerDocument.doctype && body.ownerDocument.doctype.name && regExpTest(DOCTYPE_NAME, body.ownerDocument.doctype.name)) { 18602 serializedHTML = '<!DOCTYPE ' + body.ownerDocument.doctype.name + '>\n' + serializedHTML; 18603 } 18604 /* Sanitize final string template-safe */ 18605 if (SAFE_FOR_TEMPLATES) { 18606 serializedHTML = _stripTemplateExpressions(serializedHTML); 18607 } 18608 return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? _createTrustedHTML(serializedHTML) : serializedHTML; 18609 }; 18610 DOMPurify.setConfig = function () { 18611 let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 18612 _parseConfig(cfg); 18613 SET_CONFIG = true; 18614 SET_CONFIG_ALLOWED_TAGS = ALLOWED_TAGS; 18615 SET_CONFIG_ALLOWED_ATTR = ALLOWED_ATTR; 18616 }; 18617 DOMPurify.clearConfig = function () { 18618 CONFIG = null; 18619 SET_CONFIG = false; 18620 SET_CONFIG_ALLOWED_TAGS = null; 18621 SET_CONFIG_ALLOWED_ATTR = null; 18622 // Drop any caller-supplied Trusted Types policy so it cannot poison later 18623 // `RETURN_TRUSTED_TYPE` output. The internal default policy (cached, and 18624 // never recreated â Trusted Types throws on duplicate names) is restored by 18625 // the next `_parseConfig`. See GHSA-vxr8-fq34-vvx9. 18626 trustedTypesPolicy = defaultTrustedTypesPolicy; 18627 emptyHTML = ''; 18628 }; 18629 DOMPurify.isValidAttribute = function (tag, attr, value) { 18630 /* Initialize shared config vars if necessary. */ 18631 if (!CONFIG) { 18632 _parseConfig({}); 18633 } 18634 const lcTag = transformCaseFunc(tag); 18635 const lcName = transformCaseFunc(attr); 18636 return _isValidAttribute(lcTag, lcName, value); 18637 }; 18638 DOMPurify.addHook = function (entryPoint, hookFunction) { 18639 if (typeof hookFunction !== 'function') { 18640 return; 18641 } 18642 /* Reject unknown entry points. Without this, a non-hook key (e.g. 18643 * '__proto__') indexes off the prototype chain rather than a real 18644 * hook array, and arrayPush then writes to Object.prototype. Guard 18645 * with an own-property check against the known hook names. */ 18646 if (!objectHasOwnProperty(hooks, entryPoint)) { 18647 return; 18648 } 18649 arrayPush(hooks[entryPoint], hookFunction); 18650 }; 18651 DOMPurify.removeHook = function (entryPoint, hookFunction) { 18652 if (!objectHasOwnProperty(hooks, entryPoint)) { 18653 return undefined; 18654 } 18655 if (hookFunction !== undefined) { 18656 const index = arrayLastIndexOf(hooks[entryPoint], hookFunction); 18657 return index === -1 ? undefined : arraySplice(hooks[entryPoint], index, 1)[0]; 18658 } 18659 return arrayPop(hooks[entryPoint]); 18660 }; 18661 DOMPurify.removeHooks = function (entryPoint) { 18662 if (!objectHasOwnProperty(hooks, entryPoint)) { 18663 return; 18664 } 18665 hooks[entryPoint] = []; 18666 }; 18667 DOMPurify.removeAllHooks = function () { 18668 hooks = _createHooksMap(); 18669 }; 18670 return DOMPurify; 18671 } 18672 var purify = createDOMPurify(); 18673 18674
18674const sanitizeHtmlString = (html) => purify().sanitize(html); 18675 18676 var global$6 = tinymce.util.Tools.resolve('tinymce.util.I18n'); 18677 18678 // Icons that need to be transformed in RTL 18679 const rtlTransform = { 18680 'indent': true, 18681 'outdent': true, 18682 'table-insert-column-after': true, 18683 'table-insert-column-before': true, 18684 'paste-column-after': true, 18685 'paste-column-before': true, 18686 'unordered-list': true, 18687 'list-bull-circle': true, 18688 'list-bull-disc': true, 18689 'list-bull-default': true, 18690 'list-bull-square': true 18691 }; 18692 const defaultIconName = 'temporary-placeholder'; 18693 const defaultIcon = (icons) => () => get$h(icons, defaultIconName).getOr('!not found!'); 18694 const getIconName = (name, icons) => { 18695 const lcName = name.toLowerCase();
18696 // If in rtl mode then try to see if we have a rtl icon to use instead 18697 if (global$6.isRtl()) { 18698 const rtlName = ensureTrailing(lcName, '-rtl'); 18699 return has$2(icons, rtlName) ? rtlName : lcName; 18700 } 18701 else { 18702 return lcName; 18703 } 18704 }; 18705 const lookupIcon = (name, icons) => get$h(icons, getIconName(name, icons)); 18706 const get = (name, iconProvider) => { 18707 const icons = iconProvider(); 18708 return lookupIcon(name, icons).getOrThunk(defaultIcon(icons)); 18709 }; 18710 const getOr = (name, iconProvider, fallbackIcon) => { 18711 const icons = iconProvider(); 18712 return lookupIcon(name, icons).or(fallbackIcon).getOrThunk(defaultIcon(icons)); 18713 }; 18714 const needsRtlTransform = (iconName) => global$6.isRtl() ? has$2(rtlTransform, iconName) : false; 18715 const addFocusableBehaviour = () => config('add-focusable', [ 18716 runOnAttached((comp) => { 18717 // set focusable=false on SVGs to prevent focusing the toolbar when tabbing into the editor 18718 child(comp.element, 'svg').each((svg) => set$9(svg, 'focusable', 'false')); 18719 }) 18720 ]); 18721 const renderIcon$3 = (spec, iconName, icons, fallbackIcon) => { 18722 // If RTL, add the flip icon class if the icon doesn't have a `-rtl` icon available. 18723 const rtlIconClasses = needsRtlTransform(iconName) ? ['tox-icon--flip'] : []; 18724 const iconHtml = get$h(icons, getIconName(iconName, icons)).or(fallbackIcon).getOrThunk(defaultIcon(icons)); 18725 return { 18726 dom: { 18727 tag: spec.tag, 18728 attributes: spec.attributes ?? {}, 18729 classes: spec.classes.concat(rtlIconClasses), 18730 innerHtml: iconHtml 18731 }, 18732 behaviours: derive$1([ 18733 ...spec.behaviours ?? [], 18734 addFocusableBehaviour() 18735 ]), 18736 eventOrder: spec.eventOrder ?? {} 18737 }; 18738 }; 18739 const render$4 = (iconName, spec, iconProvider, fallbackIcon = Optional.none()) => renderIcon$3(spec, iconName, iconProvider(), fallbackIcon); 18740 const renderFirst = (iconNames, spec, iconProvider) => { 18741 const icons = iconProvider(); 18742 const iconName = find$5(iconNames, (name) => has$2(icons, getIconName(name, icons))); 18743 return renderIcon$3(spec, iconName.getOr(defaultIconName), icons, Optional.none()); 18744 }; 18745 18746 const notificationIconMap = { 18747 success: 'checkmark', 18748 error: 'warning', 18749 err: 'error', 18750 warning: 'warning', 18751 warn: 'warning', 18752 info: 'info' 18753 }; 18754 const factory$4 = (detail) => { 18755 // For using the alert banner as a standalone banner 18756 const notificationTextId = generate$6('notification-text'); 18757 const memBannerText = record({ 18758 dom: fromHtml(`<p id=${notificationTextId}>${sanitizeHtmlString(detail.backstageProvider.translate(detail.text))}</p>`), 18759 behaviours: derive$1([ 18760 Replacing.config({}) 18761 ]) 18762 }); 18763 const renderPercentBar = (percent) => ({ 18764 dom: { 18765 tag: 'div', 18766 classes: ['tox-bar'], 18767 styles: { 18768 width: `${percent}%` 18769 } 18770 } 18771 }); 18772 const renderPercentText = (percent) => ({ 18773 dom: { 18774 tag: 'div', 18775 classes: ['tox-text'], 18776 innerHtml: `${percent}%` 18777 } 18778 }); 18779 const memBannerProgress = record({ 18780 dom: { 18781 tag: 'div', 18782 classes: detail.progress ? ['tox-progress-bar', 'tox-progress-indicator'] : ['tox-progress-bar'] 18783 }, 18784 components: [ 18785 { 18786 dom: { 18787 tag: 'div', 18788 classes: ['tox-bar-container'] 18789 }, 18790 components: [ 18791 renderPercentBar(0) 18792 ] 18793 }, 18794 renderPercentText(0) 18795 ], 18796 behaviours: derive$1([ 18797 Replacing.config({}) 18798 ]) 18799 }); 18800 const updateProgress = (comp, percent) => { 18801 if (comp.getSystem().isConnected()) { 18802 memBannerProgress.getOpt(comp).each((progress) => { 18803 Replacing.set(progress, [ 18804 { 18805 dom: { 18806 tag: 'div', 18807 classes: ['tox-bar-container'] 18808 }, 18809 components: [ 18810 renderPercentBar(percent) 18811 ] 18812 }, 18813 renderPercentText(percent) 18814 ]); 18815 }); 18816 } 18817 }; 18818 const updateText = (comp, text) => { 18819 if (comp.getSystem().isConnected()) { 18820 const banner = memBannerText.get(comp); 18821 Replacing.set(banner, [ 18822 text$2(text) 18823 ]); 18824 } 18825 }; 18826 const apis = { 18827 updateProgress, 18828 updateText 18829 }; 18830 const iconChoices = flatten([ 18831 detail.icon.toArray(), 18832 [detail.level], 18833 Optional.from(notificationIconMap[detail.level]).toArray() 18834 ]); 18835 const memButton = record(Button.sketch({ 18836 dom: { 18837 tag: 'button', 18838 classes: ['tox-notification__dismiss', 'tox-button', 'tox-button--naked', 'tox-button--icon'], 18839 attributes: { 18840 'aria-label': detail.backstageProvider.translate('Close') 18841 } 18842 }, 18843 components: [ 18844 render$4('close', { 18845 tag: 'span', 18846 classes: ['tox-icon'], 18847 }, detail.iconProvider) 18848 ], 18849 buttonBehaviours: derive$1([ 18850 Tabstopping.config({}), 18851 Tooltipping.config({ 18852 ...detail.backstageProvider.tooltips.getConfig({ 18853 tooltipText: detail.backstageProvider.translate('Close') 18854 }) 18855 }) 18856 ]), 18857 action: (comp) => { 18858 detail.onAction(comp); 18859 } 18860 })); 18861 const notificationIconSpec = renderFirst(iconChoices, { tag: 'div', classes: ['tox-notification__icon'] }, detail.iconProvider); 18862 const notificationBodySpec = { 18863 dom: { 18864 tag: 'div', 18865 classes: ['tox-notification__body'] 18866 }, 18867 components: [ 18868 memBannerText.asSpec() 18869 ], 18870 behaviours: derive$1([ 18871 Replacing.config({}) 18872 ]) 18873 }; 18874 const components = [notificationIconSpec, notificationBodySpec]; 18875 return { 18876 uid: detail.uid, 18877 dom: { 18878 tag: 'div', 18879 attributes: { 18880 'role': 'alert', 18881 'aria-labelledby': notificationTextId 18882 }, 18883 classes: ['tox-notification', 'tox-notification--in', `tox-notification--${detail.level}`], 18884 }, 18885 behaviours: derive$1([ 18886 Tabstopping.config({}), 18887 Focusing.config({}), 18888 Keying.config({
18889 mode: 'special', 18890 onEscape: (comp) => { 18891 detail.onAction(comp); 18892 return Optional.some(true); 18893 } 18894 }) 18895 ]), 18896 components: components 18897 .concat(detail.progress ? [memBannerProgress.asSpec()] : []) 18898 .concat([memButton.asSpec()]), 18899 apis 18900 }; 18901 }; 18902 const Notification = single({ 18903 name: 'Notification', 18904 factory: factory$4, 18905 configFields: [ 18906 defaultedStringEnum('level', 'info', ['success', 'error', 'warning', 'warn', 'info']), 18907 required$1('progress'), 18908 option$3('icon'), 18909 required$1('onAction'), 18910 required$1('text'), 18911 required$1('iconProvider'), 18912 required$1('backstageProvider'), 18913 ], 18914 apis: { 18915 updateProgress: (apis, comp, percent) => { 18916 apis.updateProgress(comp, percent); 18917 }, 18918 updateText: (apis, comp, text) => { 18919 apis.updateText(comp, text); 18920 } 18921 } 18922 }); 18923 18924 var NotificationManagerImpl = (editor, extras, uiMothership, notificationRegion) => { 18925 const sharedBackstage = extras.backstage.shared; 18926 const getBoundsContainer = () => SugarElement.fromDom(editor.queryCommandValue('ToggleView') === '' ? editor.getContentAreaContainer() : editor.getContainer()); 18927 const getBounds = () => { 18928 const contentArea = box$1(getBoundsContainer()); 18929 return Optional.some(contentArea); 18930 }; 18931 const clampComponentsToBounds = (components) => { 18932 getBounds().each((bounds) => { 18933 each$1(components, (comp) => { 18934 remove$6(comp.element, 'width'); 18935 if (get$c(comp.element) > bounds.width) { 18936 set$7(comp.element, 'width', bounds.width + 'px'); 18937 } 18938 }); 18939 }); 18940 }; 18941 const open = (settings, closeCallback, isEditorOrUIFocused) => { 18942 const close = () => { 18943 const removeNotificationAndReposition = (region) => { 18944 Replacing.remove(region, notification); 18945 reposition(); 18946 }; 18947 const manageRegionVisibility = (region, editorOrUiFocused) => { 18948 if (children(region.element).length === 0) { 18949 handleEmptyRegion(region, editorOrUiFocused); 18950 } 18951 else { 18952 handleRegionWithChildren(region, editorOrUiFocused); 18953 } 18954 }; 18955 const handleEmptyRegion = (region, editorOrUIFocused) => { 18956 InlineView.hide(region); 18957 notificationRegion.clear(); 18958 if (editorOrUIFocused) { 18959 editor.focus(); 18960 } 18961 }; 18962 const handleRegionWithChildren = (region, editorOrUIFocused) => { 18963 if (editorOrUIFocused) { 18964 Keying.focusIn(region); 18965 } 18966 }; 18967 notificationRegion.on((region) => { 18968 closeCallback(); 18969 const editorOrUIFocused = isEditorOrUIFocused(); 18970 removeNotificationAndReposition(region); 18971 manageRegionVisibility(region, editorOrUIFocused); 18972 }); 18973 }; 18974 const shouldApplyDocking = () => !isStickyToolbar(editor) || !sharedBackstage.header.isPositionedAtTop(); 18975 const notification = build$1(Notification.sketch({ 18976 text: settings.text, 18977 level: contains$2(['success', 'error', 'warning', 'warn', 'info'], settings.type) ? settings.type : undefined, 18978 progress: settings.progressBar === true, 18979 icon: settings.icon, 18980 onAction: close, 18981 iconProvider: sharedBackstage.providers.icons, 18982 backstageProvider: sharedBackstage.providers, 18983 })); 18984 if (!notificationRegion.isSet()) { 18985 const notificationWrapper = build$1(InlineView.sketch({ 18986 dom: { 18987 tag: 'div', 18988 classes: ['tox-notifications-container'], 18989 attributes: { 18990 'aria-label': 'Notifications', 18991 'role': 'region' 18992 } 18993 }, 18994 lazySink: sharedBackstage.getSink, 18995 fireDismissalEventInstead: {}, 18996 ...sharedBackstage.header.isPositionedAtTop() ? {}
18996 : { fireRepositionEventInstead: {} }, 18997 inlineBehaviours: derive$1([ 18998 Keying.config({ 18999 mode: 'cyclic', 19000 selector: '.tox-notification, .tox-notification a, .tox-notification button', 19001 }), 19002 Replacing.config({}), 19003 ...(shouldApplyDocking() 19004 ? [ 19005 Docking.config({ 19006 contextual: { 19007 lazyContext: () => Optional.some(box$1(getBoundsContainer())), 19008 fadeInClass: 'tox-notification-container-dock-fadein', 19009 fadeOutClass: 'tox-notification-container-dock-fadeout', 19010 transitionClass: 'tox-notification-container-dock-transition' 19011 }, 19012 modes: ['top'], 19013 lazyViewport: (comp) => { 19014 const optScrollingContext = detectWhenSplitUiMode(editor, comp.element); 19015 return optScrollingContext 19016 .map((sc) => { 19017 const combinedBounds = getBoundsFrom(sc); 19018 return { 19019 bounds: combinedBounds, 19020 optScrollEnv: Optional.some({ 19021 currentScrollTop: sc.element.dom.scrollTop, 19022 scrollElmTop: absolute$3(sc.element).top 19023 }) 19024 }; 19025 }).getOrThunk(() => ({ 19026 bounds: win(), 19027 optScrollEnv: Optional.none() 19028 })); 19029 } 19030 }) 19031 ] : []) 19032 ]) 19033 })); 19034 const notificationSpec = premade(notification); 19035 const anchorOverrides = { 19036 maxHeightFunction: expandable$1() 19037 }; 19038 const anchor = { 19039 ...sharedBackstage.anchors.banner(), 19040 overrides: anchorOverrides 19041 }; 19042 notificationRegion.set(notificationWrapper); 19043 uiMothership.add(notificationWrapper); 19044 InlineView.showWithinBounds(notificationWrapper, notificationSpec, { anchor }, getBounds); 19045 } 19046 else { 19047 const notificationSpec = premade(notification); 19048 notificationRegion.on((notificationWrapper) => { 19049 Replacing.append(notificationWrapper, notificationSpec); 19050 InlineView.reposition(notificationWrapper); 19051 if (notification.hasConfigured(Docking)) { 19052 Docking.refresh(notificationWrapper); 19053 } 19054 clampComponentsToBounds(notificationWrapper.components()); 19055 }); 19056 } 19057 if (isNumber(settings.timeout) && settings.timeout > 0) { 19058 global$a.setEditorTimeout(editor, () => { 19059 close(); 19060 }, settings.timeout); 19061 } 19062 const reposition = () => { 19063 notificationRegion.on((region) => { 19064 InlineView.reposition(region); 19065 if (region.hasConfigured(Docking)) { 19066 Docking.refresh(region); 19067 } 19068 clampComponentsToBounds(region.components()); 19069 }); 19070 }; 19071 const thisNotification = { 19072 close, 19073 reposition, 19074 text: (nuText) => { 19075 // check if component is still mounted 19076 Notification.updateText(notification, nuText); 19077 }, 19078 settings, 19079 getEl: () => notification.element.dom, 19080 progressBar: { 19081 value: (percent) => { 19082 Notification.updateProgress(notification, percent); 19083 } 19084 } 19085 }; 19086 return thisNotification; 19087 }; 19088 const close = (notification) => { 19089 notification.close(); 19090 }; 19091 const getArgs = (notification) => { 19092 return notification.settings; 19093 }; 19094 return { 19095 open, 19096 close, 19097 getArgs 19098 }; 19099 }; 19100 19101 const setup$c = (api, editor) => { 19102 const redirectKeyToItem = (item, e) => { 19103 emitWith(item, keydown(), { raw: e }); 19104 }; 19105 const getItem = () => api.getMenu().bind(Highlighting.getHighlighted);
19106 editor.on('keydown', (e) => { 19107 const keyCode = e.which; 19108 // If the autocompleter isn't activated then do nothing 19109 if (!api.isActive()) { 19110 return; 19111 } 19112 if (api.isMenuOpen()) { 19113 // Pressing <enter> executes any item currently selected, or does nothing 19114 if (keyCode === 13) { 19115 getItem().each(emitExecute); 19116 e.preventDefault(); 19117 // Pressing <down> either highlights the first option, or moves down the menu 19118 } 19119 else if (keyCode === 40) { 19120 getItem().fold( 19121 // No current item, so highlight the first one 19122 () => { 19123 api.getMenu().each(Highlighting.highlightFirst); 19124 }, 19125 // There is a current item, so move down in the menu 19126 (item) => { 19127 redirectKeyToItem(item, e); 19128 }); 19129 e.preventDefault(); 19130 e.stopImmediatePropagation(); 19131 // Pressing <up>, <left>, <right> gets redirected to the selected item 19132 } 19133 else if (keyCode === 37 || keyCode === 38 || keyCode === 39) { 19134 getItem().each((item) => { 19135 redirectKeyToItem(item, e); 19136 e.preventDefault(); 19137 e.stopImmediatePropagation(); 19138 }); 19139 } 19140 } 19141 else { 19142 // Pressing <enter>, <down> or <up> closes the autocompleter when it's active but the menu isn't open 19143 if (keyCode === 13 || keyCode === 38 || keyCode === 40) { 19144 api.cancelIfNecessary(); 19145 } 19146 } 19147 }); 19148 editor.on('NodeChange', () => { 19149 // Close if active, not in the middle of an onAction callback and we're no longer inside the autocompleter span 19150 if (api.isActive() && !api.isProcessingAction() && !editor.queryCommandState('mceAutoCompleterInRange')) { 19151 api.cancelIfNecessary(); 19152 } 19153 }); 19154 }; 19155 const AutocompleterEditorEvents = { 19156 setup: setup$c 19157 }; 19158 19159 var ItemResponse; 19160 (function (ItemResponse) { 19161 ItemResponse[ItemResponse["CLOSE_ON_EXECUTE"] = 0] = "CLOSE_ON_EXECUTE"; 19162 ItemResponse[ItemResponse["BUBBLE_TO_SANDBOX"] = 1] = "BUBBLE_TO_SANDBOX"; 19163 })(ItemResponse || (ItemResponse = {})); 19164 var ItemResponse$1 = ItemResponse; 19165 19166 const navClass = 'tox-menu-nav__js'; 19167 const selectableClass = 'tox-collection__item'; 19168 const colorClass = 'tox-swatch'; 19169 const presetClasses = { 19170 normal: navClass, 19171 color: colorClass 19172 }; 19173 const tickedClass = 'tox-collection__item--enabled'; 19174 const groupHeadingClass = 'tox-collection__group-heading'; 19175 const iconClass = 'tox-collection__item-icon'; 19176 const imageClass = 'tox-collection__item-image'; 19177 const imageSelectorClasll = 'tox-collection__item-image-selector'; 19178 const textClass = 'tox-collection__item-label'; 19179 const accessoryClass = 'tox-collection__item-accessory'; 19180 const caretClass = 'tox-collection__item-caret'; 19181 const checkmarkClass = 'tox-collection__item-checkmark'; 19182 const activeClass = 'tox-collection__item--active'; 19183 const containerClass = 'tox-collection__item-container'; 19184 const containerColumnClass = 'tox-collection__item-container--column'; 19185 const containerRowClass = 'tox-collection__item-container--row'; 19186 const containerAlignRightClass = 'tox-collection__item-container--align-right'; 19187 const containerAlignLeftClass = 'tox-collection__item-container--align-left'; 19188 const containerValignTopClass = 'tox-collection__item-container--valign-top'; 19189 const containerValignMiddleClass = 'tox-collection__item-container--valign-middle'; 19190 const containerValignBottomClass = 'tox-collection__item-container--valign-bottom'; 19191 const classForPreset = (presets) => get$h(presetClasses, presets).getOr(navClass); 19192 19193 const forMenu = (presets) => {
19194 if (presets === 'color') { 19195 return 'tox-swatches'; 19196 } 19197 else { 19198 return 'tox-menu'; 19199 } 19200 }; 19201 const classes = (presets) => ({ 19202 backgroundMenu: 'tox-background-menu', 19203 selectedMenu: 'tox-selected-menu', 19204 selectedItem: 'tox-collection__item--active', 19205 hasIcons: 'tox-menu--has-icons', 19206 menu: forMenu(presets), 19207 tieredMenu: 'tox-tiered-menu' 19208 }); 19209 19210 const markers = (presets) => { 19211 const menuClasses = classes(presets); 19212 return { 19213 backgroundMenu: menuClasses.backgroundMenu, 19214 selectedMenu: menuClasses.selectedMenu, 19215 menu: menuClasses.menu, 19216 selectedItem: menuClasses.selectedItem, 19217 item: classForPreset(presets) 19218 }; 19219 }; 19220 const dom = (hasIcons, columns, presets) => { 19221 const menuClasses = classes(presets); 19222 return { 19223 tag: 'div', 19224 classes: flatten([ 19225 [menuClasses.menu, `tox-menu-${columns}-column`], 19226 hasIcons ? [menuClasses.hasIcons] : [] 19227 ]) 19228 }; 19229 }; 19230 const components = [ 19231 Menu.parts.items({}) 19232 ]; 19233 // NOTE: Up to here. 19234 const part = (hasIcons, columns, presets) => { 19235 const menuClasses = classes(presets); 19236 const d = { 19237 tag: 'div', 19238 classes: flatten([ 19239 [menuClasses.tieredMenu] 19240 ]) 19241 }; 19242 return { 19243 dom: d, 19244 markers: markers(presets) 19245 }; 19246 }; 19247 19248 // This event is triggered by a menu item from a dropdown when it wants the 19249 // dropdown to refetch its contents based on a search string. 19250 const refetchTriggerEvent = generate$6('refetch-trigger-event'); 19251 // This event is triggerd by a menu item from a dropdown, when it wants to 19252 // redispatch that event to the currently active item of that dropdown menu. It will 19253 // be used in situations where the event should be firing on the item with fake focus, 19254 // but instead it is firing on the item with real focus (e.g of real focus: 19255 // menu search field) 19256 const redirectMenuItemInteractionEvent = generate$6('redirect-menu-item-interaction'); 19257 19258 // This is not stored in ItemClasses, because the searcher is not actually 19259 // contained within items. It isn't part of their navigation, and it 19260 // isn't maintained by menus. It is just part of the first menu, but 19261 // not its items. 19262 const menuSearcherClass = 'tox-menu__searcher'; 19263 // Ideally, we'd be using mementos to find it again, but we'd need to pass 19264 // that memento onto the dropdown, which isn't going to have it. Especially, 19265 // because the dropdown isn't responsible for putting this searcher component 19266 // into the menu, NestedMenus is. 19267 const findWithinSandbox = (sandboxComp) => { 19268 return descendant(sandboxComp.element, `.${menuSearcherClass}`).bind((inputElem) => sandboxComp.getSystem().getByDom(inputElem).toOptional()); 19269 }; 19270 // There is nothing sandbox-specific about this code. It just needs to be 19271 // a container that wraps the search field. 19272 const findWithinMenu = findWithinSandbox; 19273 const restoreState = (inputComp, searcherState) => { 19274 Representing.setValue(inputComp, searcherState.fetchPattern); 19275 inputComp.element.dom.selectionStart = searcherState.selectionStart; 19276 inputComp.element.dom.selectionEnd = searcherState.selectionEnd; 19277 }; 19278 const saveState = (inputComp) => { 19279 const fetchPattern = Representing.getValue(inputComp); 19280 const selectionStart = inputComp.element.dom.selectionStart; 19281 const selectionEnd = inputComp.element.dom.selectionEnd; 19282 return { 19283 fetchPattern, 19284 selectionStart, 19285 selectionEnd 19286 }; 19287 }; 19288 // Make sure there is ARIA communicating the currently active item in the results. 19289 const setActiveDescendant = (inputComp, active) => { 19290 getOpt(active.element, 'id') 19291 .each((id) => set$9(inputComp.element, 'aria-activedescendant', id)); 19292 }; 19293 const renderMenuSearcher = (spec) => { 19294 const handleByBrowser = (comp, se) => { 19295 // We "cut" this event, so that the browser still handles it, but it is not processed 19296 // by any of the above alloy components. We could also do this by stopping propagation, 19297 // but not preventing default, but it's probably good to allow some overarching thing
vendor: 5,325 bytes, lines 19298-19374
19298 // in the DOM (outside of alloy) to stop it if they want to. 19299 se.cut(); 19300 // Returning a Some here (regardless of boolean value) is going to call `stop` on the 19301 // simulated event, which is going to call: preventDefault and stopPropagation. We want 19302 // neither of these things to happen, so we return None here to say that it hasn't been 19303 // handled. But because we've cut it, it will not propagate to any other alloy components 19304 return Optional.none(); 19305 }; 19306 const handleByHighlightedItem = (comp, se) => { 19307 // Because we need to redispatch based on highlighted items that we don't know about here, 19308 // we are going to emit an event, that the sandbox listens to, and the sandbox will 19309 // redispatch the event. 19310 const eventData = { 19311 interactionEvent: se.event, 19312 eventType: se.event.raw.type 19313 }; 19314 emitWith(comp, redirectMenuItemInteractionEvent, eventData); 19315 return Optional.some(true); 19316 }; 19317 const customSearcherEventsName = 'searcher-events'; 19318 return { 19319 dom: { 19320 tag: 'div', 19321 // NOTE: This is very intentionally NOT the navigation class, because 19322 // we don't want the searcher to be part of the navigation. This class 19323 // is just for styling consistency. Perhaps it should be its own class. 19324 classes: [selectableClass] 19325 }, 19326 components: [ 19327 Input.sketch({ 19328 inputClasses: [menuSearcherClass, 'tox-textfield'], 19329 inputAttributes: { 19330 ...(spec.placeholder.map((placeholder) => ({ placeholder: spec.i18n(placeholder) })).getOr({})), 19331 // This ARIA is based on the algolia example documented in TINY-8952 19332 'type': 'search', 19333 'aria-autocomplete': 'list' 19334 }, 19335 inputBehaviours: derive$1([ 19336 config(customSearcherEventsName, [ 19337 // When the user types into the search field, we want to retrigger 19338 // a fetch on the dropdown. This will be fired from within the 19339 // dropdown's sandbox, so the dropdown is going to have to listen 19340 // for it there. See CommonDropdown.ts. 19341 run$1( 19342 // Use "input" to handle keydown, paste etc. 19343 input(), (inputComp) => { 19344 emit(inputComp, refetchTriggerEvent); 19345 }), 19346 run$1(keydown(), (inputComp, se) => { 19347 // The Special Keying config type since TINY-7005 processes the Escape 19348 // key on keyup, not keydown. We need to stop the keydown event for this 19349 // input, because some browsers (e.g. Chrome) will process a keydown 19350 // for Escape inside an input[type=search] by clearing the input value, 19351 // and then triggering an "input" event. This "input" event will trigger 19352 // a refetch, which if it completes before the keyup is fired for Escape, 19353 // will go back to only showing one level of menu. Then, when the escape 19354 // keyup is processed by Keying, it will close the single remaining menu. 19355 // This has the effect of closing *all* menus that are open when Escape is 19356 // pressed instead of the last one. So, instead, we are going to kill the 19357 // keydown event, so that it doesn't have the default browser behaviour, and 19358 // won't trigger an input (and then Refetch). Then the keyup will still fire 19359 // so just one level of the menu will close. This is all based on the underlying 19360 // assumption that preventDefault and/or stop on a keydown does not suppress 19361 // the related keyup. All of the documentation found so far, suggests it should 19362 // only suppress the keypress, not the keyup, but that might not be across all 19363 // browsers, or implemented consistently. 19364 if (se.event.raw.key === 'Escape') { 19365 se.stop(); 19366 } 19367 }) 19368 ]), 19369 // In addition to input handling, we want special handling for 19370 // Up/Down/Left/Right/Enter/Escape/Space. We can divide these into two categories 19371 // - events that we don't want to allow the overall menu system to process (left and right and space) 19372 // - events that we want to redispatch on the "highlighted item" based on the 19373 // current fake focus. 19374 Keying.config({
19375 mode: 'special', 19376 onLeft: handleByBrowser, 19377 onRight: handleByBrowser, 19378 onSpace: handleByBrowser, 19379 onEnter: handleByHighlightedItem, 19380 onEscape: handleByHighlightedItem, 19381 onUp: handleByHighlightedItem, 19382 onDown: handleByHighlightedItem 19383 }) 19384 ]), 19385 // Because we have customised handling for keydown, and we are configuring 19386 // Keying, we need to specify which "behaviour" (custom events or keying) gets to 19387 // process the keydown event first. In this situation, we want to stop escape before 19388 // anything happens (although it really isn't necessary) 19389 eventOrder: { 19390 keydown: [customSearcherEventsName, Keying.name()] 19391 } 19392 }) 19393 ] 19394 }; 19395 }; 19396 19397 const searchResultsClass = 'tox-collection--results__js'; 19398 // NOTE: this is operating on the the final AlloySpec 19399 const augmentWithAria = (item) => { 19400 if (item.dom) { 19401 return { 19402 ...item, 19403 dom: { 19404 ...item.dom, 19405 attributes: { 19406 ...item.dom.attributes ?? {}, 19407 'id': generate$6('aria-item-search-result-id'), 19408 'aria-selected': 'false' 19409 } 19410 } 19411 }; 19412 } 19413 else { 19414 return item; 19415 } 19416 }; 19417 19418 const widgetAriaLabel = 'Use arrow keys to navigate.'; 19419 const chunk = (rowDom, numColumns) => (items) => { 19420 const chunks = chunk$1(items, numColumns); 19421 return map$2(chunks, (c) => ({ 19422 dom: rowDom, 19423 components: c 19424 })); 19425 }; 19426 const forSwatch = (columns) => ({ 19427 dom: { 19428 tag: 'div', 19429 classes: ['tox-menu', 'tox-swatches-menu'], 19430 attributes: { 19431 'aria-label': global$6.translate(widgetAriaLabel) 19432 } 19433 }, 19434 components: [ 19435 { 19436 dom: { 19437 tag: 'div', 19438 classes: ['tox-swatches'] 19439 }, 19440 components: [ 19441 Menu.parts.items({ 19442 preprocess: columns !== 'auto' ? chunk({ 19443 tag: 'div', 19444 classes: ['tox-swatches__row'] 19445 }, columns) : identity 19446 }) 19447 ] 19448 } 19449 ] 19450 }); 19451 const forImageSelector = (columns) => ({ 19452 dom: { 19453 tag: 'div', 19454 classes: ['tox-menu', 'tox-image-selector-menu'] 19455 }, 19456 components: [ 19457 { 19458 dom: { 19459 tag: 'div', 19460 classes: ['tox-image-selector'] 19461 }, 19462 components: [ 19463 Menu.parts.items({ 19464 preprocess: columns !== 'auto' ? chunk({ 19465 tag: 'div', 19466 classes: ['tox-image-selector__row'] 19467 }, columns) : identity 19468 }) 19469 ] 19470 } 19471 ] 19472 }); 19473 const forToolbar = (columns) => ({ 19474 dom: { 19475 tag: 'div', 19476 // TODO: Configurable lg setting? 19477 classes: ['tox-menu', 'tox-collection', 'tox-collection--toolbar', 'tox-collection--toolbar-lg'] 19478 }, 19479 components: [ 19480 Menu.parts.items({ 19481 preprocess: chunk({ 19482 tag: 'div', 19483 classes: ['tox-collection__group'] 19484 }, columns) 19485 }) 19486 ] 19487 }); 19488 // NOTE: That type signature isn't quite true. 19489 const preprocessCollection = (items, isSeparator) => { 19490 const allSplits = []; 19491 let currentSplit = []; 19492 each$1(items, (item, i) => { 19493 if (isSeparator(item, i)) { 19494 if (currentSplit.length > 0) { 19495 allSplits.push(currentSplit); 19496 } 19497 currentSplit = []; 19498 if (has$2(item.dom, 'innerHtml') || item.components && item.components.length > 0) { 19499 currentSplit.push(item); 19500 } 19501 } 19502 else { 19503 currentSplit.push(item); 19504 } 19505 }); 19506 if (currentSplit.length > 0) { 19507 allSplits.push(currentSplit); 19508 } 19509 return map$2(allSplits, (s) => ({ 19510 dom: { 19511 tag: 'div', 19512 classes: ['tox-collection__group'] 19513 }, 19514 components: s 19515 })); 19516 }; 19517 const insertItemsPlaceholder = (columns, initItems, onItem) => {
vendor: 10,274 bytes, lines 19518-19743
19518 return Menu.parts.items({ 19519 preprocess: (rawItems) => { 19520 // Add any information to the items that is required. For example 19521 // when the items are results in a searchable menu, we need them to have 19522 // an ID that can be referenced by aria-activedescendant 19523 const enrichedItems = map$2(rawItems, onItem); 19524 if (columns !== 'auto' && columns > 1) { 19525 return chunk({ 19526 tag: 'div', 19527 classes: ['tox-collection__group'] 19528 }, columns)(enrichedItems); 19529 } 19530 else { 19531 return preprocessCollection(enrichedItems, (_item, i) => initItems[i].type === 'separator'); 19532 } 19533 } 19534 }); 19535 }; 19536 const hasWidget = (items) => exists(items, (item) => item.type === 'widget'); 19537 const forCollection = (columns, initItems, _hasIcons = true) => ({ 19538 dom: { 19539 tag: 'div', 19540 classes: ['tox-menu', 'tox-collection'].concat(columns === 1 ? ['tox-collection--list'] : ['tox-collection--grid']), 19541 attributes: { 19542 // widget item can be inserttable, colorswatch or imageselect - all of them are navigated with arrow keys 19543 ...hasWidget(initItems) ? { 'aria-label': global$6.translate(widgetAriaLabel) } : {} 19544 }, 19545 }, 19546 components: [ 19547 // We don't need to add IDs for each item because there are no 19548 // aria relationships we need to maintain 19549 insertItemsPlaceholder(columns, initItems, identity) 19550 ] 19551 }); 19552 const forCollectionWithSearchResults = (columns, initItems, _hasIcons = true) => { 19553 // A collection with results is exactly like a collection, except it also has 19554 // an ID and class on its outer div to allow for aria-controls relationships, and ids 19555 // on its items. 19556 // This connects the search bar with the list box. 19557 const ariaControlsSearchResults = generate$6('aria-controls-search-results'); 19558 return { 19559 dom: { 19560 tag: 'div', 19561 classes: ['tox-menu', 'tox-collection', searchResultsClass].concat(columns === 1 ? ['tox-collection--list'] : ['tox-collection--grid']), 19562 attributes: { 19563 id: ariaControlsSearchResults 19564 } 19565 }, 19566 components: [ 19567 // For each item, it needs to have an ID, so that we can refer to it 19568 // by the aria-activedescendant attribute 19569 insertItemsPlaceholder(columns, initItems, augmentWithAria) 19570 ] 19571 }; 19572 }; 19573 // Does a searchable menu *really* support columns !== 1 ? 19574 const forCollectionWithSearchField = (columns, initItems, searchField) => { 19575 // This connects the search bar with the list box. 19576 const ariaControlsSearchResults = generate$6('aria-controls-search-results'); 19577 return { 19578 dom: { 19579 tag: 'div', 19580 classes: ['tox-menu', 'tox-collection'].concat(columns === 1 ? ['tox-collection--list'] : ['tox-collection--grid']) 19581 }, 19582 components: [ 19583 // Importantly, the search bar is not in the "items" part, which means that it is 19584 // not given any of the item decorations by default. In order to ensure that is 19585 // not part of the navigation, however, we need to prevent it from getting the nav 19586 // class. For general collection menu items, it is navClass, which is: 19587 // tox-menu-nav__js. So simply, do not add this class when creating 19588 // the search, so that it isn't in the navigation. Ideally, it would only ever look 19589 // inside its items section, but the items aren't guaranteed to have a separate 19590 // container, and navigation candidates are found anywhere inside the menu 19591 // container. We could add configuration to alloy's Menu movement, where there was 19592 // a 'navigation container' that all items would be in. That could be another 19593 // way to solve the problem. For now, we'll just manually avoid adding the navClass 19594 renderMenuSearcher({ 19595 i18n: global$6.translate, 19596 placeholder: searchField.placeholder 19597 }), 19598 { 19599 // We need a separate container for the items, because this is the container 19600 // that multiple tox-collection__groups might go into, and will be the container 19601 // that the search bar controls. 19602 dom: { 19603 tag: 'div', 19604 classes: [ 19605 ...(columns === 1 ? ['tox-collection--list'] : ['tox-collection--grid']), 19606 searchResultsClass 19607 ], 19608 attributes: { 19609 id: ariaControlsSearchResults 19610 } 19611 }, 19612 components: [ 19613 // For each item, it needs to have an ID, so that we can refer to it 19614 // by the aria-activedescendant attribute 19615 insertItemsPlaceholder(columns, initItems, augmentWithAria) 19616 ] 19617 } 19618 ] 19619 }; 19620 }; 19621 const forHorizontalCollection = (initItems, _hasIcons = true) => ({ 19622 dom: { 19623 tag: 'div', 19624 classes: ['tox-collection', 'tox-collection--horizontal'] 19625 }, 19626 components: [ 19627 Menu.parts.items({ 19628 preprocess: (items) => preprocessCollection(items, (_item, i) => initItems[i].type === 'separator') 19629 }) 19630 ] 19631 }); 19632 19633 const menuHasIcons = (xs) => exists(xs, (item) => 'icon' in item && item.icon !== undefined); 19634 const handleError = (error) => { 19635 // eslint-disable-next-line no-console 19636 console.error(formatError(error)); 19637 // eslint-disable-next-line no-console 19638 console.log(error); 19639 return Optional.none(); 19640 }; 19641 const createHorizontalPartialMenuWithAlloyItems = (value, _hasIcons, items, _columns, _menuLayout) => { 19642 // Horizontal collections do not support different menu layout structures currently. 19643 const structure = forHorizontalCollection(items); 19644 return { 19645 value, 19646 dom: structure.dom, 19647 components: structure.components, 19648 items 19649 }; 19650 }; 19651 const createPartialMenuWithAlloyItems = (value, hasIcons, items, columns, menuLayout) => { 19652 const getNormalStructure = () => { 19653 if (menuLayout.menuType !== 'searchable') { 19654 return forCollection(columns, items); 19655 } 19656 else { 19657 return menuLayout.searchMode.searchMode === 'search-with-field' 19658 ? forCollectionWithSearchField(columns, items, menuLayout.searchMode) 19659 : forCollectionWithSearchResults(columns, items); 19660 } 19661 }; 19662 if (menuLayout.menuType === 'color') { 19663 const structure = forSwatch(columns); 19664 return { 19665 value, 19666 dom: structure.dom, 19667 components: structure.components, 19668 items 19669 }; 19670 } 19671 else if (menuLayout.menuType === 'imageselector' && columns !== 'auto') { 19672 const structure = forImageSelector(columns); 19673 return { 19674 value, 19675 dom: structure.dom, 19676 components: structure.components, 19677 items 19678 }; 19679 } 19680 else if (menuLayout.menuType === 'normal' && columns === 'auto') { 19681 const structure = forCollection(columns, items); 19682 return { 19683 value, 19684 dom: structure.dom, 19685 components: structure.components, 19686 items 19687 }; 19688 } 19689 else if (menuLayout.menuType === 'normal' || menuLayout.menuType === 'searchable') { 19690 const structure = getNormalStructure(); 19691 return { 19692 value, 19693 dom: structure.dom, 19694 components: structure.components, 19695 items 19696 }; 19697 } 19698 else if (menuLayout.menuType === 'listpreview' && columns !== 'auto') { 19699 const structure = forToolbar(columns); 19700 return { 19701 value, 19702 dom: structure.dom, 19703 components: structure.components, 19704 items 19705 }; 19706 } 19707 else { 19708 return { 19709 value, 19710 dom: dom(hasIcons, columns, menuLayout.menuType), 19711 components: components, 19712 items 19713 }; 19714 } 19715 }; 19716 19717 const type = requiredString('type'); 19718 const name = requiredString('name'); 19719 const label = requiredString('label'); 19720 const text = requiredString('text'); 19721 const title = requiredString('title'); 19722 const icon = requiredString('icon'); 19723 const url = requiredString('url'); 19724 const value = requiredString('value'); 19725 const fetch = requiredFunction('fetch'); 19726 const getSubmenuItems = requiredFunction('getSubmenuItems'); 19727 const onAction = requiredFunction('onAction'); 19728 const onItemAction = requiredFunction('onItemAction'); 19729 const onSetup = defaultedFunction('onSetup', () => noop); 19730 const optionalName = optionString('name'); 19731 const optionalText = optionString('text'); 19732 const optionalRole = optionString('role'); 19733 const optionalIcon = optionString('icon'); 19734 const optionalTooltip = optionString('tooltip'); 19735 const optionalChevronTooltip = optionString('chevronTooltip'); 19736 const optionalLabel = optionString('label'); 19737 const optionalShortcut = optionString('shortcut'); 19738 const optionalSelect = optionFunction('select'); 19739 const active = defaultedBoolean('active', false); 19740 const borderless = defaultedBoolean('borderless', false); 19741 const enabled = defaultedBoolean('enabled', true); 19742 const primary = defaultedBoolean('primary', false); 19743 const defaultedColumns = (num) => defaulted('columns', num);
19744 const defaultedMeta = defaulted('meta', {}); 19745 const defaultedOnAction = defaultedFunction('onAction', noop); 19746 const defaultedType = (type) => defaultedString('type', type); 19747 const generatedName = (namePrefix) => field$1('name', 'name', defaultedThunk(() => generate$6(`${namePrefix}-name`)), string); 19748 const generatedValue = (valuePrefix) => field$1('value', 'value', defaultedThunk(() => generate$6(`${valuePrefix}-value`)), anyValue()); 19749 19750 const alertBannerFields = [ 19751 type, 19752 text, 19753 requiredStringEnum('level', ['info', 'warn', 'error', 'success']), 19754 icon, 19755 defaulted('url', '') 19756 ]; 19757 const alertBannerSchema = objOf(alertBannerFields); 19758 19759 const createBarFields = (itemsField) => [ 19760 type, 19761 itemsField 19762 ]; 19763 19764 const buttonFields = [ 19765 type, 19766 text, 19767 enabled, 19768 generatedName('button'), 19769 optionalIcon, 19770 borderless, 19771 // this should be defaulted to `secondary` but the implementation needs to manage the deprecation 19772 optionStringEnum('buttonType', ['primary', 'secondary', 'toolbar']), 19773 // this should be removed, but must live here because FieldSchema doesn't have a way to manage deprecated fields 19774 primary, 19775 defaultedString('context', 'mode:design') 19776 ]; 19777 const buttonSchema = objOf(buttonFields); 19778 19779 const formComponentFields = [ 19780 type, 19781 name 19782 ]; 19783 const formComponentWithLabelFields = formComponentFields.concat([ 19784 optionalLabel 19785 ]); 19786 19787 const checkboxFields = formComponentFields.concat([ 19788 label, 19789 enabled, 19790 defaultedString('context', 'mode:design') 19791 ]); 19792 const checkboxSchema = objOf(checkboxFields); 19793 const checkboxDataProcessor = boolean; 19794 19795 const collectionFields = formComponentWithLabelFields.concat([ 19796 defaultedColumns('auto'), 19797 defaultedString('context', 'mode:design') 19798 ]); 19799 const collectionSchema = objOf(collectionFields); 19800 // TODO: Make type for CollectionItem 19801 const collectionDataProcessor = arrOfObj([ 19802 value, 19803 text, 19804 icon 19805 ]); 19806 19807 const colorInputFields = formComponentWithLabelFields.concat([ 19808 defaultedString('storageKey', 'default'), 19809 defaultedString('context', 'mode:design'), 19810 ]); 19811 const colorInputSchema = objOf(colorInputFields); 19812 const colorInputDataProcessor = string; 19813 19814 const colorPickerFields = formComponentWithLabelFields; 19815 const colorPickerSchema = objOf(colorPickerFields); 19816 const colorPickerDataProcessor = string; 19817 19818 const customEditorFields = formComponentFields.concat([ 19819 defaultedString('tag', 'textarea'), 19820 requiredString('scriptId'), 19821 requiredString('scriptUrl'), 19822 optionFunction('onFocus'), 19823 defaultedPostMsg('settings', undefined) 19824 ]); 19825 const customEditorFieldsOld = formComponentFields.concat([ 19826 defaultedString('tag', 'textarea'), 19827 requiredFunction('init') 19828 ]); 19829 const customEditorSchema = valueOf((v) => asRaw('customeditor.old', objOfOnly(customEditorFieldsOld), v).orThunk(() => asRaw('customeditor.new', objOfOnly(customEditorFields), v))); 19830 const customEditorDataProcessor = string; 19831 19832 const commonMenuItemFields = [ 19833 enabled, 19834 optionalText, 19835 optionalRole, 19836 optionalShortcut, 19837 generatedValue('menuitem'), 19838 defaultedMeta, 19839 defaultedString('context', 'mode:design') 19840 ]; 19841 19842 const dialogToggleMenuItemSchema = objOf([ 19843 type, 19844 name 19845 ].concat(commonMenuItemFields)); 19846 const dialogToggleMenuItemDataProcessor = boolean; 19847 19848 const baseFooterButtonFields = [ 19849 generatedName('button'), 19850 optionalIcon, 19851 defaultedStringEnum('align', 'end', ['start', 'end']), 19852 // this should be removed, but must live here because FieldSchema doesn't have a way to manage deprecated fields 19853 primary, 19854 enabled, 19855 // this should be defaulted to `secondary` but the implementation needs to manage the deprecation 19856 optionStringEnum('buttonType', ['primary', 'secondary']), 19857 defaultedString('context', 'mode:design') 19858 ]; 19859 const dialogFooterButtonFields = [ 19860 ...baseFooterButtonFields, 19861 text 19862 ]; 19863 const normalFooterButtonFields = [ 19864 requiredStringEnum('type', ['submit', 'cancel', 'custom']), 19865 ...dialogFooterButtonFields 19866 ]; 19867 const menuFooterButtonFields = [ 19868 requiredStringEnum('type', ['menu']), 19869 optionalText, 19870 optionalTooltip, 19871 optionalIcon, 19872 requiredArrayOf('items', dialogToggleMenuItemSchema), 19873 ...baseFooterButtonFields 19874 ]; 19875 const toggleButtonSpecFields = [ 19876 ...baseFooterButtonFields, 19877 requiredStringEnum('type', ['togglebutton']), 19878 optionalTooltip, 19879 optionalIcon, 19880 optionalText, 19881 defaultedBoolean('active', false) 19882 ]; 19883 const dialogFooterButtonSchema = choose$1('type', { 19884 submit: normalFooterButtonFields, 19885 cancel: normalFooterButtonFields, 19886 custom: normalFooterButtonFields, 19887 menu: menuFooterButtonFields, 19888 togglebutton: toggleButtonSpecFields 19889 }); 19890 19891 const dropZoneFields = formComponentWithLabelFields.concat([ 19892 defaultedString('context', 'mode:design'), 19893 optionString('dropAreaLabel'), 19894 optionString('buttonLabel'), 19895 optionString('allowedFileTypes'), 19896 optionArrayOf('allowedFileExtensions', string), 19897 defaultedFunction('onInvalidFiles', () => Promise.resolve()) 19898 ]); 19899 const dropZoneSchema = objOf(dropZoneFields); 19900 const dropZoneDataProcessor = arrOfVal(); 19901 19902 const createGridFields = (itemsField) => [ 19903 type, 19904 requiredNumber('columns'), 19905 itemsField 19906 ]; 19907 19908 const htmlPanelFields = [ 19909 type, 19910 requiredString('html'), 19911 defaultedStringEnum('presets', 'presentation', ['presentation', 'document']), 19912 defaultedFunction('onInit', noop), 19913 defaultedBoolean('stretched', false), 19914 ]; 19915 const htmlPanelSchema = objOf(htmlPanelFields); 19916 19917 const iframeFields = formComponentWithLabelFields.concat([ 19918 defaultedBoolean('border', false), 19919 defaultedBoolean('sandboxed', true), 19920 defaultedBoolean('streamContent', false), 19921 defaultedBoolean('transparent', true) 19922 ]); 19923 const iframeSchema = objOf(iframeFields); 19924 const iframeDataProcessor = string; 19925 19926 const imagePreviewSchema = objOf(formComponentFields.concat([ 19927 optionString('height'), 19928 ])); 19929 const imagePreviewDataProcessor = objOf([ 19930 requiredString('url'), 19931 optionNumber('zoom'), 19932 optionNumber('cachedWidth'), 19933 optionNumber('cachedHeight'), 19934 ]); 19935 19936 const inputFields = formComponentWithLabelFields.concat([ 19937 optionString('inputMode'), 19938 optionString('placeholder'), 19939 defaultedBoolean('maximized', false), 19940 enabled, 19941 defaultedString('context', 'mode:design'), 19942 ]); 19943 const inputSchema = objOf(inputFields); 19944 const inputDataProcessor = string; 19945 19946 const createLabelFields = (itemsField) => [ 19947 type, 19948 label, 19949 itemsField, 19950 defaultedStringEnum('align', 'start', ['start', 'center', 'end']), 19951 optionString('for') 19952 ]; 19953 19954 const listBoxSingleItemFields = [ 19955 text, 19956 value 19957 ]; 19958 const listBoxNestedItemFields = [ 19959 text, 19960 requiredArrayOf('items', thunkOf('items', () => listBoxItemSchema)) 19961 ]; 19962 const listBoxItemSchema = oneOf([ 19963 objOf(listBoxSingleItemFields), 19964 objOf(listBoxNestedItemFields) 19965 ]); 19966 const listBoxFields = formComponentWithLabelFields.concat([ 19967 requiredArrayOf('items', listBoxItemSchema), 19968 enabled, 19969 defaultedString('context', 'mode:design') 19970 ]); 19971 const listBoxSchema = objOf(listBoxFields); 19972 const listBoxDataProcessor = string; 19973 19974 const selectBoxFields = formComponentWithLabelFields.concat([ 19975 requiredArrayOfObj('items', [ 19976 text, 19977 value 19978 ]), 19979 defaultedNumber('size', 1), 19980 enabled, 19981 defaultedString('context', 'mode:design') 19982 ]); 19983 const selectBoxSchema = objOf(selectBoxFields); 19984 const selectBoxDataProcessor = string; 19985 19986 const sizeInputFields = formComponentWithLabelFields.concat([ 19987 defaultedBoolean('constrain', true), 19988 enabled, 19989 defaultedString('context', 'mode:design') 19990 ]); 19991 const sizeInputSchema = objOf(sizeInputFields); 19992 const sizeInputDataProcessor = objOf([ 19993 requiredString('width'), 19994 requiredString('height') 19995 ]); 19996 19997 const sliderFields = formComponentFields.concat([ 19998 label, 19999 defaultedNumber('min', 0), 20000 defaultedNumber('max', 0), 20001 ]); 20002 const sliderSchema = objOf(sliderFields); 20003 const sliderInputDataProcessor = number; 20004 20005 const tableFields = [ 20006 type, 20007 requiredArrayOf('header', string), 20008 requiredArrayOf('cells', arrOf(string)) 20009 ]; 20010 const tableSchema = objOf(tableFields); 20011 20012 const textAreaFields = formComponentWithLabelFields.concat([ 20013 optionString('placeholder'), 20014 defaultedBoolean('maximized', false), 20015 enabled, 20016 defaultedString('context', 'mode:design'), 20017 optionBoolean('spellcheck'), 20018 ]); 20019 const textAreaSchema = objOf(textAreaFields); 20020 const textAreaDataProcessor = string; 20021 20022 const baseMenuButtonFields = [ 20023 defaultedString('buttonType', 'default'), 20024 optionString('text'), 20025 optionString('tooltip'), 20026 optionString('icon'), 20027 defaultedOf('search', false, 20028 // So our boulder validation are: 20029 // a) boolean -> we need to map it into an Option 20030 // b) object -> we need to map it into a Some 20031 oneOf([ 20032 // Unfortunately, due to objOf not checking to see that the 20033 // input is an object, the boolean check MUST be first 20034 boolean, 20035 objOf([ 20036 optionString('placeholder') 20037 ]) 20038 ], 20039 // This function allows you to standardise the output. 20040 (x) => { 20041 if (isBoolean(x)) { 20042 return x ? Optional.some({ placeholder: Optional.none() }) : Optional.none(); 20043 } 20044 else { 20045 return Optional.some(x); 20046 } 20047 })), 20048 requiredFunction('fetch'), 20049 defaultedFunction('onSetup', () => noop), 20050 defaultedString('context', 'mode:design') 20051 ]; 20052 20053 const MenuButtonSchema = objOf([ 20054 type, 20055 ...baseMenuButtonFields 20056 ]); 20057 const createMenuButton = (spec) => asRaw('menubutton', MenuButtonSchema, spec); 20058 20059 const baseTreeItemFields = [ 20060 requiredStringEnum('type', ['directory', 'leaf']), 20061 title, 20062 requiredString('id'), 20063 optionOf('menu', MenuButtonSchema), 20064 optionString('customStateIcon'), 20065 optionString('customStateIconTooltip'), 20066 ]; 20067 const treeItemLeafFields = baseTreeItemFields; 20068 const treeItemLeafSchema = objOf(treeItemLeafFields); 20069 const treeItemDirectoryFields = baseTreeItemFields.concat([ 20070 requiredArrayOf('children', thunkOf('children', () => { 20071 return choose$2('type', { 20072 directory: treeItemDirectorySchema, 20073 leaf: treeItemLeafSchema, 20074 }); 20075 })), 20076 ]); 20077 const treeItemDirectorySchema = objOf(treeItemDirectoryFields); 20078 const treeItemSchema = choose$2('type', { 20079 directory: treeItemDirectorySchema, 20080 leaf: treeItemLeafSchema, 20081 }); 20082 const treeFields = [ 20083 type, 20084 requiredArrayOf('items', treeItemSchema), 20085 optionFunction('onLeafAction'), 20086 optionFunction('onToggleExpand'), 20087 defaultedArrayOf('defaultExpandedIds', [], string), 20088 optionString('defaultSelectedId'), 20089 ]; 20090 const treeSchema = objOf(treeFields); 20091 20092 const urlInputFields = formComponentWithLabelFields.concat([
20093 defaultedStringEnum('filetype', 'file', ['image', 'media', 'file']), 20094 enabled, 20095 optionString('picker_text'), 20096 defaultedString('context', 'mode:design') 20097 ]); 20098 const urlInputSchema = objOf(urlInputFields); 20099 const urlInputDataProcessor = objOf([ 20100 value, 20101 defaultedMeta 20102 ]); 20103 20104 const createItemsField = (name) => field$1('items', 'items', required$2(), arrOf(valueOf((v) => asRaw(`Checking item of ${name}`, itemSchema$1, v).fold((sErr) => Result.error(formatError(sErr)), (passValue) => Result.value(passValue))))); 20105 // We're using a thunk here so we can refer to panel fields 20106 const itemSchema$1 = valueThunk(() => choose$2('type', { 20107 alertbanner: alertBannerSchema, 20108 bar: objOf(createBarFields(createItemsField('bar'))), 20109 button: buttonSchema, 20110 checkbox: checkboxSchema, 20111 colorinput: colorInputSchema, 20112 colorpicker: colorPickerSchema, 20113 dropzone: dropZoneSchema, 20114 grid: objOf(createGridFields(createItemsField('grid'))), 20115 iframe: iframeSchema, 20116 input: inputSchema, 20117 listbox: listBoxSchema, 20118 selectbox: selectBoxSchema, 20119 sizeinput: sizeInputSchema, 20120 slider: sliderSchema, 20121 textarea: textAreaSchema, 20122 urlinput: urlInputSchema, 20123 customeditor: customEditorSchema, 20124 htmlpanel: htmlPanelSchema, 20125 imagepreview: imagePreviewSchema, 20126 collection: collectionSchema, 20127 label: objOf(createLabelFields(createItemsField('label'))), 20128 table: tableSchema, 20129 tree: treeSchema, 20130 panel: panelSchema 20131 })); 20132 const panelFields = [ 20133 type, 20134 defaulted('classes', []), 20135 requiredArrayOf('items', itemSchema$1) 20136 ]; 20137 const panelSchema = objOf(panelFields); 20138 20139 const tabFields = [ 20140 generatedName('tab'), 20141 title, 20142 requiredArrayOf('items', itemSchema$1) 20143 ]; 20144 const tabPanelFields = [ 20145 type, 20146 requiredArrayOfObj('tabs', tabFields), 20147 defaultedBoolean('dynamicHeight', false) 20148 ]; 20149 const tabPanelSchema = objOf(tabPanelFields); 20150 20151 const dialogButtonFields = dialogFooterButtonFields; 20152 const dialogButtonSchema = dialogFooterButtonSchema; 20153 const dialogSchema = objOf([ 20154 requiredString('title'), 20155 requiredOf('body', choose$2('type', { 20156 panel: panelSchema, 20157 tabpanel: tabPanelSchema 20158 })), 20159 defaultedString('size', 'normal'), 20160 defaultedArrayOf('buttons', [], dialogButtonSchema), 20161 defaulted('initialData', {}), 20162 defaultedFunction('onAction', noop), 20163 defaultedFunction('onChange', noop), 20164 defaultedFunction('onSubmit', noop), 20165 defaultedFunction('onClose', noop), 20166 defaultedFunction('onCancel', noop), 20167 defaultedFunction('onTabChange', noop) 20168 ]); 20169 const createDialog = (spec) => asRaw('dialog', dialogSchema, spec); 20170 20171 const urlDialogButtonSchema = objOf([ 20172 requiredStringEnum('type', ['cancel', 'custom']), 20173 ...dialogButtonFields 20174 ]); 20175 const urlDialogSchema = objOf([ 20176 requiredString('title'), 20177 requiredString('url'), 20178 optionNumber('height'), 20179 optionNumber('width'), 20180 optionArrayOf('buttons', urlDialogButtonSchema), 20181 defaultedFunction('onAction', noop), 20182 defaultedFunction('onCancel', noop), 20183 defaultedFunction('onClose', noop), 20184 defaultedFunction('onMessage', noop) 20185 ]); 20186 const createUrlDialog = (spec) => asRaw('dialog', urlDialogSchema, spec); 20187 20188 // eslint-disable-next-line @typescript-eslint/no-wrapper-object-types 20189 const getAllObjects = (obj) => { 20190 if (isObject(obj)) { 20191 return [obj].concat(bind$3(values(obj), getAllObjects)); 20192 } 20193 else if (isArray(obj)) { 20194 return bind$3(obj, getAllObjects); 20195 } 20196 else { 20197 return []; 20198 } 20199 }; 20200 20201 const isNamedItem = (obj) => isString(obj.type) && isString(obj.name); 20202 const dataProcessors = { 20203 checkbox: checkboxDataProcessor, 20204 colorinput: colorInputDataProcessor, 20205 colorpicker: colorPickerDataProcessor, 20206 dropzone: dropZoneDataProcessor, 20207 input: inputDataProcessor, 20208 iframe: iframeDataProcessor, 20209 imagepreview: imagePreviewDataProcessor, 20210 selectbox: selectBoxDataProcessor, 20211 sizeinput: sizeInputDataProcessor, 20212 slider: sliderInputDataProcessor, 20213 listbox: listBoxDataProcessor, 20214 size: sizeInputDataProcessor, 20215 textarea: textAreaDataProcessor, 20216 urlinput: urlInputDataProcessor, 20217 customeditor: customEditorDataProcessor, 20218 collection: collectionDataProcessor, 20219 togglemenuitem: dialogToggleMenuItemDataProcessor 20220 }; 20221 const getDataProcessor = (item) => Optional.from(dataProcessors[item.type]); 20222 const getNamedItems = (structure) => filter$2(getAllObjects(structure), isNamedItem); 20223 20224 const createDataValidator = (structure) => { 20225 const namedItems = getNamedItems(structure); 20226 const fields = bind$3(namedItems, (item) => getDataProcessor(item).fold(() => [], (schema) => [requiredOf(item.name, schema)])); 20227 return objOf(fields); 20228 }; 20229 20230 const extract = (structure) => { 20231 const internalDialog = getOrDie(createDialog(structure)); 20232 const dataValidator = createDataValidator(structure); 20233 // We used to validate data here, but it's done when loading the dialog in tinymce 20234 const initialData = structure.initialData ?? {}; 20235 return { 20236 internalDialog, 20237 dataValidator, 20238 initialData 20239 }; 20240 }; 20241 const DialogManager = { 20242 open: (factory, structure) => { 20243 const extraction = extract(structure); 20244 return factory(extraction.internalDialog, extraction.initialData, extraction.dataValidator); 20245 }, 20246 openUrl: (factory, structure) => { 20247 const internalDialog = getOrDie(createUrlDialog(structure)); 20248 return factory(internalDialog); 20249 }, 20250 redial: (structure) => extract(structure) 20251 }; 20252 20253 const separatorMenuItemSchema = objOf([ 20254 type, 20255 optionalText 20256 ]); 20257 const createSeparatorMenuItem = (spec) => asRaw('separatormenuitem', separatorMenuItemSchema, spec); 20258 20259 const autocompleterItemSchema = objOf([ 20260 // Currently, autocomplete items don't support configuring type, active, disabled, meta 20261 defaultedType('autocompleteitem'), 20262 active, 20263 enabled, 20264 defaultedMeta, 20265 value, 20266 optionalText, 20267 optionalIcon 20268 ]); 20269 objOf([ 20270 type, 20271 requiredString('trigger'), 20272 defaultedNumber('minChars', 1), 20273 defaultedColumns(1), 20274 defaultedNumber('maxResults', 10), 20275 optionFunction('matches'), 20276 fetch, 20277 onAction, 20278 defaultedArrayOf('highlightOn', [], string) 20279 ]); 20280 const createSeparatorItem = (spec) => asRaw('Autocompleter.Separator', separatorMenuItemSchema, spec); 20281 const createAutocompleterItem = (spec) => asRaw('Autocompleter.Item', autocompleterItemSchema, spec); 20282 20283 const baseToolbarButtonFields = [ 20284 enabled, 20285 optionalTooltip, 20286 optionalIcon, 20287 optionalText, 20288 onSetup, 20289 defaultedString('context', 'mode:design') 20290 ]; 20291 const toolbarButtonSchema = objOf([ 20292 type, 20293 onAction, 20294 optionalShortcut 20295 ].concat(baseToolbarButtonFields)); 20296 const createToolbarButton = (spec) => asRaw('toolbarbutton', toolbarButtonSchema, spec); 20297 20298 const baseToolbarToggleButtonFields = [ 20299 active 20300 ].concat(baseToolbarButtonFields); 20301 const toggleButtonSchema = objOf(baseToolbarToggleButtonFields.concat([ 20302 type, 20303 onAction, 20304 optionalShortcut 20305 ])); 20306 const createToggleButton = (spec) => asRaw('ToggleButton', toggleButtonSchema, spec); 20307 20308 const contextBarFields = [ 20309 defaultedFunction('predicate', never), 20310 defaultedStringEnum('scope', 'node', ['node', 'editor']), 20311 defaultedStringEnum('position', 'selection', ['node', 'selection', 'line']) 20312 ]; 20313 20314 const contextButtonFields = baseToolbarButtonFields.concat([ 20315 defaultedType('contextformbutton'), 20316 defaultedString('align', 'end'), 20317 primary, 20318 onAction, 20319 customField('original', identity) 20320 ]); 20321 const contextToggleButtonFields = baseToolbarToggleButtonFields.concat([ 20322 defaultedType('contextformbutton'), 20323 defaultedString('align', 'end'), 20324 primary, 20325 onAction, 20326 customField('original', identity) 20327 ]); 20328 const launchButtonFields$1 = baseToolbarButtonFields.concat([ 20329 defaultedType('contextformbutton') 20330 ]); 20331 const launchToggleButtonFields = baseToolbarToggleButtonFields.concat([ 20332 defaultedType('contextformtogglebutton') 20333 ]); 20334 const toggleOrNormal = choose$1('type', { 20335 contextformbutton: contextButtonFields, 20336 contextformtogglebutton: contextToggleButtonFields 20337 }); 20338 const baseContextFormFields = [ 20339 optionalLabel, 20340 requiredArrayOf('commands', toggleOrNormal), 20341 optionOf('launch', choose$1('type', { 20342 contextformbutton: launchButtonFields$1, 20343 contextformtogglebutton: launchToggleButtonFields 20344 })), 20345 defaultedFunction('onInput', noop), 20346 defaultedFunction('onSetup', noop) 20347 ]; 20348 const contextFormFields = [ 20349 ...contextBarFields, 20350 ...baseContextFormFields, 20351 requiredStringEnum('type', ['contextform']), 20352 defaultedFunction('initValue', constant$1('')), 20353 optionString('placeholder'), 20354 ]; 20355 const contextSliderFormFields = [ 20356 ...contextBarFields, 20357 ...baseContextFormFields, 20358 requiredStringEnum('type', ['contextsliderform']), 20359 defaultedFunction('initValue', constant$1(0)), 20360 defaultedFunction('min', constant$1(0)), 20361 defaultedFunction('max', constant$1(100)) 20362 ]; 20363 const contextSizeInputFormFields = [ 20364 ...contextBarFields, 20365 ...baseContextFormFields, 20366 requiredStringEnum('type', ['contextsizeinputform']), 20367 defaultedFunction('initValue', constant$1({ width: '', height: '' })) 20368 ]; 20369 const contextFormSchema = choose$1('type', { 20370 contextform: contextFormFields, 20371 contextsliderform: contextSliderFormFields, 20372 contextsizeinputform: contextSizeInputFormFields 20373 }); 20374 const createContextForm = (spec) => asRaw('ContextForm', contextFormSchema, spec); 20375 20376 const launchButtonFields = baseToolbarButtonFields.concat([ 20377 defaultedType('contexttoolbarbutton') 20378 ]); 20379 const contextToolbarSchema = objOf([ 20380 defaultedType('contexttoolbar'), 20381 optionObjOf('launch', launchButtonFields), 20382 requiredOf('items', oneOf([
20383 string, 20384 arrOfObj([ 20385 optionString('name'), 20386 optionString('label'), 20387 requiredArrayOf('items', string) 20388 ]) 20389 ])), 20390 ].concat(contextBarFields)); 20391 const toolbarGroupBackToSpec = (toolbarGroup) => ({ 20392 name: toolbarGroup.name.getOrUndefined(), 20393 label: toolbarGroup.label.getOrUndefined(), 20394 items: toolbarGroup.items 20395 }); 20396 const contextToolbarToSpec = (contextToolbar) => ({ 20397 ...contextToolbar, 20398 launch: contextToolbar.launch.getOrUndefined(), 20399 items: isString(contextToolbar.items) ? contextToolbar.items : map$2(contextToolbar.items, toolbarGroupBackToSpec) 20400 }); 20401 const createContextToolbar = (spec) => asRaw('ContextToolbar', contextToolbarSchema, spec); 20402 20403 const cardImageFields = [ 20404 type, 20405 requiredString('src'), 20406 optionString('alt'), 20407 defaultedArrayOf('classes', [], string) 20408 ]; 20409 const cardImageSchema = objOf(cardImageFields); 20410 20411 const cardTextFields = [ 20412 type, 20413 text, 20414 optionalName, 20415 defaultedArrayOf('classes', ['tox-collection__item-label'], string) 20416 ]; 20417 const cardTextSchema = objOf(cardTextFields); 20418 20419 const itemSchema = valueThunk(() => choose$2('type', { 20420 cardimage: cardImageSchema, 20421 cardtext: cardTextSchema, 20422 cardcontainer: cardContainerSchema 20423 })); 20424 const cardContainerSchema = objOf([ 20425 type, 20426 defaultedString('direction', 'horizontal'), 20427 defaultedString('align', 'left'), 20428 defaultedString('valign', 'middle'), 20429 requiredArrayOf('items', itemSchema) 20430 ]); 20431 20432 const cardMenuItemSchema = objOf([ 20433 type, 20434 optionalLabel, 20435 requiredArrayOf('items', itemSchema), 20436 onSetup, 20437 defaultedOnAction 20438 ].concat(commonMenuItemFields)); 20439 const createCardMenuItem = (spec) => asRaw('cardmenuitem', cardMenuItemSchema, spec); 20440 20441 const choiceMenuItemSchema = objOf([ 20442 type, 20443 active, 20444 optionalIcon, 20445 optionalLabel 20446 ].concat(commonMenuItemFields)); 20447 const createChoiceMenuItem = (spec) => asRaw('choicemenuitem', choiceMenuItemSchema, spec); 20448 20449 const baseFields = [ 20450 type, 20451 requiredString('fancytype'), 20452 defaultedOnAction 20453 ]; 20454 const insertTableFields = [ 20455 defaulted('initData', {}) 20456 ].concat(baseFields); 20457 const colorSwatchFields = [ 20458 optionFunction('select'), 20459 defaultedObjOf('initData', {}, [ 20460 defaultedBoolean('allowCustomColors', true), 20461 defaultedString('storageKey', 'default'), 20462 // Note: We don't validate the colors as they are instead validated by choiceschema when rendering 20463 optionArrayOf('colors', anyValue()) 20464 ]) 20465 ].concat(baseFields); 20466 const imageSelectFields = [ 20467 optionFunction('select'), 20468 requiredObjOf('initData', [ 20469 requiredNumber('columns'), 20470 // Note: We don't validate the items as they are instead validated by imageMenuItemSchema when rendering 20471 defaultedArrayOf('items', [], anyValue()) 20472 ]) 20473 ].concat(baseFields); 20474 const fancyMenuItemSchema = choose$1('fancytype', { 20475 inserttable: insertTableFields, 20476 colorswatch: colorSwatchFields, 20477 imageselect: imageSelectFields 20478 }); 20479 const createFancyMenuItem = (spec) => asRaw('fancymenuitem', fancyMenuItemSchema, spec); 20480 20481 const imageMenuItemSchema = objOf([ 20482 type, 20483 active, 20484 url, 20485 optionalLabel, 20486 optionalTooltip 20487 ].concat(commonMenuItemFields)); 20488 const resetImageItemSchema = objOf([ 20489 type, 20490 active, 20491 icon, 20492 label, 20493 optionalTooltip, 20494 value 20495 ].concat(commonMenuItemFields)); 20496 const createImageMenuItem = (spec) => asRaw('imagemenuitem', imageMenuItemSchema, spec); 20497 const createResetImageItem = (spec) => asRaw('resetimageitem', resetImageItemSchema, spec); 20498 20499 const menuItemSchema = objOf([ 20500 type, 20501 onSetup, 20502 defaultedOnAction, 20503 optionalIcon 20504 ].concat(commonMenuItemFields)); 20505 const createMenuItem = (spec) => asRaw('menuitem', menuItemSchema, spec); 20506 20507 const nestedMenuItemSchema = objOf([ 20508 type, 20509 getSubmenuItems, 20510 onSetup, 20511 optionalIcon 20512 ].concat(commonMenuItemFields)); 20513 const createNestedMenuItem = (spec) => asRaw('nestedmenuitem', nestedMenuItemSchema, spec); 20514 20515 const toggleMenuItemSchema = objOf([ 20516 type, 20517 optionalIcon, 20518 active, 20519 onSetup, 20520 onAction 20521 ].concat(commonMenuItemFields)); 20522 const createToggleMenuItem = (spec) => asRaw('togglemenuitem', toggleMenuItemSchema, spec); 20523 20524 const sidebarSchema = objOf([ 20525 optionalIcon, 20526 optionalTooltip, 20527 defaultedFunction('onShow', noop),
20528 defaultedFunction('onHide', noop), 20529 onSetup 20530 ]); 20531 const createSidebar = (spec) => asRaw('sidebar', sidebarSchema, spec); 20532 20533 const groupToolbarButtonSchema = objOf([ 20534 type, 20535 requiredOf('items', oneOf([ 20536 arrOfObj([ 20537 name, 20538 requiredArrayOf('items', string) 20539 ]), 20540 string 20541 ])) 20542 ].concat(baseToolbarButtonFields)); 20543 const createGroupToolbarButton = (spec) => asRaw('GroupToolbarButton', groupToolbarButtonSchema, spec); 20544 20545 const splitButtonSchema = objOf([ 20546 type, 20547 optionalTooltip, 20548 optionalChevronTooltip, 20549 optionalIcon, 20550 optionalText, 20551 optionalSelect, 20552 fetch, 20553 onSetup, 20554 // TODO: Validate the allowed presets 20555 defaultedStringEnum('presets', 'normal', ['normal', 'color', 'listpreview']), 20556 defaultedColumns(1), 20557 onAction, 20558 onItemAction, 20559 defaultedString('context', 'mode:design') 20560 ]); 20561 const createSplitButton = (spec) => asRaw('SplitButton', splitButtonSchema, spec); 20562 20563 const baseButtonFields = [ 20564 optionalText, 20565 optionalIcon, 20566 optionString('tooltip'), 20567 defaultedStringEnum('buttonType', 'secondary', ['primary', 'secondary']), 20568 defaultedBoolean('borderless', false), 20569 requiredFunction('onAction'), 20570 defaultedString('context', 'mode:design') 20571 ]; 20572 const normalButtonFields = [ 20573 ...baseButtonFields, 20574 text, 20575 requiredStringEnum('type', ['button']), 20576 ]; 20577 const toggleButtonFields = [ 20578 ...baseButtonFields, 20579 defaultedBoolean('active', false), 20580 requiredStringEnum('type', ['togglebutton']) 20581 ]; 20582 const schemaWithoutGroupButton = { 20583 button: normalButtonFields, 20584 togglebutton: toggleButtonFields, 20585 }; 20586 const groupFields = [ 20587 requiredStringEnum('type', ['group']), 20588 defaultedArrayOf('buttons', [], choose$1('type', schemaWithoutGroupButton)) 20589 ]; 20590 const viewButtonSchema = choose$1('type', { 20591 ...schemaWithoutGroupButton, 20592 group: groupFields 20593 }); 20594 20595 const viewSchema = objOf([ 20596 defaultedArrayOf('buttons', [], viewButtonSchema), 20597 requiredFunction('onShow'), 20598 requiredFunction('onHide') 20599 ]); 20600 const createView = (spec) => asRaw('view', viewSchema, spec); 20601 20602 const detectSize = (comp, margin, selectorClass) => { 20603 const descendants$1 = descendants(comp.element, '.' + selectorClass); 20604 // TODO: This seems to cause performance issues in the emoji dialog 20605 if (descendants$1.length > 0) { 20606 const columnLength = findIndex$1(descendants$1, (c) => { 20607 const thisTop = c.dom.getBoundingClientRect().top; 20608 const cTop = descendants$1[0].dom.getBoundingClientRect().top; 20609 return Math.abs(thisTop - cTop) > margin; 20610 }).getOr(descendants$1.length); 20611 return Optional.some({ 20612 numColumns: columnLength, 20613 numRows: Math.ceil(descendants$1.length / columnLength) 20614 }); 20615 } 20616 else { 20617 return Optional.none(); 20618 } 20619 }; 20620 20621 // Consider moving to alloy once it takes shape. 20622 const namedEvents = (name, handlers) => derive$1([ 20623 config(name, handlers) 20624 ]); 20625 const unnamedEvents = (handlers) => namedEvents(generate$6('unnamed-events'), handlers); 20626 const SimpleBehaviours = { 20627 namedEvents, 20628 unnamedEvents 20629 }; 20630 20631 const item = (disabled) => Disabling.config({ 20632 disabled, 20633 disableClass: 'tox-collection__item--state-disabled' 20634 }); 20635 const button = (disabled) => Disabling.config({ 20636 disabled 20637 }); 20638 const splitButton = (disabled) => Disabling.config({ 20639 disabled, 20640 disableClass: 'tox-tbtn--disabled' 20641 }); 20642 const toolbarButton = (disabled) => Disabling.config({ 20643 disabled, 20644 disableClass: 'tox-tbtn--disabled', 20645 useNative: false 20646 }); 20647 const DisablingConfigs = { 20648 item, 20649 button, 20650 splitButton, 20651 toolbarButton 20652 }; 20653 20654 const runWithApi = (info, comp) => { 20655 const api = info.getApi(comp); 20656 return (f) => { 20657 f(api); 20658 }; 20659 }; 20660 // These handlers are used for providing common onAttached and onDetached handlers. 20661 // Essentially, the `editorOffCell` is used store the onDestroy function returned 20662 // by onSetup. The reason onControlAttached doesn't create the cell itself, is because 20663 // it also has to be passed into onControlDetached. We could make this function return 20664 // the cell and the onAttachedHandler, but that would provide too much complexity. 20665 const onControlAttached = (info, editorOffCell) => runOnAttached((comp) => { 20666 if (isFunction(info.onBeforeSetup)) { 20667 info.onBeforeSetup(comp); 20668 } 20669 const run = runWithApi(info, comp); 20670 run((api) => { 20671 const onDestroy = info.onSetup(api); 20672 if (isFunction(onDestroy)) { 20673 editorOffCell.set(onDestroy); 20674 } 20675 }); 20676 }); 20677 const onControlDetached = (getApi, editorOffCell) => runOnDetached((comp) => runWithApi(getApi, comp)(editorOffCell.get())); 20678 const onContextFormControlDetached = (getApi, editorOffCell, valueState) => runOnDetached((comp) => { 20679 valueState.set(Representing.getValue(comp)); 20680 return runWithApi(getApi, comp)(editorOffCell.get()); 20681 }); 20682 20683 const UiStateChannel = 'silver.uistate'; 20684 const messageSetDisabled = 'setDisabled'; 20685 const messageSetEnabled = 'setEnabled'; 20686 const messageInit = 'init'; 20687 const messageSwitchMode = 'switchmode'; 20688 const modeContextMessages = [messageSwitchMode, messageInit]; 20689 const broadcastEvents = (uiRefs, messageType) => { 20690 const outerContainer = uiRefs.mainUi.outerContainer; 20691 const motherships = [uiRefs.mainUi.mothership, ...uiRefs.uiMotherships]; 20692 if (messageType === messageSetDisabled) { 20693 each$1(motherships, (m) => { 20694 m.broadcastOn([dismissPopups()], { target: outerContainer.element }); 20695 }); 20696 } 20697 each$1(motherships, (m) => { 20698 m.broadcastOn([UiStateChannel], messageType); 20699 }); 20700 }; 20701 const setupEventsForUi = (editor, uiRefs) => { 20702 editor.on('init SwitchMode', (event) => { 20703 broadcastEvents(uiRefs, event.type); 20704 }); 20705 editor.on('DisabledStateChange', (event) => { 20706 if (!event.isDefaultPrevented()) { 20707 // When the event state indicates the editor is **enabled** (`event.state` is false), 20708 // we send an 'init' message instead of 'setEnabled' because the editor might be in read-only mode. 20709 // Sending 'setEnabled' would enable all the toolbar buttons, which is undesirable if the editor is read-only. 20710 const messageType = event.state ? messageSetDisabled : messageInit; 20711 broadcastEvents(uiRefs, messageType); 20712 // After refreshing the state of the buttons, trigger a NodeChange event. 20713 if (!event.state) { 20714 editor.nodeChanged(); 20715 } 20716 } 20717 }); 20718 editor.on('NodeChange', (e) => { 20719 const messageType = editor.ui.isEnabled() ? e.type : messageSetDisabled; 20720 broadcastEvents(uiRefs, messageType); 20721 }); 20722 if (isReadOnly(editor)) { 20723 editor.mode.set('readonly'); 20724 } 20725 }; 20726 const toggleOnReceive = (getContext) => Receiving.config({ 20727 channels: { 20728 [UiStateChannel]: { 20729 onReceive: (comp, messageType) => { 20730 if (messageType === messageSetDisabled || messageType === messageSetEnabled) { 20731 Disabling.set(comp, messageType === messageSetDisabled); 20732 return; 20733 } 20734 const { contextType, shouldDisable } = getContext(); 20735 if (contextType === 'mode' && !contains$2(modeContextMessages, messageType)) { 20736 return; 20737 } 20738 Disabling.set(comp, shouldDisable); 20739 } 20740 } 20741 } 20742 }); 20743 20744 // Perform `action` when an item is clicked on, close menus, and stop event 20745 const onMenuItemExecute = (info, itemResponse) => runOnExecute$1((comp, simulatedEvent) => { 20746 // If there is an action, run the action 20747 runWithApi(info, comp)(info.onAction); 20748 if (!info.triggersSubmenu && itemResponse === ItemResponse$1.CLOSE_ON_EXECUTE) { 20749 if (comp.getSystem().isConnected()) { 20750 emit(comp, sandboxClose()); 20751 } 20752 simulatedEvent.stop(); 20753 } 20754 }); 20755 const menuItemEventOrder = { 20756 // TODO: use the constants provided by behaviours. 20757 [execute$5()]: ['disabling', 'alloy.base.behaviour', 'toggling', 'item-events'] 20758 }; 20759 20760 const componentRenderPipeline = cat; 20761 const renderCommonItem = (spec, structure, itemResponse, providersBackstage) => { 20762 const editorOffCell = Cell(noop); 20763 return { 20764 type: 'item', 20765 dom: structure.dom, 20766 components: componentRenderPipeline(structure.optComponents), 20767 data: spec.data, 20768 eventOrder: menuItemEventOrder, 20769 hasSubmenu: spec.triggersSubmenu, 20770 itemBehaviours: derive$1([ 20771 config('item-events', [ 20772 onMenuItemExecute(spec, itemResponse), 20773 onControlAttached(spec, editorOffCell), 20774 onControlDetached(spec, editorOffCell) 20775 ]), 20776 DisablingConfigs.item(() => !spec.enabled || providersBackstage.checkUiComponentContext(spec.c
20776ontext).shouldDisable), 20777 toggleOnReceive(() => providersBackstage.checkUiComponentContext(spec.context)), 20778 Replacing.config({}) 20779 ].concat(spec.itemBehaviours)) 20780 }; 20781 }; 20782 const buildData = (source) => ({ 20783 value: source.value, 20784 meta: { 20785 text: source.text.getOr(''), 20786 ...source.meta 20787 } 20788 }); 20789 20790 const renderImage$1 = (spec, imageUrl) => { 20791 const spinnerElement = SugarElement.fromTag('div'); 20792 add$2(spinnerElement, 'tox-image-selector-loading-spinner'); 20793 const addSpinnerElement = (loadingElement) => { 20794 add$2(loadingElement, 'tox-image-selector-loading-spinner-wrapper'); 20795 append$2(loadingElement, spinnerElement); 20796 }; 20797 const removeSpinnerElement = (loadingElement) => { 20798 remove$3(loadingElement, 'tox-image-selector-loading-spinner-wrapper'); 20799 remove$7(spinnerElement); 20800 }; 20801 return { 20802 dom: { 20803 tag: spec.tag, 20804 attributes: spec.attributes ?? {}, 20805 classes: spec.classes, 20806 }, 20807 components: [ 20808 { 20809 dom: { 20810 tag: 'div', 20811 classes: ['tox-image-selector-image-wrapper'] 20812 }, 20813 components: [ 20814 { 20815 dom: { 20816 tag: 'img', 20817 attributes: { src: imageUrl }, 20818 classes: ['tox-image-selector-image-img'] 20819 } 20820 }, 20821 ] 20822 }, 20823 ...spec.checkMark.toArray() 20824 ], 20825 behaviours: derive$1([ 20826 ...spec.behaviours ?? [], 20827 config('render-image-events', [ 20828 runOnAttached((component) => { 20829 addSpinnerElement(component.element); 20830 descendant(component.element, 'img').each((image$1) => { 20831 image(image$1).catch((e) => { 20832 // eslint-disable-next-line no-console 20833 console.error(e); 20834 }).finally(() => { 20835 removeSpinnerElement(component.element); 20836 }); 20837 }); 20838 }) 20839 ]), 20840 ]) 20841 }; 20842 }; 20843 const render$3 = (imageUrl, spec) => renderImage$1(spec, imageUrl); 20844 20845 // Converts shortcut format to Mac/PC variants 20846 // Note: This is different to the help shortcut converter, as it doesn't padd the + symbol with spaces 20847 // so as to not take up large amounts of space in the menus 20848 const convertText = (source) => { 20849 const isMac = global$7.os.isMacOS() || global$7.os.isiOS(); 20850 const mac = { 20851 alt: '\u2325', 20852 ctrl: '\u2303', 20853 shift: '\u21E7', 20854 meta: '\u2318', 20855 access: '\u2303\u2325' 20856 }; 20857 const other = { 20858 meta: 'Ctrl', 20859 access: 'Shift+Alt' 20860 }; 20861 const replace = isMac ? mac : other; 20862 const shortcut = source.split('+'); 20863 const updated = map$2(shortcut, (segment) => { 20864 // search lowercase, but if not found use the original 20865 const search = segment.toLowerCase().trim(); 20866 return has$2(replace, search) ? replace[search] : segment; 20867 }); 20868 return isMac ? updated.join('') : updated.join('+'); 20869 }; 20870 20871 const renderIcon$2 = (name, icons, classes = [iconClass]) => render$4(name, { tag: 'div', classes }, icons); 20872 const renderText = (text) => ({ 20873 dom: { 20874 tag: 'div', 20875 classes: [textClass] 20876 }, 20877 components: [text$2(global$6.translate(text))] 20878 }); 20879 const renderHtml = (html, classes) => ({ 20880 dom: { 20881 tag: 'div', 20882 classes, 20883 innerHtml: html 20884 } 20885 }); 20886 const renderStyledText = (style, text) => ({ 20887 dom: { 20888 tag: 'div', 20889 classes: [textClass] 20890 }, 20891 components: [ 20892 { 20893 dom: { 20894 tag: style.tag, 20895 styles: style.styles 20896 }, 20897 components: [text$2(global$6.translate(text))] 20898 } 20899 ] 20900 }); 20901 const renderShortcut = (shortcut) => ({ 20902 dom: { 20903 tag: 'div', 20904 classes: [accessoryClass] 20905 }, 20906 components: [ 20907 text$2(convertText(shortcut)) 20908 ] 20909 }); 20910 const renderCheckmark = (icons) => renderIcon$2('checkmark', icons, [checkmarkClass]); 20911 const renderSubmenuCaret = (icons) => renderIcon$2('chevron-right', icons, [caretClass]); 20912 const renderDownwardsCaret = (icons) => renderIcon$2('chevron-down', icons, [caretClass]); 20913 const renderContainer = (container, components) => { 20914 const directionClass = container.direction === 'vertical' ? containerColumnClass : containerRowClass; 20915 const alignClass = container.align === 'left' ? containerAlignLeftClass : containerAlignRightClass; 20916 const getValignClass = () => { 20917 switch (container.valign) { 20918 case 'top': 20919 return containerValignTopClass; 20920 case 'middle': 20921 return containerValignMiddleClass; 20922 case 'bottom': 20923 return containerValignBottomClass; 20924 } 20925 }; 20926 return { 20927 dom: { 20928 tag: 'div', 20929 classes: [ 20930 containerClass, 20931 directionClass, 20932 alignClass, 20933 getValignClass() 20934 ] 20935 }, 20936 components 20937 }; 20938 }; 20939 const renderImage = (src, classes, alt) => ({ 20940 dom: { 20941 tag: 'img', 20942 classes, 20943 attributes: { 20944 src, 20945 alt: alt.getOr('') 20946 } 20947 } 20948 }); 20949 20950 const renderColorStructure = (item, providerBackstage, fallbackIcon) => { 20951 const colorPickerCommand = 'custom'; 20952 const removeColorCommand = 'remove'; 20953 const itemValue = item.value; 20954 const iconSvg = item.iconContent.map((name) => getOr(name, providerBackstage.icons, fallbackIcon)); 20955 const attributes = item.ariaLabel.map((al) => ({ 20956 'aria-label': providerBackstage.translate(al), 20957 'data-mce-name': al 20958 })).getOr({}); 20959 const getDom = () => { 20960 const common = colorClass; 20961 const icon = iconSvg.getOr(''); 20962 const baseDom = { 20963 tag: 'div', 20964 attributes, 20965 classes: [common] 20966 }; 20967 if (itemValue === colorPickerCommand) { 20968 return { 20969 ...baseDom, 20970 tag: 'button', 20971 classes: [...baseDom.classes, 'tox-swatches__picker-btn'], 20972 innerHtml: icon 20973 }; 20974 } 20975 else if (itemValue === removeColorCommand) { 20976 return { 20977 ...baseDom, 20978 classes: [...baseDom.classes, 'tox-swatch--remove'], 20979 innerHtml: icon 20980 }; 20981 } 20982 else if (isNonNullable(itemValue)) { 20983 return { 20984 ...baseDom, 20985 attributes: { 20986 ...baseDom.attributes, 20987 'data-mce-color': itemValue 20988 }, 20989 styles: { 20990 'background-color': itemValue 20991 }, 20992 innerHtml: icon 20993 }; 20994 } 20995 else { 20996 return baseDom; 20997 } 20998 }; 20999 return { 21000 dom: getDom(), 21001 optComponents: [] 21002 }; 21003 }; 21004 const renderItemDomStructure = (ariaLabel, classes) => { 21005 const domTitle = ariaLabel.map((label) => ({ 21006 attributes: { 21007 'id': generate$6('menu-item'), 21008 'aria-label': global$6.translate(label) 21009 } 21010 })).getOr({}); 21011 return { 21012 tag: 'div', 21013 classes: [navClass, selectableClass].concat(classes), 21014 ...domTitle 21015 }; 21016 }; 21017 const createLabel = (label) => { 21018 return { 21019 dom: { 21020 tag: 'label' 21021 }, 21022 components: [ 21023 text$2(label) 21024 ] 21025 }; 21026 }; 21027 const renderNormalItemStructure = (info, providersBackstage, renderIcons, fallbackIcon) => { 21028 // TODO: TINY-3036 Work out a better way of dealing with custom icons 21029 const iconSpec = { tag: 'div', classes: [iconClass] }; 21030 const renderIcon = (iconName) => render$4(iconName, iconSpec, providersBackstage.icons, fallbackIcon); 21031 const renderEmptyIcon = () => Optional.some({ dom: iconSpec }); 21032 // Note: renderIcons indicates if any icons are present in the menu - if false then the icon column will not be present for the whole menu 21033 const leftIcon = renderIcons ? info.iconContent.map(renderIcon).orThunk(renderEmptyIcon) : Optional.none(); 21034 // TINY-3345: Dedicated columns for icon and checkmark if applicable 21035 const checkmark = info.checkMark; 21036 // Style items and autocompleter both have meta. Need to branch on
21036style 21037 // This could probably be more stable... 21038 const textRender = Optional.from(info.meta).fold(() => renderText, (meta) => has$2(meta, 'style') ? curry(renderStyledText, meta.style) : renderText); 21039 const content = info.htmlContent.fold(() => info.textContent.map(textRender), (html) => Optional.some(renderHtml(html, [textClass]))); 21040 const menuItem = { 21041 dom: renderItemDomStructure(info.ariaLabel, []), 21042 optComponents: [ 21043 leftIcon, 21044 content, 21045 info.shortcutContent.map(renderShortcut), 21046 checkmark, 21047 info.caret, 21048 info.labelContent.map(createLabel) 21049 ] 21050 }; 21051 return menuItem; 21052 }; 21053 const renderImgItemStructure = (info) => { 21054 const menuItem = { 21055 dom: renderItemDomStructure(info.ariaLabel, [imageSelectorClasll]), 21056 optComponents: [ 21057 Optional.some(render$3(info.iconContent.getOrDie(), { tag: 'div', classes: [imageClass], checkMark: info.checkMark })), 21058 info.labelContent.map(createLabel) 21059 ] 21060 }; 21061 return menuItem; 21062 }; 21063 // TODO: Maybe need aria-label 21064 const renderItemStructure = (info, providersBackstage, renderIcons, fallbackIcon = Optional.none()) => { 21065 if (info.presets === 'color') { 21066 return renderColorStructure(info, providersBackstage, fallbackIcon); 21067 } 21068 else if (info.presets === 'img') { 21069 return renderImgItemStructure(info); 21070 } 21071 else { 21072 return renderNormalItemStructure(info, providersBackstage, renderIcons, fallbackIcon); 21073 } 21074 }; 21075 21076 // Use meta to pass through special information about the tooltip 21077 // (yes this is horrible but it is not yet public API) 21078 const tooltipBehaviour = (meta, sharedBackstage, tooltipText) => get$h(meta, 'tooltipWorker') 21079 .map((tooltipWorker) => [ 21080 Tooltipping.config({ 21081 lazySink: sharedBackstage.getSink, 21082 tooltipDom: { 21083 tag: 'div', 21084 classes: ['tox-tooltip-worker-container'] 21085 }, 21086 tooltipComponents: [], 21087 anchor: (comp) => ({ 21088 type: 'submenu', 21089 item: comp, 21090 overrides: { 21091 // NOTE: this avoids it setting overflow and max-height. 21092 maxHeightFunction: expandable$1 21093 } 21094 }), 21095 mode: 'follow-highlight', 21096 onShow: (component, _tooltip) => { 21097 tooltipWorker((elm) => { 21098 Tooltipping.setComponents(component, [ 21099 external({ element: SugarElement.fromDom(elm) }) 21100 ]); 21101 }); 21102 } 21103 }) 21104 ]) 21105 .getOrThunk(() => { 21106 return tooltipText.map((text) => [ 21107 Tooltipping.config({ 21108 ...sharedBackstage.providers.tooltips.getConfig({ 21109 tooltipText: text 21110 }), 21111 mode: 'follow-highlight' 21112 }) 21113 ]).getOr([]); 21114 }); 21115 const encodeText = (text) => global$9.DOM.encode(text); 21116 const replaceText = (text, matchText) => { 21117 const translated = global$6.translate(text); 21118 const encoded = encodeText(translated); 21119 if (matchText.length > 0) { 21120 const escapedMatchRegex = new RegExp(escape(matchText), 'gi'); 21121 return encoded.replace(escapedMatchRegex, (match) => `<span class="tox-autocompleter-highlight">${match}</span>`); 21122 } 21123 else { 21124 return encoded; 21125 } 21126 }; 21127 const renderAutocompleteItem = (spec, matchText, useText, presets, onItemValueHandler, itemResponse, sharedBackstage, renderIcons = true) => { 21128 const structure = renderItemStructure({ 21129 presets, 21130 textContent: Optional.none(), 21131 htmlContent: useText ? spec.text.map((text) => replaceText(text, matchText)) : Optional.none(), 21132 ariaLabel: spec.text, 21133 labelContent: Optional.none(), 21134 iconContent: spec.icon, 21135 shortcutContent: Optional.none(), 21136 checkMark: Optional.none(), 21137 caret: Optional.none(), 21138 value: spec.value 21139 }, sharedBackstage.providers, renderIcons, spec.icon); 21140 const tooltipString = spec.text.filter((text) => !useText && text !== ''); 21141 return renderCommonItem({ 21142 context: 'mode:design', 21143 data: buildData(spec), 21144 enabled: spec.enabled, 21145 getApi: constant$1({}), 21146 onAction: (_api) => onItemValueHandler(spec.value, spec.meta), 21147 onSetup: constant$1(noop), 21148 triggersSubmenu: false, 21149 itemBehaviours: tooltipBehaviour(spec, sharedBackstage, tooltipString) 21150 }, structure, itemResponse, sharedBackstage.providers); 21151 }; 21152 21153 const renderChoiceItem = (spec, useText, presets, onItemValueHandler, isSelected, itemResponse, providersBackstage, renderIcons = true) => { 21154 const getApi = (component) => ({
21155 setActive: (state) => { 21156 Toggling.set(component, state); 21157 }, 21158 isActive: () => Toggling.isOn(component), 21159 isEnabled: () => !Disabling.isDisabled(component), 21160 setEnabled: (state) => Disabling.set(component, !state) 21161 }); 21162 const structure = renderItemStructure({ 21163 presets, 21164 textContent: useText ? spec.text : Optional.none(), 21165 htmlContent: Optional.none(), 21166 labelContent: spec.label, 21167 ariaLabel: spec.text, 21168 iconContent: spec.icon, 21169 shortcutContent: useText ? spec.shortcut : Optional.none(), 21170 // useText essentially says that we have one column. In one column lists, we should show a tick 21171 // The tick is controlled by the tickedClass (via css). It is always present 21172 // but is hidden unless the tickedClass is present. 21173 checkMark: useText ? Optional.some(renderCheckmark(providersBackstage.icons)) : Optional.none(), 21174 caret: Optional.none(), 21175 value: spec.value 21176 }, providersBackstage, renderIcons); 21177 const optTooltipping = spec.text 21178 .filter(constant$1(!useText)) 21179 .map((t) => Tooltipping.config(providersBackstage.tooltips.getConfig({ 21180 tooltipText: providersBackstage.translate(t) 21181 }))); 21182 return deepMerge(renderCommonItem({ 21183 context: spec.context, 21184 data: buildData(spec), 21185 enabled: spec.enabled, 21186 getApi, 21187 onAction: (_api) => onItemValueHandler(spec.value), 21188 onSetup: (api) => { 21189 api.setActive(isSelected); 21190 return noop; 21191 }, 21192 triggersSubmenu: false, 21193 itemBehaviours: [ 21194 ...optTooltipping.toArray() 21195 ] 21196 }, structure, itemResponse, providersBackstage), { 21197 toggling: { 21198 toggleClass: tickedClass, 21199 toggleOnExecute: false, 21200 selected: spec.active, 21201 exclusive: true 21202 } 21203 }); 21204 }; 21205 21206 const renderSeparatorItem = (spec) => ({ 21207 type: 'separator', 21208 dom: { 21209 tag: 'div', 21210 classes: [selectableClass, groupHeadingClass] 21211 }, 21212 components: spec.text.map(text$2).toArray() 21213 }); 21214 21215 // Note, this does not create a valid SketchSpec. 21216 const renderNormalItem = (spec, itemResponse, providersBackstage, renderIcons = true) => { 21217 const getApi = (component) => ({ 21218 isEnabled: () => !Disabling.isDisabled(component), 21219 setEnabled: (state) => Disabling.set(component, !state) 21220 }); 21221 const structure = renderItemStructure({ 21222 presets: 'normal', 21223 iconContent: spec.icon, 21224 textContent: spec.text, 21225 htmlContent: Optional.none(), 21226 labelContent: Optional.none(), 21227 ariaLabel: spec.text, 21228 caret: Optional.none(), 21229 checkMark: Optional.none(), 21230 shortcutContent: spec.shortcut 21231 }, providersBackstage, renderIcons); 21232 return renderCommonItem({ 21233 context: spec.context, 21234 data: buildData(spec), 21235 getApi, 21236 enabled: spec.enabled, 21237 onAction: spec.onAction, 21238 onSetup: spec.onSetup, 21239 triggersSubmenu: false, 21240 itemBehaviours: [] 21241 }, structure, itemResponse, providersBackstage); 21242 }; 21243 21244 // Note, this does not create a valid SketchSpec. 21245 const renderNestedItem = (spec, itemResponse, providersBackstage, renderIcons = true, downwardsCaret = false) => { 21246 const caret = downwardsCaret ? renderDownwardsCaret(providersBackstage.icons) : renderSubmenuCaret(providersBackstage.icons); 21247 const getApi = (component) => ({ 21248 isEnabled: () => !Disabling.isDisabled(component), 21249 setEnabled: (state) => Disabling.set(component, !state), 21250 setIconFill: (id, value) => { 21251 descendant(component.element, `svg path[class="${id}"], rect[class="${id}"]`).each((underlinePath) => { 21252 set$9(underlinePath, 'fill', value); 21253 }); 21254 }, 21255 setTooltip: (tooltip) => { 21256 const translatedTooltip = providersBackstage.translate(tooltip); 21257 set$9(component.element, 'aria-label', translatedTooltip); 21258 } 21259 }); 21260 const structure = renderItemStructure({ 21261 presets: 'normal', 21262 iconContent: spec.icon, 21263 textContent: spec.text, 21264 htmlContent: Optional.none(), 21265 ariaLabel: spec.text, 21266 labelContent: Optional.none(), 21267 caret: Optional.some(caret), 21268 checkMark: Optional.none(), 21269 shortcutContent: spec.shortcut 21270 }, providersBackstage, renderIcons); 21271 return renderCommonItem({ 21272 context: spec.context, 21273 data: buildData(spec), 21274 getApi, 21275 enabled: spec.enabled, 21276 onAction: noop, 21277 onSetup: spec.onSetup, 21278 triggersSubmenu: true, 21279 itemBehaviours: [] 21280 }, structure, itemResponse, providersBackstage); 21281 }; 21282 21283 const renderToggleMenuItem = (spec, itemResponse, providersBackstage, renderIcons = true) => { 21284 const getApi = (component) => ({
vendor: 12,749 bytes, lines 21285-21619
21285 setActive: (state) => { 21286 Toggling.set(component, state); 21287 }, 21288 isActive: () => Toggling.isOn(component), 21289 isEnabled: () => !Disabling.isDisabled(component), 21290 setEnabled: (state) => Disabling.set(component, !state) 21291 }); 21292 // BespokeSelects use meta to pass through styling information. Bespokes should only 21293 // be togglemenuitems hence meta is only passed through in this MenuItem. 21294 const structure = renderItemStructure({ 21295 iconContent: spec.icon, 21296 textContent: spec.text, 21297 htmlContent: Optional.none(), 21298 labelContent: Optional.none(), 21299 ariaLabel: spec.text, 21300 checkMark: Optional.some(renderCheckmark(providersBackstage.icons)), 21301 caret: Optional.none(), 21302 shortcutContent: spec.shortcut, 21303 presets: 'normal', 21304 meta: spec.meta 21305 }, providersBackstage, renderIcons); 21306 return deepMerge(renderCommonItem({ 21307 context: spec.context, 21308 data: buildData(spec), 21309 enabled: spec.enabled, 21310 getApi, 21311 onAction: spec.onAction, 21312 onSetup: spec.onSetup, 21313 triggersSubmenu: false, 21314 itemBehaviours: [] 21315 }, structure, itemResponse, providersBackstage), { 21316 toggling: { 21317 toggleClass: tickedClass, 21318 toggleOnExecute: false, 21319 selected: spec.active 21320 }, 21321 role: spec.role.getOrUndefined() 21322 }); 21323 }; 21324 21325 const hexColour = (value) => ({ 21326 value: normalizeHex(value) 21327 }); 21328 const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; 21329 const longformRegex = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i; 21330 const isHexString = (hex) => shorthandRegex.test(hex) || longformRegex.test(hex); 21331 const normalizeHex = (hex) => removeLeading(hex, '#').toUpperCase(); 21332 const fromString$1 = (hex) => isHexString(hex) ? Optional.some({ value: normalizeHex(hex) }) : Optional.none(); 21333 // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") 21334 const getLongForm = (hex) => { 21335 const hexString = hex.value.replace(shorthandRegex, (m, r, g, b) => r + r + g + g + b + b); 21336 return { value: hexString }; 21337 }; 21338 const extractValues = (hex) => { 21339 const longForm = getLongForm(hex); 21340 const splitForm = longformRegex.exec(longForm.value); 21341 return splitForm === null ? ['FFFFFF', 'FF', 'FF', 'FF'] : splitForm; 21342 }; 21343 const toHex = (component) => { 21344 const hex = component.toString(16); 21345 return (hex.length === 1 ? '0' + hex : hex).toUpperCase(); 21346 }; 21347 const fromRgba = (rgbaColour) => { 21348 const value = toHex(rgbaColour.red) + toHex(rgbaColour.green) + toHex(rgbaColour.blue); 21349 return hexColour(value); 21350 }; 21351 21352 const hsvColour = (hue, saturation, value) => ({ 21353 hue, 21354 saturation, 21355 value 21356 }); 21357 const fromRgb = (rgbaColour) => { 21358 let h = 0; 21359 let s = 0; 21360 let v = 0; 21361 const r = rgbaColour.red / 255; 21362 const g = rgbaColour.green / 255; 21363 const b = rgbaColour.blue / 255; 21364 const minRGB = Math.min(r, Math.min(g, b)); 21365 const maxRGB = Math.max(r, Math.max(g, b)); 21366 if (minRGB === maxRGB) { 21367 v = minRGB; 21368 return hsvColour(0, 0, v * 100); 21369 } 21370 /* eslint no-nested-ternary:0 */ 21371 const d = (r === minRGB) ? g - b : ((b === minRGB) ? r - g : b - r); 21372 h = (r === minRGB) ? 3 : ((b === minRGB) ? 1 : 5); 21373 h = 60 * (h - d / (maxRGB - minRGB)); 21374 s = (maxRGB - minRGB) / maxRGB; 21375 v = maxRGB; 21376 return hsvColour(Math.round(h), Math.round(s * 100), Math.round(v * 100)); 21377 }; 21378 21379 const min = Math.min; 21380 const max = Math.max; 21381 const round$1 = Math.round; 21382 const rgbRegex = /^\s*rgb\s*\(\s*(\d+)\s*[,\s]\s*(\d+)\s*[,\s]\s*(\d+)\s*\)\s*$/i; 21383 // This regex will match rgba(0, 0, 0, 0.5) or rgba(0, 0, 0, 50%) , or without commas 21384 const rgbaRegex = /^\s*rgba\s*\(\s*(\d+)\s*[,\s]\s*(\d+)\s*[,\s]\s*(\d+)\s*[,\s]\s*((?:\d?\.\d+|\d+)%?)\s*\)\s*$/i; 21385 const rgbaColour = (red, green, blue, alpha) => ({ 21386 red, 21387 green, 21388 blue, 21389 alpha 21390 }); 21391 const isRgbaComponent = (value) => { 21392 const num = parseInt(value, 10); 21393 return num.toString() === value && num >= 0 && num <= 255; 21394 }; 21395 const fromHsv = (hsv) => { 21396 let r; 21397 let g; 21398 let b; 21399 const hue = (hsv.hue || 0) % 360; 21400 let saturation = hsv.saturation / 100; 21401 let brightness = hsv.value / 100; 21402 saturation = max(0, min(saturation, 1)); 21403 brightness = max(0, min(brightness, 1)); 21404 if (saturation === 0) { 21405 r = g = b = round$1(255 * brightness); 21406 return rgbaColour(r, g, b, 1); 21407 } 21408 const side = hue / 60; 21409 const chroma = brightness * saturation; 21410 const x = chroma * (1 - Math.abs(side % 2 - 1)); 21411 const match = brightness - chroma; 21412 switch (Math.floor(side)) { 21413 case 0: 21414 r = chroma; 21415 g = x; 21416 b = 0; 21417 break; 21418 case 1: 21419 r = x; 21420 g = chroma; 21421 b = 0; 21422 break; 21423 case 2: 21424 r = 0; 21425 g = chroma; 21426 b = x; 21427 break; 21428 case 3: 21429 r = 0; 21430 g = x; 21431 b = chroma; 21432 break; 21433 case 4: 21434 r = x; 21435 g = 0; 21436 b = chroma; 21437 break; 21438 case 5: 21439 r = chroma; 21440 g = 0; 21441 b = x; 21442 break; 21443 default: 21444 r = g = b = 0; 21445 } 21446 r = round$1(255 * (r + match)); 21447 g = round$1(255 * (g + match)); 21448 b = round$1(255 * (b + match)); 21449 return rgbaColour(r, g, b, 1); 21450 }; 21451 // Temporarily using: https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb 21452 const fromHex = (hexColour) => { 21453 const result = extractValues(hexColour); 21454 const red = parseInt(result[1], 16); 21455 const green = parseInt(result[2], 16); 21456 const blue = parseInt(result[3], 16); 21457 return rgbaColour(red, green, blue, 1); 21458 }; 21459 const fromStringValues = (red, green, blue, alpha) => { 21460 const r = parseInt(red, 10); 21461 const g = parseInt(green, 10); 21462 const b = parseInt(blue, 10); 21463 const a = parseFloat(alpha); 21464 return rgbaColour(r, g, b, a); 21465 }; 21466 const fromString = (rgbaString) => { 21467 const rgbMatch = rgbRegex.exec(rgbaString); 21468 if (rgbMatch !== null) { 21469 return Optional.some(fromStringValues(rgbMatch[1], rgbMatch[2], rgbMatch[3], '1')); 21470 } 21471 const rgbaMatch = rgbaRegex.exec(rgbaString); 21472 if (rgbaMatch !== null) { 21473 return Optional.some(fromStringValues(rgbaMatch[1], rgbaMatch[2], rgbaMatch[3], rgbaMatch[4])); 21474 } 21475 return Optional.none(); 21476 }; 21477 const toString = (rgba) => `rgba(${rgba.red},${rgba.green},${rgba.blue},${rgba.alpha})`; 21478 const red = rgbaColour(255, 0, 0, 1); 21479 21480 const hexToHsv = (hex) => fromRgb(fromHex(hex)); 21481 const hsvToHex = (hsv) => fromRgba(fromHsv(hsv)); 21482 const anyToHex = (color) => fromString$1(color) 21483 .orThunk(() => fromString(color).map(fromRgba)) 21484 .getOrThunk(() => { 21485 // Not dealing with Hex or RGBA so use a canvas to parse the color 21486 const canvas = document.createElement('canvas'); 21487 canvas.height = 1; 21488 canvas.width = 1; 21489 const canvasContext = canvas.getContext('2d'); 21490 // all valid colors after this point 21491 canvasContext.clearRect(0, 0, canvas.width, canvas.height); 21492 // invalid colors will be shown as white - the first assignment will pass and the second may be ignored 21493 canvasContext.fillStyle = '#FFFFFF'; 21494 canvasContext.fillStyle = color; 21495 canvasContext.fillRect(0, 0, 1, 1); 21496 const rgba = canvasContext.getImageData(0, 0, 1, 1).data; 21497 const r = rgba[0]; 21498 const g = rgba[1]; 21499 const b = rgba[2]; 21500 const a = rgba[3]; 21501 return fromRgba(rgbaColour(r, g, b, a)); 21502 }); 21503 21504 const fireSkinLoaded$1 = (editor) => { 21505 editor.dispatch('SkinLoaded'); 21506 }; 21507 const fireSkinLoadError$1 = (editor, error) => { 21508 editor.dispatch('SkinLoadError', error); 21509 }; 21510 const fireResizeEditor = (editor) => { 21511 editor.dispatch('ResizeEditor'); 21512 }; 21513 const fireResizeContent = (editor, e) => { 21514 editor.dispatch('ResizeContent', e); 21515 }; 21516 const fireScrollContent = (editor, e) => { 21517 editor.dispatch('ScrollContent', e); 21518 }; 21519 const fireTextColorChange = (editor, data) => { 21520 editor.dispatch('TextColorChange', data); 21521 }; 21522 const fireAfterProgressState = (editor, state) => { 21523 editor.dispatch('AfterProgressState', { state }); 21524 }; 21525 const fireResolveName = (editor, node) => editor.dispatch('ResolveName', { 21526 name: node.nodeName.toLowerCase(), 21527 target: node 21528 }); 21529 const fireToggleToolbarDrawer = (editor, state) => { 21530 editor.dispatch('ToggleToolbarDrawer', { state }); 21531 }; 21532 const fireStylesTextUpdate = (editor, data) => { 21533 editor.dispatch('StylesTextUpdate', data); 21534 }; 21535 const fireAlignTextUpdate = (editor, data) => { 21536 editor.dispatch('AlignTextUpdate', data); 21537 }; 21538 const fireFontSizeTextUpdate = (editor, data) => { 21539 editor.dispatch('FontSizeTextUpdate', data); 21540 }; 21541 const fireFontSizeInputTextUpdate = (editor, data) => { 21542 editor.dispatch('FontSizeInputTextUpdate', data); 21543 }; 21544 const fireBlocksTextUpdate = (editor, data) => { 21545 editor.dispatch('BlocksTextUpdate', data); 21546 }; 21547 const fireFontFamilyTextUpdate = (editor, data) => { 21548 editor.dispatch('FontFamilyTextUpdate', data); 21549 }; 21550 const fireToggleSidebar = (editor) => { 21551 editor.dispatch('ToggleSidebar'); 21552 }; 21553 const fireToggleView = (editor) => { 21554 editor.dispatch('ToggleView'); 21555 }; 21556 const fireContextToolbarClose = (editor) => { 21557 editor.dispatch('ContextToolbarClose'); 21558 }; 21559 const fireContextFormSlideBack = (editor) => { 21560 editor.dispatch('ContextFormSlideBack'); 21561 }; 21562 21563 const composeUnbinders = (f, g) => () => { 21564 f(); 21565 g(); 21566 }; 21567 const onSetupEditableToggle = (editor, enabledPredicate = always) => onSetupEvent(editor, 'NodeChange', (api) => { 21568 api.setEnabled(editor.selection.isEditable() && enabledPredicate()); 21569 }); 21570 const onSetupFormatToggle = (editor, name) => (api) => { 21571 const boundFormatChangeCallback = unbindable(); 21572 const init = () => { 21573 api.setActive(editor.formatter.match(name)); 21574 const binding = editor.formatter.formatChanged(name, api.setActive); 21575 boundFormatChangeCallback.set(binding); 21576 }; 21577 // The editor may or may not have been setup yet, so check for that 21578 editor.initialized ? init() : editor.once('init', init); 21579 return () => { 21580 editor.off('init', init); 21581 boundFormatChangeCallback.clear(); 21582 }; 21583 }; 21584 const onSetupStateToggle = (editor, name) => (api) => { 21585 const unbindEditableToogle = onSetupEditableToggle(editor)(api); 21586 const unbindFormatToggle = onSetupFormatToggle(editor, name)(api); 21587 return () => { 21588 unbindEditableToogle(); 21589 unbindFormatToggle(); 21590 }; 21591 }; 21592 const onSetupEvent = (editor, event, f) => (api) => { 21593 const handleEvent = () => f(api); 21594 const init = () => { 21595 f(api); 21596 editor.on(event, handleEvent); 21597 }; 21598 // The editor may or may not have been setup yet, so check for that 21599 editor.initialized ? init() : editor.once('init', init); 21600 return () => { 21601 editor.off('init', init); 21602 editor.off(event, handleEvent); 21603 }; 21604 }; 21605 const onActionToggleFormat$1 = (editor) => (rawItem) => () => { 21606 editor.undoManager.transact(() => { 21607 editor.focus(); 21608 editor.execCommand('mceToggleFormat', false, rawItem.format); 21609 }); 21610 }; 21611 const onActionExecCommand = (editor, command) => () => editor.execCommand(command); 21612 21613 var global$5 = tinymce.util.Tools.resolve('tinymce.util.LocalStorage'); 21614 21615 const cacheStorage = {}; 21616 const ColorCache = (storageId, max = 10) => { 21617 const storageString = global$5.getItem(storageId); 21618 const localstorage = isString(storageString) ? JSON.parse(storageString) : []; 21619 const prune = (list) => {
21620 // When the localStorage cache is too big, 21621 // remove the difference from the tail (head is fresh, tail is stale!) 21622 const diff = max - list.length; 21623 return (diff < 0) ? list.slice(0, max) : list; 21624 }; 21625 const cache = prune(localstorage); 21626 const add = (key) => { 21627 // Remove duplicates first. 21628 indexOf(cache, key).each(remove); 21629 cache.unshift(key); 21630 // When max size is exceeded, the oldest colors will be removed 21631 if (cache.length > max) { 21632 cache.pop(); 21633 } 21634 global$5.setItem(storageId, JSON.stringify(cache)); 21635 }; 21636 const remove = (idx) => { 21637 cache.splice(idx, 1); 21638 }; 21639 const state = () => cache.slice(0); 21640 return { 21641 add, 21642 state 21643 }; 21644 }; 21645 const getCacheForId = (id) => get$h(cacheStorage, id).getOrThunk(() => { 21646 const storageId = `tinymce-custom-colors-${id}`; 21647 const currentData = global$5.getItem(storageId); 21648 if (isNullable(currentData)) { 21649 const legacyDefault = global$5.getItem('tinymce-custom-colors'); 21650 global$5.setItem(storageId, isNonNullable(legacyDefault) ? legacyDefault : '[]'); 21651 } 21652 const storage = ColorCache(storageId, 10); 21653 cacheStorage[id] = storage; 21654 return storage; 21655 }); 21656 const getCurrentColors = (id) => map$2(getCacheForId(id).state(), (color) => ({ 21657 type: 'choiceitem', 21658 text: color, 21659 icon: 'checkmark', 21660 value: color 21661 })); 21662 const addColor = (id, color) => { 21663 getCacheForId(id).add(color); 21664 }; 21665 21666 const foregroundId = 'forecolor'; 21667 const backgroundId = 'hilitecolor'; 21668 const fallbackCols = 5; 21669 const mapColors = (colorMap) => mapColorsRaw(colorMap.map((color, index) => { 21670 if (index % 2 === 0) { 21671 return '#' + anyToHex(color).value; 21672 } 21673 return color; 21674 })); 21675 const mapColorsRaw = (colorMap) => { 21676 const colors = []; 21677 for (let i = 0; i < colorMap.length; i += 2) { 21678 colors.push({ 21679 text: colorMap[i + 1], 21680 value: colorMap[i], 21681 icon: 'checkmark', 21682 type: 'choiceitem' 21683 }); 21684 } 21685 return colors; 21686 }; 21687 const option$1 = (name) => (editor) => editor.options.get(name); 21688 const fallbackColor = '#000000'; 21689 const register$e = (editor) => { 21690 const registerOption = editor.options.register; 21691 const colorProcessor = (value) => { 21692 if (isArrayOf(value, isString)) {
21693 return { value: mapColors(value), valid: true }; 21694 } 21695 else { 21696 return { valid: false, message: 'Must be an array of strings.' }; 21697 } 21698 }; 21699 const colorProcessorRaw = (value) => { 21700 if (isArrayOf(value, isString)) { 21701 return { value: mapColorsRaw(value), valid: true }; 21702 } 21703 else { 21704 return { valid: false, message: 'Must be an array of strings.' }; 21705 } 21706 }; 21707 const colorColsProcessor = (value) => { 21708 if (isNumber(value) && value > 0) { 21709 return { value, valid: true }; 21710 } 21711 else { 21712 return { valid: false, message: 'Must be a positive number.' }; 21713 } 21714 }; 21715 registerOption('color_map', { 21716 processor: colorProcessor, 21717 default: [ 21718 '#BFEDD2', 'Light Green', 21719 '#FBEEB8', 'Light Yellow', 21720 '#F8CAC6', 'Light Red', 21721 '#ECCAFA', 'Light Purple', 21722 '#C2E0F4', 'Light Blue', 21723 '#2DC26B', 'Green', 21724 '#F1C40F', 'Yellow', 21725 '#E03E2D', 'Red', 21726 '#B96AD9', 'Purple', 21727 '#3598DB', 'Blue', 21728 '#169179', 'Dark Turquoise', 21729 '#E67E23', 'Orange', 21730 '#BA372A', 'Dark Red', 21731 '#843FA1', 'Dark Purple', 21732 '#236FA1', 'Dark Blue', 21733 '#ECF0F1', 'Light Gray', 21734 '#CED4D9', 'Medium Gray', 21735 '#95A5A6', 'Gray', 21736 '#7E8C8D', 'Dark Gray', 21737 '#34495E', 'Navy Blue', 21738 '#000000', 'Black', 21739 '#ffffff', 'White' 21740 ] 21741 }); 21742 registerOption('color_map_raw', { 21743 processor: colorProcessorRaw, 21744 }); 21745 registerOption('color_map_background', { 21746 processor: colorProcessor 21747 }); 21748 registerOption('color_map_foreground', { 21749 processor: colorProcessor 21750 }); 21751 registerOption('color_cols', { 21752 processor: colorColsProcessor, 21753 default: calcCols(editor) 21754 }); 21755 registerOption('color_cols_foreground', { 21756 processor: colorColsProcessor, 21757 default: defaultCols(editor, foregroundId) 21758 }); 21759 registerOption('color_cols_background', { 21760 processor: colorColsProcessor, 21761 default: defaultCols(editor, backgroundId) 21762 }); 21763 registerOption('custom_colors', { 21764 processor: 'boolean', 21765 default: true 21766 }); 21767 registerOption('color_default_foreground', { 21768 processor: 'string', 21769 default: fallbackColor 21770 }); 21771 registerOption('color_default_background', { 21772 processor: 'string', 21773 default: fallbackColor 21774 }); 21775 }; 21776 const getColors$2 = (editor, id) => { 21777 if (id === foregroundId && editor.options.isSet('color_map_foreground')) { 21778 return option$1('color_map_foreground')(editor); 21779 } 21780 else if (id === backgroundId && editor.options.isSet('color_map_background')) { 21781 return option$1('color_map_background')(editor); 21782 } 21783 else if (editor.options.isSet('color_map_raw')) { 21784 return option$1('color_map_raw')(editor); 21785 } 21786 else { 21787 return option$1('color_map')(editor); 21788 } 21789 }; 21790 const calcCols = (editor, id = 'default') => Math.max(fallbackCols, Math.ceil(Math.sqrt(getColors$2(editor, id).length))); 21791 const defaultCols = (editor, id) => { 21792 const defaultCols = option$1('color_cols')(editor); 21793 const calculatedCols = calcCols(editor, id); 21794 if (defaultCols === calcCols(editor)) { 21795 return calculatedCols; 21796 } 21797 else { 21798 return defaultCols; 21799 } 21800 }; 21801 const getColorCols$1 = (editor, id = 'default') => { 21802 const getCols = () => { 21803 if (id === foregroundId) { 21804 return option$1('color_cols_foreground')(editor); 21805 } 21806 else if (id === backgroundId) { 21807 return option$1('color_cols_background')(editor); 21808 } 21809 else { 21810 return option$1('color_cols')(editor); 21811 } 21812 }; 21813 return Math.round(getCols()); 21814 }; 21815 const hasCustomColors$1 = option$1('custom_colors'); 21816 const getDefaultForegroundColor = option$1('color_default_foreground');
21817 const getDefaultBackgroundColor = option$1('color_default_background'); 21818 21819 const defaultBackgroundColor = 'rgba(0, 0, 0, 0)'; 21820 const isValidBackgroundColor = (value) => fromString(value).exists((c) => c.alpha !== 0); 21821 // Climb up the tree to find the value of the background until finding a non-transparent value or defaulting. 21822 const getClosestCssBackgroundColorValue = (scope) => { 21823 return closest(scope, (node) => { 21824 if (isElement$1(node)) { 21825 const color = get$e(node, 'background-color'); 21826 return someIf(isValidBackgroundColor(color), color); 21827 } 21828 else { 21829 return Optional.none(); 21830 } 21831 }).getOr(defaultBackgroundColor); 21832 }; 21833 const getCurrentColor = (editor, format) => { 21834 const node = SugarElement.fromDom(editor.selection.getStart()); 21835 const cssRgbValue = format === 'hilitecolor' 21836 ? getClosestCssBackgroundColorValue(node) 21837 : get$e(node, 'color'); 21838 return fromString(cssRgbValue).map((rgba) => '#' + fromRgba(rgba).value); 21839 }; 21840 const applyFormat = (editor, format, value) => { 21841 editor.undoManager.transact(() => { 21842 editor.focus(); 21843 editor.formatter.apply(format, { value }); 21844 editor.nodeChanged(); 21845 }); 21846 }; 21847 const removeFormat = (editor, format) => { 21848 editor.undoManager.transact(() => { 21849 editor.focus(); 21850 editor.formatter.remove(format, { value: null }, undefined, true); 21851 editor.nodeChanged(); 21852 }); 21853 }; 21854 const registerCommands = (editor) => { 21855 editor.addCommand('mceApplyTextcolor', (format, value) => { 21856 applyFormat(editor, format, value); 21857 }); 21858 editor.addCommand('mceRemoveTextcolor', (format) => { 21859 removeFormat(editor, format); 21860 }); 21861 }; 21862 const getAdditionalColors = (hasCustom) => { 21863 const type = 'choiceitem'; 21864 const remove = { 21865 type, 21866 text: 'Remove color', 21867 icon: 'color-swatch-remove-color', 21868 value: 'remove' 21869 }; 21870 const custom = { 21871 type, 21872 text: 'Custom color', 21873 icon: 'color-picker', 21874 value: 'custom' 21875 };
21876 return hasCustom ? [ 21877 remove, 21878 custom 21879 ] : [remove]; 21880 }; 21881 const applyColor = (editor, format, value, onChoice) => { 21882 if (value === 'custom') { 21883 const dialog = colorPickerDialog(editor); 21884 dialog((colorOpt) => { 21885 colorOpt.each((color) => { 21886 addColor(format, color); 21887 editor.execCommand('mceApplyTextcolor', format, color); 21888 onChoice(color); 21889 }); 21890 }, getCurrentColor(editor, format).getOr(fallbackColor)); 21891 } 21892 else if (value === 'remove') { 21893 onChoice(''); 21894 editor.execCommand('mceRemoveTextcolor', format); 21895 } 21896 else { 21897 onChoice(value); 21898 editor.execCommand('mceApplyTextcolor', format, value); 21899 } 21900 }; 21901 const getColors$1 = (colors, id, hasCustom) => colors.concat(getCurrentColors(id).concat(getAdditionalColors(hasCustom))); 21902 const getFetch$1 = (colors, id, hasCustom) => (callback) => { 21903 callback(getColors$1(colors, id, hasCustom)); 21904 }; 21905 const setIconColor = (splitButtonApi, name, newColor) => { 21906 const id = name === 'forecolor' ? 'tox-icon-text-color__color' : 'tox-icon-highlight-bg-color__color'; 21907 splitButtonApi.setIconFill(id, newColor); 21908 }; 21909 const setTooltip = (buttonApi, tooltip) => { 21910 buttonApi.setTooltip(tooltip); 21911 }; 21912 const select$1 = (editor, format) => (value) => { 21913 const optCurrentHex = getCurrentColor(editor, format); 21914 return is$1(optCurrentHex, value.toUpperCase()); 21915 }; 21916 // Selecting `Remove Color` would set the lastColor to '' 21917 const getToolTipText = (editor, format, lastColor) => { 21918 if (isEmpty(lastColor)) { 21919 return format === 'forecolor' ? 'Text color' : 'Background color'; 21920 } 21921 const tooltipPrefix = format === 'forecolor' ? 'Text color {0}' : 'Background color {0}'; 21922 const colors = getColors$1(getColors$2(editor, format), format, false); 21923 const colorText = find$5(colors, (c) => c.value === lastColor).getOr({ text: '' }).text; 21924 return editor.translate([tooltipPrefix, editor.translate(colorText)]); 21925 }; 21926 const registerTextColorButton = (editor, name, format, lastColor) => { 21927 editor.ui.registry.addSplitButton(name, { 21928 tooltip: getToolTipText(editor, format, lastColor.get()), 21929 chevronTooltip: name === 'forecolor' ? 'Text color menu' : 'Background color menu', 21930 presets: 'color', 21931 icon: name === 'forecolor' ? 'text-color' : 'highlight-bg-color', 21932 select: select$1(editor, format), 21933 columns: getColorCols$1(editor, format), 21934 fetch: getFetch$1(getColors$2(editor, format), format, hasCustomColors$1(editor)), 21935 onAction: (_splitButtonApi) => { 21936 applyColor(editor, format, lastColor.get(), noop); 21937 }, 21938 onItemAction: (_splitButtonApi, value) => { 21939 applyColor(editor, format, value, (newColor) => { 21940 lastColor.set(newColor); 21941 fireTextColorChange(editor, { 21942 name, 21943 color: newColor 21944 }); 21945 }); 21946 }, 21947 onSetup: (splitButtonApi) => { 21948 setIconColor(splitButtonApi, name, lastColor.get()); 21949 const handler = (e) => { 21950 if (e.name === name) { 21951 setIconColor(splitButtonApi, e.name, e.color); 21952 setTooltip(splitButtonApi, getToolTipText(editor, format, e.color)); 21953 } 21954 }; 21955 editor.on('TextColorChange', handler); 21956 return composeUnbinders(onSetupEditableToggle(editor)(splitButtonApi), () => { 21957 editor.off('TextColorChange', handler); 21958 }); 21959 } 21960 }); 21961 }; 21962 const registerTextColorMenuItem = (editor, name, format, text, lastColor) => { 21963 editor.ui.registry.addNestedMenuItem(name, { 21964 text, 21965 icon: name === 'forecolor' ? 'text-color' : 'highlight-bg-color', 21966 onSetup: (api) => { 21967 setTooltip(api, getToolTipText(editor, format, lastColor.get())); 21968 setIconColor(api, name, lastColor.get()); 21969 return onSetupEditableToggle(editor)(api); 21970 }, 21971 getSubmenuItems: () => [ 21972 { 21973 type: 'fancymenuitem', 21974 fancytype: 'colorswatch', 21975 select: select$1(editor, format), 21976 initData: { 21977 storageKey: format, 21978 }, 21979 onAction: (data) => { 21980 applyColor(editor, format, data.value, (newColor) => { 21981 lastColor.set(newColor); 21982 fireTextColorChange(editor, { 21983 name, 21984 color: newColor 21985 }); 21986 }); 21987 }, 21988 } 21989 ] 21990 }); 21991 }; 21992 const colorPickerDialog = (editor) => (callback, value) => { 21993 let isValid = false;
vendor: 2,415 bytes, lines 21994-22061
21994 const onSubmit = (api) => { 21995 const data = api.getData(); 21996 const hex = data.colorpicker; 21997 if (isValid) { 21998 callback(Optional.from(hex)); 21999 api.close(); 22000 } 22001 else { 22002 editor.windowManager.alert(editor.translate(['Invalid hex color code: {0}', hex])); 22003 } 22004 }; 22005 const onAction = (_api, details) => { 22006 if (details.name === 'hex-valid') { 22007 isValid = details.value; 22008 } 22009 }; 22010 const initialData = { 22011 colorpicker: value 22012 }; 22013 editor.windowManager.open({ 22014 title: 'Color Picker', 22015 size: 'normal', 22016 body: { 22017 type: 'panel', 22018 items: [ 22019 { 22020 type: 'colorpicker', 22021 name: 'colorpicker', 22022 label: 'Color' 22023 } 22024 ] 22025 }, 22026 buttons: [ 22027 { 22028 type: 'cancel', 22029 name: 'cancel', 22030 text: 'Cancel' 22031 }, 22032 { 22033 type: 'submit', 22034 name: 'save', 22035 text: 'Save', 22036 primary: true 22037 } 22038 ], 22039 initialData, 22040 onAction, 22041 onSubmit, 22042 onClose: noop, 22043 onCancel: () => { 22044 callback(Optional.none()); 22045 } 22046 }); 22047 }; 22048 const register$d = (editor) => { 22049 registerCommands(editor); 22050 const fallbackColorForeground = getDefaultForegroundColor(editor); 22051 const fallbackColorBackground = getDefaultBackgroundColor(editor); 22052 const lastForeColor = Cell(fallbackColorForeground); 22053 const lastBackColor = Cell(fallbackColorBackground); 22054 registerTextColorButton(editor, 'forecolor', 'forecolor', lastForeColor); 22055 registerTextColorButton(editor, 'backcolor', 'hilitecolor', lastBackColor); 22056 registerTextColorMenuItem(editor, 'forecolor', 'forecolor', 'Text color', lastForeColor); 22057 registerTextColorMenuItem(editor, 'backcolor', 'hilitecolor', 'Background color', lastBackColor); 22058 }; 22059 22060 const renderImgItem = (spec, onItemValueHandler, isSelected, itemResponse, providersBackstage) => { 22061 const getApi = (component) => ({
22062 setActive: (state) => { 22063 Toggling.set(component, state); 22064 }, 22065 isActive: () => Toggling.isOn(component), 22066 isEnabled: () => !Disabling.isDisabled(component), 22067 setEnabled: (state) => Disabling.set(component, !state) 22068 }); 22069 const structure = renderItemStructure({ 22070 presets: 'img', 22071 textContent: Optional.none(), 22072 htmlContent: Optional.none(), 22073 ariaLabel: spec.tooltip, 22074 iconContent: Optional.some(spec.url), 22075 labelContent: spec.label, 22076 shortcutContent: Optional.none(), 22077 checkMark: Optional.some(renderCheckmark(providersBackstage.icons)), 22078 caret: Optional.none(), 22079 value: spec.value 22080 }, providersBackstage, true); 22081 const optTooltipping = spec.tooltip 22082 .map((t) => Tooltipping.config(providersBackstage.tooltips.getConfig({ 22083 tooltipText: providersBackstage.translate(t) 22084 }))); 22085 return deepMerge(renderCommonItem({ 22086 context: spec.context, 22087 data: buildData(spec), 22088 enabled: spec.enabled, 22089 getApi, 22090 onAction: (api) => { 22091 onItemValueHandler(spec.value); 22092 api.setActive(true); 22093 }, 22094 onSetup: (api) => { 22095 api.setActive(isSelected); 22096 return noop; 22097 }, 22098 triggersSubmenu: false, 22099 itemBehaviours: [ 22100 ...optTooltipping.toArray() 22101 ] 22102 }, structure, itemResponse, providersBackstage), { 22103 toggling: { 22104 toggleClass: tickedClass, 22105 toggleOnExecute: false, 22106 selected: spec.active, 22107 exclusive: true 22108 } 22109 }); 22110 }; 22111 22112 const createPartialChoiceMenu = (value, items, onItemValueHandler, columns, presets, itemResponse, select, providersBackstage) => { 22113 const hasIcons = menuHasIcons(items); 22114 const presetItemTypes = presets !== 'color' ? 'normal' : 'color'; 22115 const alloyItems = createChoiceItems(items, onItemValueHandler, columns, presetItemTypes, itemResponse, select, providersBackstage); 22116 const menuLayout = { 22117 menuType: presets 22118 }; 22119 return createPartialMenuWithAlloyItems(value, hasIcons, alloyItems, columns, menuLayout); 22120 }; 22121 const createChoiceItems = (items, onItemValueHandler, columns, itemPresets, itemResponse, select, providersBackstage) => cat(map$2(items, (item) => { 22122 if (item.type === 'choiceitem') { 22123 return createChoiceMenuItem(item).fold(handleError, (d) => Optional.some(renderChoiceItem(d, columns === 1, itemPresets, onItemValueHandler, select(d.value), itemResponse, providersBackstage, menuHasIcons(items)))); 22124 } 22125 else if (item.type === 'imageitem') { 22126 return createImageMenuItem(item).fold(handleError, (d) => Optional.some(renderImgItem(d, onItemValueHandler, select(d.value), itemResponse, providersBackstage))); 22127 } 22128 else if (item.type === 'resetimage') { 22129 return createResetImageItem(item).fold(handleError, (d) => Optional.some(renderChoiceItem(({ 22130 ...d, 22131 type: 'choiceitem', 22132 text: d.tooltip, 22133 icon: Optional.some(d.icon), 22134 label: Optional.some(d.label), 22135 }), columns === 1, itemPresets, onItemValueHandler, select(d.value), itemResponse, providersBackstage, menuHasIcons(items)))); 22136 } 22137 else { 22138 return Optional.none(); 22139 } 22140 })); 22141 22142 const deriveMenuMovement = (columns, presets) => { 22143 const menuMarkers = markers(presets); 22144 if (columns === 1) { 22145 return { mode: 'menu' }; 22146 } 22147 else if (columns === 'auto') { 22148 return { 22149 mode: 'grid', 22150 selector: '.' + menuMarkers.item, 22151 initSize: { 22152 numColumns: 1, 22153 numRows: 1 22154 } 22155 }; 22156 } 22157 else { 22158 const rowClass = { 22159 color: 'tox-swatches__row', 22160 imageselector: 'tox-image-selector__row', 22161 listpreview: 'tox-collection__group', 22162 normal: 'tox-collection__group' 22163 }[presets]; 22164 return { 22165 mode: 'matrix', 22166 rowSelector: '.' + rowClass, 22167 previousSelector: (menu) => { 22168 // We only want the navigation to start on the selected item if we are in color-mode (The colorswatch) 22169 return presets === 'color' 22170 ? descendant(menu.element, '[aria-checked=true]') 22171 : Optional.none(); 22172 } 22173 }; 22174 } 22175 }; 22176 const deriveCollectionMovement = (columns, presets) => { 22177 if (columns === 1) { 22178 return { 22179 mode: 'menu', 22180 selector: '.tox-collection__item' 22181 }; 22182 } 22183 else if (columns === 'auto') { 22184 return { 22185 mode: 'flatgrid', 22186 selector: '.' + 'tox-collection__item', 22187 initSize: { 22188 numColumns: 1, 22189 numRows: 1 22190 } 22191 }; 22192 } 22193 else { 22194 return { 22195 mode: 'matrix', 22196 selectors: { 22197 row: presets === 'color' ? '.tox-swatches__row' : '.tox-collection__group', 22198 cell: presets === 'color' ? `.${colorClass}` : `.${selectableClass}` 22199 } 22200 }; 22201 } 22202 }; 22203 22204 const renderColorSwatchItem = (spec, backstage) => { 22205 const items = getColorItems(spec, backstage); 22206 const columns = backstage.colorinput.getColorCols(spec.initData.storageKey); 22207 const presets = 'color';
22208 const menuSpec = createPartialChoiceMenu(generate$6('menu-value'), items, (value) => { 22209 spec.onAction({ value }); 22210 }, columns, presets, ItemResponse$1.CLOSE_ON_EXECUTE, spec.select.getOr(never), backstage.shared.providers); 22211 const widgetSpec = { 22212 ...menuSpec, 22213 markers: markers(presets), 22214 movement: deriveMenuMovement(columns, presets), 22215 // TINY-10806: Avoid duplication of ARIA role="menu" in the accessibility tree for Color Swatch menu item. 22216 showMenuRole: false 22217 }; 22218 return { 22219 type: 'widget', 22220 data: { value: generate$6('widget-id') }, 22221 dom: { 22222 tag: 'div', 22223 classes: ['tox-fancymenuitem'] 22224 }, 22225 autofocus: true, 22226 components: [ 22227 parts$7.widget(Menu.sketch(widgetSpec)) 22228 ] 22229 }; 22230 }; 22231 const getColorItems = (spec, backstage) => { 22232 const useCustomColors = spec.initData.allowCustomColors && backstage.colorinput.hasCustomColors(); 22233 return spec.initData.colors.fold(() => getColors$1(backstage.colorinput.getColors(spec.initData.storageKey), spec.initData.storageKey, useCustomColors), (colors) => colors.concat(getAdditionalColors(useCustomColors))); 22234 }; 22235 22236 const renderImageSelector = (spec, backstage) => { 22237 const presets = 'imageselector'; 22238 const columns = spec.initData.columns; 22239 const menuSpec = createPartialChoiceMenu(generate$6('menu-value'), spec.initData.items, (value) => { 22240 spec.onAction({ value }); 22241 }, columns, presets, ItemResponse$1.CLOSE_ON_EXECUTE, spec.select.getOr(never), backstage.shared.providers); 22242 const widgetSpec = { 22243 ...menuSpec, 22244 markers: markers(presets), 22245 movement: deriveMenuMovement(columns, presets), 22246 // TINY-10806: Avoid duplication of ARIA role="menu" in the accessibility tree for Image Selector menu item. 22247 showMenuRole: false 22248 }; 22249 return { 22250 type: 'widget', 22251 data: { value: generate$6('widget-id') }, 22252 dom: { 22253 tag: 'div', 22254 classes: ['tox-fancymenuitem', 'tox-collection--toolbar'] 22255 }, 22256 autofocus: true, 22257 components: [ 22258 parts$7.widget(Menu.sketch(widgetSpec)) 22259 ] 22260 }; 22261 }; 22262 22263 const cellOverEvent = generate$6('cell-over'); 22264 const cellExecuteEvent = generate$6('cell-execute'); 22265 const makeAnnouncementText = (backstage) => (row, col) => backstage.shared.providers.translate(['{0} columns, {1} rows', col, row]); 22266 const makeCell = (row, col, label) => { 22267 const emitCellOver = (c) => emitWith(c, cellOverEvent, { row, col }); 22268 const emitExecute = (c) => emitWith(c, cellExecuteEvent, { row, col }); 22269 const onClick = (c, se) => { 22270 se.stop(); 22271 emitExecute(c); 22272 }; 22273 return build$1({ 22274 dom: { 22275 tag: 'div', 22276 attributes: { 22277 role: 'button', 22278 ['aria-label']: label 22279 } 22280 }, 22281 behaviours: derive$1([ 22282 config('insert-table-picker-cell', [ 22283 run$1(mouseover(), Focusing.focus), 22284 run$1(execute$5(), emitExecute), 22285 run$1(click(), onClick), 22286 run$1(tap(), onClick) 22287 ]), 22288 Toggling.config({ 22289 toggleClass: 'tox-insert-table-picker__selected', 22290 toggleOnExecute: false 22291 }), 22292 Focusing.config({ onFocus: emitCellOver }) 22293 ]) 22294 }); 22295 }; 22296 const makeCells = (getCellLabel, numRows, numCols) => { 22297 const cells = []; 22298 for (let i = 0; i < numRows; i++) { 22299 const row = []; 22300 for (let j = 0; j < numCols; j++) { 22301 const label = getCellLabel(i + 1, j + 1); 22302 row.push(makeCell(i, j, label)); 22303 } 22304 cells.push(row); 22305 } 22306 return cells; 22307 }; 22308 const selectCells = (cells, selectedRow, selectedColumn, numRows, numColumns) => { 22309 for (let i = 0; i < numRows; i++) { 22310 for (let j = 0; j < numColumns; j++) { 22311 Toggling.set(cells[i][j], i <= selectedRow && j <= selectedColumn); 22312 } 22313 } 22314 }; 22315 const makeComponents = (cells) =>
vendor: 6,712 bytes, lines 22315-22465
22315 bind$3(cells, (cellRow) => map$2(cellRow, premade)); 22316 const makeLabelText = (row, col) => text$2(`${col}x${row}`); 22317 const renderInsertTableMenuItem = (spec, backstage) => { 22318 const numRows = 10; 22319 const numColumns = 10; 22320 const getCellLabel = makeAnnouncementText(backstage); 22321 const cells = makeCells(getCellLabel, numRows, numColumns); 22322 const emptyLabelText = makeLabelText(0, 0); 22323 const memLabel = record({ 22324 dom: { 22325 tag: 'span', 22326 classes: ['tox-insert-table-picker__label'], 22327 }, 22328 components: [emptyLabelText], 22329 behaviours: derive$1([ 22330 Replacing.config({}) 22331 ]) 22332 }); 22333 return { 22334 type: 'widget', 22335 data: { value: generate$6('widget-id') }, 22336 dom: { 22337 tag: 'div', 22338 classes: ['tox-fancymenuitem'] 22339 }, 22340 autofocus: true, 22341 components: [parts$7.widget({ 22342 dom: { 22343 tag: 'div', 22344 classes: ['tox-insert-table-picker'] 22345 }, 22346 components: makeComponents(cells).concat(memLabel.asSpec()), 22347 behaviours: derive$1([ 22348 config('insert-table-picker', [ 22349 runOnAttached((c) => { 22350 // Restore the empty label when opened, otherwise it may still be using an old label from last time it was opened 22351 Replacing.set(memLabel.get(c), [emptyLabelText]); 22352 }), 22353 runWithTarget(cellOverEvent, (c, t, e) => { 22354 const { row, col } = e.event; 22355 selectCells(cells, row, col, numRows, numColumns); 22356 Replacing.set(memLabel.get(c), [makeLabelText(row + 1, col + 1)]); 22357 }), 22358 runWithTarget(cellExecuteEvent, (c, _, e) => { 22359 const { row, col } = e.event; 22360 // Close the sandbox before triggering the action 22361 emit(c, sandboxClose()); 22362 spec.onAction({ numRows: row + 1, numColumns: col + 1 }); 22363 }) 22364 ]), 22365 Keying.config({ 22366 initSize: { 22367 numRows, 22368 numColumns 22369 }, 22370 mode: 'flatgrid', 22371 selector: '[role="button"]' 22372 }) 22373 ]) 22374 })] 22375 }; 22376 }; 22377 22378 const fancyMenuItems = { 22379 inserttable: renderInsertTableMenuItem, 22380 colorswatch: renderColorSwatchItem, 22381 imageselect: renderImageSelector 22382 }; 22383 const renderFancyMenuItem = (spec, backstage) => get$h(fancyMenuItems, spec.fancytype).map((render) => render(spec, backstage)); 22384 22385 const render$2 = (items, extras) => map$2(items, (item) => { 22386 switch (item.type) { 22387 case 'cardcontainer': 22388 return renderContainer(item, render$2(item.items, extras)); 22389 case 'cardimage': 22390 return renderImage(item.src, item.classes, item.alt); 22391 case 'cardtext': 22392 // Only highlight targeted text components 22393 const shouldHighlight = item.name.exists((name) => contains$2(extras.cardText.highlightOn, name)); 22394 const matchText = shouldHighlight ? Optional.from(extras.cardText.matchText).getOr('') : ''; 22395 return renderHtml(replaceText(item.text, matchText), item.classes); 22396 } 22397 }); 22398 const renderCardMenuItem = (spec, itemResponse, sharedBackstage, extras) => { 22399 const getApi = (component) => ({ 22400 isEnabled: () => !Disabling.isDisabled(component), 22401 setEnabled: (state) => { 22402 Disabling.set(component, !state); 22403 // Disable sub components 22404 each$1(descendants(component.element, '*'), (elm) => { 22405 component.getSystem().getByDom(elm).each((comp) => { 22406 if (comp.hasConfigured(Disabling)) { 22407 Disabling.set(comp, !state); 22408 } 22409 }); 22410 }); 22411 } 22412 }); 22413 const structure = { 22414 dom: renderItemDomStructure(spec.label, []), 22415 optComponents: [ 22416 Optional.some({ 22417 dom: { 22418 tag: 'div', 22419 classes: [containerClass, containerRowClass] 22420 }, 22421 components: render$2(spec.items, extras) 22422 }) 22423 ] 22424 }; 22425 return renderCommonItem({ 22426 context: 'mode:design', 22427 data: buildData({ text: Optional.none(), ...spec }), 22428 enabled: spec.enabled, 22429 getApi, 22430 onAction: spec.onAction, 22431 onSetup: spec.onSetup, 22432 triggersSubmenu: false, 22433 itemBehaviours: Optional.from(extras.itemBehaviours).getOr([]) 22434 }, structure, itemResponse, sharedBackstage.providers); 22435 }; 22436 22437 const identifyMenuLayout = (searchMode) => { 22438 switch (searchMode.searchMode) { 22439 case 'no-search': { 22440 return { 22441 menuType: 'normal' 22442 }; 22443 } 22444 default: { 22445 return { 22446 menuType: 'searchable', 22447 searchMode 22448 }; 22449 } 22450 } 22451 }; 22452 const handleRefetchTrigger = (originalSandboxComp) => { 22453 // At the moment, a Sandbox is "Represented" by its triggering Dropdown. 22454 // We'll want to make this an official API, in case we change it later. 22455 const dropdown = Representing.getValue(originalSandboxComp); 22456 // Because refetch will replace the entire menu, we need to store the 22457 // original version of the searcher state, so that we can reinstate it 22458 // after the fetch completes (which is async) 22459 const optSearcherState = findWithinSandbox(originalSandboxComp).map(saveState); 22460 Dropdown.refetch(dropdown).get(() => { 22461 // It has completed, so now find the searcher and set its value 22462 // again. We can't just use the originalSandbox, because that will 22463 // have been thrown away and recreated by now. 22464 const newSandboxComp = Coupling.getCoupled(dropdown, 'sandbox'); 22465 optSearcherState.each((searcherState) =>
22465 findWithinSandbox(newSandboxComp).each((inputComp) => restoreState(inputComp, searcherState))); 22466 }); 22467 }; 22468 // This event is triggered by the searcher for key events 22469 // that should be handled by the currently selected item 22470 // (that is, the one with *fake* focus, not real focus). So we 22471 // need to redispatch them to the selected item in the sandbox. 22472 const handleRedirectToMenuItem = (sandboxComp, se) => { 22473 getActiveMenuItemFrom(sandboxComp).each((activeItem) => { 22474 retargetAndDispatchWith(sandboxComp, activeItem.element, se.event.eventType, se.event.interactionEvent); 22475 }); 22476 }; 22477 // This function is useful when you have fakeFocus, so you can't just find the 22478 // currently focused item (or the item that triggered a key event). It relies on 22479 // the following relationships between components 22480 // The Sandbox creates a tieredmenu, so Sandboxing.getState returns the TieredMenu 22481 // The TieredMenu uses Highlighting for managing which menus are active, so 22482 // Highlighting.getHighlighted(tmenu) is the current active menu 22483 // The Menu uses highlighting to manage the active item, so use 22484 // Highlighting.getHighlighted(menu) to get the current item. 22485 const getActiveMenuItemFrom = (sandboxComp) => { 22486 // Consider moving some of these things into shared APIs. For example, make an extra API 22487 // for TieredMenu to get the highlighted item. 22488 return Sandboxing.getState(sandboxComp) 22489 .bind(Highlighting.getHighlighted) 22490 .bind(Highlighting.getHighlighted); 22491 }; 22492 const getSearchResults = (activeMenuComp) => { 22493 // Depending on the menu layout, the search results will either be the entire 22494 // menu, or something within the menu. 22495 return has(activeMenuComp.element, searchResultsClass) 22496 ? Optional.some(activeMenuComp.element) 22497 : descendant(activeMenuComp.element, '.' + searchResultsClass); 22498 }; 22499 // Model the interaction with ARIA 22500 const updateAriaOnHighlight = (tmenuComp, menuComp, itemComp) => { 22501 // This ARIA behaviour is based on the algolia example documented in TINY-8952 22502 findWithinMenu(tmenuComp).each((inputComp) => {
22503 setActiveDescendant(inputComp, itemComp); 22504 const optActiveResults = getSearchResults(menuComp); 22505 optActiveResults.each((resultsElem) => { 22506 // Link aria-controls of the input to the id of the results container. 22507 getOpt(resultsElem, 'id') 22508 .each((controlledId) => set$9(inputComp.element, 'aria-controls', controlledId)); 22509 }); 22510 }); 22511 // Update the aria-selected on the item. The removal is handled by onDehighlight 22512 set$9(itemComp.element, 'aria-selected', 'true'); 22513 }; 22514 const updateAriaOnDehighlight = (tmenuComp, menuComp, itemComp) => { 22515 // This ARIA behaviour is based on the algolia example documented in TINY-8952 22516 set$9(itemComp.element, 'aria-selected', 'false'); 22517 }; 22518 const focusSearchField = (tmenuComp) => { 22519 findWithinMenu(tmenuComp).each((searcherComp) => Focusing.focus(searcherComp)); 22520 }; 22521 const getSearchPattern = (dropdownComp) => { 22522 // Dropdowns are "coupled" with their sandbox and generally, create them on demand. 22523 // When using "getExistingCoupled" of Coupling, it only returns the coupled 22524 // component (here: the sandbox) if it already exists ... it won't do any creation. 22525 // So here, we are trying to get possible fetchContext information for our fetch 22526 // callback. If there is no sandbox, then there is no open menu, and we 22527 // don't have any search context, so use an empty string. Otherwise, dive into 22528 // the sandbox, and find the search field's current pattern. 22529 const optSandboxComp = Coupling.getExistingCoupled(dropdownComp, 'sandbox'); 22530 return optSandboxComp 22531 .bind(findWithinSandbox) 22532 .map(saveState) 22533 .map((state) => state.fetchPattern) 22534 .getOr(''); 22535 }; 22536 22537 var FocusMode; 22538 (function (FocusMode) { 22539 FocusMode[FocusMode["ContentFocus"] = 0] = "ContentFocus"; 22540 FocusMode[FocusMode["UiFocus"] = 1] = "UiFocus"; 22541 })(FocusMode || (FocusMode = {})); 22542 const createMenuItemFromBridge = (item, itemResponse, backstage, menuHasIcons, isHorizontalMenu) => { 22543 const providersBackstage = backstage.shared.providers; 22544 // If we're making a horizontal menu (mobile context menu) we want text OR icons 22545 // to simplify the UI. We also don't want shortcut text. 22546 const parseForHorizontalMenu = (menuitem) => !isHorizontalMenu ? menuitem : ({ 22547 ...menuitem, 22548 shortcut: Optional.none(), 22549 icon: menuitem.text.isSome() ? Optional.none() : menuitem.icon 22550 }); 22551 switch (item.type) { 22552 case 'menuitem': 22553 return createMenuItem(item).fold(handleError, (d) => Optional.some(renderNormalItem(parseForHorizontalMenu(d), itemResponse, providersBackstage, menuHasIcons))); 22554 case 'nestedmenuitem': 22555 return createNestedMenuItem(item).fold(handleError, (d) => Optional.some(renderNestedItem(parseForHorizontalMenu(d), itemResponse, providersBackstage, menuHasIcons, isHorizontalMenu))); 22556 case 'togglemenuitem': 22557 return createToggleMenuItem(item).fold(handleError, (d) => Optional.some(renderToggleMenuItem(parseForHorizontalMenu(d), itemResponse, providersBackstage, menuHasIcons))); 22558 case 'separator': 22559 return createSeparatorMenuItem(item).fold(handleError, (d) => Optional.some(renderSeparatorItem(d))); 22560 case 'fancymenuitem': 22561 return createFancyMenuItem(item).fold(handleError, 22562 // Fancy menu items don't have shortcuts or icons 22563 (d) => renderFancyMenuItem(d, backstage)); 22564 default: { 22565 // eslint-disable-next-line no-console 22566 console.error('Unknown item in general menu', item); 22567 return Optional.none(); 22568 } 22569 } 22570 }; 22571 const createAutocompleteItems = (items, matchText, onItemValueHandler, columns, itemResponse, sharedBackstage, highlightOn) => { 22572 // Render text and icons if we're using a single column, otherwise only render icons 22573 const renderText = columns === 1; 22574 const renderIcons = !renderText || menuHasIcons(items); 22575 return cat(map$2(items, (item) => { 22576 switch (item.type) { 22577 case 'separator': 22578 return createSeparatorItem(item).fold(handleError, (d) => Optional.some(renderSeparatorItem(d))); 22579 case 'cardmenuitem': 22580 return createCardMenuItem(item).fold(handleError, (d) => Optional.some(renderCardMenuItem({ 22581 ...d, 22582 // Intercept action 22583 onAction: (api) => { 22584 d.onAction(api); 22585 onItemValueHandler(d.value, d.meta); 22586 } 22587 }, itemResponse, sharedBackstage, { 22588 itemBehaviours: tooltipBehaviour(d.meta, sharedBackstage, Optional.none()), 22589 cardText: { 22590 matchText, 22591 highlightOn 22592 } 22593 }))); 22594 case 'autocompleteitem': 22595 default: 22596 return createAutocompleterItem(item).fold(handleError, (d) => Optional.some(renderAutocompleteItem(d, matchText, renderText, 'normal', onItemValueHandler, itemResponse, sharedBackstage, renderIcons))); 22597 } 22598 })); 22599 };
22600 const createPartialMenu = (value, items, itemResponse, backstage, isHorizontalMenu, searchMode) => { 22601 const hasIcons = menuHasIcons(items); 22602 const alloyItems = cat(map$2(items, (item) => { 22603 // Have to check each item for an icon, instead of as part of hasIcons above, 22604 // else in horizontal menus, items with an icon but without text will display 22605 // with neither 22606 const itemHasIcon = (i) => isHorizontalMenu ? !has$2(i, 'text') : hasIcons; 22607 const createItem = (i) => createMenuItemFromBridge(i, itemResponse, backstage, itemHasIcon(i), isHorizontalMenu); 22608 if (item.type === 'nestedmenuitem' && item.getSubmenuItems().length <= 0) { 22609 return createItem({ ...item, enabled: false }); 22610 } 22611 else { 22612 return createItem(item); 22613 } 22614 })); 22615 // The menu layout is dependent upon our search mode. 22616 const menuLayout = identifyMenuLayout(searchMode); 22617 const createPartial = isHorizontalMenu ? 22618 createHorizontalPartialMenuWithAlloyItems : 22619 createPartialMenuWithAlloyItems; 22620 return createPartial(value, hasIcons, alloyItems, 1, menuLayout); 22621 }; 22622 const createTieredDataFrom = (partialMenu) => tieredMenu.singleData(partialMenu.value, partialMenu); 22623 const createInlineMenuFrom = (partialMenu, columns, focusMode, presets) => { 22624 const movement = deriveMenuMovement(columns, presets); 22625 const menuMarkers = markers(presets); 22626 return { 22627 data: createTieredDataFrom({ 22628 ...partialMenu, 22629 movement, 22630 menuBehaviours: SimpleBehaviours.unnamedEvents(columns !== 'auto' ? [] : [ 22631 runOnAttached((comp, _se) => { 22632 detectSize(comp, 4, menuMarkers.item).each(({ numColumns, numRows }) => { 22633 Keying.setGridSize(comp, numRows, numColumns); 22634 }); 22635 }) 22636 ]) 22637 }), 22638 menu: { 22639 markers: markers(presets), 22640 fakeFocus: focusMode === FocusMode.ContentFocus 22641 } 22642 }; 22643 }; 22644 22645 const rangeToSimRange = (r) => SimRange.create(SugarElement.fromDom(r.startContainer), r.startOffset, SugarElement.fromDom(r.endContainer), r.endOffset); 22646 const register$c = (editor, sharedBackstage) => { 22647 const autocompleterId = generate$6('autocompleter'); 22648 const processingAction = Cell(false); 22649 const activeState = Cell(false); 22650 const activeRange = value$2(); 22651 const autocompleter = build$1(InlineView.sketch({ 22652 dom: { 22653 tag: 'div', 22654 classes: ['tox-autocompleter'], 22655 attributes: { 22656 id: autocompleterId 22657 } 22658 }, 22659 components: [], 22660 fireDismissalEventInstead: {}, 22661 inlineBehaviours: derive$1([ 22662 config('dismissAutocompleter', [ 22663 run$1(dismissRequested(), () => cancelIfNecessary()), 22664 run$1(highlight$1(), (_, se) => { 22665 getOpt(se.event.target, 'id').each((id) => set$9(SugarElement.fromDom(editor.getBody()), 'aria-activedescendant', id)); 22666 }), 22667 ]) 22668 ]), 22669 lazySink: sharedBackstage.getSink 22670 })); 22671 const isMenuOpen = () => InlineView.isOpen(autocompleter); 22672 const isActive = activeState.get; 22673 const hideIfNecessary = () => { 22674 if (isMenuOpen()) { 22675 InlineView.hide(autocompleter); 22676 editor.dom.remove(autocompleterId, false); 22677 const editorBody = SugarElement.fromDom(editor.getBody()); 22678 getOpt(editorBody, 'aria-owns') 22679 .filter((ariaOwnsAttr) => ariaOwnsAttr === autocompleterId) 22680 .each(() => { 22681 remove$8(editorBody, 'aria-owns'); 22682 remove$8(editorBody, 'aria-activedescendant'); 22683 }); 22684 } 22685 }; 22686 const getMenu = () => InlineView.getContent(autocompleter).bind((tmenu) => { 22687 // The autocompleter menu will be the first child component of the tiered menu. 22688 // Unfortunately a memento can't be used to do this lookup because the component 22689 // id is changed while generating the tiered menu. 22690 return get$i(tmenu.components(), 0); 22691 }); 22692 const cancelIfNecessary = () => editor.execCommand('mceAutocompleterClose'); 22693 const getCombinedItems = (matches) => { 22694 const columns = findMap(matches, (m) => Optional.from(m.columns)).getOr(1); 22695 return bind$3(matches, (match) => { 22696 const choices = match.items; 22697 return createAutocompleteItems(choices, match.matchText, (itemValue, itemMeta) => { 22698 const autocompleterApi = { 22699 hide: () => cancelIfNecessary(), 22700 reload: (fetchOptions) => { 22701 hideIfNecessary(); 22702 editor.execCommand('mceAutocompleterReload', false, { fetchOptions }); 22703 } 22704 }; 22705 // Asks the editor for a new active range that emits an event that updates 22706 // the activeRange state not ideal but trying to avoid direct method calls to the core. 22707 // We need to get a fresh range since when you hit enter the IME commits and the updates the DOM so we then need to rescan. 22708 editor.execCommand('mceAutocompleterRefreshActiveRange'); 22709 activeRange.get().each((range) => { 22710 processingAction.set(true); 22711 match.onAction(autocompleterApi, range, itemValue, itemMeta); 22712 processingAction.set(false); 22713 }); 22714 }, columns, ItemResponse$1.BUBBLE_TO_SANDBOX, sharedBackstage, match.highlightOn); 22715 }); 22716 }; 22717 const display = (lookupData, items) => { 22718 // Display the autocompleter menu 22719 const columns = findMap(lookupData, (ld) => Optional.from(ld.columns)).getOr(1); 22720 InlineView.showMenuAt(autocompleter, { 22721 anchor: { 22722 type: 'selection', 22723 getSelection: () => activeRange.get().map(rangeToSimRange), 22724 root: SugarElement.fromDom(editor.getBody()), 22725 } 22726 }
22726, createInlineMenuFrom(createPartialMenuWithAlloyItems('autocompleter-value', true, items, columns, { menuType: 'normal' }), columns, FocusMode.ContentFocus, 22727 // Use the constant. 22728 'normal')); 22729 getMenu().each(Highlighting.highlightFirst); 22730 }; 22731 const updateDisplay = (lookupData) => { 22732 const combinedItems = getCombinedItems(lookupData); 22733 // Open the autocompleter if there are items to show 22734 if (combinedItems.length > 0) { 22735 display(lookupData, combinedItems); 22736 set$9(SugarElement.fromDom(editor.getBody()), 'aria-owns', autocompleterId); 22737 if (!editor.inline) { 22738 cloneAutocompleterToEditorDoc(); 22739 } 22740 } 22741 else { 22742 hideIfNecessary(); 22743 } 22744 }; 22745 const cloneAutocompleterToEditorDoc = () => { 22746 if (editor.dom.get(autocompleterId)) { 22747 editor.dom.remove(autocompleterId, false); 22748 } 22749 const docElm = editor.getDoc().documentElement; 22750 const selection = editor.selection.getNode(); 22751 const newElm = deep(autocompleter.element); 22752 setAll(newElm, { 22753 border: '0', 22754 clip: 'rect(0 0 0 0)', 22755 height: '1px', 22756 margin: '-1px', 22757 overflow: 'hidden', 22758 padding: '0', 22759 position: 'absolute', 22760 width: '1px', 22761 top: `${selection.offsetTop}px`, 22762 left: `${selection.offsetLeft}px`, 22763 }); 22764 editor.dom.add(docElm, newElm.dom); 22765 // Clean up positioning styles so that the "hidden" autocompleter is around the selection 22766 descendant(newElm, '[role="menu"]').each((child) => { 22767 remove$6(child, 'position'); 22768 remove$6(child, 'max-height'); 22769 }); 22770 }; 22771 editor.on('AutocompleterStart', ({ lookupData }) => { 22772 activeState.set(true); 22773 processingAction.set(false); 22774 updateDisplay(lookupData); 22775 }); 22776 editor.on('AutocompleterUpdate', ({ lookupData }) => updateDisplay(lookupData)); 22777 editor.on('AutocompleterUpdateActiveRange', ({ range }) => activeRange.set(range)); 22778 editor.on('AutocompleterEnd', () => { 22779 // Hide the menu and reset 22780 hideIfNecessary(); 22781 activeState.set(false); 22782 processingAction.set(false); 22783 activeRange.clear(); 22784 }); 22785 const autocompleterUiApi = { 22786 cancelIfNecessary, 22787 isMenuOpen, 22788 isActive, 22789 isProcessingAction: processingAction.get, 22790 getMenu 22791 }; 22792 AutocompleterEditorEvents.setup(autocompleterUiApi, editor); 22793 }; 22794 const Autocompleter = { 22795 register: register$c 22796 }; 22797 22798 const renderBar = (spec, backstage) => ({ 22799 dom: { 22800 tag: 'div', 22801 classes: ['tox-bar', 'tox-form__controls-h-stack'] 22802 }, 22803 components: map$2(spec.items, backstage.interpreter) 22804 }); 22805 22806 var global$4 = tinymce.util.Tools.resolve('tinymce.html.Entities'); 22807 22808 const renderFormFieldWith = (pLabel, pField, extraClasses, extraBehaviours) => { 22809 const spec = renderFormFieldSpecWith(pLabel, pField, extraClasses, extraBehaviours); 22810 return FormField.sketch(spec); 22811 }; 22812 const renderFormField = (pLabel, pField) => renderFormFieldWith(pLabel, pField, [], []); 22813 const renderFormFieldSpecWith = (pLabel, pField, extraClasses, extraBehaviours) => ({ 22814 dom: renderFormFieldDomWith(extraClasses), 22815 components: pLabel.toArray().concat([pField]), 22816 fieldBehaviours: derive$1(extraBehaviours) 22817 }); 22818 const renderFormFieldDom = () => renderFormFieldDomWith([]); 22819 const renderFormFieldDomWith = (extraClasses) => ({ 22820 tag: 'div', 22821 classes: ['tox-form__group'].concat(extraClasses) 22822 }); 22823 const renderLabel$3 = (label, providersBackstage) => FormField.parts.label({ 22824 dom: { 22825 tag: 'label', 22826 classes: ['tox-label'] 22827 }, 22828 components: [ 22829 text$2(providersBackstage.translate(label)) 22830 ] 22831 }); 22832 22833 const formChangeEvent = generate$6('form-component-change'); 22834 const formInputEvent = generate$6('form-component-input'); 22835 const formCloseEvent = generate$6('form-close'); 22836 const formCancelEvent = generate$6('form-cancel'); 22837 const formActionEvent = generate$6('form-action'); 22838 const formSubmitEvent = generate$6('form-submit'); 22839 const formBlockEvent = generate$6('form-block'); 22840 const formUnblockEvent = generate$6('form-unblock'); 22841 const formTabChangeEvent = generate$6('form-tabchange'); 22842 const formResizeEvent = generate$6('form-resize'); 22843 22844 const renderCollection = (spec, providersBackstage, initialData) => { 22845 // DUPE with TextField. 22846 const pLabel = spec.label.map((label) => renderLabel$3(label, providersBackstage)); 22847 const icons = providersBackstage.icons();
22848 // TINY-10174: Icon string is either in icon pack or displayed directly 22849 const getIcon = (icon) => icons[icon] ?? icon; 22850 const runOnItem = (f) => (comp, se) => { 22851 closest$3(se.event.target, '[data-collection-item-value]').each((target) => { 22852 f(comp, se, target, get$g(target, 'data-collection-item-value')); 22853 }); 22854 }; 22855 const setContents = (comp, items) => { 22856 // Giving it a default `mode:design` context, these shouldn't run at all in mode:readonly 22857 const disabled = providersBackstage.checkUiComponentContext('mode:design').shouldDisable || providersBackstage.isDisabled(); 22858 const disabledClass = disabled ? ' tox-collection__item--state-disabled' : ''; 22859 const htmlLines = map$2(items, (item) => { 22860 const itemText = global$6.translate(item.text); 22861 const textContent = spec.columns === 1 ? `<div class="tox-collection__item-label">${itemText}</div>` : ''; 22862 const iconContent = `<div class="tox-collection__item-icon">${getIcon(item.icon)}</div>`; 22863 // Replacing the hyphens and underscores in collection items with spaces 22864 // to ensure the screen readers pronounce the words correctly. 22865 // This is only for aria purposes. Emoticon and Special Character names will still use _ and - for autocompletion. 22866 const mapItemName = { 22867 '_': ' ', 22868 ' - ': ' ', 22869 '-': ' ' 22870 }; 22871 // Using aria-label here overrides the Apple description of emojis and special characters in Mac/ MS description in Windows. 22872 // But if only the title attribute is used instead, the names are read out twice. i.e., the description followed by the item.text. 22873 const ariaLabel = itemText.replace(/\_| \- |\-/g, (match) => mapItemName[match]); 22874 return `<div data-mce-tooltip="${ariaLabel}" class="tox-collection__item${disabledClass}" tabindex="-1" data-collection-item-value="${global$4.encodeAllRaw(item.value)}" aria-label="${ariaLabel}">${iconContent}${textContent}</div>`; 22875 }); 22876 const chunks = spec.columns !== 'auto' && spec.columns > 1 ? chunk$1(htmlLines, spec.columns) : [htmlLines]; 22877 const html = map$2(chunks, (ch) => `<div class="tox-collection__group">${ch.join('')}</div>`); 22878 set$8(comp.element, html.join('')); 22879 }; 22880 const onClick = runOnItem((comp, se, tgt, itemValue) => { 22881 se.stop(); 22882 if (!(providersBackstage.checkUiComponentContext('mode:design').shouldDisable || providersBackstage.isDisabled())) { 22883 emitWith(comp, formActionEvent, { 22884 name: spec.name, 22885 value: itemValue 22886 }); 22887 } 22888 }); 22889 const collectionEvents = [ 22890 run$1(mouseover(), runOnItem((comp, se, tgt) => { 22891 focus$4(tgt, true); 22892 })), 22893 run$1(click(), onClick), 22894 run$1(tap(), onClick), 22895 run$1(focusin(), runOnItem((comp, se, tgt) => { 22896 descendant(comp.element, '.' + activeClass).each((currentActive) => { 22897 remove$3(currentActive, activeClass); 22898 }); 22899 add$2(tgt, activeClass); 22900 })), 22901 run$1(focusout(), runOnItem((comp) => { 22902 descendant(comp.element, '.' + activeClass).each((currentActive) => { 22903 remove$3(currentActive, activeClass); 22904 blur$1(currentActive); 22905 }); 22906 })), 22907 runOnExecute$1(runOnItem((comp, se, tgt, itemValue) => { 22908 emitWith(comp, formActionEvent, { 22909 name: spec.name, 22910 value: itemValue 22911 }); 22912 })), 22913 ]; 22914 const iterCollectionItems = (comp, applyAttributes) => map$2(descendants(comp.element, '.tox-collection__item'), applyAttributes); 22915 const pField = FormField.parts.field({ 22916 dom: { 22917 tag: 'div', 22918 // FIX: Read from columns
22919 classes: ['tox-collection'].concat(spec.columns !== 1 ? ['tox-collection--grid'] : ['tox-collection--list']) 22920 }, 22921 components: [], 22922 factory: { sketch: identity }, 22923 behaviours: derive$1([ 22924 Disabling.config({ 22925 disabled: () => providersBackstage.checkUiComponentContext(spec.context).shouldDisable, 22926 onDisabled: (comp) => { 22927 iterCollectionItems(comp, (childElm) => { 22928 add$2(childElm, 'tox-collection__item--state-disabled'); 22929 set$9(childElm, 'aria-disabled', true); 22930 }); 22931 }, 22932 onEnabled: (comp) => { 22933 iterCollectionItems(comp, (childElm) => { 22934 remove$3(childElm, 'tox-collection__item--state-disabled'); 22935 remove$8(childElm, 'aria-disabled'); 22936 }); 22937 } 22938 }), 22939 toggleOnReceive(() => providersBackstage.checkUiComponentContext(spec.context)), 22940 Replacing.config({}), 22941 Tooltipping.config({ 22942 ...providersBackstage.tooltips.getConfig({ 22943 tooltipText: '', 22944 onShow: (comp) => { 22945 descendant(comp.element, '.' + activeClass + '[data-mce-tooltip]').each((current) => { 22946 getOpt(current, 'data-mce-tooltip').each((text) => { 22947 Tooltipping.setComponents(comp, providersBackstage.tooltips.getComponents({ tooltipText: text })); 22948 }); 22949 }); 22950 } 22951 }), 22952 mode: 'children-keyboard-focus', 22953 anchor: (comp) => ({ 22954 type: 'node', 22955 node: descendant(comp.element, '.' + activeClass).orThunk(() => first('.tox-collection__item')), 22956 root: comp.element, 22957 layouts: { 22958 onLtr: constant$1([south$2, north$2, southeast$2, northeast$2, southwest$2, northwest$2]), 22959 onRtl: constant$1([south$2, north$2, southeast$2, northeast$2, southwest$2, northwest$2]) 22960 }, 22961 bubble: nu$6(0, -2, {}), 22962 }) 22963 }), 22964 Representing.config({ 22965 store: { 22966 mode: 'memory', 22967 initialValue: initialData.getOr([]) 22968 }, 22969 onSetValue: (comp, items) => { 22970 setContents(comp, items);
22971 if (spec.columns === 'auto') { 22972 detectSize(comp, 5, 'tox-collection__item').each(({ numRows, numColumns }) => { 22973 Keying.setGridSize(comp, numRows, numColumns); 22974 }); 22975 } 22976 emit(comp, formResizeEvent); 22977 } 22978 }), 22979 Tabstopping.config({}), 22980 Keying.config(deriveCollectionMovement(spec.columns, 'normal')), 22981 config('collection-events', collectionEvents) 22982 ]), 22983 eventOrder: { 22984 [execute$5()]: ['disabling', 'alloy.base.behaviour', 'collection-events'], 22985 [focusin()]: ['collection-events', 'tooltipping'], 22986 } 22987 }); 22988 const extraClasses = ['tox-form__group--collection']; 22989 return renderFormFieldWith(pLabel, pField, extraClasses, []); 22990 }; 22991 22992 const renderPanelButton = (spec, sharedBackstage) => Dropdown.sketch({ 22993 dom: spec.dom, 22994 components: spec.components, 22995 toggleClass: 'mce-active', 22996 dropdownBehaviours: derive$1([ 22997 DisablingConfigs.button(() => sharedBackstage.providers.isDisabled() || sharedBackstage.providers.checkUiComponentContext(spec.context).shouldDisable), 22998 toggleOnReceive(() => sharedBackstage.providers.checkUiComponentContext(spec.context)), 22999 Unselecting.config({}), 23000 Tabstopping.config({}) 23001 ]), 23002 layouts: spec.layouts, 23003 sandboxClasses: ['tox-dialog__popups'], 23004 lazySink: sharedBackstage.getSink, 23005 fetch: (comp) => Future.nu((callback) => spec.fetch(callback)).map((items) => Optional.from(createTieredDataFrom(deepMerge(createPartialChoiceMenu(generate$6('menu-value'), items, (value) => { 23006 spec.onItemAction(comp, value); 23007 }, spec.columns, spec.presets, ItemResponse$1.CLOSE_ON_EXECUTE, 23008 // No colour is ever selected on opening 23009 never, sharedBackstage.providers), { 23010 movement: deriveMenuMovement(spec.columns, spec.presets) 23011 })))), 23012 parts: { 23013 menu: part(false, 1, spec.presets) 23014 } 23015 }); 23016 23017 const colorInputChangeEvent = generate$6('color-input-change'); 23018 const colorSwatchChangeEvent = generate$6('color-swatch-change'); 23019 const colorPickerCancelEvent = generate$6('color-picker-cancel'); 23020 const renderColorInput = (spec, sharedBackstage, colorInputBackstage, initialData) => { 23021 const pField = FormField.parts.field({ 23022 factory: Input, 23023 inputClasses: ['tox-textfield'], 23024 data: initialData, 23025 onSetValue: (c) => Invalidating.run(c).get(noop), 23026 inputBehaviours: derive$1([ 23027 Disabling.config({ 23028 disabled: () => sharedBackstage.providers.isDisabled() || sharedBackstage.providers.checkUiComponentContext(spec.context).shouldDisable 23029 }), 23030 toggleOnReceive(() => sharedBackstage.providers.checkUiComponentContext(spec.context)), 23031 Tabstopping.config({}), 23032 Invalidating.config({ 23033 invalidClass: 'tox-textbox-field-invalid', 23034 getRoot: (comp) => parentElement(comp.element), 23035 notify: { 23036 onValid: (comp) => { 23037 // onValid should pass through the value here 23038 // We need a snapshot of the value validated. 23039 const val = Representing.getValue(comp); 23040 emitWith(comp, colorInputChangeEvent, { 23041 color: val 23042 }); 23043 } 23044 }, 23045 validator: { 23046 validateOnLoad: false, 23047 validate: (input) => { 23048 const inputValue = Representing.getValue(input); 23049 // Consider empty strings valid colours 23050 if (inputValue.length === 0) { 23051 return Future.pure(Result.value(true)); 23052 } 23053 else { 23054 const span = SugarElement.fromTag('span'); 23055 set$7(span, 'background-color', inputValue); 23056 const res = getRaw(span, 'background-color').fold( 23057 // TODO: Work out what we want to do here. 23058 () => Result.error('blah'), (_) => Result.value(inputValue)); 23059 return Future.pure(res); 23060 } 23061 } 23062 } 23063 }) 23064 ]), 23065 selectOnFocus: false 23066 }); 23067 const pLabel = spec.label.map((label) => renderLabel$3(label, sharedBackstage.providers)); 23068 const emitSwatchChange = (colorBit, value) => { 23069 emitWith(colorBit, colorSwatchChangeEvent, { 23070 value 23071 }); 23072 }; 23073 const onItemAction = (comp, value) => { 23074 memColorButton.getOpt(comp).each((colorBit) => { 23075 if (value === 'custom') { 23076 colorInputBackstage.colorPicker((valueOpt) => { 23077 valueOpt.fold(() => emit(colorBit, colorPickerCancelEvent), (value) => { 23078 emitSwatchChange(colorBit, value);
vendor: 4,985 bytes, lines 23079-23208
23079 addColor(spec.storageKey, value); 23080 }); 23081 }, '#ffffff'); 23082 } 23083 else if (value === 'remove') { 23084 emitSwatchChange(colorBit, ''); 23085 } 23086 else { 23087 emitSwatchChange(colorBit, value); 23088 } 23089 }); 23090 }; 23091 const memColorButton = record(renderPanelButton({ 23092 dom: { 23093 tag: 'span', 23094 attributes: { 23095 'aria-label': sharedBackstage.providers.translate('Color swatch') 23096 } 23097 }, 23098 layouts: { 23099 onRtl: () => [southwest$2, southeast$2, south$2], 23100 onLtr: () => [southeast$2, southwest$2, south$2] 23101 }, 23102 components: [], 23103 fetch: getFetch$1(colorInputBackstage.getColors(spec.storageKey), spec.storageKey, colorInputBackstage.hasCustomColors()), 23104 columns: colorInputBackstage.getColorCols(spec.storageKey), 23105 presets: 'color', 23106 onItemAction, 23107 context: spec.context 23108 }, sharedBackstage)); 23109 return FormField.sketch({ 23110 dom: { 23111 tag: 'div', 23112 classes: ['tox-form__group'] 23113 }, 23114 components: pLabel.toArray().concat([ 23115 { 23116 dom: { 23117 tag: 'div', 23118 classes: ['tox-color-input'] 23119 }, 23120 components: [ 23121 pField, 23122 memColorButton.asSpec() 23123 ] 23124 } 23125 ]), 23126 fieldBehaviours: derive$1([ 23127 config('form-field-events', [ 23128 run$1(colorInputChangeEvent, (comp, se) => { 23129 memColorButton.getOpt(comp).each((colorButton) => { 23130 set$7(colorButton.element, 'background-color', se.event.color); 23131 }); 23132 emitWith(comp, formChangeEvent, { name: spec.name }); 23133 }), 23134 run$1(colorSwatchChangeEvent, (comp, se) => { 23135 FormField.getField(comp).each((field) => { 23136 Representing.setValue(field, se.event.value); 23137 // Focus the field now that we've set its value 23138 Composing.getCurrent(comp).each(Focusing.focus); 23139 }); 23140 }), 23141 run$1(colorPickerCancelEvent, (comp, _se) => { 23142 FormField.getField(comp).each((_field) => { 23143 Composing.getCurrent(comp).each(Focusing.focus); 23144 }); 23145 }) 23146 ]) 23147 ]) 23148 }); 23149 }; 23150 23151 // TODO: Move this to alloy if the concept works out 23152 // eslint-disable-next-line consistent-this 23153 const self = () => Composing.config({ 23154 find: Optional.some 23155 }); 23156 const memento$1 = (mem) => Composing.config({ 23157 find: mem.getOpt 23158 }); 23159 const childAt = (index) => Composing.config({ 23160 find: (comp) => child$2(comp.element, index) 23161 .bind((element) => comp.getSystem().getByDom(element).toOptional()) 23162 }); 23163 const ComposingConfigs = { 23164 self, 23165 memento: memento$1, 23166 childAt 23167 }; 23168 23169 const processors = objOf([ 23170 defaulted('preprocess', identity), 23171 defaulted('postprocess', identity) 23172 ]); 23173 const memento = (mem, rawProcessors) => { 23174 const ps = asRawOrDie$1('RepresentingConfigs.memento processors', processors, rawProcessors); 23175 return Representing.config({ 23176 store: { 23177 mode: 'manual', 23178 getValue: (comp) => { 23179 const other = mem.get(comp); 23180 const rawValue = Representing.getValue(other); 23181 return ps.postprocess(rawValue); 23182 }, 23183 setValue: (comp, rawValue) => { 23184 const newValue = ps.preprocess(rawValue); 23185 const other = mem.get(comp); 23186 Representing.setValue(other, newValue); 23187 } 23188 } 23189 }); 23190 }; 23191 const withComp = (optInitialValue, getter, setter) => Representing.config({ 23192 store: { 23193 mode: 'manual', 23194 ...optInitialValue.map((initialValue) => ({ initialValue })).getOr({}), 23195 getValue: getter, 23196 setValue: setter 23197 } 23198 }); 23199 const withElement = (initialValue, getter, setter) => withComp(initialValue, (c) => getter(c.element), (c, v) => setter(c.element, v)); 23200 const domHtml = (optInitialValue) => withElement(optInitialValue, get$f, set$8); 23201 const memory = (initialValue) => Representing.config({ 23202 store: { 23203 mode: 'memory', 23204 initialValue 23205 } 23206 }); 23207 23208 const fieldsUpdate = generate$6('rgb-hex-update');
23209 const sliderUpdate = generate$6('slider-update'); 23210 const paletteUpdate = generate$6('palette-update'); 23211 23212 const sliderFactory = (translate, getClass) => { 23213 const spectrum = Slider.parts.spectrum({ 23214 dom: { 23215 tag: 'div', 23216 classes: [getClass('hue-slider-spectrum')], 23217 attributes: { 23218 role: 'presentation' 23219 } 23220 } 23221 }); 23222 const thumb = Slider.parts.thumb({ 23223 dom: { 23224 tag: 'div', 23225 classes: [getClass('hue-slider-thumb')], 23226 attributes: { 23227 role: 'presentation' 23228 } 23229 } 23230 }); 23231 return Slider.sketch({ 23232 dom: { 23233 tag: 'div', 23234 classes: [getClass('hue-slider')], 23235 attributes: { 23236 'role': 'slider', 23237 'aria-valuemin': 0, 23238 'aria-valuemax': 360, 23239 'aria-valuenow': 120, 23240 } 23241 }, 23242 rounded: false, 23243 model: { 23244 mode: 'y', 23245 getInitialValue: constant$1(0) 23246 }, 23247 components: [ 23248 spectrum, 23249 thumb 23250 ], 23251 sliderBehaviours: derive$1([ 23252 Focusing.config({}) 23253 ]), 23254 onChange: (slider, _thumb, value) => { 23255 set$9(slider.element, 'aria-valuenow', Math.floor(360 - (value * 3.6))); 23256 emitWith(slider, sliderUpdate, { 23257 value 23258 }); 23259 } 23260 }); 23261 }; 23262 23263 const validInput = generate$6('valid-input'); 23264 const invalidInput = generate$6('invalid-input'); 23265 const validatingInput = generate$6('validating-input'); 23266 const translatePrefix = 'colorcustom.rgb.'; 23267 const uninitiatedTooltipApi = { 23268 isEnabled: always, 23269 setEnabled: noop, 23270 immediatelyShow: noop, 23271 immediatelyHide: noop, 23272 }; 23273 const rgbFormFactory = (translate, getClass, onValidHexx, onInvalidHexx, tooltipGetConfig, makeIcon) => { 23274 const setTooltipEnabled = (enabled, tooltipApi) => { 23275 const api = tooltipApi.get(); 23276 if (enabled === api.isEnabled()) { 23277 return; 23278 } 23279 api.setEnabled(enabled); 23280 if (enabled) { 23281 api.immediatelyShow(); 23282 } 23283 else { 23284 api.immediatelyHide(); 23285 } 23286 }; 23287 const invalidation = (label, isValid, tooltipApi) => Invalidating.config({ 23288 invalidClass: getClass('invalid'), 23289 notify: { 23290 onValidate: (comp) => { 23291 emitWith(comp, validatingInput, { 23292 type: label 23293 }); 23294 }, 23295 onValid: (comp) => { 23296 setTooltipEnabled(false, tooltipApi); 23297 emitWith(comp, validInput, { 23298 type: label, 23299 value: Representing.getValue(comp) 23300 }); 23301 }, 23302 onInvalid: (comp) => { 23303 setTooltipEnabled(true, tooltipApi); 23304 emitWith(comp, invalidInput, { 23305 type: label, 23306 value: Representing.getValue(comp) 23307 }); 23308 } 23309 }, 23310 validator: { 23311 validate: (comp) => { 23312 const value = Representing.getValue(comp); 23313 const res = isValid(value) ? Result.value(true) : Result.error(translate('aria.input.invalid')); 23314 return Future.pure(res); 23315 }, 23316 validateOnLoad: false 23317 } 23318 }); 23319 const renderTextField = (isValid, name, label, description, data) => { 23320 const tooltipApi = Cell(uninitiatedTooltipApi); 23321 const helptext = translate(translatePrefix + 'range'); 23322 const pLabel = FormField.parts.label({ 23323 dom: { tag: 'label' }, 23324 components: [text$2(label)] 23325 }); 23326 const pField = FormField.parts.field({ 23327 data, 23328 factory: Input, 23329 inputAttributes: { 23330 'type': 'text', 23331 'aria-label': description, 23332 ...name === 'hex' ? { 'aria-live': 'polite' } : {} 23333 }, 23334 inputClasses: [getClass('textfield')],
23335 // Have basic invalidating and tabstopping behaviour. 23336 inputBehaviours: derive$1([ 23337 invalidation(name, isValid, tooltipApi), 23338 Tabstopping.config({}), 23339 Tooltipping.config({ 23340 ...tooltipGetConfig({ 23341 tooltipText: '', 23342 onSetup: (comp) => { 23343 tooltipApi.set({ 23344 isEnabled: () => { 23345 return Tooltipping.isEnabled(comp); 23346 }, 23347 setEnabled: (enabled) => { 23348 return Tooltipping.setEnabled(comp, enabled); 23349 }, 23350 immediatelyShow: () => { 23351 return Tooltipping.immediateOpenClose(comp, true); 23352 }, 23353 immediatelyHide: () => { 23354 return Tooltipping.immediateOpenClose(comp, false); 23355 }, 23356 }); 23357 Tooltipping.setEnabled(comp, false); 23358 }, 23359 onShow: (component, _tooltip) => { 23360 Tooltipping.setComponents(component, [ 23361 { 23362 dom: { 23363 tag: 'p', 23364 classes: [ 23365 getClass('rgb-warning-note') 23366 ] 23367 }, 23368 components: [text$2(translate(name === 'hex' ? 'colorcustom.rgb.invalidHex' : 'colorcustom.rgb.invalid'))] 23369 } 23370 ]); 23371 }, 23372 }) 23373 }) 23374 ]),
23375 // If it was invalid, and the value was set, run validation against it. 23376 onSetValue: (input) => { 23377 if (Invalidating.isInvalid(input)) { 23378 const run = Invalidating.run(input); 23379 run.get(noop); 23380 } 23381 } 23382 }); 23383 const errorId = generate$6('aria-invalid'); 23384 const memInvalidIcon = record(makeIcon('invalid', Optional.some(errorId), 'warning')); 23385 const memStatus = record({ 23386 dom: { 23387 tag: 'div', 23388 classes: [getClass('invalid-icon')] 23389 }, 23390 components: [ 23391 memInvalidIcon.asSpec() 23392 ] 23393 }); 23394 const comps = [pLabel, pField, memStatus.asSpec()]; 23395 const concats = name !== 'hex' ? [FormField.parts['aria-descriptor']({ 23396 text: helptext 23397 })] : []; 23398 const components = comps.concat(concats); 23399 return { 23400 dom: { 23401 tag: 'div', 23402 attributes: { 23403 role: 'presentation' 23404 }, 23405 classes: [ 23406 getClass('rgb-container'), 23407 ] 23408 }, 23409 components 23410 }; 23411 }; 23412 const copyRgbToHex = (form, rgba) => { 23413 const hex = fromRgba(rgba); 23414 Form.getField(form, 'hex').each((hexField) => { 23415 // Not amazing, but it turns out that if we have an invalid RGB field, and no hex code 23416 // and then type in a valid three digit hex code, the RGB field will be overriden, then validate and then set 23417 // the hex field to be the six digit version of that same three digit hex code. This is incorrect. 23418 if (!Focusing.isFocused(hexField)) { 23419 Representing.setValue(form, { 23420 hex: hex.value 23421 }); 23422 } 23423 }); 23424 return hex; 23425 };
vendor: 10,351 bytes, lines 23426-23679
23426 const copyRgbToForm = (form, rgb) => { 23427 const red = rgb.red; 23428 const green = rgb.green; 23429 const blue = rgb.blue; 23430 Representing.setValue(form, { red, green, blue }); 23431 }; 23432 const memPreview = record({ 23433 dom: { 23434 tag: 'div', 23435 classes: [getClass('rgba-preview')], 23436 styles: { 23437 'background-color': 'white' 23438 }, 23439 attributes: { 23440 role: 'presentation' 23441 } 23442 } 23443 }); 23444 const updatePreview = (anyInSystem, hex) => { 23445 memPreview.getOpt(anyInSystem).each((preview) => { 23446 set$7(preview.element, 'background-color', '#' + hex.value); 23447 }); 23448 }; 23449 const factory = () => { 23450 const state = { 23451 red: Cell(Optional.some(255)), 23452 green: Cell(Optional.some(255)), 23453 blue: Cell(Optional.some(255)), 23454 hex: Cell(Optional.some('ffffff')) 23455 }; 23456 const copyHexToRgb = (form, hex) => { 23457 const rgb = fromHex(hex); 23458 copyRgbToForm(form, rgb); 23459 setValueRgb(rgb); 23460 }; 23461 const get = (prop) => state[prop].get(); 23462 const set = (prop, value) => { 23463 state[prop].set(value); 23464 }; 23465 const getValueRgb = () => get('red').bind((red) => get('green').bind((green) => get('blue').map((blue) => rgbaColour(red, green, blue, 1)))); 23466 // TODO: Find way to use this for palette and slider updates 23467 const setValueRgb = (rgb) => { 23468 const red = rgb.red; 23469 const green = rgb.green; 23470 const blue = rgb.blue; 23471 set('red', Optional.some(red)); 23472 set('green', Optional.some(green)); 23473 set('blue', Optional.some(blue)); 23474 }; 23475 const onInvalidInput = (form, simulatedEvent) => { 23476 const data = simulatedEvent.event; 23477 if (data.type !== 'hex') { 23478 set(data.type, Optional.none()); 23479 } 23480 else { 23481 onInvalidHexx(form); 23482 } 23483 }; 23484 const onValidHex = (form, value) => { 23485 onValidHexx(form); 23486 const hex = hexColour(value); 23487 set('hex', Optional.some(hex.value)); 23488 const rgb = fromHex(hex); 23489 copyRgbToForm(form, rgb); 23490 setValueRgb(rgb); 23491 emitWith(form, fieldsUpdate, { 23492 hex 23493 }); 23494 updatePreview(form, hex); 23495 }; 23496 const onValidRgb = (form, prop, value) => { 23497 const val = parseInt(value, 10); 23498 set(prop, Optional.some(val)); 23499 getValueRgb().each((rgb) => { 23500 const hex = copyRgbToHex(form, rgb); 23501 emitWith(form, fieldsUpdate, { 23502 hex 23503 }); 23504 updatePreview(form, hex); 23505 }); 23506 }; 23507 const isHexInputEvent = (data) => data.type === 'hex'; 23508 const onValidInput = (form, simulatedEvent) => { 23509 const data = simulatedEvent.event; 23510 if (isHexInputEvent(data)) { 23511 onValidHex(form, data.value); 23512 } 23513 else { 23514 onValidRgb(form, data.type, data.value); 23515 } 23516 }; 23517 const formPartStrings = (key) => ({ 23518 label: translate(translatePrefix + key + '.label'), 23519 description: translate(translatePrefix + key + '.description') 23520 }); 23521 const redStrings = formPartStrings('red'); 23522 const greenStrings = formPartStrings('green'); 23523 const blueStrings = formPartStrings('blue'); 23524 const hexStrings = formPartStrings('hex'); 23525 // TODO: Provide a nice way of adding APIs to existing sketchers 23526 return deepMerge(Form.sketch((parts) => ({ 23527 dom: { 23528 tag: 'form', 23529 classes: [getClass('rgb-form')], 23530 attributes: { 'aria-label': translate('aria.color.picker') } 23531 }, 23532 components: [ 23533 parts.field('red', FormField.sketch(renderTextField(isRgbaComponent, 'red', redStrings.label, redStrings.description, 255))), 23534 parts.field('green', FormField.sketch(renderTextField(isRgbaComponent, 'green', greenStrings.label, greenStrings.description, 255))), 23535 parts.field('blue', FormField.sketch(renderTextField(isRgbaComponent, 'blue', blueStrings.label, blueStrings.description, 255))), 23536 parts.field('hex', FormField.sketch(renderTextField(isHexString, 'hex', hexStrings.label, hexStrings.description, 'ffffff'))), 23537 memPreview.asSpec() 23538 ], 23539 formBehaviours: derive$1([ 23540 Invalidating.config({ 23541 invalidClass: getClass('form-invalid') 23542 }), 23543 config('rgb-form-events', [ 23544 run$1(validInput, onValidInput), 23545 run$1(invalidInput, onInvalidInput), 23546 run$1(validatingInput, onInvalidInput) 23547 ]) 23548 ]) 23549 })), { 23550 apis: { 23551 updateHex: (form, hex) => { 23552 Representing.setValue(form, { 23553 hex: hex.value 23554 }); 23555 copyHexToRgb(form, hex); 23556 updatePreview(form, hex); 23557 } 23558 } 23559 }); 23560 }; 23561 const rgbFormSketcher = single({ 23562 factory, 23563 name: 'RgbForm', 23564 configFields: [], 23565 apis: { 23566 updateHex: (apis, form, hex) => { 23567 apis.updateHex(form, hex); 23568 } 23569 }, 23570 extraApis: {} 23571 }); 23572 return rgbFormSketcher; 23573 }; 23574 23575 const paletteFactory = (translate, getClass) => { 23576 const spectrumPart = Slider.parts.spectrum({ 23577 dom: { 23578 tag: 'canvas', 23579 attributes: { 23580 role: 'presentation' 23581 }, 23582 classes: [getClass('sv-palette-spectrum')] 23583 } 23584 }); 23585 const thumbPart = Slider.parts.thumb({ 23586 dom: { 23587 tag: 'div', 23588 attributes: { 23589 role: 'presentation' 23590 }, 23591 classes: [getClass('sv-palette-thumb')], 23592 innerHtml: `<div class=${getClass('sv-palette-inner-thumb')} role="presentation"></div>` 23593 } 23594 }); 23595 const setColour = (canvas, rgba) => { 23596 const { width, height } = canvas; 23597 const ctx = canvas.getContext('2d'); 23598 if (ctx === null) { 23599 return; 23600 } 23601 ctx.fillStyle = rgba; 23602 ctx.fillRect(0, 0, width, height); 23603 const grdWhite = ctx.createLinearGradient(0, 0, width, 0); 23604 grdWhite.addColorStop(0, 'rgba(255,255,255,1)'); 23605 grdWhite.addColorStop(1, 'rgba(255,255,255,0)'); 23606 ctx.fillStyle = grdWhite; 23607 ctx.fillRect(0, 0, width, height); 23608 const grdBlack = ctx.createLinearGradient(0, 0, 0, height); 23609 grdBlack.addColorStop(0, 'rgba(0,0,0,0)'); 23610 grdBlack.addColorStop(1, 'rgba(0,0,0,1)'); 23611 ctx.fillStyle = grdBlack; 23612 ctx.fillRect(0, 0, width, height); 23613 }; 23614 const setPaletteHue = (slider, hue) => { 23615 const canvas = slider.components()[0].element.dom; 23616 const hsv = hsvColour(hue, 100, 100); 23617 const rgba = fromHsv(hsv); 23618 setColour(canvas, toString(rgba)); 23619 }; 23620 const setPaletteThumb = (slider, hex) => { 23621 const hsv = fromRgb(fromHex(hex)); 23622 Slider.setValue(slider, { x: hsv.saturation, y: 100 - hsv.value }); 23623 set$9(slider.element, 'aria-valuetext', translate(['Saturation {0}%, Brightness {1}%', hsv.saturation, hsv.value])); 23624 }; 23625 const factory = (_detail) => { 23626 const getInitialValue = constant$1({ 23627 x: 0, 23628 y: 0 23629 }); 23630 const onChange = (slider, _thumb, value) => { 23631 if (!isNumber(value)) { 23632 set$9(slider.element, 'aria-valuetext', translate(['Saturation {0}%, Brightness {1}%', Math.floor(value.x), Math.floor(100 - value.y)])); 23633 } 23634 emitWith(slider, paletteUpdate, { 23635 value 23636 }); 23637 }; 23638 const onInit = (_slider, _thumb, spectrum, _value) => { 23639 // Maybe make this initial value configurable? 23640 setColour(spectrum.element.dom, toString(red)); 23641 }; 23642 const sliderBehaviours = derive$1([ 23643 Composing.config({ 23644 find: Optional.some 23645 }), 23646 Focusing.config({}) 23647 ]); 23648 return Slider.sketch({ 23649 dom: { 23650 tag: 'div', 23651 attributes: { 23652 'role': 'slider', 23653 'aria-valuetext': translate(['Saturation {0}%, Brightness {1}%', 0, 0]) 23654 }, 23655 classes: [getClass('sv-palette')] 23656 }, 23657 model: { 23658 mode: 'xy', 23659 getInitialValue, 23660 }, 23661 rounded: false, 23662 components: [ 23663 spectrumPart, 23664 thumbPart 23665 ], 23666 onChange, 23667 onInit, 23668 sliderBehaviours 23669 }); 23670 }; 23671 const saturationBrightnessPaletteSketcher = single({ 23672 factory, 23673 name: 'SaturationBrightnessPalette', 23674 configFields: [], 23675 apis: { 23676 setHue: (_apis, slider, hue) => { 23677 setPaletteHue(slider, hue); 23678 }, 23679 setThumb: (_apis, slider, hex) => {
23680 setPaletteThumb(slider, hex); 23681 } 23682 }, 23683 extraApis: {} 23684 }); 23685 return saturationBrightnessPaletteSketcher; 23686 }; 23687 23688 const makeFactory = (translate, getClass, tooltipConfig, makeIcon) => { 23689 const factory = (detail) => { 23690 const rgbForm = rgbFormFactory(translate, getClass, detail.onValidHex, detail.onInvalidHex, tooltipConfig, makeIcon); 23691 const sbPalette = paletteFactory(translate, getClass); 23692 const hueSliderToDegrees = (hue) => (100 - hue) / 100 * 360; 23693 const hueDegreesToSlider = (hue) => 100 - (hue / 360) * 100; 23694 const state = { 23695 paletteRgba: Cell(red), 23696 paletteHue: Cell(0) 23697 }; 23698 const memSlider = record(sliderFactory(translate, getClass)); 23699 const memPalette = record(sbPalette.sketch({})); 23700 const memRgb = record(rgbForm.sketch({})); 23701 const updatePalette = (anyInSystem, _hex, hue) => { 23702 memPalette.getOpt(anyInSystem).each((palette) => { 23703 sbPalette.setHue(palette, hue); 23704 }); 23705 }; 23706 const updateFields = (anyInSystem, hex) => { 23707 memRgb.getOpt(anyInSystem).each((form) => { 23708 rgbForm.updateHex(form, hex); 23709 }); 23710 }; 23711 const updateSlider = (anyInSystem, _hex, hue) => { 23712 memSlider.getOpt(anyInSystem).each((slider) => { 23713 Slider.setValue(slider, hueDegreesToSlider(hue)); 23714 }); 23715 }; 23716 const updatePaletteThumb = (anyInSystem, hex) => { 23717 memPalette.getOpt(anyInSystem).each((palette) => { 23718 sbPalette.setThumb(palette, hex); 23719 }); 23720 }; 23721 const updateState = (hex, hue) => { 23722 const rgba = fromHex(hex); 23723 state.paletteRgba.set(rgba); 23724 state.paletteHue.set(hue); 23725 }; 23726 const runUpdates = (anyInSystem, hex, hue, updates) => { 23727 updateState(hex, hue); 23728 each$1(updates, (update) => { 23729 update(anyInSystem, hex, hue); 23730 }); 23731 }; 23732 const onPaletteUpdate = () => { 23733 const updates = [updateFields]; 23734 return (form, simulatedEvent) => { 23735 const value = simulatedEvent.event.value; 23736 const oldHue = state.paletteHue.get(); 23737 const newHsv = hsvColour(oldHue, value.x, (100 - value.y)); 23738 const newHex = hsvToHex(newHsv); 23739 runUpdates(form, newHex, oldHue, updates); 23740 }; 23741 }; 23742 const onSliderUpdate = () => { 23743 const updates = [updatePalette, updateFields]; 23744 return (form, simulatedEvent) => { 23745 const hue = hueSliderToDegrees(simulatedEvent.event.value); 23746 const oldRgb = state.paletteRgba.get(); 23747 const oldHsv = fromRgb(oldRgb); 23748 const newHsv = hsvColour(hue, oldHsv.saturation, oldHsv.value); 23749 const newHex = hsvToHex(newHsv); 23750 runUpdates(form, newHex, hue, updates); 23751 }; 23752 }; 23753 const onFieldsUpdate = () => { 23754 const updates = [updatePalette, updateSlider, updatePaletteThumb]; 23755 return (form, simulatedEvent) => { 23756 const hex = simulatedEvent.event.hex; 23757 const hsv = hexToHsv(hex); 23758 runUpdates(form, hex, hsv.hue, updates); 23759 }; 23760 }; 23761 return { 23762 uid: detail.uid, 23763 dom: detail.dom, 23764 components: [ 23765 memPalette.asSpec(), 23766 memSlider.asSpec(), 23767 memRgb.asSpec() 23768 ], 23769 behaviours: derive$1([ 23770 config('colour-picker-events', [ 23771 run$1(fieldsUpdate, onFieldsUpdate()), 23772 run$1(paletteUpdate, onPaletteUpdate()),
23773 run$1(sliderUpdate, onSliderUpdate()) 23774 ]), 23775 Composing.config({ 23776 find: (comp) => memRgb.getOpt(comp) 23777 }), 23778 Keying.config({ 23779 mode: 'acyclic' 23780 }) 23781 ]) 23782 }; 23783 }; 23784 const colourPickerSketcher = single({ 23785 name: 'ColourPicker', 23786 configFields: [ 23787 required$1('dom'), 23788 defaulted('onValidHex', noop), 23789 defaulted('onInvalidHex', noop) 23790 ], 23791 factory 23792 }); 23793 return colourPickerSketcher; 23794 }; 23795 23796 const english = { 23797 'colorcustom.rgb.red.label': 'R', 23798 'colorcustom.rgb.red.description': 'Red channel', 23799 'colorcustom.rgb.green.label': 'G', 23800 'colorcustom.rgb.green.description': 'Green channel', 23801 'colorcustom.rgb.blue.label': 'B', 23802 'colorcustom.rgb.blue.description': 'Blue channel', 23803 'colorcustom.rgb.hex.label': '#', 23804 'colorcustom.rgb.hex.description': 'Hex color code', 23805 'colorcustom.rgb.range': 'Range 0 to 255', 23806 'colorcustom.rgb.invalid': 'Numbers only, 0 to 255', 23807 'colorcustom.rgb.invalidHex': 'Hexadecimal only, 000000 to FFFFFF', 23808 'aria.color.picker': 'Color Picker', 23809 'aria.input.invalid': 'Invalid input' 23810 }; 23811 const translate = (providerBackstage) => (key) => { 23812 if (isString(key)) { 23813 return providerBackstage.translate(english[key]); 23814 } 23815 else { 23816 return providerBackstage.translate(key); 23817 } 23818 }; 23819 const renderColorPicker = (_spec, providerBackstage, initialData) => { 23820 const getClass = (key) => 'tox-' + key; 23821 const renderIcon = (name, errId, icon = name, label = name) => render$4(icon, { 23822 tag: 'div', 23823 classes: ['tox-icon', 'tox-control-wrap__status-icon-' + name], 23824 attributes: { 23825 'title': providerBackstage.translate(label), 23826 'aria-live': 'polite', 23827 ...errId.fold(() => ({}), (id) => ({ id })) 23828 } 23829 }, providerBackstage.icons); 23830 const colourPickerFactory = makeFactory(translate(providerBackstage), getClass, providerBackstage.tooltips.getConfig, renderIcon); 23831 const onValidHex = (form) => { 23832 emitWith(form, formActionEvent, { name: 'hex-valid', value: true }); 23833 }; 23834 const onInvalidHex = (form) => { 23835 emitWith(form, formActionEvent, { name: 'hex-valid', value: false }); 23836 }; 23837 const memPicker = record(colourPickerFactory.sketch({ 23838 dom: { 23839 tag: 'div', 23840 classes: [getClass('color-picker-container')], 23841 attributes: { 23842 role: 'presentation' 23843 } 23844 }, 23845 onValidHex, 23846 onInvalidHex 23847 })); 23848 return { 23849 dom: { 23850 tag: 'div' 23851 }, 23852 components: [ 23853 memPicker.asSpec() 23854 ], 23855 behaviours: derive$1([ 23856 // We'll allow invalid values 23857 withComp(initialData, (comp) => { 23858 const picker = memPicker.get(comp); 23859 const optRgbForm = Composing.getCurrent(picker); 23860 const optHex = optRgbForm.bind((rgbForm) => { 23861 const formValues = Representing.getValue(rgbForm); 23862 return formValues.hex; 23863 }); 23864 return optHex.map((hex) => '#' + removeLeading(hex, '#')).getOr(''); 23865 }, (comp, newValue) => { 23866 const pattern = /^#([a-fA-F0-9]{3}(?:[a-fA-F0-9]{3})?)/; 23867 const valOpt = Optional.from(pattern.exec(newValue)).bind((matches) => get$i(matches, 1)); 23868 const picker = memPicker.get(comp); 23869 const optRgbForm = Composing.getCurrent(picker); 23870 optRgbForm.fold(() => { 23871 // eslint-disable-next-line no-console 23872 console.log('Can not find form'); 23873 }, (rgbForm) => { 23874 Representing.setValue(rgbForm, { 23875 hex: valOpt.getOr('') 23876 }); 23877 // So not the way to do this. 23878 Form.getField(rgbForm, 'hex').each((hexField) => { 23879 emit(hexField, input()); 23880 }); 23881 }); 23882 }), 23883 ComposingConfigs.self() 23884 ]) 23885 }; 23886 }; 23887 23888 var global$3 = tinymce.util.Tools.resolve('tinymce.Resource'); 23889
23890 const isOldCustomEditor = (spec) => has$2(spec, 'init'); 23891 const renderCustomEditor = (spec) => { 23892 const editorApi = value$2(); 23893 const memReplaced = record({ 23894 dom: { 23895 tag: spec.tag 23896 } 23897 }); 23898 const initialValue = value$2(); 23899 const focusBehaviour = !isOldCustomEditor(spec) && spec.onFocus.isSome() ? [ 23900 Focusing.config({ 23901 onFocus: (comp) => { 23902 spec.onFocus.each((onFocusFn) => { 23903 onFocusFn(comp.element.dom); 23904 }); 23905 } 23906 }), 23907 Tabstopping.config({}) 23908 ] : []; 23909 return { 23910 dom: { 23911 tag: 'div', 23912 classes: ['tox-custom-editor'] 23913 }, 23914 behaviours: derive$1([ 23915 config('custom-editor-events', [ 23916 runOnAttached((component) => { 23917 memReplaced.getOpt(component).each((ta) => { 23918 // eslint-disable-next-line @typescript-eslint/no-floating-promises 23919 (isOldCustomEditor(spec) 23920 ? spec.init(ta.element.dom) 23921 : global$3.load(spec.scriptId, spec.scriptUrl).then((init) => init(ta.element.dom, spec.settings))).then((ea) => { 23922 initialValue.on((cvalue) => { 23923 ea.setValue(cvalue); 23924 }); 23925 initialValue.clear(); 23926 editorApi.set(ea); 23927 }); 23928 }); 23929 }) 23930 ]), 23931 withComp(Optional.none(), () => editorApi.get().fold(() => initialValue.get().getOr(''), (ed) => ed.getValue()), (_component, value) => { 23932 editorApi.get().fold(() => initialValue.set(value), (ed) => ed.setValue(value)); 23933 }), 23934 ComposingConfigs.self() 23935 ].concat(focusBehaviour)), 23936 components: [memReplaced.asSpec()] 23937 }; 23938 }; 23939 23940 var global$2 = tinymce.util.Tools.resolve('tinymce.util.Tools'); 23941 23942 const browseFilesEvent = generate$6('browse.files.event'); 23943 const filterByExtension = (files, providersBackstage, allowedFileExtensions) => { 23944 const allowedImageFileTypes = global$2.explode(providersBackstage.getOption('images_file_types')); 23945 const isFileInAllowedTypes = (file) => allowedFileExtensions.fold(() => exists(allowedImageFileTypes, (type) => endsWith(file.name.toLowerCase(), `.${type.toLowerCase()}`)), (exts) => exists(exts, (type) => endsWith(file.name.toLowerCase(), `.${type.toLowerCase()}`))); 23946 return filter$2(from(files), isFileInAllowedTypes); 23947 }; 23948 const renderDropZone = (spec, providersBackstage, initialData) => { 23949 // TODO: Consider moving to alloy 23950 const stopper = (_, se) => { 23951 se.stop(); 23952 }; 23953 // TODO: Consider moving to alloy 23954 const sequence = (actions) => (comp, se) => { 23955 each$1(actions, (a) => { 23956 a(comp, se); 23957 }); 23958 }; 23959 const onDrop = (comp, se) => { 23960 if (!Disabling.isDisabled(comp)) { 23961 const transferEvent = se.event.raw; 23962 emitWith(comp, browseFilesEvent, { files: transferEvent.dataTransfer?.files }); 23963 } 23964 }; 23965 const onSelect = (component, simulatedEvent) => { 23966 const input = simulatedEvent.event.raw.target; 23967 emitWith(component, browseFilesEvent, { files: input.files }); 23968 }; 23969 const handleFiles = (component, files) => { 23970 if (files) { 23971 const filteredFiles = filterByExtension(files, providersBackstage, spec.allowedFileExtensions); 23972 Representing.setValue(component, filteredFiles); 23973 emitWith(component, formChangeEvent, { name: spec.name }); 23974 if (filteredFiles.length === 0) { 23975 spec.onInvalidFiles().finally(() => { 23976 component.element.dom.focus(); 23977 }).catch(noop); 23978 } 23979 } 23980 }; 23981 const memInput = record({ 23982 dom: { 23983 tag: 'input', 23984 attributes: { 23985 type: 'file', 23986 accept: spec.allowedFileTypes.getOr('image/*') 23987 }, 23988 styles: { 23989 display: 'none' 23990 } 23991 }, 23992 behaviours: derive$1([ 23993 config('input-file-events', [ 23994 cutter(click()), 23995 cutter(tap()) 23996 ]) 23997 ]) 23998 }); 23999 const pLabel = spec.label.map((label) => renderLabel$3(label, providersBackstage)); 24000 const pField = FormField.parts.field({ 24001 factory: Button, 24002 dom: { 24003 tag: 'button', 24004 styles: { 24005 position: 'relative' 24006 }, 24007 classes: ['tox-button', 'tox-button--secondary'] 24008 }, 24009 components: [ 24010 text$2(providersBackstage.translate(spec.buttonLabel.getOr('Browse for an image'))), 24011 memInput.asSpec() 24012 ], 24013 action: (comp) => { 24014 const inputComp = memInput.get(comp); 24015 inputComp.element.dom.click(); 24016 }, 24017 buttonBehaviours: derive$1([ 24018 ComposingConfigs.self(), 24019 memory(initialData.getOr([])), 24020 Tabstopping.config({}), 24021 DisablingConfigs.button(() =>
24021 providersBackstage.checkUiComponentContext(spec.context).shouldDisable), 24022 toggleOnReceive(() => providersBackstage.checkUiComponentContext(spec.context)) 24023 ]) 24024 }); 24025 const wrapper = { 24026 dom: { 24027 tag: 'div', 24028 classes: ['tox-dropzone-container'] 24029 }, 24030 behaviours: derive$1([ 24031 Disabling.config({ 24032 disabled: () => providersBackstage.checkUiComponentContext(spec.context).shouldDisable 24033 }), 24034 toggleOnReceive(() => providersBackstage.checkUiComponentContext(spec.context)), 24035 Toggling.config({ 24036 toggleClass: 'dragenter', 24037 toggleOnExecute: false 24038 }), 24039 config('dropzone-events', [ 24040 run$1('dragenter', sequence([stopper, Toggling.toggle])), 24041 run$1('dragleave', sequence([stopper, Toggling.toggle])), 24042 run$1('dragover', stopper), 24043 run$1('drop', sequence([stopper, onDrop])), 24044 run$1(change(), onSelect) 24045 ]) 24046 ]), 24047 components: [ 24048 { 24049 dom: { 24050 tag: 'div', 24051 classes: ['tox-dropzone'], 24052 styles: {} 24053 }, 24054 components: [ 24055 { 24056 dom: { 24057 tag: 'p' 24058 }, 24059 components: [ 24060 text$2(providersBackstage.translate(spec.dropAreaLabel.getOr('Drop an image here'))) 24061 ] 24062 }, 24063 pField 24064 ] 24065 } 24066 ] 24067 }; 24068 return renderFormFieldWith(pLabel, wrapper, ['tox-form__group--stretched'], [config('handle-files', [ 24069 run$1(browseFilesEvent, (comp, se) => { 24070 FormField.getField(comp).each((field) => { 24071 handleFiles(field, se.event.files); 24072 }); 24073 }) 24074 ])]); 24075 }; 24076 24077 const renderGrid = (spec, backstage) => ({ 24078 dom: { 24079 tag: 'div', 24080 classes: ['tox-form__grid', `tox-form__grid--${spec.columns}col`] 24081 }, 24082 components: map$2(spec.items, backstage.interpreter) 24083 }); 24084 24085 const beforeObject = generate$6('alloy-fake-before-tabstop'); 24086 const afterObject = generate$6('alloy-fake-after-tabstop'); 24087 const craftWithClasses = (classes) => { 24088 return { 24089 dom: { 24090 tag: 'div', 24091 styles: { 24092 width: '1px', 24093 height: '1px', 24094 outline: 'none' 24095 }, 24096 attributes: { 24097 tabindex: '0' // Capture native tabbing in the appropriate order 24098 }, 24099 classes 24100 }, 24101 behaviours: derive$1([ 24102 Focusing.config({ ignore: true }), 24103 Tabstopping.config({}) 24104 ]) 24105 }; 24106 }; 24107 const craft = (containerClasses, spec) => { 24108 return { 24109 dom: { 24110 tag: 'div', 24111 classes: ['tox-navobj', ...containerClasses.getOr([])] 24112 }, 24113 components: [ 24114 craftWithClasses([beforeObject]), 24115 spec, 24116 craftWithClasses([afterObject]) 24117 ], 24118 behaviours: derive$1([ 24119 ComposingConfigs.childAt(1) 24120 ]) 24121 }; 24122 }; 24123 // TODO: Create an API in alloy to do this. 24124 const triggerTab = (placeholder, shiftKey) => { 24125 emitWith(placeholder, keydown(), { 24126 raw: { 24127 which: 9, 24128 shiftKey 24129 } 24130 }); 24131 }; 24132 const onFocus = (container, targetComp) => { 24133 const target = targetComp.element; 24134 // If focus has shifted naturally to a before object, the tab direction is backwards. 24135 if (has(target, beforeObject)) { 24136 triggerTab(container, true); 24137 } 24138 else if (has(target, afterObject)) { 24139 triggerTab(container, false); 24140 } 24141 }; 24142 const isPseudoStop = (element) => { 24143 return closest$1(element, ['.' + beforeObject, '.' + afterObject].join(','), never); 24144 }; 24145 24146 const dialogChannel = generate$6('update-dialog'); 24147 const titleChannel = generate$6('update-title'); 24148 const bodyChannel = generate$6('update-body'); 24149 const footerChannel = generate$6('update-footer'); 24150 const bodySendMessageChannel = generate$6('body-send-message'); 24151 const dialogFocusShiftedChannel = generate$6('dialog-focus-shifted'); 24152 24153 const browser = detect$1().browser; 24154 const isSafari = browser.isSafari(); 24155 const isFirefox = browser.isFirefox(); 24156 const isSafariOrFirefox = isSafari || isFirefox; 24157 const isChromium = browser.isChromium(); 24158 const isElementScrollAtBottom = ({ scrollTop, scrollHeight, clientHeight }) =>
24158 Math.ceil(scrollTop) + clientHeight >= scrollHeight; 24159 const scrollToY = (win, y) => 24160 // TINY-10128: The iframe body is occasionally null when we attempt to scroll, so instead of using body.scrollHeight, use a 24161 // fallback value of 99999999. To minimise the potential impact of future browser changes, this fallback is significantly smaller 24162 // than the minimum of the maximum value Window.scrollTo would take on supported browsers: 24163 // Chromium: > Number.MAX_SAFE_INTEGER 24164 // Safari: 2^31 - 1 = 2147483647 24165 // Firefox: 2147483583 24166 win.scrollTo(0, y === 'bottom' ? 99999999 : y); 24167 const getScrollingElement = (doc, html) => { 24168 // TINY-10110: The scrolling element can change between body and documentElement depending on whether there 24169 // is a doctype declaration. However, this behavior is inconsistent on Chrome and Safari so checking for 24170 // the scroll properties is the most reliable way to determine which element is the scrolling element, at 24171 // least for the purposes of determining whether scroll is at bottom. 24172 const body = doc.body; 24173 return Optional.from(!/^<!DOCTYPE (html|HTML)/.test(html) && 24174 (!isChromium && !isSafari || isNonNullable(body) && (body.scrollTop !== 0 || Math.abs(body.scrollHeight - body.clientHeight) > 1)) 24175 ? body : doc.documentElement); 24176 }; 24177 const writeValue = (iframeElement, html, fallbackFn) => { 24178 const iframe = iframeElement.dom; 24179 Optional.from(iframe.contentDocument).fold(fallbackFn, (doc) => { 24180 let lastScrollTop = 0; 24181 // TINY-10032: If documentElement (or body) is nullable, we assume document is empty and so scroll is at bottom. 24182 const isScrollAtBottom = getScrollingElement(doc, html).map((el) => { 24183 lastScrollTop = el.scrollTop; 24184 return el; 24185 }).forall(isElementScrollAtBottom); 24186 const scrollAfterWrite = () => { 24187 const win = iframe.contentWindow; 24188 if (isNonNullable(win)) { 24189 if (isScrollAtBottom) { 24190 scrollToY(win, 'bottom'); 24191 } 24192 else if (!isScrollAtBottom && isSafariOrFirefox && lastScrollTop !== 0) { 24193 // TINY-10078: Safari and Firefox reset scroll to top on each document.write(), so we need to restore scroll manually 24194 scrollToY(win, lastScrollTop); 24195 } 24196 } 24197 }; 24198 // TINY-10109: On Safari, attempting to scroll before the iframe has finished loading will cause scroll to reset to top upon load. 24199 // TINY-10128: We will not wait for the load event on Chrome and Firefox since doing so causes the scroll to jump around erratically, 24200 // especially on Firefox. However, not waiting for load has the trade-off of potentially losing bottom scroll when updating at a very 24201 // rapid rate, as attempting to scroll before the iframe body is loaded will not work. 24202 if (isSafari) { 24203 iframe.addEventListener('load', scrollAfterWrite, { once: true }); 24204 } 24205 doc.open(); 24206 doc.write(html); 24207 doc.close(); 24208 if (!isSafari) { 24209 scrollAfterWrite(); 24210 } 24211 }); 24212 }; 24213 // TINY-10078: On Firefox, throttle to 200ms to improve scrolling experience. Since we are manually maintaining previous scroll position 24214 // on each update, when updating rapidly without a throttle, attempting to scroll around the iframe can feel stuck. 24215 // TINY-10097: On Safari, throttle to 500ms to reduce flickering as the document.write() method still observes significant flickering. 24216 // Also improves scrolling, as scroll positions are maintained manually similar to Firefox. 24217 const throttleInterval = someIf(isSafariOrFirefox, isSafari ? 500 : 200); 24218 // TINY-10078: Use Throttler.adaptable to ensure that any content added during the waiting period is not lost. 24219 const writeValueThrottler = throttleInterval.map((interval) => adaptable(writeValue, interval)); 24220 const getDynamicSource = (initialData, stream) => { 24221 const cachedValue = Cell(initialData.getOr('')); 24222 return { 24223 getValue: (_frameComponent) => 24224 // Ideally we should fetch data from the iframe...innerHtml, this triggers Cors errors 24225 cachedValue.get(),
24226 setValue: (frameComponent, html) => { 24227 if (cachedValue.get() !== html) { 24228 const iframeElement = frameComponent.element; 24229 const setSrcdocValue = () => set$9(iframeElement, 'srcdoc', html); 24230 if (stream) { 24231 writeValueThrottler.fold(constant$1(writeValue), (throttler) => throttler.throttle)(iframeElement, html, setSrcdocValue); 24232 } 24233 else { 24234 // TINY-3769: We need to use srcdoc here, instead of src with a data URI, otherwise browsers won't retain the Origin. 24235 // See https://bugs.chromium.org/p/chromium/issues/detail?id=58999#c11 24236 setSrcdocValue(); 24237 } 24238 } 24239 cachedValue.set(html); 24240 } 24241 }; 24242 }; 24243 const renderIFrame = (spec, providersBackstage, initialData) => { 24244 const baseClass = 'tox-dialog__iframe'; 24245 const opaqueClass = spec.transparent ? [] : [`${baseClass}--opaque`]; 24246 const containerBorderedClass = spec.border ? [`tox-navobj-bordered`] : []; 24247 const attributes = { 24248 ...spec.label.map((title) => ({ title })).getOr({}), 24249 ...initialData.map((html) => ({ srcdoc: html })).getOr({}), 24250 ...spec.sandboxed ? { sandbox: 'allow-scripts allow-same-origin' } : {} 24251 }; 24252 const sourcing = getDynamicSource(initialData, spec.streamContent); 24253 const pLabel = spec.label.map((label) => renderLabel$3(label, providersBackstage)); 24254 const factory = (newSpec) => craft(Optional.from(containerBorderedClass), { 24255 // We need to use the part uid or the label and field won't be linked with ARIA 24256 uid: newSpec.uid, 24257 dom: { 24258 tag: 'iframe', 24259 attributes, 24260 classes: [ 24261 baseClass, 24262 ...opaqueClass 24263 ] 24264 }, 24265 behaviours: derive$1([ 24266 Tabstopping.config({}), 24267 Focusing.config({}), 24268 withComp(initialData, sourcing.getValue, sourcing.setValue), 24269 Receiving.config({ 24270 channels: { 24271 [dialogFocusShiftedChannel]: { 24272 onReceive: (comp, message) => { 24273 message.newFocus.each((newFocus) => { 24274 parentElement(comp.element).each((parent) => { 24275 const f = eq(comp.element, newFocus) ? add$2 : remove$3; 24276 f(parent, 'tox-navobj-bordered-focus'); 24277 }); 24278 }); 24279 } 24280 } 24281 } 24282 }) 24283 ]) 24284 }); 24285 // Note, it's not going to handle escape at this point. 24286 const pField = FormField.parts.field({ 24287 factory: { sketch: factory } 24288 }); 24289 return renderFormFieldWith(pLabel, pField, ['tox-form__group--stretched'], []); 24290 }; 24291 24292 const calculateImagePosition = (panelWidth, panelHeight, imageWidth, imageHeight, zoom) => { 24293 const width = imageWidth * zoom; 24294 const height = imageHeight * zoom; 24295 const left = Math.max(0, panelWidth / 2 - width / 2); 24296 const top = Math.max(0, panelHeight / 2 - height / 2); 24297 return { 24298 left: left.toString() + 'px', 24299 top: top.toString() + 'px', 24300 width: width.toString() + 'px', 24301 height: height.toString() + 'px', 24302 }; 24303 }; 24304 const zoomToFit = (panel, width, height) => { 24305 const panelW = get$c(panel); 24306 const panelH = get$d(panel); 24307 return Math.min(panelW / width, panelH / height, 1); 24308 }; 24309 const renderImagePreview = (spec, initialData) => { 24310 const cachedData = Cell(initialData.getOr({ url: '' })); 24311 const memImage = record({ 24312 dom: { 24313 tag: 'img', 24314 classes: ['tox-imagepreview__image'], 24315 attributes: initialData.map((data) => ({ src: data.url })).getOr({}) 24316 }, 24317 }); 24318 const memContainer = record({ 24319 dom: { 24320 tag: 'div', 24321 classes: ['tox-imagepreview__container'], 24322 attributes: { 24323 role: 'presentation' 24324 }, 24325 }, 24326 components: [ 24327 memImage.asSpec() 24328 ] 24329 });
24330 const setValue = (frameComponent, data) => { 24331 const translatedData = { 24332 url: data.url 24333 }; 24334 // update properties that are set by the data 24335 data.zoom.each((z) => translatedData.zoom = z); 24336 data.cachedWidth.each((z) => translatedData.cachedWidth = z); 24337 data.cachedHeight.each((z) => translatedData.cachedHeight = z); 24338 cachedData.set(translatedData); 24339 const applyFramePositioning = () => { 24340 const { cachedWidth, cachedHeight, zoom } = translatedData; 24341 if (!isUndefined(cachedWidth) && !isUndefined(cachedHeight)) { 24342 if (isUndefined(zoom)) { 24343 const z = zoomToFit(frameComponent.element, cachedWidth, cachedHeight); 24344 // sneaky mutation since we own the object 24345 translatedData.zoom = z; 24346 } 24347 const position = calculateImagePosition(get$c(frameComponent.element), get$d(frameComponent.element), cachedWidth, cachedHeight, translatedData.zoom); 24348 memContainer.getOpt(frameComponent).each((container) => { 24349 setAll(container.element, position); 24350 }); 24351 } 24352 }; 24353 memImage.getOpt(frameComponent).each((imageComponent) => { 24354 const img = imageComponent.element; 24355 if (data.url !== get$g(img, 'src')) { 24356 set$9(img, 'src', data.url); 24357 remove$3(frameComponent.element, 'tox-imagepreview__loaded'); 24358 } 24359 applyFramePositioning(); 24360 // eslint-disable-next-line @typescript-eslint/no-floating-promises 24361 image(img).then((img) => { 24362 // Ensure the component hasn't been removed while the image was loading 24363 // if it is disconnected, just do nothing 24364 if (frameComponent.getSystem().isConnected()) { 24365 add$2(frameComponent.element, 'tox-imagepreview__loaded'); 24366 // sneaky mutation since we own the object 24367 translatedData.cachedWidth = img.dom.naturalWidth; 24368 translatedData.cachedHeight = img.dom.naturalHeight; 24369 applyFramePositioning(); 24370 } 24371 }); 24372 }); 24373 }; 24374 const styles = {}; 24375 spec.height.each((h) => styles.height = h); 24376 // TODO: TINY-8393 Use the initial data properly once it's validated 24377 const fakeValidatedData = initialData.map((d) => ({ 24378 url: d.url, 24379 zoom: Optional.from(d.zoom), 24380 cachedWidth: Optional.from(d.cachedWidth), 24381 cachedHeight: Optional.from(d.cachedHeight), 24382 })); 24383 return { 24384 dom: { 24385 tag: 'div', 24386 classes: ['tox-imagepreview'], 24387 styles, 24388 attributes: { 24389 role: 'presentation' 24390 } 24391 }, 24392 components: [ 24393 memContainer.asSpec(), 24394 ], 24395 behaviours: derive$1([ 24396 ComposingConfigs.self(), 24397 withComp(fakeValidatedData, () => 24398 /* 24399 NOTE: This is intentionally returning the cached image width and height. 24400 24401 Including those details in the dialog data helps when `setData` only changes the URL, as 24402 the old image must continue to be displayed at the old size until the new image has loaded. 24403 */ 24404 cachedData.get(), setValue), 24405 ]) 24406 }; 24407 }; 24408 24409 const renderLabel$2 = (spec, backstageShared, getCompByName) => { 24410 const baseClass = 'tox-label'; 24411 const centerClass = spec.align === 'center' ? [`${baseClass}--center`] : []; 24412 const endClass = spec.align === 'end' ? [`${baseClass}--end`] : []; 24413 const label = record({ 24414 dom: { 24415 tag: 'label', 24416 classes: [baseClass, ...centerClass, ...endClass] 24417 }, 24418 components: [ 24419 text$2(backstageShared.providers.translate(spec.label)) 24420 ] 24421 }); 24422 const comps = map$2(spec.items, backstageShared.interpreter); 24423 return { 24424 dom: { 24425 tag: 'div', 24426 classes: ['tox-form__group'] 24427 }, 24428 components: [ 24429 label.asSpec(), 24430 ...comps 24431 ], 24432 behaviours: derive$1([ 24433 ComposingConfigs.self(), 24434 Replacing.config({}), 24435 domHtml(Optional.none()), 24436 Keying.config({ 24437 mode: 'acyclic' 24438 }), 24439 config('label', [ 24440 runOnAttached((comp) => { 24441 spec.for.each((name) => { 24442 getCompByName(name).each((target) => { 24443 label.getOpt(comp).each((labelComp) => { 24444 const id = get$g(target.element, 'id') ?? generate$6('form-field'); 24445 set$9(target.element, 'id', id); 24446 set$9(labelComp.element, 'for', id); 24447 }); 24448 }); 24449 }); 24450 }) 24451 ]), 24452 ]) 24453 }; 24454 }; 24455 24456 const internalToolbarButtonExecute = generate$6('toolbar.button.execute'); 24457 // Perform `action` when an item is clicked on, close menus, and stop event 24458 const onToolbarButtonExecute = (info) => runOnExecute$1((comp, _simulatedEvent) => { 24459 // If there is an action, run the action 24460 runWithApi(info, comp)((itemApi) => { 24461 emitWith(comp, internalToolbarButtonExecute, { 24462 buttonApi: itemApi 24463 }); 24464 info.onAction(itemApi); 24465 }); 24466 }); 24467 const commonButtonDisplayEvent = generate$6('common-button-display-events'); 24468 const toolbarButtonEventOrder = { 24469 // TODO: use the constants provided by behaviours. 24470 [execute$5()]: ['disabling', 'alloy.base.behaviour', 'toggling', 'toolbar-button-events', 'tooltipping'], 24471 [attachedToDom()]: [ 24472 'toolbar-button-events', 24473 commonButtonDisplayEvent 24474 ], 24475 [detachedFromDom()]: ['toolbar-button-events', 'dropdown-events', 'tooltipping'], 24476 [mousedown()]: [
24477 'focusing', 24478 'alloy.base.behaviour', 24479 commonButtonDisplayEvent 24480 ] 24481 }; 24482 24483 const forceInitialSize = (comp) => set$7(comp.element, 'width', get$e(comp.element, 'width')); 24484 24485 const renderIcon$1 = (iconName, iconsProvider, behaviours) => render$4(iconName, { 24486 tag: 'span', 24487 classes: ["tox-icon" /* ToolbarButtonClasses.Icon */, "tox-tbtn__icon-wrap" /* ToolbarButtonClasses.IconWrap */], 24488 behaviours 24489 }, iconsProvider); 24490 const renderIconFromPack$1 = (iconName, iconsProvider) => renderIcon$1(iconName, iconsProvider, []); 24491 const renderReplaceableIconFromPack = (iconName, iconsProvider) => renderIcon$1(iconName, iconsProvider, [Replacing.config({})]); 24492 const renderLabel$1 = (text, prefix, providersBackstage) => ({ 24493 dom: { 24494 tag: 'span', 24495 classes: [`${prefix}__select-label`] 24496 }, 24497 components: [ 24498 text$2(providersBackstage.translate(text)) 24499 ], 24500 behaviours: derive$1([ 24501 Replacing.config({}) 24502 ]) 24503 }); 24504 24505 const updateMenuText = generate$6('update-menu-text'); 24506 const updateMenuIcon = generate$6('update-menu-icon'); 24507 const updateTooltiptext = generate$6('update-tooltip-text'); 24508 // TODO: Use renderCommonStructure here. 24509 const renderCommonDropdown = (spec, prefix, sharedBackstage, btnName) => { 24510 const editorOffCell = Cell(noop); 24511 const tooltip = Cell(spec.tooltip); 24512 // We need mementos for display text and display icon because on the events 24513 // updateMenuText and updateMenuIcon respectively, their contents are changed 24514 // via Replacing. These events are generally emitted by dropdowns that want the 24515 // main text and icon to match the current selection (e.g. bespokes like font family) 24516 const optMemDisplayText = spec.text.map((text) => record(renderLabel$1(text, prefix, sharedBackstage.providers))); 24517 const optMemDisplayIcon = spec.icon.map((iconName) => record(renderReplaceableIconFromPack(iconName, sharedBackstage.providers.icons))); 24518 /* 24519 * The desired behaviour here is: 24520 * 24521 * when left or right is pressed, and it isn't associated with expanding or 24522 * collapsing a submenu, then it should navigate to the next menu item, and 24523 * expand it (without highlighting any items in the expanded menu). 24524 * It also needs to close the previous menu 24525 */ 24526 const onLeftOrRightInMenu = (comp, se) => { 24527 // The originating dropdown is stored on the sandbox itself. This is just an 24528 // implementation detail of alloy. We really need to make it a fully-fledged API. 24529 // TODO: TINY-9014 Make SandboxAPI have a function that just delegates to Representing 24530 const dropdown = Representing.getValue(comp); 24531 // Focus the dropdown. Current workaround required to make FlowLayout recognise the current focus. 24532 // The triggering keydown is going to try to move the focus left or 24533 // right of the current menu, so it needs to know what the current menu dropdown is. It 24534 // can't work it out by the current focus, because the current focus is *in* the menu, so 24535 // we help it by moving the focus to the button, so it can work out what the next menu to 24536 // the left or right is. 24537 Focusing.focus(dropdown); 24538 emitWith(dropdown, 'keydown', { 24539 raw: se.event.raw 24540 }); 24541 // Because we have just navigated off this open menu, we want to close it. 24542 // INVESTIGATE: TINY-9014: Is this handling situations where there were no menus 24543 // to move to? Does it matter if we still close it when there are no other menus? 24544 Dropdown.close(dropdown); 24545 // The Optional.some(true) tells the keyboard handler that this event was handled, 24546 // which will do things like stopPropagation and preventDefault. 24547 return Optional.some(true); 24548 }; 24549 const role = spec.role.fold(() => ({}), (role) => ({ role })); 24550 const listRole = Optional.from(spec.listRole).map((listRole) => ({ listRole })).getOr({}); 24551 const ariaLabelAttribute = spec.ariaLabel.fold(() => ({}), (ariaLabel) => { 24552 const translatedAriaLabel = sharedBackstage.providers.translate(ariaLabel); 24553 return { 24554 'aria-label': translatedAriaLabel 24555 }; 24556 }); 24557 const iconSpec = render$4('chevron-down', { 24558 tag: 'div', 24559 classes: [`${prefix}__select-chevron`] 24560 }, sharedBackstage.providers.icons); 24561 const fixWidthBehaviourName = generate$6('common-button-display-events'); 24562 // Should we use Id.generate here? 24563 const customEventsName = 'dropdown-events'; 24564 const memDropdown = record(Dropdown.sketch({ 24565 ...spec.uid ? { uid: spec.uid } : {}, 24566 ...role, 24567 ...listRole, 24568 dom: { 24569 tag: 'button', 24570 classes: [prefix, `${prefix}--select`].concat(map$2(spec.classes, (c) => `${prefix}--${c}`)), 24571 attributes: { 24572 ...ariaLabelAttribute, 24573 ...(isNonNullable(btnName) ? { 'data-mce-name': btnName } : {}) 24574 } 24575 }, 24576 components: componentRenderPipeline([ 24577 optMemDisplayIcon.map((mem) => mem.asSpec()), 24578 optMemDisplayText.map((mem) => mem.asSpec()), 24579 Optional.some(iconSpec) 24580 ]), 24581 matchWidth: true, 24582 useMinWidth: true, 24583 // When the dropdown opens, if we are in search mode, then we want to 24584 // focus our searcher. 24585 onOpen: (anchor, dropdownComp, tmenuComp) => { 24586 if (spec.searchable) { 24587 focusSearchField(tmenuComp); 24588 } 24589 }, 24590 dropdownBehaviours: derive$1([ 24591 ...spec.dropdownBehaviours, 24592 DisablingConfigs.button(() => spec.disabled || sharedBackstage.providers.checkUiComponentContext(spec.c
24592ontext).shouldDisable), 24593 toggleOnReceive(() => sharedBackstage.providers.checkUiComponentContext(spec.context)), 24594 // INVESTIGATE (TINY-9012): There was a old comment here about something not quite working, and that 24595 // we can still get the button focused. It was probably related to Unselecting. 24596 Unselecting.config({}), 24597 Replacing.config({}), 24598 ...(spec.tooltip.map((t) => Tooltipping.config(sharedBackstage.providers.tooltips.getConfig({ 24599 tooltipText: sharedBackstage.providers.translate(t), 24600 onShow: (comp) => { 24601 if (lift2(tooltip.get(), spec.tooltip, (tooltipStr, tt) => tt !== tooltipStr).getOr(false)) { 24602 const translatedTooltip = sharedBackstage.providers.translate(tooltip.get().getOr('')); 24603 Tooltipping.setComponents(comp, sharedBackstage.providers.tooltips.getComponents({ tooltipText: translatedTooltip })); 24604 } 24605 } 24606 })))).toArray(), 24607 // This is the generic way to make onSetup and onDestroy call as the component is attached / 24608 // detached from the page/DOM. 24609 config(customEventsName, [ 24610 onControlAttached(spec, editorOffCell), 24611 onControlDetached(spec, editorOffCell) 24612 ]), 24613 config(fixWidthBehaviourName, [ 24614 runOnAttached((comp, _se) => { 24615 if (spec.listRole !== 'listbox') { 24616 forceInitialSize(comp); 24617 } 24618 }), 24619 ]), 24620 config('update-dropdown-width-variable', [ 24621 run$1(windowResize(), (comp, _se) => Dropdown.close(comp)), 24622 ]), 24623 config('menubutton-update-display-text', [ 24624 // These handlers are just using Replacing to replace either the menu 24625 // text or the icon. 24626 run$1(updateMenuText, (comp, se) => { 24627 optMemDisplayText.bind((mem) => mem.getOpt(comp)).each((displayText) => { 24628 Replacing.set(displayText, [text$2(sharedBackstage.providers.translate(se.event.text))]); 24629 }); 24630 }), 24631 run$1(updateMenuIcon, (comp, se) => { 24632 optMemDisplayIcon.bind((mem) => mem.getOpt(comp)).each((displayIcon) => { 24633 Replacing.set(displayIcon, [ 24634 renderReplaceableIconFromPack(se.event.icon, sharedBackstage.providers.icons) 24635 ]); 24636 }); 24637 }), 24638 run$1(updateTooltiptext, (comp, se) => { 24639 const translatedTooltip = sharedBackstage.providers.translate(se.event.text); 24640 set$9(comp.element, 'aria-label', translatedTooltip); 24641 tooltip.set(Optional.some(se.event.text)); 24642 }) 24643 ]) 24644 ]), 24645 eventOrder: deepMerge(toolbarButtonEventOrder, { 24646 // INVESTIGATE (TINY-9014): Explain why we need the events in this order. 24647 // Ideally, have a test that fails when they are in a different order if order 24648 // is important 24649 [mousedown()]: ['focusing', 'alloy.base.behaviour', 'item-type-events', 'normal-dropdown-events'], 24650 [attachedToDom()]: [ 24651 'toolbar-button-events', 24652 Tooltipping.name(), 24653 customEventsName, 24654 fixWidthBehaviourName, 24655 ] 24656 }), 24657 sandboxBehaviours: derive$1([ 24658 Keying.config({ 24659 mode: 'special', 24660 onLeft: onLeftOrRightInMenu, 24661 onRight: onLeftOrRightInMenu, 24662 }), 24663 config('dropdown-sandbox-events', [ 24664 run$1(refetchTriggerEvent, (originalSandboxComp, se) => { 24665 handleRefetchTrigger(originalSandboxComp); 24666 // It's a custom event that no-one else should be listening to, so stop it. 24667 se.stop(); 24668 }), 24669 run$1(redirectMenuItemInteractionEvent, (sandboxComp, se) => { 24670 handleRedirectToMenuItem(sandboxComp, se); 24671 // It's a custom event that no-one else should be listening to, so stop it. 24672 se.stop(); 24673 }) 24674 ]) 24675 ]), 24676 lazySink: sharedBackstage.getSink, 24677 toggleClass: `${prefix}--active`, 24678 parts: { 24679 menu: { 24680 ...part(false, spec.columns, spec.presets), 24681 // When the menu is "searchable", use fakeFocus so that keyboard
24682 // focus stays in the search field 24683 fakeFocus: spec.searchable, 24684 // We don't want to update the `aria-selected` on highlight or dehighlight for the `listbox` role because that is used to indicate the selected item 24685 ...(spec.listRole === 'listbox' ? {} : { 24686 onHighlightItem: updateAriaOnHighlight, 24687 onCollapseMenu: (tmenuComp, itemCompCausingCollapse, nowActiveMenuComp) => { 24688 // We want to update ARIA on collapsing as well, because it isn't changing 24689 // the highlights. So what we need to do is get the right parameters to 24690 // pass to updateAriaOnHighlight 24691 Highlighting.getHighlighted(nowActiveMenuComp).each((itemComp) => { 24692 updateAriaOnHighlight(tmenuComp, nowActiveMenuComp, itemComp); 24693 }); 24694 }, 24695 onDehighlightItem: updateAriaOnDehighlight 24696 }) 24697 } 24698 }, 24699 getAnchorOverrides: () => { 24700 return { 24701 maxHeightFunction: (element, available) => { 24702 anchored()(element, available - 10); 24703 }, 24704 }; 24705 }, 24706 fetch: (comp) => Future.nu(curry(spec.fetch, comp)) 24707 })); 24708 return memDropdown.asSpec(); 24709 }; 24710 24711 const isMenuItemReference = (item) => isString(item); 24712 const isSeparator$2 = (item) => item.type === 'separator'; 24713 const isExpandingMenuItem = (item) => has$2(item, 'getSubmenuItems'); 24714 const separator$2 = { 24715 type: 'separator' 24716 }; 24717 const unwrapReferences = (items, menuItems) => { 24718 // Unwrap any string based menu item references 24719 const realItems = foldl(items, (acc, item) => { 24720 if (isMenuItemReference(item)) { 24721 if (item === '') { 24722 return acc; 24723 } 24724 else if (item === '|') { 24725 // Ignore the separator if it's at the start or a duplicate 24726 return acc.length > 0 && !isSeparator$2(acc[acc.length - 1]) ? acc.concat([separator$2]) : acc; 24727 } 24728 else if (has$2(menuItems, item.toLowerCase())) { 24729 return acc.concat([menuItems[item.toLowerCase()]]); 24730 } 24731 else { 24732 // TODO: Add back after TINY-3232 is implemented 24733 // console.error('No representation for menuItem: ' + item); 24734 return acc; 24735 } 24736 } 24737 else { 24738 return acc.concat([item]); 24739 } 24740 }, []); 24741 // Remove any trailing separators 24742 if (realItems.length > 0 && isSeparator$2(realItems[realItems.length - 1])) { 24743 realItems.pop(); 24744 } 24745 return realItems; 24746 }; 24747 const getFromExpandingItem = (item, menuItems) => { 24748 const submenuItems = item.getSubmenuItems(); 24749 const rest = expand(submenuItems, menuItems); 24750 const newMenus = deepMerge(rest.menus, { [item.value]: rest.items }); 24751 const newExpansions = deepMerge(rest.expansions, { [item.value]: item.value }); 24752 return { 24753 item, 24754 menus: newMenus, 24755 expansions: newExpansions 24756 }; 24757 }; 24758 const generateValueIfRequired = (item) => { 24759 // Use the value already in item if it has one. 24760 const itemValue = get$h(item, 'value').getOrThunk(() => generate$6('generated-menu-item')); 24761 return deepMerge({ value: itemValue }, item); 24762 }; 24763 // Takes items, and consolidates them into its return value 24764 const expand = (items, menuItems) => { 24765 // Fistly, we do all substitution using the registry for any items referenced by their 24766 // string key. 24767 const realItems = unwrapReferences(isString(items) ? items.split(' ') : items, menuItems); 24768 // Now that we have complete bridge Item specs for all items, we need to collect the 24769 // submenus, items in the primary menu, and triggering menu items all into one 24770 // giant object to from the building blocks on our TieredData 24771 return foldr(realItems, (acc, item) => { 24772 if (isExpandingMenuItem(item)) { 24773 // We generate a random value for item, but only if there isn't an existing value 24774 const itemWithValue = generateValueIfRequired(item);
vendor: 4,234 bytes, lines 24775-24852
24775 // The newData isn't quite in the format you might expect. The list of items 24776 // for an item with nested items is just the single parent item. All of the nested 24777 // items becomes part of '.menus'. Finally, the expansions is just a map from 24778 // the triggering item to the first submenu. Incidentally, they are given the same 24779 // value (triggering item and submenu), for convenience. 24780 const newData = getFromExpandingItem(itemWithValue, menuItems); 24781 return { 24782 // Combine all of our current submenus and items with the new submenus created by 24783 // this item with nested subitems 24784 menus: deepMerge(acc.menus, newData.menus), 24785 // Add our parent item into the list of items in the *current menu*. 24786 items: [newData.item, ...acc.items], 24787 // Merge together our "this item opens this submenu" objects 24788 expansions: deepMerge(acc.expansions, newData.expansions) 24789 }; 24790 } 24791 else { 24792 // If we aren't creating any submenus, then all we need to do is add this item 24793 // to the list of items in the current menu. So this is the same as an expanding 24794 // menu item, except it doesn't add to `menus` or `expansions`. 24795 return { 24796 ...acc, 24797 items: [item, ...acc.items] 24798 }; 24799 } 24800 }, { 24801 menus: {}, 24802 expansions: {}, 24803 items: [] 24804 }); 24805 }; 24806 24807 const getSearchModeForField = (settings) => { 24808 return settings.search.fold(() => ({ searchMode: 'no-search' }), (searchSettings) => ({ 24809 searchMode: 'search-with-field', 24810 placeholder: searchSettings.placeholder 24811 })); 24812 }; 24813 const getSearchModeForResults = (settings) => { 24814 return settings.search.fold(() => ({ searchMode: 'no-search' }), (_) => ({ searchMode: 'search-with-results' })); 24815 }; 24816 const build = (items, itemResponse, backstage, settings) => { 24817 const primary = generate$6('primary-menu'); 24818 // The expand process identifies all the items, submenus, and triggering items 24819 // defined by the list of items. It substitutes the strings using the values registered 24820 // in the menuItem registry where necessary. It is the building blocks of TieredData, 24821 // but everything is still just in the bridge item format ... nothing has been turned 24822 // into AlloySpecs. 24823 const data = expand(items, backstage.shared.providers.menuItems()); 24824 if (data.items.length === 0) { 24825 return Optional.none(); 24826 } 24827 // Only the main menu has a searchable widget (if it is enabled) 24828 const mainMenuSearchMode = getSearchModeForField(settings); 24829 const mainMenu = createPartialMenu(primary, data.items, itemResponse, backstage, settings.isHorizontalMenu, mainMenuSearchMode); 24830 // The submenus do not have the search field, but will have search results for 24831 // connecting to the search field via aria-controls 24832 const submenuSearchMode = getSearchModeForResults(settings); 24833 const submenus = map$1(data.menus, (menuItems, menuName) => createPartialMenu(menuName, menuItems, itemResponse, backstage, 24834 // Currently, submenus cannot be horizontal menus (so always false) 24835 false, submenuSearchMode)); 24836 const menus = deepMerge(submenus, wrap(primary, mainMenu)); 24837 return Optional.from(tieredMenu.tieredData(primary, menus, data.expansions)); 24838 }; 24839 24840 const isSingleListItem = (item) => !has$2(item, 'items'); 24841 const dataAttribute = 'data-value'; 24842 const fetchItems = (dropdownComp, name, items, selectedValue, hasNestedItems) => map$2(items, (item) => { 24843 if (!isSingleListItem(item)) { 24844 return { 24845 type: 'nestedmenuitem', 24846 text: item.text, 24847 getSubmenuItems: () => fetchItems(dropdownComp, name, item.items, selectedValue, hasNestedItems) 24848 }; 24849 } 24850 else { 24851 return { 24852 type: 'togglemenuitem',
24853 ...(hasNestedItems ? {} : { role: 'option' }), 24854 text: item.text, 24855 value: item.value, 24856 active: item.value === selectedValue, 24857 onAction: () => { 24858 Representing.setValue(dropdownComp, item.value); 24859 emitWith(dropdownComp, formChangeEvent, { name }); 24860 Focusing.focus(dropdownComp); 24861 } 24862 }; 24863 } 24864 }); 24865 const findItemByValue = (items, value) => findMap(items, (item) => { 24866 if (!isSingleListItem(item)) { 24867 return findItemByValue(item.items, value); 24868 } 24869 else { 24870 return someIf(item.value === value, item); 24871 } 24872 }); 24873 const renderListBox = (spec, backstage, initialData) => { 24874 const hasNestedItems = exists(spec.items, (item) => !isSingleListItem(item)); 24875 const providersBackstage = backstage.shared.providers; 24876 const initialItem = initialData 24877 .bind((value) => findItemByValue(spec.items, value)) 24878 .orThunk(() => head(spec.items).filter(isSingleListItem)); 24879 const pLabel = spec.label.map((label) => renderLabel$3(label, providersBackstage)); 24880 const pField = FormField.parts.field({ 24881 dom: {}, 24882 factory: { 24883 sketch: (sketchSpec) => renderCommonDropdown({ 24884 context: spec.context, 24885 uid: sketchSpec.uid, 24886 text: initialItem.map((item) => item.text), 24887 icon: Optional.none(), 24888 tooltip: Optional.none(), 24889 role: someIf(!hasNestedItems, 'combobox'), 24890 ...(hasNestedItems ? {} : { listRole: 'listbox' }), 24891 ariaLabel: spec.label, 24892 fetch: (comp, callback) => { 24893 const items = fetchItems(comp, spec.name, spec.items, Representing.getValue(comp), hasNestedItems); 24894 callback(build(items, ItemResponse$1.CLOSE_ON_EXECUTE, backstage, { 24895 isHorizontalMenu: false, 24896 search: Optional.none() 24897 })); 24898 }, 24899 onSetup: constant$1(noop), 24900 getApi: constant$1({}), 24901 columns: 1, 24902 presets: 'normal', 24903 classes: [], 24904 dropdownBehaviours: [ 24905 Tabstopping.config({}), 24906 withComp(initialItem.map((item) => item.value), (comp) => get$g(comp.element, dataAttribute), (comp, data) => { 24907 // We only want to update the saved value if the value set is a valid property 24908 findItemByValue(spec.items, data) 24909 .each((item) => { 24910 set$9(comp.element, dataAttribute, item.value); 24911 emitWith(comp, updateMenuText, { text: item.text }); 24912 }); 24913 }) 24914 ] 24915 }, 'tox-listbox', backstage.shared) 24916 } 24917 }); 24918 const listBoxWrap = { 24919 dom: { 24920 tag: 'div', 24921 classes: ['tox-listboxfield'] 24922 }, 24923 components: [pField] 24924 }; 24925 return FormField.sketch({ 24926 dom: { 24927 tag: 'div', 24928 classes: ['tox-form__group'] 24929 }, 24930 components: flatten([pLabel.toArray(), [listBoxWrap]]), 24931 fieldBehaviours: derive$1([ 24932 Disabling.config({ 24933 disabled: () => !spec.enabled || providersBackstage.checkUiComponentContext(spec.context).shouldDisable, 24934 onDisabled: (comp) => { 24935 FormField.getField(comp).each(Disabling.disable); 24936 }, 24937 onEnabled: (comp) => { 24938 FormField.getField(comp).each(Disabling.enable); 24939 } 24940 }) 24941 ]) 24942 }); 24943 }; 24944 24945 const renderPanel = (spec, backstage) => ({ 24946 dom: { 24947 tag: 'div', 24948 classes: spec.classes 24949 }, 24950 // All of the items passed through the form need to be put through the interpreter 24951 // with their form part preserved. 24952 components: map$2(spec.items, backstage.shared.interpreter) 24953 }); 24954 24955 const renderSelectBox = (spec, providersBackstage, initialData) => { 24956 const translatedOptions = map$2(spec.items, (item) => ({ 24957 text: providersBackstage.translate(item.text), 24958 value: item.value 24959 })); 24960 // DUPE with TextField. 24961 const pLabel = spec.label.map((label) => renderLabel$3(label, providersBackstage)); 24962 const pField = FormField.parts.field({ 24963 // TODO: Alloy should not allow dom changing of an HTML select! 24964 dom: {}, 24965 ...initialData.map((data) => ({ data })).getOr({}), 24966 selectAttributes: { 24967 size: spec.size 24968 }, 24969 options: translatedOptions, 24970 factory: HtmlSelect, 24971 selectBehaviours: derive$1([ 24972 Disabling.config({ 24973 disabled: () => !spec.enabled || providersBackstage.checkUiComponentContext(spec.c
24973ontext).shouldDisable 24974 }), 24975 Tabstopping.config({}), 24976 config('selectbox-change', [ 24977 run$1(change(), (component, _) => { 24978 emitWith(component, formChangeEvent, { name: spec.name }); 24979 }) 24980 ]) 24981 ]) 24982 }); 24983 const chevron = spec.size > 1 ? Optional.none() : 24984 Optional.some(render$4('chevron-down', { tag: 'div', classes: ['tox-selectfield__icon-js'] }, providersBackstage.icons)); 24985 const selectWrap = { 24986 dom: { 24987 tag: 'div', 24988 classes: ['tox-selectfield'] 24989 }, 24990 components: flatten([[pField], chevron.toArray()]) 24991 }; 24992 return FormField.sketch({ 24993 dom: { 24994 tag: 'div', 24995 classes: ['tox-form__group'] 24996 }, 24997 components: flatten([pLabel.toArray(), [selectWrap]]), 24998 fieldBehaviours: derive$1([ 24999 Disabling.config({ 25000 disabled: () => !spec.enabled || providersBackstage.checkUiComponentContext(spec.context).shouldDisable, 25001 onDisabled: (comp) => { 25002 FormField.getField(comp).each(Disabling.disable); 25003 }, 25004 onEnabled: (comp) => { 25005 FormField.getField(comp).each(Disabling.enable); 25006 } 25007 }), 25008 toggleOnReceive(() => providersBackstage.checkUiComponentContext(spec.context)) 25009 ]) 25010 }); 25011 }; 25012 25013 const formatSize = (size) => { 25014 const unitDec = { 25015 '': 0, 25016 'px': 0, 25017 'pt': 1, 25018 'mm': 1, 25019 'pc': 2, 25020 'ex': 2, 25021 'em': 2, 25022 'ch': 2, 25023 'rem': 2, 25024 'cm': 3, 25025 'in': 4, 25026 '%': 4 25027 }; 25028 const maxDecimal = (unit) => unit in unitDec ? unitDec[unit] : 1; 25029 let numText = size.value.toFixed(maxDecimal(size.unit)); 25030 if (numText.indexOf('.') !== -1) { 25031 numText = numText.replace(/\.?0*$/, ''); 25032 } 25033 return numText + size.unit; 25034 }; 25035 const parseSize = (sizeText) => { 25036 const numPattern = /^\s*(\d+(?:\.\d+)?)\s*(|cm|mm|in|px|pt|pc|em|ex|ch|rem|vw|vh|vmin|vmax|%)\s*$/; 25037 const match = numPattern.exec(sizeText); 25038 if (match !== null) { 25039 const value = parseFloat(match[1]); 25040 const unit = match[2]; 25041 return Result.value({ value, unit }); 25042 } 25043 else { 25044 return Result.error(sizeText); 25045 } 25046 }; 25047 const convertUnit = (size, unit) => { 25048 const inInch = { 25049 '': 96, 25050 'px': 96, 25051 'pt': 72, 25052 'cm': 2.54, 25053 'pc': 12, 25054 'mm': 25.4, 25055 'in': 1 25056 }; 25057 const supported = (u) => has$2(inInch, u); 25058 if (size.unit === unit) { 25059 return Optional.some(size.value); 25060 } 25061 else if (supported(size.unit) && supported(unit)) { 25062 if (inInch[size.unit] === inInch[unit]) { 25063 return Optional.some(size.value); 25064 } 25065 else { 25066 return Optional.some(size.value / inInch[size.unit] * inInch[unit]); 25067 } 25068 } 25069 else { 25070 return Optional.none(); 25071 } 25072 }; 25073 const noSizeConversion = (_input) => Optional.none(); 25074 const ratioSizeConversion = (scale, unit) => (size) => convertUnit(size, unit).map((value) => ({ value: value * scale, unit })); 25075 const makeRatioConverter = (currentFieldText, otherFieldText) => { 25076 const cValue = parseSize(currentFieldText).toOptional(); 25077 const oValue = parseSize(otherFieldText).toOptional(); 25078 return lift2(cValue, oValue, (cSize, oSize) => convertUnit(cSize, oSize.unit).map((val) => oSize.value / val).map((r) => ratioSizeConversion(r, oSize.unit)).getOr(noSizeConversion)).getOr(noSizeConversion); 25079 }; 25080 25081 const renderSizeInput = (spec, providersBackstage) => { 25082 let converter = noSizeConversion; 25083 const ratioEvent = generate$6('ratio-event'); 25084 const makeIcon = (iconName) => render$4(iconName, { tag: 'span', classes: ['tox-icon', 'tox-lock-icon__' + iconName] }, providersBackstage.icons); 25085 const disabled = () => !spec.enabled || providersBackstage.checkUiComponentContext(spec.c
25085ontext).shouldDisable; 25086 const toggleOnReceive$1 = toggleOnReceive(() => providersBackstage.checkUiComponentContext(spec.context)); 25087 const label = spec.label.getOr('Constrain proportions'); 25088 const translatedLabel = providersBackstage.translate(label); 25089 const pLock = FormCoupledInputs.parts.lock({ 25090 dom: { 25091 tag: 'button', 25092 classes: ['tox-lock', 'tox-button', 'tox-button--naked', 'tox-button--icon'], 25093 attributes: { 25094 'aria-label': translatedLabel, 25095 'data-mce-name': label 25096 } 25097 }, 25098 components: [ 25099 makeIcon('lock'), 25100 makeIcon('unlock') 25101 ], 25102 buttonBehaviours: derive$1([ 25103 Disabling.config({ disabled }), 25104 toggleOnReceive$1, 25105 Tabstopping.config({}), 25106 Tooltipping.config(providersBackstage.tooltips.getConfig({ 25107 tooltipText: translatedLabel 25108 })) 25109 ]) 25110 }); 25111 const formGroup = (components) => ({ 25112 dom: { 25113 tag: 'div', 25114 classes: ['tox-form__group'] 25115 }, 25116 components 25117 }); 25118 const getFieldPart = (isField1) => FormField.parts.field({ 25119 factory: Input, 25120 inputClasses: ['tox-textfield'], 25121 inputBehaviours: derive$1([ 25122 Disabling.config({ disabled }), 25123 toggleOnReceive$1, 25124 Tabstopping.config({}), 25125 config('size-input-events', [ 25126 run$1(focusin(), (component, _simulatedEvent) => { 25127 emitWith(component, ratioEvent, { isField1 }); 25128 }), 25129 run$1(change(), (component, _simulatedEvent) => { 25130 emitWith(component, formChangeEvent, { name: spec.name }); 25131 }) 25132 ]) 25133 ]), 25134 selectOnFocus: false 25135 }); 25136 const getLabel = (label) => ({ 25137 dom: { 25138 tag: 'label', 25139 classes: ['tox-label'] 25140 }, 25141 components: [ 25142 text$2(providersBackstage.translate(label)) 25143 ] 25144 }); 25145 const widthField = FormCoupledInputs.parts.field1(formGroup([FormField.parts.label(getLabel('Width')), getFieldPart(true)])); 25146 const heightField = FormCoupledInputs.parts.field2(formGroup([FormField.parts.label(getLabel('Height')), getFieldPart(false)])); 25147 return FormCoupledInputs.sketch({ 25148 dom: { 25149 tag: 'div', 25150 classes: ['tox-form__group'] 25151 }, 25152 components: [ 25153 { 25154 dom: { 25155 tag: 'div', 25156 classes: ['tox-form__controls-h-stack'] 25157 }, 25158 components: [ 25159 // NOTE: Form coupled inputs to the FormField.sketch themselves. 25160 widthField, 25161 heightField, 25162 formGroup([ 25163 getLabel(nbsp), 25164 pLock 25165 ]) 25166 ] 25167 } 25168 ], 25169 field1Name: 'width', 25170 field2Name: 'height', 25171 locked: true, 25172 markers: { 25173 lockClass: 'tox-locked' 25174 }, 25175 onLockedChange: (current, other, _lock) => { 25176 parseSize(Representing.getValue(current)).each((size) => { 25177 converter(size).each((newSize) => { 25178 Representing.setValue(other, formatSize(newSize)); 25179 }); 25180 }); 25181 }, 25182 coupledFieldBehaviours: derive$1([ 25183 Disabling.config({ 25184 disabled, 25185 onDisabled: (comp) => { 25186 FormCoupledInputs.getField1(comp).bind(FormField.getField).each(Disabling.disable); 25187 FormCoupledInputs.getField2(comp).bind(FormField.getField).each(Disabling.disable); 25188 FormCoupledInputs.getLock(comp).each(Disabling.disable); 25189 }, 25190 onEnabled: (comp) => { 25191 FormCoupledInputs.getField1(comp).bind(FormField.getField).each(Disabling.enable); 25192 FormCoupledInputs.getField2(comp).bind(FormField.getField).each(Disabling.enable); 25193 FormCoupledInputs.getLock(comp).each(Disabling.enable); 25194 } 25195 }), 25196 toggleOnReceive(() => providersBackstage.checkUiComponentContext('mode:design')), 25197 config('size-input-events2', [ 25198 run$1(ratioEvent, (component, simulatedEvent) => { 25199 const isField1 = simulatedEvent.event.is
vendor: 4,139 bytes, lines 25199-25316
25199Field1; 25200 const optCurrent = isField1 ? FormCoupledInputs.getField1(component) : FormCoupledInputs.getField2(component); 25201 const optOther = isField1 ? FormCoupledInputs.getField2(component) : FormCoupledInputs.getField1(component); 25202 const value1 = optCurrent.map(Representing.getValue).getOr(''); 25203 const value2 = optOther.map(Representing.getValue).getOr(''); 25204 converter = makeRatioConverter(value1, value2); 25205 }) 25206 ]) 25207 ]) 25208 }); 25209 }; 25210 25211 const renderSlider = (spec, providerBackstage, initialData) => { 25212 const labelPart = Slider.parts.label({ 25213 dom: { 25214 tag: 'label', 25215 classes: ['tox-label'] 25216 }, 25217 components: [ 25218 text$2(providerBackstage.translate(spec.label)) 25219 ] 25220 }); 25221 const spectrum = Slider.parts.spectrum({ 25222 dom: { 25223 tag: 'div', 25224 classes: ['tox-slider__rail'], 25225 attributes: { 25226 role: 'presentation' 25227 } 25228 } 25229 }); 25230 const thumb = Slider.parts.thumb({ 25231 dom: { 25232 tag: 'div', 25233 classes: ['tox-slider__handle'], 25234 attributes: { 25235 role: 'presentation' 25236 } 25237 } 25238 }); 25239 return Slider.sketch({ 25240 dom: { 25241 tag: 'div', 25242 classes: ['tox-slider'], 25243 attributes: { 25244 role: 'presentation' 25245 } 25246 }, 25247 model: { 25248 mode: 'x', 25249 minX: spec.min, 25250 maxX: spec.max, 25251 getInitialValue: constant$1(initialData.getOrThunk(() => (Math.abs(spec.max) - Math.abs(spec.min)) / 2)) 25252 }, 25253 components: [ 25254 labelPart, 25255 spectrum, 25256 thumb 25257 ], 25258 sliderBehaviours: derive$1([ 25259 ComposingConfigs.self(), 25260 Focusing.config({}) 25261 ]), 25262 onChoose: (component, thumb, value) => { 25263 emitWith(component, formChangeEvent, { name: spec.name, value }); 25264 }, 25265 onChange: (component, thumb, value) => { 25266 emitWith(component, formChangeEvent, { name: spec.name, value }); 25267 }, 25268 }); 25269 }; 25270 25271 const renderTable = (spec, providersBackstage) => { 25272 const renderTh = (text) => ({ 25273 dom: { 25274 tag: 'th', 25275 innerHtml: providersBackstage.translate(text) 25276 } 25277 }); 25278 const renderHeader = (header) => ({ 25279 dom: { 25280 tag: 'thead' 25281 }, 25282 components: [ 25283 { 25284 dom: { 25285 tag: 'tr' 25286 }, 25287 components: map$2(header, renderTh) 25288 } 25289 ] 25290 }); 25291 const renderTd = (text) => ({ dom: { tag: 'td', innerHtml: providersBackstage.translate(text) } }); 25292 const renderTr = (row) => ({ dom: { tag: 'tr' }, components: map$2(row, renderTd) }); 25293 const renderRows = (rows) => ({ dom: { tag: 'tbody' }, components: map$2(rows, renderTr) }); 25294 return { 25295 dom: { 25296 tag: 'table', 25297 classes: ['tox-dialog__table'] 25298 }, 25299 components: [ 25300 renderHeader(spec.header), 25301 renderRows(spec.cells) 25302 ], 25303 behaviours: derive$1([ 25304 Tabstopping.config({}), 25305 Focusing.config({}) 25306 ]) 25307 }; 25308 }; 25309 25310 const renderTextField = (spec, providersBackstage) => { 25311 const pLabel = spec.label.map((label) => renderLabel$3(label, providersBackstage)); 25312 const baseInputBehaviours = [ 25313 Disabling.config({ 25314 disabled: () => spec.disabled || providersBackstage.checkUiComponentContext(spec.context).shouldDisable 25315 }), 25316 toggleOnReceive(() =>
25316 providersBackstage.checkUiComponentContext(spec.context)), 25317 Keying.config({ 25318 mode: 'execution', 25319 useEnter: spec.multiline !== true, 25320 useControlEnter: spec.multiline === true, 25321 execute: (comp) => { 25322 emit(comp, formSubmitEvent); 25323 return Optional.some(true); 25324 } 25325 }), 25326 config('textfield-change', [ 25327 run$1(input(), (component, _) => { 25328 emitWith(component, formChangeEvent, { name: spec.name }); 25329 }), 25330 run$1(postPaste(), (component, _) => { 25331 emitWith(component, formChangeEvent, { name: spec.name }); 25332 }) 25333 ]), 25334 Tabstopping.config({}) 25335 ]; 25336 const validatingBehaviours = spec.validation.map((vl) => Invalidating.config({ 25337 getRoot: (input) => { 25338 return parentElement(input.element); 25339 }, 25340 invalidClass: 'tox-invalid', 25341 validator: { 25342 validate: (input) => { 25343 const v = Representing.getValue(input); 25344 const result = vl.validator(v); 25345 return Future.pure(result === true ? Result.value(v) : Result.error(result)); 25346 }, 25347 validateOnLoad: vl.validateOnLoad 25348 } 25349 })).toArray(); 25350 const placeholder = spec.placeholder.fold(constant$1({}), (p) => ({ placeholder: providersBackstage.translate(p) })); 25351 const inputMode = spec.inputMode.fold(constant$1({}), (mode) => ({ inputmode: mode })); 25352 const spellcheck = spec.spellcheck.fold(constant$1({}), (spellchecker) => ({ spellcheck: spellchecker })); 25353 const inputAttributes = { 25354 ...spellcheck, 25355 ...placeholder, 25356 ...inputMode, 25357 'data-mce-name': spec.name 25358 }; 25359 const pField = FormField.parts.field({ 25360 tag: spec.multiline === true ? 'textarea' : 'input', 25361 ...spec.data.map((data) => ({ data })).getOr({}), 25362 inputAttributes, 25363 inputClasses: [spec.classname], 25364 inputBehaviours: derive$1(flatten([ 25365 baseInputBehaviours, 25366 validatingBehaviours 25367 ])), 25368 selectOnFocus: false, 25369 factory: Input 25370 }); 25371 // TINY-9331: This wrapper is needed to avoid border-radius rendering issues when the textarea has a scrollbar 25372 const pTextField = spec.multiline ? { 25373 dom: { 25374 tag: 'div', 25375 classes: ['tox-textarea-wrap'] 25376 }, 25377 components: [pField] 25378 } : pField; 25379 const extraClasses = spec.flex ? ['tox-form__group--stretched'] : []; 25380 const extraClasses2 = extraClasses.concat(spec.maximized ? ['tox-form-group--maximize'] : []); 25381 const extraBehaviours = [ 25382 Disabling.config({ 25383 disabled: () => spec.disabled || providersBackstage.checkUiComponentContext(spec.context).shouldDisable, 25384 onDisabled: (comp) => { 25385 FormField.getField(comp).each(Disabling.disable); 25386 }, 25387 onEnabled: (comp) => { 25388 FormField.getField(comp).each(Disabling.enable); 25389 } 25390 }), 25391 toggleOnReceive(() => providersBackstage.checkUiComponentContext(spec.context)), 25392 ]; 25393 return renderFormFieldWith(pLabel, pTextField, extraClasses2, extraBehaviours); 25394 }; 25395 const renderInput = (spec, providersBackstage, initialData) => renderTextField({ 25396 name: spec.name, 25397 multiline: false, 25398 label: spec.label, 25399 inputMode: spec.inputMode, 25400 placeholder: spec.placeholder, 25401 flex: false, 25402 disabled: !spec.enabled, 25403 classname: 'tox-textfield', 25404 validation: Optional.none(), 25405 maximized: spec.maximized, 25406 data: initialData, 25407 context: spec.context, 25408 spellcheck: Optional.none(), 25409 }, providersBackstage); 25410 const renderTextarea = (spec, providersBackstage, initialData) => renderTextField({ 25411 name: spec.name, 25412 multiline: true, 25413 label: spec.label, 25414 inputMode: Optional.none(), // type attribute is not val
25414id for textareas 25415 placeholder: spec.placeholder, 25416 flex: true, 25417 disabled: !spec.enabled, 25418 classname: 'tox-textarea', 25419 validation: Optional.none(), 25420 maximized: spec.maximized, 25421 data: initialData, 25422 context: spec.context, 25423 spellcheck: spec.spellcheck, 25424 }, providersBackstage); 25425 25426 const getMenuButtonApi = (component) => ({ 25427 isEnabled: () => !Disabling.isDisabled(component), 25428 setEnabled: (state) => Disabling.set(component, !state), 25429 setActive: (state) => { 25430 // Note: We can't use the toggling behaviour here, as the dropdown for the menu also relies on it. 25431 // As such, we'll need to do this manually 25432 const elm = component.element; 25433 if (state) { 25434 add$2(elm, "tox-tbtn--enabled" /* ToolbarButtonClasses.Ticked */); 25435 set$9(elm, 'aria-pressed', true); 25436 } 25437 else { 25438 remove$3(elm, "tox-tbtn--enabled" /* ToolbarButtonClasses.Ticked */); 25439 remove$8(elm, 'aria-pressed'); 25440 } 25441 }, 25442 isActive: () => has(component.element, "tox-tbtn--enabled" /* ToolbarButtonClasses.Ticked */), 25443 setTooltip: (tooltip) => { 25444 emitWith(component, updateTooltiptext, { 25445 text: tooltip 25446 }); 25447 }, 25448 setText: (text) => { 25449 emitWith(component, updateMenuText, { 25450 text 25451 }); 25452 }, 25453 setIcon: (icon) => emitWith(component, updateMenuIcon, { 25454 icon 25455 }) 25456 }); 25457 const renderMenuButton = (spec, prefix, backstage, role, tabstopping = true, btnName) => { 25458 const classes = spec.buttonType === 'bordered' ? ['bordered'] : []; 25459 return renderCommonDropdown({ 25460 text: spec.text, 25461 icon: spec.icon, 25462 tooltip: spec.tooltip, 25463 ariaLabel: spec.tooltip, 25464 searchable: spec.search.isSome(), 25465 // https://www.w3.org/TR/wai-aria-practices/examples/menubar/menubar-2/menubar-2.html 25466 role, 25467 fetch: (dropdownComp, callback) => { 25468 const fetchContext = { 25469 pattern: spec.search.isSome() ? getSearchPattern(dropdownComp) : '' 25470 }; 25471 spec.fetch((items) => { 25472 callback(build(items, ItemResponse$1.CLOSE_ON_EXECUTE, backstage, { 25473 isHorizontalMenu: false, 25474 // MenuButtons are the only dropdowns that support searchable (2022-08-16) 25475 search: spec.search 25476 })); 25477 }, fetchContext, getMenuButtonApi(dropdownComp)); 25478 }, 25479 onSetup: spec.onSetup, 25480 getApi: (comp) => getMenuButtonApi(comp), 25481 columns: 1, 25482 presets: 'normal', 25483 classes, 25484 dropdownBehaviours: [ 25485 ...(tabstopping ? [Tabstopping.config({})] : []), 25486 ], 25487 context: spec.context 25488 }, prefix, backstage.shared, btnName); 25489 }; 25490 const getFetch = (items, getButton, backstage) => { 25491 const getMenuItemAction = (item) => (api) => { 25492 // Update the menu item state 25493 const newValue = !api.isActive(); 25494 api.setActive(newValue); 25495 item.storage.set(newValue); 25496 // Fire the form action event 25497 backstage.shared.getSink().each((sink) => { 25498 getButton().getOpt(sink).each((orig) => { 25499 focus$4(orig.element); 25500 emitWith(orig, formActionEvent, { 25501 name: item.name, 25502 value: item.storage.get() 25503 }); 25504 }); 25505 }); 25506 }; 25507 const getMenuItemSetup = (item) => (api) => { 25508 api.setActive(item.storage.get()); 25509 }; 25510 return (success) => { 25511 success(map$2(items, (item) => { 25512 const text = item.text.fold(() => ({}), (text) => ({ 25513 text 25514 })); 25515 return { 25516 type: item.type, 25517 active: false, 25518 ...text, 25519 context: item.context, 25520 onAction: getMenuItemAction(item), 25521 onSetup: getMenuItemSetup(item) 25522 }; 25523 })); 25524 }; 25525 }; 25526 25527 const renderLabel = (text) => ({ 25528 dom: { 25529 tag: 'span', 25530 classes: ['tox-tree__label'], 25531 attributes: { 25532 'aria-label': text, 25533 } 25534 }, 25535 components: [ 25536 text$2(text) 25537 ], 25538 }); 25539 const renderCustomStateIcon = (container, components, backstage) => { 25540 container.customStateIcon.each((icon) => components.push(renderIcon(icon, backstage.shared.providers.icons, container.customStateIconTooltip.fold(() => [], (tooltip) => [ 25541 Tooltipping.config(backstage.shared.providers.tooltips.getConfig({ 25542 tooltipText: tooltip 25543 })) 25544 ]), ['tox-icon-custom-state']))); 25545 }; 25546 const leafLabelEventsId = generate$6('leaf-label-event-id'); 25547 const renderLeafLabel = ({ leaf, onLeafAction, visible, treeId, selectedId, backstage }) => {
25548 const internalMenuButton = leaf.menu.map((btn) => renderMenuButton(btn, 'tox-mbtn', backstage, Optional.none(), visible)); 25549 const components = [renderLabel(leaf.title)]; 25550 renderCustomStateIcon(leaf, components, backstage); 25551 internalMenuButton.each((btn) => components.push(btn)); 25552 return Button.sketch({ 25553 dom: { 25554 tag: 'div', 25555 classes: ['tox-tree--leaf__label', 'tox-trbtn'] 25556 .concat(visible ? ['tox-tree--leaf__label--visible'] : []), 25557 }, 25558 components, 25559 role: 'treeitem', 25560 action: (button) => { 25561 onLeafAction(leaf.id); 25562 button.getSystem().broadcastOn([`update-active-item-${treeId}`], { 25563 value: leaf.id 25564 }); 25565 }, 25566 eventOrder: { 25567 [keydown()]: [ 25568 leafLabelEventsId, 25569 'keying', 25570 ] 25571 }, 25572 buttonBehaviours: derive$1([ 25573 ...(visible ? [Tabstopping.config({})] : []), 25574 Toggling.config({ 25575 toggleClass: 'tox-trbtn--enabled', 25576 toggleOnExecute: false, 25577 aria: { 25578 mode: 'selected' 25579 } 25580 }), 25581 Receiving.config({ 25582 channels: { 25583 [`update-active-item-${treeId}`]: { 25584 onReceive: (comp, message) => { 25585 (message.value === leaf.id ? Toggling.on : Toggling.off)(comp); 25586 } 25587 } 25588 } 25589 }), 25590 config(leafLabelEventsId, [ 25591 runOnAttached((comp, _se) => { 25592 selectedId.each((id) => { 25593 const toggle = id === leaf.id ? Toggling.on : Toggling.off; 25594 toggle(comp); 25595 }); 25596 }), 25597 run$1(keydown(), (comp, se) => { 25598 const isLeftArrowKey = se.event.raw.code === 'ArrowLeft'; 25599 const isRightArrowKey = se.event.raw.code === 'ArrowRight'; 25600 if (isLeftArrowKey) { 25601 ancestor$1(comp.element, '.tox-tree--directory').each((dirElement) => { 25602 comp.getSystem().getByDom(dirElement).each((dirComp) => { 25603 child(dirElement, '.tox-tree--directory__label').each((dirLabelElement) => { 25604 dirComp.getSystem().getByDom(dirLabelElement).each(Focusing.focus); 25605 }); 25606 }); 25607 }); 25608 se.stop(); 25609 } 25610 else if (isRightArrowKey) { 25611 se.stop(); 25612 } 25613 }) 25614 ]) 25615 ]), 25616 }); 25617 }; 25618 const renderIcon = (iconName, iconsProvider, behaviours, extraClasses, extraAttributes) => render$4(iconName, { 25619 tag: 'span', 25620 classes: [ 25621 'tox-tree__icon-wrap', 25622 'tox-icon', 25623 ].concat(extraClasses || []), 25624 behaviours, 25625 attributes: extraAttributes 25626 }, iconsProvider); 25627 const renderIconFromPack = (iconName, iconsProvider) => renderIcon(iconName, iconsProvider, []); 25628 const directoryLabelEventsId = generate$6('directory-label-event-id'); 25629 const renderDirectoryLabel = ({ directory, visible, noChildren, backstage }) => { 25630 const internalMenuButton = directory.menu.map((btn) => renderMenuButton(btn, 'tox-mbtn', backstage, Optional.none())); 25631 const components = [ 25632 { 25633 dom: { 25634 tag: 'div', 25635 classes: ['tox-chevron'], 25636 }, 25637 components: [ 25638 renderIconFromPack('chevron-right', backstage.shared.providers.icons), 25639 ] 25640 }, 25641 renderLabel(directory.title) 25642 ]; 25643 renderCustomStateIcon(directory, components, backstage);
25644 internalMenuButton.each((btn) => { 25645 components.push(btn); 25646 }); 25647 const toggleExpandChildren = (button) => { 25648 ancestor$1(button.element, '.tox-tree--directory').each((directoryEle) => { 25649 button.getSystem().getByDom(directoryEle).each((directoryComp) => { 25650 const willExpand = !Toggling.isOn(directoryComp); 25651 Toggling.toggle(directoryComp); 25652 emitWith(button, 'expand-tree-node', { expanded: willExpand, node: directory.id }); 25653 }); 25654 }); 25655 }; 25656 return Button.sketch({ 25657 dom: { 25658 tag: 'div', 25659 classes: ['tox-tree--directory__label', 'tox-trbtn'].concat(visible ? ['tox-tree--directory__label--visible'] : []), 25660 }, 25661 components, 25662 action: toggleExpandChildren, 25663 eventOrder: { 25664 [keydown()]: [ 25665 directoryLabelEventsId, 25666 'keying', 25667 ] 25668 }, 25669 buttonBehaviours: derive$1([ 25670 ...(visible ? [Tabstopping.config({})] : []), 25671 config(directoryLabelEventsId, [ 25672 run$1(keydown(), (comp, se) => { 25673 const isRightArrowKey = se.event.raw.code === 'ArrowRight'; 25674 const isLeftArrowKey = se.event.raw.code === 'ArrowLeft'; 25675 if (isRightArrowKey && noChildren) { 25676 se.stop(); 25677 } 25678 if (isRightArrowKey || isLeftArrowKey) { 25679 ancestor$1(comp.element, '.tox-tree--directory').each((directoryEle) => { 25680 comp.getSystem().getByDom(directoryEle).each((directoryComp) => { 25681 if (!Toggling.isOn(directoryComp) && isRightArrowKey || Toggling.isOn(directoryComp) && isLeftArrowKey) { 25682 toggleExpandChildren(comp); 25683 se.stop(); 25684 } 25685 else if (isLeftArrowKey && !Toggling.isOn(directoryComp)) { 25686 ancestor$1(directoryComp.element, '.tox-tree--directory').each((parentDirElement) => { 25687 child(parentDirElement, '.tox-tree--directory__label').each((parentDirLabelElement) => { 25688 directoryComp.getSystem().getByDom(parentDirLabelElement).each(Focusing.focus); 25689 }); 25690 }); 25691 se.stop(); 25692 } 25693 }); 25694 }); 25695 } 25696 }) 25697 ]) 25698 ]) 25699 }); 25700 }; 25701 const renderDirectoryChildren = ({ children, onLeafAction, visible, treeId, expandedIds, selectedId, backstage }) => { 25702 return { 25703 dom: { 25704 tag: 'div', 25705 classes: ['tox-tree--directory__children'], 25706 }, 25707 components: children.map((item) => { 25708 return item.type === 'leaf' ? 25709 renderLeafLabel({ leaf: item, selectedId, onLeafAction, visible, treeId, backstage }) : 25710 renderDirectory({ directory: item, expandedIds, selectedId, onLeafAction, labelTabstopping: visible, treeId, backstage }); 25711 }), 25712 behaviours: derive$1([ 25713 Sliding.config({ 25714 dimension: { 25715 property: 'height' 25716 }, 25717 closedClass: 'tox-tree--directory__children--closed', 25718 openClass: 'tox-tree--directory__children--open', 25719 growingClass: 'tox-tree--directory__children--growing', 25720 shrinkingClass: 'tox-tree--directory__children--shrinking', 25721 expanded: visible, 25722 }), 25723 Replacing.config({}) 25724 ]) 25725 }; 25726 }; 25727 const directoryEventsId = generate$6('directory-event-id'); 25728 const renderDirectory = ({ directory, onLeafAction, labelTabstopping, treeId, backstage, expandedIds, selectedId }) => { 25729 const { children } = directory; 25730 const expandedIdsCell = Cell(expandedIds); 25731 const computedChildrenComponents = (visible) => children.map((item) => { 25732 return item.type === 'leaf' ? 25733 renderLeafLabel({ leaf: item, selectedId, onLeafAction, visible, treeId, backstage }) : 25734 renderDirectory({ directory: item, expandedIds: expandedIdsCell.get(), selectedId, onLeafAction, labelTabstopping: visible, treeId, backstage }); 25735 }); 25736 const childrenVisible = expandedIds.includes(directory.id); 25737 return ({ 25738 dom: { 25739 tag: 'div', 25740 classes: ['tox-tree--directory'], 25741 attributes: { 25742 role: 'treeitem' 25743 } 25744 }, 25745 components: [ 25746 renderDirectoryLabel({ directory, visible: labelTabstopping, noChildren: directory.children.length === 0, backstage }), 25747 renderDirectoryChildren({ children, expandedIds, selectedId, onLeafAction, visible: childrenVisible, treeId, backstage }) 25748 ], 25749 behaviours: derive$1([ 25750 config(directoryEventsId, [ 25751 runOnAttached((comp, _se) => { 25752 Toggling.set(comp, childrenVisible); 25753 }), 25754 run$1('expand-tree-node', (_cmp, se) => { 25755 const { expanded, node } = se.event; 25756 expandedIdsCell.set(expanded ? 25757 [...expandedIdsCell.get(), node] : 25758 expandedIdsCell.get().filter((id) => id !== node)); 25759 }), 25760 ]), 25761 Toggling.config({ 25762 ...(directory.children.length > 0 ? { 25763 aria: { 25764 mode: 'expanded', 25765 }, 25766 } : {}), 25767 toggleClass: 'tox-tree--directory--expanded', 25768 onToggled: (comp, childrenVisible) => { 25769 const childrenComp = comp.components()[1]; 25770 const newChildren = computedChildrenComponents(childrenVisible); 25771 if (childrenVisible) { 25772 Sliding.grow(childrenComp); 25773 } 25774 else { 25775 Sliding.shrink(childrenComp); 25776 } 25777 Replacing.set(childrenComp, newChildren); 25778 }, 25779 }), 25780 ]) 25781 }); 25782 }; 25783 const treeEventsId = generate$6('tree-event-id'); 25784 const renderTree = (spec, backstage) => { 25785 const onLeafAction = spec.onLeafAction.getOr(noop); 25786 const onToggleExpand = spec.onToggleExpand.getOr(noop); 25787 const defaultExpandedIds = spec.defaultExpandedIds; 25788 const expandedIds = Cell(defaultExpandedIds); 25789 const selectedIdCell = Cell(spec.defaultSelectedId); 25790 const treeId = generate$6('tree-id'); 25791 const children = (selectedId, expandedIds) => spec.items.map((item) => { 25792 return item.type === 'leaf' ? 25793 renderLeafLabel({ leaf: item, selectedId, onLeafAction, visible: true, treeId, backstage }) : 25794 renderDirectory({ directory: item, selectedId, onLeafAction, expandedIds, labelTabstopping: true, treeId, backstage }); 25795 }); 25796 return { 25797 dom: { 25798 tag: 'div', 25799 classes: ['tox-tree'], 25800 attributes: { 25801 role: 'tree' 25802 } 25803 }, 25804 components: children(selectedIdCell.get(), expandedIds.get()), 25805 behaviours: derive$1([ 25806 Keying.config({ 25807 mode: 'flow', 25808 selector: '.tox-tree--leaf__label--visible, .tox-tree--directory__label--visible', 25809 cycles: false, 25810 }), 25811 config(treeEventsId, [ 25812 run$1('expand-tree-node', (_cmp, se) => { 25813 const { expanded, node } = se.event; 25814 expandedIds.set(expanded ? 25815 [...expandedIds.get(), node] : 25816 expandedIds.get().filter((id) => id !== node)); 25817 onToggleExpand(expandedIds.get(), { expanded, node }); 25818 }) 25819 ]), 25820 Receiving.config({ 25821 channels: { 25822 [`update-active-item-${treeId}`]: { 25823 onReceive: (comp, message) => { 25824 selectedIdCell.set(Optional.some(message.value)); 25825 Replacing.set(comp, children(Optional.some(message.value), expandedIds.get())); 25826 } 25827 } 25828 } 25829 }), 25830 Replacing.config({}) 25831 ]) 25832 }; 25833 }; 25834 25835 const renderCommonSpec = (spec, actionOpt, extraBehaviours = [], dom, components, tooltip, providersBackstage) => { 25836 const action = actionOpt.fold(() => ({}), (action) => ({ 25837 action 25838 })); 25839 const common = { 25840 buttonBehaviours: derive$1([ 25841 DisablingConfigs.item(() => !spec.enabled || providersBackstage.checkUiComponentContext(spec.c
25841ontext).shouldDisable), 25842 toggleOnReceive(() => providersBackstage.checkUiComponentContext(spec.context)), 25843 Tabstopping.config({}), 25844 ...tooltip.map((t) => Tooltipping.config(providersBackstage.tooltips.getConfig({ 25845 tooltipText: providersBackstage.translate(t) 25846 }))).toArray(), 25847 config('button press', [ 25848 preventDefault('click') 25849 ]) 25850 ].concat(extraBehaviours)), 25851 eventOrder: { 25852 click: ['button press', 'alloy.base.behaviour'], 25853 mousedown: ['button press', 'alloy.base.behaviour'] 25854 }, 25855 ...action 25856 }; 25857 const domFinal = deepMerge(common, { dom }); 25858 return deepMerge(domFinal, { components }); 25859 }; 25860 // An IconButton just seems to be a button that *cannot* have text, but 25861 // can have a tooltip. It's only used for the More Drawer button at the moment. 25862 const renderIconButtonSpec = (spec, action, providersBackstage, extraBehaviours = [], btnName) => { 25863 const tooltipAttributes = spec.tooltip.map((tooltip) => ({ 25864 'aria-label': providersBackstage.translate(tooltip), 25865 })).getOr({}); 25866 const dom = { 25867 tag: 'button', 25868 classes: ["tox-tbtn" /* ToolbarButtonClasses.Button */], 25869 attributes: { ...tooltipAttributes, 'data-mce-name': btnName } 25870 }; 25871 const icon = spec.icon.map((iconName) => renderIconFromPack$1(iconName, providersBackstage.icons)); 25872 const components = componentRenderPipeline([ 25873 icon 25874 ]); 25875 return renderCommonSpec(spec, action, extraBehaviours, dom, components, spec.tooltip, providersBackstage); 25876 }; 25877 const calculateClassesFromButtonType = (buttonType) => { 25878 switch (buttonType) { 25879 case 'primary': 25880 return ['tox-button']; 25881 case 'toolbar': 25882 return ['tox-tbtn']; 25883 case 'secondary': 25884 default: 25885 return ['tox-button', 'tox-button--secondary']; 25886 } 25887 }; 25888 // Maybe the list of extraBehaviours is better than doing a Merger.deepMerge that 25889 // we do elsewhere? Not sure. 25890 const renderButtonSpec = (spec, action, providersBackstage, extraBehaviours = [], extraClasses = []) => { 25891 // It's a bit confusing that this is called text. It seems to be a tooltip. Although I can see 25892 // that it's used if there is no icon 25893 const translatedText = providersBackstage.translate(spec.text); 25894 const icon = spec.icon.map((iconName) => renderIconFromPack$1(iconName, providersBackstage.icons)); 25895 const components = [icon.getOrThunk(() => text$2(translatedText))]; 25896 // The old default is based on the now-deprecated 'primary' property. `buttonType` takes precedence now. 25897 const buttonType = spec.buttonType.getOr(!spec.primary && !spec.borderless ? 'secondary' : 'primary'); 25898 const baseClasses = calculateClassesFromButtonType(buttonType); 25899 const classes = [ 25900 ...baseClasses, 25901 ...icon.isSome() ? ['tox-button--icon'] : [], 25902 ...spec.borderless ? ['tox-button--naked'] : [], 25903 ...extraClasses 25904 ]; 25905 const dom = { 25906 tag: 'button', 25907 classes, 25908 attributes: { 25909 'aria-label': translatedText, 25910 'data-mce-name': spec.text 25911 } 25912 }; 25913 // Only provide a tooltip if we are using an icon. This is because above, a button is only an icon 25914 // or text, and not both. 25915 const optTooltip = spec.icon.map(constant$1(translatedText)); 25916 return renderCommonSpec(spec, action, extraBehaviours, dom, components, optTooltip, providersBackstage); 25917 }; 25918 // This actually seems to be a button on the dialog for UrlInput only (browse). Interesting. 25919 const renderButton$1 = (spec, action, providersBackstage, extraBehaviours = [], extraClasses = []) => { 25920 const buttonSpec = renderButtonSpec(spec, Optional.some(action), providersBackstage, extraBehaviours, extraClasse
25920s); 25921 return Button.sketch(buttonSpec); 25922 }; 25923 const getAction = (name, buttonType) => (comp) => { 25924 if (buttonType === 'custom') { 25925 emitWith(comp, formActionEvent, { 25926 name, 25927 value: {} 25928 }); 25929 } 25930 else if (buttonType === 'submit') { 25931 emit(comp, formSubmitEvent); 25932 } 25933 else if (buttonType === 'cancel') { 25934 emit(comp, formCancelEvent); 25935 } 25936 else { 25937 // eslint-disable-next-line no-console 25938 console.error('Unknown button type: ', buttonType); 25939 } 25940 }; 25941 const isMenuFooterButtonSpec = (spec, buttonType) => buttonType === 'menu'; 25942 const isNormalFooterButtonSpec = (spec, buttonType) => buttonType === 'custom' || buttonType === 'cancel' || buttonType === 'submit'; 25943 const isToggleButtonSpec = (spec, buttonType) => buttonType === 'togglebutton'; 25944 const renderToggleButton = (spec, providers, btnName) => { 25945 const optMemIcon = spec.icon 25946 .map((memIcon) => renderReplaceableIconFromPack(memIcon, providers.icons)) 25947 .map(record); 25948 const action = (comp) => { 25949 emitWith(comp, formActionEvent, { 25950 name: spec.name, 25951 value: { 25952 setIcon: (newIcon) => { 25953 optMemIcon.map((memIcon) => memIcon.getOpt(comp).each((displayIcon) => { 25954 Replacing.set(displayIcon, [ 25955 renderReplaceableIconFromPack(newIcon, providers.icons) 25956 ]); 25957 })); 25958 } 25959 } 25960 }); 25961 }; 25962 // The old default is based on the now-deprecated 'primary' property. `buttonType` takes precedence now. 25963 const buttonType = spec.buttonType.getOr(!spec.primary ? 'secondary' : 'primary'); 25964 const buttonSpec = { 25965 ...spec, 25966 name: spec.name ?? '', 25967 primary: buttonType === 'primary', 25968 tooltip: spec.tooltip, 25969 enabled: spec.enabled ?? false, 25970 borderless: false 25971 }; 25972 const tooltipAttributes = buttonSpec.tooltip.or(spec.text).map((tooltip) => ({ 25973 'aria-label': providers.translate(tooltip), 25974 })).getOr({}); 25975 const buttonTypeClasses = calculateClassesFromButtonType(buttonType ?? 'secondary'); 25976 const showIconAndText = spec.icon.isSome() && spec.text.isSome(); 25977 const dom = { 25978 tag: 'button', 25979 classes: [ 25980 ...buttonTypeClasses.concat(spec.icon.isSome() ? ['tox-button--icon'] : []), 25981 ...(spec.active ? ["tox-button--enabled" /* ViewButtonClasses.Ticked */] : []), 25982 ...(showIconAndText ? ['tox-button--icon-and-text'] : []) 25983 ], 25984 attributes: { 25985 ...tooltipAttributes, 25986 ...(isNonNullable(btnName) ? { 'data-mce-name': btnName } : {}) 25987 } 25988 }; 25989 const extraBehaviours = []; 25990 const translatedText = providers.translate(spec.text.getOr('')); 25991 const translatedTextComponed = text$2(translatedText); 25992 const iconComp = componentRenderPipeline([optMemIcon.map((memIcon) => memIcon.asSpec())]); 25993 const components = [ 25994 ...iconComp, 25995 ...(spec.text.isSome() ? [translatedTextComponed] : []) 25996 ]; 25997 const iconButtonSpec = renderCommonSpec(buttonSpec, Optional.some(action), extraBehaviours, dom, components, spec.tooltip, providers); 25998 return Button.sketch(iconButtonSpec); 25999 }; 26000 const renderFooterButton = (spec, buttonType, backstage) => { 26001 if (isMenuFooterButtonSpec(spec, buttonType)) { 26002 const getButton = () => memButton; 26003 const menuButtonSpec = spec; 26004 const fixedSpec = { 26005 ...spec, 26006 buttonType: 'default', 26007 type: 'menubutton', 26008 // Currently, dialog-based menu buttons cannot be searchable. 26009 search: Optional.none(), 26010 onSetup: (api) => { 26011 api.setEnabled(spec.enabled); 26012 return noop; 26013 }, 26014 fetch: getFetch(menuButtonSpec.items, getButton, backstage) 26015 }; 26016 const memButton = record(renderMenuButton(fixedSpec, "tox-tbtn" /* ToolbarButtonClasse
26016s.Button */, backstage, Optional.none(), true, spec.text.or(spec.tooltip).getOrUndefined())); 26017 return memButton.asSpec(); 26018 } 26019 else if (isNormalFooterButtonSpec(spec, buttonType)) { 26020 const action = getAction(spec.name, buttonType); 26021 const buttonSpec = { 26022 ...spec, 26023 context: buttonType === 'cancel' ? 'any' : spec.context, 26024 borderless: false 26025 }; 26026 return renderButton$1(buttonSpec, action, backstage.shared.providers, []); 26027 } 26028 else if (isToggleButtonSpec(spec, buttonType)) { 26029 return renderToggleButton(spec, backstage.shared.providers, spec.text.or(spec.tooltip).getOrUndefined()); 26030 } 26031 else { 26032 // eslint-disable-next-line no-console 26033 console.error('Unknown footer button type: ', buttonType); 26034 throw new Error('Unknown footer button type'); 26035 } 26036 }; 26037 const renderDialogButton = (spec, providersBackstage) => { 26038 const action = getAction(spec.name, 'custom'); 26039 return renderFormField(Optional.none(), FormField.parts.field({ 26040 factory: Button, 26041 ...renderButtonSpec(spec, Optional.some(action), providersBackstage, [ 26042 memory(''), 26043 ComposingConfigs.self() 26044 ]) 26045 })); 26046 }; 26047 26048 const separator$1 = { 26049 type: 'separator' 26050 }; 26051 const toMenuItem = (target) => ({ 26052 type: 'menuitem', 26053 value: target.url, 26054 text: target.title, 26055 meta: { 26056 attach: target.attach 26057 }, 26058 onAction: noop 26059 }); 26060 const staticMenuItem = (title, url) => ({ 26061 type: 'menuitem', 26062 value: url, 26063 text: title, 26064 meta: { 26065 attach: undefined 26066 }, 26067 onAction: noop 26068 }); 26069 const toMenuItems = (targets) => map$2(targets, toMenuItem); 26070 const filterLinkTargets = (type, targets) => filter$2(targets, (target) => target.type === type); 26071 const filteredTargets = (type, targets) => toMenuItems(filterLinkTargets(type, targets)); 26072 const headerTargets = (linkInfo) => filteredTargets('header', linkInfo.targets); 26073 const anchorTargets = (linkInfo) => filteredTargets('anchor', linkInfo.targets); 26074 const anchorTargetTop = (linkInfo) =>
26074 Optional.from(linkInfo.anchorTop).map((url) => staticMenuItem('<top>', url)).toArray(); 26075 const anchorTargetBottom = (linkInfo) => Optional.from(linkInfo.anchorBottom).map((url) => staticMenuItem('<bottom>', url)).toArray(); 26076 const historyTargets = (history) => map$2(history, (url) => staticMenuItem(url, url)); 26077 const joinMenuLists = (items) => { 26078 return foldl(items, (a, b) => { 26079 const bothEmpty = a.length === 0 || b.length === 0; 26080 return bothEmpty ? a.concat(b) : a.concat(separator$1, b); 26081 }, []); 26082 }; 26083 const filterByQuery = (term, menuItems) => { 26084 const lowerCaseTerm = term.toLowerCase(); 26085 return filter$2(menuItems, (item) => { 26086 const text = item.meta !== undefined && item.meta.text !== undefined ? item.meta.text : item.text; 26087 const value = item.value ?? ''; 26088 return contains$1(text.toLowerCase(), lowerCaseTerm) || contains$1(value.toLowerCase(), lowerCaseTerm); 26089 }); 26090 }; 26091 26092 const getItems = (fileType, input, urlBackstage) => { 26093 const urlInputValue = Representing.getValue(input); 26094 const term = urlInputValue?.meta?.text ?? urlInputValue.value; 26095 const info = urlBackstage.getLinkInformation(); 26096 return info.fold(() => [], (linkInfo) => { 26097 const history = filterByQuery(term, historyTargets(urlBackstage.getHistory(fileType))); 26098 return fileType === 'file' ? joinMenuLists([ 26099 history, 26100 filterByQuery(term, headerTargets(linkInfo)), 26101 filterByQuery(term, flatten([ 26102 anchorTargetTop(linkInfo), 26103 anchorTargets(linkInfo), 26104 anchorTargetBottom(linkInfo) 26105 ])) 26106 ]) 26107 : history; 26108 }); 26109 }; 26110 const errorId = generate$6('aria-invalid'); 26111 const renderUrlInput = (spec, backstage, urlBackstage, initialData) => { 26112 const providersBackstage = backstage.shared.providers; 26113 const updateHistory = (component) => { 26114 const urlEntry = Representing.getValue(component); 26115 urlBackstage.addToHistory(urlEntry.value, spec.filetype); 26116 }; 26117 // TODO: Make alloy's typeahead only swallow enter and escape if menu is open 26118 const typeaheadSpec = { 26119 ...initialData.map((initialData) => ({ initialData })).getOr({}), 26120 dismissOnBlur: true, 26121 inputClasses: ['tox-textfield'], 26122 sandboxClasses: ['tox-dialog__popups'], 26123 inputAttributes: { 26124 type: 'url' 26125 }, 26126 minChars: 0, 26127 responseTime: 0, 26128 fetch: (input) => { 26129 const items = getItems(spec.filetype, input, urlBackstage); 26130 const tdata = build(items, ItemResponse$1.BUBBLE_TO_SANDBOX, backstage, { 26131 isHorizontalMenu: false, 26132 search: Optional.none() 26133 }); 26134 return Future.pure(tdata); 26135 }, 26136 getHotspot: (comp) => memUrlBox.getOpt(comp), 26137 onSetValue: (comp, _newValue) => { 26138 if (comp.hasConfigured(Invalidating)) { 26139 Invalidating.run(comp).get(noop); 26140 } 26141 }, 26142 typeaheadBehaviours: derive$1([ 26143 ...urlBackstage.getValidationHandler().map((handler) => Invalidating.config({ 26144 getRoot: (comp) => parentElement(comp.element), 26145 invalidClass: 'tox-control-wrap--status-invalid', 26146 notify: { 26147 onInvalid: (comp, err) => { 26148 memInvalidIcon.getOpt(comp).each((invalidComp) => { 26149 set$9(invalidComp.element, 'title', providersBackstage.translate(err)); 26150 }); 26151 } 26152 }, 26153 validator: { 26154 validate: (input) => { 26155 const urlEntry = Representing.getValue(input); 26156 return FutureResult.nu((completer) => { 26157 handler({ type: spec.filetype, url: urlEntry.value }, (validation) => { 26158 if (validation.status === 'invali
vendor: 4,600 bytes, lines 26158-26260
26158d') { 26159 set$9(input.element, 'aria-errormessage', errorId); 26160 const err = Result.error(validation.message); 26161 completer(err); 26162 } 26163 else { 26164 remove$8(input.element, 'aria-errormessage'); 26165 const val = Result.value(validation.message); 26166 completer(val); 26167 } 26168 }); 26169 }); 26170 }, 26171 validateOnLoad: false 26172 } 26173 })).toArray(), 26174 Disabling.config({ 26175 disabled: () => !spec.enabled || providersBackstage.checkUiComponentContext(spec.context).shouldDisable 26176 }), 26177 Tabstopping.config({}), 26178 config('urlinput-events', 26179 // We want to get fast feedback for the link dialog, but not sure about others 26180 [ 26181 run$1(input(), (comp) => { 26182 const currentValue = get$5(comp.element); 26183 const trimmedValue = currentValue.trim(); 26184 if (trimmedValue !== currentValue) { 26185 set$4(comp.element, trimmedValue); 26186 } 26187 if (spec.filetype === 'file') { 26188 emitWith(comp, formChangeEvent, { name: spec.name }); 26189 } 26190 }), 26191 run$1(change(), (comp) => { 26192 emitWith(comp, formChangeEvent, { name: spec.name }); 26193 updateHistory(comp); 26194 }), 26195 run$1(postPaste(), (comp) => { 26196 emitWith(comp, formChangeEvent, { name: spec.name }); 26197 updateHistory(comp); 26198 }) 26199 ]) 26200 ]), 26201 eventOrder: { 26202 [input()]: ['streaming', 'urlinput-events', 'invalidating'] 26203 }, 26204 model: { 26205 getDisplayText: (itemData) => itemData.value, 26206 selectsOver: false, 26207 populateFromBrowse: false 26208 }, 26209 markers: { 26210 openClass: 'tox-textfield--popup-open' 26211 }, 26212 lazySink: backstage.shared.getSink, 26213 parts: { 26214 menu: part(false, 1, 'normal') 26215 }, 26216 onExecute: (_menu, component, _entry) => { 26217 emitWith(component, formSubmitEvent, {}); 26218 }, 26219 onItemExecute: (typeahead, _sandbox, _item, _value) => { 26220 updateHistory(typeahead); 26221 emitWith(typeahead, formChangeEvent, { name: spec.name }); 26222 } 26223 }; 26224 const pField = FormField.parts.field({ 26225 ...typeaheadSpec, 26226 factory: Typeahead 26227 }); 26228 const pLabel = spec.label.map((label) => renderLabel$3(label, providersBackstage)); 26229 // TODO: Consider a way of merging with Checkbox. 26230 const makeIcon = (name, errId, icon = name, label = name) => render$4(icon, { 26231 tag: 'div', 26232 classes: ['tox-icon', 'tox-control-wrap__status-icon-' + name], 26233 attributes: { 26234 'title': providersBackstage.translate(label), 26235 'aria-live': 'polite', 26236 ...errId.fold(() => ({}), (id) => ({ id })) 26237 } 26238 }, providersBackstage.icons); 26239 const memInvalidIcon = record(makeIcon('invalid', Optional.some(errorId), 'warning')); 26240 const memStatus = record({ 26241 dom: { 26242 tag: 'div', 26243 classes: ['tox-control-wrap__status-icon-wrap'] 26244 }, 26245 components: [ 26246 // Include the 'valid' and 'unknown' icons here only if they are to be displayed 26247 memInvalidIcon.asSpec() 26248 ] 26249 }); 26250 const optUrlPicker = urlBackstage.getUrlPicker(spec.filetype); 26251 const browseUrlEvent = generate$6('browser.url.event'); 26252 const memUrlBox = record({ 26253 dom: { 26254 tag: 'div', 26255 classes: ['tox-control-wrap'] 26256 }, 26257 components: [pField, memStatus.asSpec()], 26258 behaviours: derive$1([ 26259 Disabling.config({ 26260 disabled: () => !spec.enabled || providersBackstage.checkUiComponentContext(spec.c
26260ontext).shouldDisable 26261 }) 26262 ]) 26263 }); 26264 const memUrlPickerButton = record(renderButton$1({ 26265 context: spec.context, 26266 name: spec.name, 26267 icon: Optional.some('browse'), 26268 text: spec.picker_text.or(spec.label).getOr(''), 26269 enabled: spec.enabled, 26270 primary: false, 26271 buttonType: Optional.none(), 26272 borderless: true 26273 }, (component) => emit(component, browseUrlEvent), providersBackstage, [], ['tox-browse-url'])); 26274 const controlHWrapper = () => ({ 26275 dom: { 26276 tag: 'div', 26277 classes: ['tox-form__controls-h-stack'] 26278 }, 26279 components: flatten([ 26280 [memUrlBox.asSpec()], 26281 optUrlPicker.map(() => memUrlPickerButton.asSpec()).toArray() 26282 ]) 26283 }); 26284 const openUrlPicker = (comp) => { 26285 Composing.getCurrent(comp).each((field) => { 26286 const componentData = Representing.getValue(field); 26287 const urlData = { 26288 fieldname: spec.name, 26289 ...componentData 26290 }; 26291 optUrlPicker.each((picker) => { 26292 picker(urlData).get((chosenData) => { 26293 Representing.setValue(field, chosenData); 26294 emitWith(comp, formChangeEvent, { name: spec.name }); 26295 }); 26296 }); 26297 }); 26298 }; 26299 return FormField.sketch({ 26300 dom: renderFormFieldDom(), 26301 components: pLabel.toArray().concat([ 26302 controlHWrapper() 26303 ]), 26304 fieldBehaviours: derive$1([ 26305 Disabling.config({ 26306 disabled: () => !spec.enabled || providersBackstage.checkUiComponentContext(spec.context).shouldDisable, 26307 onDisabled: (comp) => { 26308 FormField.getField(comp).each(Disabling.disable); 26309 memUrlPickerButton.getOpt(comp).each(Disabling.disable); 26310 }, 26311 onEnabled: (comp) => { 26312 FormField.getField(comp).each(Disabling.enable); 26313 memUrlPickerButton.getOpt(comp).each(Disabling.enable); 26314 } 26315 }), 26316 toggleOnReceive(() => providersBackstage.checkUiComponentContext(spec.context)), 26317 config('url-input-events', [ 26318 run$1(browseUrlEvent, openUrlPicker) 26319 ]) 26320 ]) 26321 }); 26322 }; 26323 26324 const renderAlertBanner = (spec, providersBackstage) => { 26325 const icon = get(spec.icon, providersBackstage.icons); 26326 // For using the alert banner inside a dialog 26327 return Container.sketch({ 26328 dom: { 26329 tag: 'div', 26330 attributes: { 26331 role: 'alert' 26332 }, 26333 classes: ['tox-notification', 'tox-notification--in', `tox-notification--${spec.level}`] 26334 }, 26335 components: [ 26336 { 26337 dom: { 26338 tag: 'div', 26339 classes: ['tox-notification__icon'], 26340 innerHtml: !spec.url ? icon : undefined 26341 }, 26342 components: spec.url ? [ 26343 Button.sketch({ 26344 dom: { 26345 tag: 'button', 26346 classes: ['tox-button', 'tox-button--naked', 'tox-button--icon'], 26347 innerHtml: icon, 26348 attributes: { 26349 title: providersBackstage.translate(spec.iconTooltip) 26350 } 26351 }, 26352 // TODO: aria label this button! 26353 action: (comp) => emitWith(comp, formActionEvent, { name: 'alert-banner', value: spec.url }), 26354 buttonBehaviours: derive$1([ 26355 addFocusableBehaviour() 26356 ]) 26357 }) 26358 ] : undefined 26359 }, 26360 { 26361 dom: { 26362 tag: 'div', 26363 classes: ['tox-notification__body'], 26364 // TODO: AP-247: Escape this text so that it can't contain script tags 26365 innerHtml: providersBackstage.translate(spec.text) 26366 } 26367 } 26368 ] 26369 }); 26370 }; 26371 26372 const renderCheckbox = (spec, providerBackstage, initialData) => { 26373 const toggleCheckboxHandler = (comp) => { 26374 comp.element.dom.click(); 26375 return Optional.some(true); 26376 }; 26377 const pField = FormField.parts.field({ 26378 factory: { sketch: identity }, 26379 dom: { 26380 tag: 'input', 26381 classes: ['tox-checkbox__input'], 26382 attributes: { 26383 type: 'checkbox' 26384 } 26385 }, 26386 behaviours: derive$1([ 26387 ComposingConfigs.self(), 26388 Disabling.config({ 26389 disabled: () => !spec.enabled || providerBackstage.checkUiComponentContext(spec.c
26389ontext).shouldDisable, 26390 onDisabled: (component) => { 26391 parentElement(component.element).each((element) => add$2(element, 'tox-checkbox--disabled')); 26392 }, 26393 onEnabled: (component) => { 26394 parentElement(component.element).each((element) => remove$3(element, 'tox-checkbox--disabled')); 26395 } 26396 }), 26397 Tabstopping.config({}), 26398 Focusing.config({}), 26399 withElement(initialData, get$9, set$5), 26400 Keying.config({ 26401 mode: 'special', 26402 onEnter: toggleCheckboxHandler, 26403 onSpace: toggleCheckboxHandler, 26404 stopSpaceKeyup: true 26405 }), 26406 config('checkbox-events', [ 26407 run$1(change(), (component, _) => { 26408 emitWith(component, formChangeEvent, { name: spec.name }); 26409 }) 26410 ]) 26411 ]) 26412 }); 26413 const pLabel = FormField.parts.label({ 26414 dom: { 26415 tag: 'span', 26416 classes: ['tox-checkbox__label'] 26417 }, 26418 components: [ 26419 text$2(providerBackstage.translate(spec.label)) 26420 ], 26421 behaviours: derive$1([ 26422 Unselecting.config({}) 26423 ]) 26424 }); 26425 const makeIcon = (className) => { 26426 const iconName = className === 'checked' ? 'selected' : 'unselected'; 26427 return render$4(iconName, { tag: 'span', classes: ['tox-icon', 'tox-checkbox-icon__' + className] }, providerBackstage.icons); 26428 }; 26429 const memIcons = record({ 26430 dom: { 26431 tag: 'div', 26432 classes: ['tox-checkbox__icons'] 26433 }, 26434 components: [ 26435 makeIcon('checked'), 26436 makeIcon('unchecked') 26437 ] 26438 }); 26439 return FormField.sketch({ 26440 dom: { 26441 tag: 'label', 26442 classes: ['tox-checkbox'] 26443 }, 26444 components: [ 26445 pField, 26446 memIcons.asSpec(), 26447 pLabel 26448 ], 26449 fieldBehaviours: derive$1([ 26450 Disabling.config({ 26451 disabled: () => !spec.enabled || providerBackstage.checkUiComponentContext(spec.context).shouldDisable, 26452 }), 26453 toggleOnReceive(() => providerBackstage.checkUiComponentContext(spec.context)) 26454 ]) 26455 }); 26456 }; 26457 26458 const renderHtmlPanel = (spec, providersBackstage) => { 26459 const classes = ['tox-form__group', ...(spec.stretched ? ['tox-form__group--stretched'] : [])]; 26460 const init = config('htmlpanel', [ 26461 runOnAttached((comp) => { 26462 spec.onInit(comp.element.dom); 26463 }) 26464 ]); 26465 if (spec.presets === 'presentation') { 26466 return Container.sketch({ 26467 dom: { 26468 tag: 'div', 26469 classes, 26470 innerHtml: spec.html 26471 }, 26472 containerBehaviours: derive$1([ 26473 Tooltipping.config({ 26474 ...providersBackstage.tooltips.getConfig({ 26475 tooltipText: '', 26476 onShow: (comp) => { 26477 descendant(comp.element, '[data-mce-tooltip]:hover').orThunk(() => search(comp.element)) 26478 .each((current) => { 26479 getOpt(current, 'data-mce-tooltip').each((text) => { 26480 Tooltipping.setComponents(comp, providersBackstage.tooltips.getComponents({ tooltipText: text })); 26481 }); 26482 }); 26483 }, 26484 }), 26485 mode: 'children-normal', 26486 anchor: (comp) => ({ 26487 type: 'node', 26488 node: descendant(comp.element, '[data-mce-tooltip]:hover') 26489 .orThunk(() => search(comp.element).filter((current) => getOpt(current, 'data-mce-tooltip').isSome())), 26490 root: comp.element, 26491 layouts: { 26492 onLtr: constant$1([south$2, north$2, southeast$2, northeast$2, southwest$2, northwest$2]), 26493 onRtl: constant$1([south$2, north$2, southeast$2, northeast$2, southwest$2, northwest$2]) 26494 }, 26495 bubble: nu$6(0, -2, {}), 26496 }) 26497 }), 26498 init 26499 ]) 26500 }); 26501 } 26502 else { 26503 return Container.sketch({ 26504 dom: { 26505 tag: 'div', 26506 classes, 26507 innerHtml: spec.html, 26508 attributes: { 26509 role: 'document' 26510 } 26511 }, 26512 containerBehaviours: derive$1([ 26513 Tabstopping.config({}), 26514 Focusing.config({}), 26515 init 26516 ]) 26517 }); 26518 } 26519 }; 26520 26521 const make$1 = (render) => { 26522 return (parts, spec, dialogData, backstage, getCompByName) => get$h(spec, 'name').fold(() => render(spec, backstage, Optional.none(), getCompByName), (fieldName) => parts.field(fieldName, render(spec, backstage, get$h(dialogData, fieldName), getCompByName))); 26523 }; 26524 const makeIframe = (render) => (parts, spec, dialogData, backstage, getCompByName) => { 26525 const iframeSpec = deepMerge(spec, { 26526 source: 'dynamic' 26527 }); 26528 return make$1(render)(parts, iframeSpec, dialogData, backstage, getCompByName); 26529 }; 26530 const factories = { 26531 bar: make$1((spec, backstage) => renderBar(spec, backstage.shared)), 26532 collection: make$1((spec, backstage, data) => renderCollection(spec, backstage.shared.providers, data)), 26533 alertbanner: make$1((spec, backstage) => renderAlertBanner(spec, backstage.shared.providers)), 26534 input: make$1((spec, backstage, data) => renderInput(spec, backstage.shared.providers, data)), 26535 textarea: make$1((spec, backstage, data) => renderTextarea(spec, backstage.shared.providers, data)), 26536 label: make$1((spec, backstage, _data, getCompByName) => renderLabel$2(spec, backstage.shared, getCompByName)), 26537 iframe: makeIframe((spec, backstage, data) => renderIFrame(spec, backstage.shared.providers, data)), 26538 button: make$1((spec, backstage) => renderDialogButton(spec, backstage.shared.providers)), 26539 checkbox: make$1((spec, backstage, data) => renderCheckbox(spec, backstage.shared.providers, data)), 26540 colorinput: make$1((spec, backstage, data) => renderColorInput(spec, backstage.shared, backstage.colorinput, data)), 26541 colorpicker: make$1((spec, backstage, data) => renderColorPicker(spec, backstage.shared.providers, data)), // Not sure if this needs name. 26542 dropzone: make$1((spec, backstage, data) => renderDropZone(spec, backstage.shared.providers, data)), 26543 grid: make$1((spec, backstage) => renderGrid(spec, backstage.shared)), 26544 listbox: make$1((spec, backstage, data) =>
26544 renderListBox(spec, backstage, data)), 26545 selectbox: make$1((spec, backstage, data) => renderSelectBox(spec, backstage.shared.providers, data)), 26546 sizeinput: make$1((spec, backstage) => renderSizeInput(spec, backstage.shared.providers)), 26547 slider: make$1((spec, backstage, data) => renderSlider(spec, backstage.shared.providers, data)), 26548 urlinput: make$1((spec, backstage, data) => renderUrlInput(spec, backstage, backstage.urlinput, data)), 26549 customeditor: make$1(renderCustomEditor), 26550 htmlpanel: make$1((spec, backstage) => renderHtmlPanel(spec, backstage.shared.providers)), 26551 imagepreview: make$1((spec, _, data) => renderImagePreview(spec, data)), 26552 table: make$1((spec, backstage) => renderTable(spec, backstage.shared.providers)), 26553 tree: make$1((spec, backstage) => renderTree(spec, backstage)), 26554 panel: make$1((spec, backstage) => renderPanel(spec, backstage)) 26555 }; 26556 const noFormParts = { 26557 // This is cast as we only actually want an alloy spec and don't need the actual part here 26558 field: (_name, spec) => spec, 26559 record: constant$1([]) 26560 }; 26561 const interpretInForm = (parts, spec, dialogData, oldBackstage, getCompByName) => { 26562 // Now, we need to update the backstage to use the parts variant. 26563 const newBackstage = deepMerge(oldBackstage, { 26564 // Add the interpreter based on the form parts. 26565 shared: { 26566 interpreter: (childSpec) => interpretParts(parts, childSpec, dialogData, newBackstage, getCompByName) 26567 } 26568 }); 26569 return interpretParts(parts, spec, dialogData, newBackstage, getCompByName); 26570 }; 26571 const interpretParts = (parts, spec, dialogData, backstage, getCompByName) => get$h(factories, spec.type).fold(() => { 26572 console.error(`Unknown factory type "${spec.type}", defaulting to container: `, spec); 26573 return spec; 26574 }, (factory) => factory(parts, spec, dialogData, backstage, getCompByName)); 26575 const interpretWithoutForm = (spec, dialogData, backstage, getCompByName) => interpretParts(noFormParts, spec, dialogData, backstage, getCompByName); 26576 26577 const bubbleAlignments$2 = { 26578 valignCentre: [], 26579 alignCentre: [], 26580 alignLeft: [], 26581 alignRight: [], 26582 right: [], 26583 left: [], 26584 bottom: [], 26585 top: [] 26586 }; 26587 const getInlineDialogAnchor = (contentAreaElement, lazyAnchorbar, lazyUseEditableAreaAnchor) => { 26588 const bubbleSize = 12; 26589 const overrides = { 26590 maxHeightFunction: expandable$1() 26591 }; 26592 const editableAreaAnchor = () => ({ 26593 type: 'node', 26594 root: getContentContainer(getRootNode(contentAreaElement())), 26595 node: Optional.from(contentAreaElement()), 26596 bubble: nu$6(bubbleSize, bubbleSize, bubbleAlignments$2), 26597 layouts: { 26598 onRtl: () => [northeast$1], 26599 onLtr: () => [northwest$1] 26600 }, 26601 overrides 26602 }); 26603 const standardAnchor = () => ({ 26604 type: 'hotspot', 26605 hotspot: lazyAnchorbar(), 26606 bubble: nu$6(-bubbleSize, bubbleSize, bubbleAlignments$2), 26607 layouts: { 26608 onRtl: () => [southeast$2, southwest$2, south$2], 26609 onLtr: () => [southwest$2, southeast$2, south$2] 26610 }, 26611 overrides 26612 }); 26613 return () => lazyUseEditableAreaAnchor() ? editableAreaAnchor() : standardAnchor(); 26614 }; 26615 const getInlineBottomDialogAnchor = (inline, contentAreaElement, lazyBottomAnchorBar, lazyUseEditableAreaAnchor) => { 26616 const bubbleSize = 12; 26617 const overrides = { 26618 maxHeightFunction: expandable$1() 26619 }; 26620 const editableAreaAnchor = () => ({ 26621 type: 'node', 26622 root: getContentContainer(getRootNode(contentAreaElement())), 26623 node: Optional.from(contentAreaElement()), 26624 bubble: nu$6(bubbleSize, bubbleSize, bubbleAlignments$2), 26625 layouts: { 26626 onRtl: () => [north$1], 26627 onLtr: () => [north$1] 26628 }, 26629 overrides 26630 }); 26631 const standardAnchor = () => inline ? 26632 ({ 26633 type: 'node', 26634 root: getContentContainer(getRootNode(contentAreaElement())), 26635 node: Optional.from(contentAreaElement()), 26636 bubble: nu$6(0, -getOuter$1(contentAreaElement()), bubbleAlignments$2), 26637 layouts: { 26638 onRtl: () => [north$2], 26639 onLtr: () => [north$2] 26640 }, 26641 overrides 26642 }) 26643 : ({ 26644 type: 'hotspot', 26645 hotspot: lazyBottomAnchorBar(), 26646 bubble: nu$6(0, 0, bubbleAlignments$2), 26647 layouts: { 26648 onRtl: () => [north$2], 26649 onLtr: () => [north$2] 26650 }, 26651 overrides 26652 }); 26653 return () => lazyUseEditableAreaAnchor() ? editableAreaAnchor() : standardAnchor(); 26654 }; 26655 const getBannerAnchor = (contentAreaElement, lazyAnchorbar, lazyUseEditableAreaAnchor) => { 26656 const editableAreaAnchor = () => ({ 26657 type: 'node', 26658 root: getContentContainer(getRootNode(contentAreaElement())), 26659 node: Optional.from(contentAreaElement()), 26660 layouts: { 26661 onRtl: () => [north$1], 26662 onLtr: () => [north$1] 26663 } 26664 }); 26665 const standardAnchor = () => ({ 26666 type: 'hotspot',
vendor: 7,738 bytes, lines 26667-26833
26667 hotspot: lazyAnchorbar(), 26668 layouts: { 26669 onRtl: () => [south$2], 26670 onLtr: () => [south$2] 26671 } 26672 }); 26673 return () => lazyUseEditableAreaAnchor() ? editableAreaAnchor() : standardAnchor(); 26674 }; 26675 const getCursorAnchor = (editor, bodyElement) => () => ({ 26676 type: 'selection', 26677 root: bodyElement(), 26678 getSelection: () => { 26679 const rng = editor.selection.getRng(); 26680 // Only return a range if there is a selection of more than one cell. 26681 const selectedCells = editor.model.table.getSelectedCells(); 26682 if (selectedCells.length > 1) { 26683 const firstCell = selectedCells[0]; 26684 const lastCell = selectedCells[selectedCells.length - 1]; 26685 const selectionTableCellRange = { 26686 firstCell: SugarElement.fromDom(firstCell), 26687 lastCell: SugarElement.fromDom(lastCell) 26688 }; 26689 return Optional.some(selectionTableCellRange); 26690 } 26691 return Optional.some(SimSelection.range(SugarElement.fromDom(rng.startContainer), rng.startOffset, SugarElement.fromDom(rng.endContainer), rng.endOffset)); 26692 } 26693 }); 26694 const getNodeAnchor$1 = (bodyElement) => (element) => ({ 26695 type: 'node', 26696 root: bodyElement(), 26697 node: element 26698 }); 26699 const getAnchors = (editor, lazyAnchorbar, lazyBottomAnchorBar, isToolbarTop) => { 26700 const useFixedToolbarContainer = useFixedContainer(editor); 26701 const bodyElement = () => SugarElement.fromDom(editor.getBody()); 26702 const contentAreaElement = () => SugarElement.fromDom(editor.getContentAreaContainer()); 26703 // If using fixed_toolbar_container or if the toolbar is positioned at the bottom 26704 // of the editor, some things should anchor to the top of the editable area. 26705 const lazyUseEditableAreaAnchor = () => useFixedToolbarContainer || !isToolbarTop(); 26706 return { 26707 inlineDialog: getInlineDialogAnchor(contentAreaElement, lazyAnchorbar, lazyUseEditableAreaAnchor), 26708 inlineBottomDialog: getInlineBottomDialogAnchor(editor.inline, contentAreaElement, lazyBottomAnchorBar, lazyUseEditableAreaAnchor), 26709 banner: getBannerAnchor(contentAreaElement, lazyAnchorbar, lazyUseEditableAreaAnchor), 26710 cursor: getCursorAnchor(editor, bodyElement), 26711 node: getNodeAnchor$1(bodyElement) 26712 }; 26713 }; 26714 26715 const colorPicker = (editor) => (callback, value) => { 26716 const dialog = colorPickerDialog(editor); 26717 dialog(callback, value); 26718 }; 26719 const hasCustomColors = (editor) => () => hasCustomColors$1(editor); 26720 const getColors = (editor) => (id) => getColors$2(editor, id); 26721 const getColorCols = (editor) => (id) => getColorCols$1(editor, id); 26722 const ColorInputBackstage = (editor) => ({ 26723 colorPicker: colorPicker(editor), 26724 hasCustomColors: hasCustomColors(editor), 26725 getColors: getColors(editor), 26726 getColorCols: getColorCols(editor) 26727 }); 26728 26729 const isDraggableModal = (editor) => () => isDraggableModal$1(editor); 26730 const DialogBackstage = (editor) => ({ 26731 isDraggableModal: isDraggableModal(editor) 26732 }); 26733 26734 const HeaderBackstage = (editor) => { 26735 const mode = Cell(isToolbarLocationBottom(editor) ? 'bottom' : 'top'); 26736 return { 26737 isPositionedAtTop: () => mode.get() === 'top', 26738 getDockingMode: mode.get, 26739 setDockingMode: mode.set 26740 }; 26741 }; 26742 26743 const isNestedFormat = (format) => hasNonNullableKey(format, 'items'); 26744 const isFormatReference = (format) => hasNonNullableKey(format, 'format'); 26745 const defaultStyleFormats = [ 26746 { 26747 title: 'Headings', items: [ 26748 { title: 'Heading 1', format: 'h1' }, 26749 { title: 'Heading 2', format: 'h2' }, 26750 { title: 'Heading 3', format: 'h3' }, 26751 { title: 'Heading 4', format: 'h4' }, 26752 { title: 'Heading 5', format: 'h5' }, 26753 { title: 'Heading 6', format: 'h6' } 26754 ] 26755 }, 26756 { 26757 title: 'Inline', items: [ 26758 { title: 'Bold', format: 'bold' }, 26759 { title: 'Italic', format: 'italic' }, 26760 { title: 'Underline', format: 'underline' }, 26761 { title: 'Strikethrough', format: 'strikethrough' }, 26762 { title: 'Superscript', format: 'superscript' }, 26763 { title: 'Subscript', format: 'subscript' }, 26764 { title: 'Code', format: 'code' } 26765 ] 26766 }, 26767 { 26768 title: 'Blocks', items: [ 26769 { title: 'Paragraph', format: 'p' }, 26770 { title: 'Blockquote', format: 'blockquote' }, 26771 { title: 'Div', format: 'div' }, 26772 { title: 'Pre', format: 'pre' } 26773 ] 26774 }, 26775 { 26776 title: 'Align', items: [ 26777 { title: 'Left', format: 'alignleft' }, 26778 { title: 'Center', format: 'aligncenter' }, 26779 { title: 'Right', format: 'alignright' }, 26780 { title: 'Justify', format: 'alignjustify' } 26781 ] 26782 } 26783 ]; 26784 // Note: Need to cast format below to expected type, as Obj.has uses "K keyof T", which doesn't work with aliases 26785 const isNestedFormats = (format) => has$2(format, 'items'); 26786 const isBlockFormat = (format) => has$2(format, 'block'); 26787 const isInlineFormat = (format) => has$2(format, 'inline'); 26788 const isSelectorFormat = (format) => has$2(format, 'selector'); 26789 const mapFormats = (userFormats) => foldl(userFormats, (acc, fmt) => { 26790 if (isNestedFormats(fmt)) { 26791 // Map the child formats 26792 const result = mapFormats(fmt.items); 26793 return { 26794 customFormats: acc.customFormats.concat(result.customFormats), 26795 formats: acc.formats.concat([{ title: fmt.title, items: result.formats }]) 26796 }; 26797 } 26798 else if (isInlineFormat(fmt) || isBlockFormat(fmt) || isSelectorFormat(fmt)) { 26799 // Convert the format to a reference and add the original to the custom formats to be registered 26800 const formatName = isString(fmt.name) ? fmt.name : fmt.title.toLowerCase(); 26801 const formatNameWithPrefix = `custom-${formatName}`; 26802 return { 26803 customFormats: acc.customFormats.concat([{ name: formatNameWithPrefix, format: fmt }]), 26804 formats: acc.formats.concat([{ title: fmt.title, format: formatNameWithPrefix, icon: fmt.icon }]) 26805 }; 26806 } 26807 else { 26808 return { ...acc, formats: acc.formats.concat(fmt) }; 26809 } 26810 }, { customFormats: [], formats: [] }); 26811 const registerCustomFormats = (editor, userFormats) => { 26812 const result = mapFormats(userFormats); 26813 const registerFormats = (customFormats) => { 26814 each$1(customFormats, (fmt) => { 26815 // Only register the custom format with the editor, if it's not already registered 26816 if (!editor.formatter.has(fmt.name)) { 26817 editor.formatter.register(fmt.name, fmt.format); 26818 } 26819 }); 26820 }; 26821 // The editor may not yet be initialized, so check for that 26822 if (editor.formatter) { 26823 registerFormats(result.customFormats); 26824 } 26825 else { 26826 editor.on('init', () => { 26827 registerFormats(result.customFormats); 26828 }); 26829 } 26830 return result.formats; 26831 }; 26832 const getStyleFormats = (editor) => getUserStyleFormats(editor).map((userFormats) => { 26833 // Ensure that any custom formats specified by the user are registered with the editor
26834 const registeredUserFormats = registerCustomFormats(editor, userFormats); 26835 // Merge the default formats with the custom formats if required 26836 return shouldMergeStyleFormats(editor) ? defaultStyleFormats.concat(registeredUserFormats) : registeredUserFormats; 26837 }).getOr(defaultStyleFormats); 26838 26839 const isSeparator$1 = (format) => { 26840 const keys$1 = keys(format); 26841 return keys$1.length === 1 && contains$2(keys$1, 'title'); 26842 }; 26843 const processBasic = (item, isSelectedFor, getPreviewFor) => ({ 26844 ...item, 26845 type: 'formatter', 26846 isSelected: isSelectedFor(item.format), 26847 getStylePreview: getPreviewFor(item.format) 26848 }); 26849 // TODO: This is adapted from StyleFormats in the mobile theme. Consolidate. 26850 const register$b = (editor, formats, isSelectedFor, getPreviewFor) => { 26851 const enrichSupported = (item) => processBasic(item, isSelectedFor, getPreviewFor); 26852 // Item that triggers a submenu 26853 const enrichMenu = (item) => { 26854 const newItems = doEnrich(item.items); 26855 return { 26856 ...item, 26857 type: 'submenu', 26858 getStyleItems: constant$1(newItems) 26859 }; 26860 }; 26861 const enrichCustom = (item) => { 26862 const formatName = isString(item.name) ? item.name : generate$6(item.title); 26863 const formatNameWithPrefix = `custom-${formatName}`; 26864 const newItem = { 26865 ...item, 26866 type: 'formatter', 26867 format: formatNameWithPrefix, 26868 isSelected: isSelectedFor(formatNameWithPrefix), 26869 getStylePreview: getPreviewFor(formatNameWithPrefix) 26870 }; 26871 editor.formatter.register(formatName, newItem); 26872 return newItem; 26873 }; 26874 const doEnrich = (items) => map$2(items, (item) => { 26875 // If it is a submenu, enrich all the subitems. 26876 if (isNestedFormat(item)) { 26877 return enrichMenu(item); 26878 } 26879 else if (isFormatReference(item)) { 26880 return enrichSupported(item); 26881 // NOTE: This branch is added from the original StyleFormats in mobile 26882 } 26883 else if (isSeparator$1(item)) { 26884 return { ...item, type: 'separator' }; 26885 } 26886 else { 26887 return enrichCustom(item); 26888 } 26889 }); 26890 return doEnrich(formats); 26891 }; 26892 26893 const init$1 = (editor) => { 26894 const isSelectedFor = (format) => () => editor.formatter.match(format); 26895 const getPreviewFor = (format) => () => { 26896 const fmt = editor.formatter.get(format); 26897 return fmt !== undefined ? Optional.some({ 26898 tag: fmt.length > 0 ? fmt[0].inline || fmt[0].block || 'div' : 'div', 26899 styles: editor.dom.parseStyle(editor.formatter.getCssText(format)) 26900 }) : Optional.none(); 26901 }; 26902 const settingsFormats = Cell([]); 26903 const eventsFormats = Cell([]); 26904 const replaceSettings = Cell(false); 26905 editor.on('PreInit', (_e) => { 26906 const formats = getStyleFormats(editor); 26907 const enriched = register$b(editor, formats, isSelectedFor, getPreviewFor); 26908 settingsFormats.set(enriched); 26909 }); 26910 editor.on('addStyleModifications', (e) => { 26911 // Is there going to be an order issue here? 26912 const modifications = register$b(editor, e.items, isSelectedFor, getPreviewFor); 26913 eventsFormats.set(modifications); 26914 replaceSettings.set(e.replace); 26915 }); 26916 const getData = () => { 26917 const fromSettings = replaceSettings.get() ? [] : settingsFormats.get(); 26918 const fromEvents = eventsFormats.get(); 26919 return fromSettings.concat(fromEvents); 26920 }; 26921 return { 26922 getData 26923 }; 26924 }; 26925 26926 const TooltipsBackstage = (getSink) => { 26927 const tooltipDelay = 300; 26928 const intervalDelay = tooltipDelay * 0.2; // Arbitrary value 26929 let numActiveTooltips = 0; 26930 const alreadyShowingTooltips = () => numActiveTooltips > 0; 26931 const getComponents = (spec) => { 26932 return [ 26933 { 26934 dom: { 26935 tag: 'div', 26936 classes: ['tox-tooltip__body'] 26937 }, 26938 components: [ 26939 text$2(spec.tooltipText) 26940 ] 26941 } 26942 ]; 26943 }; 26944 const getConfig = (spec) => { 26945 return { 26946 delayForShow: () => alreadyShowingTooltips() ? intervalDelay : tooltipDelay, 26947 delayForHide: constant$1(tooltipDelay), 26948 exclusive: true, 26949 lazySink: getSink, 26950 tooltipDom: { 26951 tag: 'div', 26952 classes: ['tox-tooltip', 'tox-tooltip--up'] 26953 }, 26954 tooltipComponents: getComponents(spec), 26955 onShow: (comp, tooltip) => { 26956 numActiveTooltips++; 26957 if (spec.onShow) { 26958 spec.onShow(comp, tooltip); 26959 } 26960 }, 26961 onHide: (comp, tooltip) => { 26962 numActiveTooltips--; 26963 if (spec.onHide) { 26964 spec.onHide(comp, tooltip); 26965 } 26966 }, 26967 onSetup: spec.onSetup, 26968 }; 26969 }; 26970 return { 26971 getConfig, 26972 getComponents 26973 }; 26974 }; 26975 26976 const isElement = (node) => isNonNullable(node) && node.nodeType === 1; 26977 const trim = global$2.trim; 26978 const hasContentEditableState = (value) => { 26979 return (node) => { 26980 if (isElement(node)) { 26981 if (node.contentEditable === value) { 26982 return true; 26983 } 26984 if (node.getAttribute('data-mce-contenteditable') === value) { 26985 return true; 26986 } 26987 } 26988 return false;
26989 }; 26990 }; 26991 const isContentEditableTrue = hasContentEditableState('true'); 26992 const isContentEditableFalse = hasContentEditableState('false'); 26993 const create = (type, title, url, level, attach) => ({ 26994 type, 26995 title, 26996 url, 26997 level, 26998 attach 26999 }); 27000 const isChildOfContentEditableTrue = (node) => { 27001 let tempNode = node; 27002 while ((tempNode = tempNode.parentNode)) { 27003 const value = tempNode.contentEditable; 27004 if (value && value !== 'inherit') { 27005 return isContentEditableTrue(tempNode); 27006 } 27007 } 27008 return false; 27009 }; 27010 const select = (selector, root) => { 27011 return map$2(descendants(SugarElement.fromDom(root), selector), (element) => { 27012 return element.dom; 27013 }); 27014 }; 27015 const getElementText = (elm) => { 27016 return elm.innerText || elm.textContent; 27017 }; 27018 const getOrGenerateId = (elm) => { 27019 return elm.id ? elm.id : generate$6('h'); 27020 }; 27021 const isAnchor = (elm) => { 27022 return elm && elm.nodeName === 'A' && (elm.id || elm.name) !== undefined; 27023 }; 27024 const isValidAnchor = (elm) => { 27025 return isAnchor(elm) && isEditable(elm); 27026 }; 27027 const isHeader = (elm) => { 27028 return elm && /^(H[1-6])$/.test(elm.nodeName); 27029 }; 27030 const isEditable = (elm) => { 27031 return isChildOfContentEditableTrue(elm) && !isContentEditableFalse(elm); 27032 }; 27033 const isValidHeader = (elm) => { 27034 return isHeader(elm) && isEditable(elm); 27035 }; 27036 const getLevel = (elm) => { 27037 return isHeader(elm) ? parseInt(elm.nodeName.substr(1), 10) : 0; 27038 }; 27039 const headerTarget = (elm) => { 27040 const headerId = getOrGenerateId(elm); 27041 const attach = () => { 27042 elm.id = headerId; 27043 }; 27044 return create('header', getElementText(elm) ?? '', '#' + headerId, getLevel(elm), attach); 27045 }; 27046 const anchorTarget = (elm) => { 27047 const anchorId = elm.id || elm.name; 27048 const anchorText = getElementText(elm); 27049 return create('anchor', anchorText ? anchorText : '#' + anchorId, '#' + anchorId, 0, noop); 27050 }; 27051 const getHeaderTargets = (elms) => { 27052 return map$2(filter$2(elms, isValidHeader), headerTarget); 27053 }; 27054 const getAnchorTargets = (elms) => { 27055 return map$2(filter$2(elms, isValidAnchor), anchorTarget); 27056 }; 27057 const getTargetElements = (elm) => { 27058 const elms = select('h1,h2,h3,h4,h5,h6,a:not([href])', elm); 27059 return elms; 27060 }; 27061 const hasTitle = (target) => { 27062 return trim(target.title).length > 0; 27063 }; 27064 const find = (elm) => { 27065 const elms = getTargetElements(elm); 27066 return filter$2(getHeaderTargets(elms).concat(getAnchorTargets(elms)), hasTitle); 27067 }; 27068 const LinkTargets = { 27069 find 27070 }; 27071 27072 const STORAGE_KEY = 'tinymce-url-history'; 27073 const HISTORY_LENGTH = 5; 27074 // validation functions 27075 const isHttpUrl = (url) => isString(url) && /^https?/.test(url); 27076 const isArrayOfUrl = (a) => isArray(a) && a.length <= HISTORY_LENGTH && forall(a, isHttpUrl); 27077 const isRecordOfUrlArray = (r) =>
27077 isObject(r) && find$4(r, (value) => !isArrayOfUrl(value)).isNone(); 27078 const getAllHistory = () => { 27079 const unparsedHistory = global$5.getItem(STORAGE_KEY); 27080 if (unparsedHistory === null) { 27081 return {}; 27082 } 27083 // parse history 27084 let history; 27085 try { 27086 history = JSON.parse(unparsedHistory); 27087 } 27088 catch (e) { 27089 if (e instanceof SyntaxError) { 27090 // eslint-disable-next-line no-console 27091 console.log('Local storage ' + STORAGE_KEY + ' was not valid JSON', e); 27092 return {}; 27093 } 27094 throw e; 27095 } 27096 // validate the parsed value 27097 if (!isRecordOfUrlArray(history)) { 27098 // eslint-disable-next-line no-console 27099 console.log('Local storage ' + STORAGE_KEY + ' was not valid format', history); 27100 return {}; 27101 } 27102 return history; 27103 }; 27104 const setAllHistory = (history) => { 27105 if (!isRecordOfUrlArray(history)) { 27106 throw new Error('Bad format for history:\n' + JSON.stringify(history)); 27107 } 27108 global$5.setItem(STORAGE_KEY, JSON.stringify(history)); 27109 }; 27110 const getHistory = (fileType) => { 27111 const history = getAllHistory(); 27112 return get$h(history, fileType).getOr([]); 27113 }; 27114 const addToHistory = (url, fileType) => { 27115 if (!isHttpUrl(url)) { 27116 return; 27117 } 27118 const history = getAllHistory(); 27119 const items = get$h(history, fileType).getOr([]); 27120 const itemsWithoutUrl = filter$2(items, (item) => item !== url); 27121 history[fileType] = [url].concat(itemsWithoutUrl).slice(0, HISTORY_LENGTH); 27122 setAllHistory(history); 27123 }; 27124 27125 const isTruthy = (value) => !!value; 27126 const makeMap = (value) => map$1(global$2.makeMap(value, /[, ]/), isTruthy); 27127 const getPicker = (editor) => Optional.from(getFilePickerCallback(editor)); 27128 const getPickerTypes = (editor) => { 27129 const optFileTypes = Optional.from(getFilePickerTypes(editor)).filter(isTruthy).map(makeMap); 27130 return getPicker(editor).fold(never, (_picker) => optFileTypes.fold(always, (types) => keys(types).length > 0 ? types : false)); 27131 }; 27132 const getPickerSetting = (editor, filetype) => { 27133 const pickerTypes = getPickerTypes(editor); 27134 if (isBoolean(pickerTypes)) { 27135 return pickerTypes ? getPicker(editor) : Optional.none(); 27136 } 27137 else { 27138 return pickerTypes[filetype] ? getPicker(editor) : Optional.none(); 27139 } 27140 }; 27141 const getUrlPicker = (editor, filetype) => getPickerSetting(editor, filetype).map((picker) => (entry) => Future.nu((completer) => { 27142 const handler = (value, meta) => { 27143 if (!isString(value)) { 27144 throw new Error('Expected value to be string'); 27145 } 27146 if (meta !== undefined && !isObject(meta)) { 27147 throw new Error('Expected meta to be a object'); 27148 } 27149 const r = { value, meta }; 27150 completer(r); 27151 }; 27152 const meta = { 27153 filetype, 27154 fieldname: entry.fieldname, 27155 ...Optional.from(entry.meta).getOr({}) 27156 }; 27157 // file_picker_callback(callback, currentValue, metaData) 27158 picker.call(editor, handler, entry.value, meta); 27159 })); 27160 const getTextSetting = (value) => Optional.from(value).filter(isString).getOrUndefined(); 27161 const getLinkInformation = (editor) => { 27162 if (!useTypeaheadUrls(editor)) { 27163 return Optional.none(); 27164 } 27165 return Optional.some({ 27166 targets: LinkTargets.find(editor.getBody()), 27167 anchorTop: getTextSetting(getAnchorTop(editor)), 27168 anchorBottom: getTextSetting(getAnchorBottom(editor)) 27169 }); 27170 }; 27171 const getValidationHandler = (editor) => Optional.from(getFilePickerValidatorHandler(editor)); 27172 const UrlInputBackstage = (editor) => ({ 27173 getHistory, 27174 addToHistory, 27175 getLinkInformation: () => getLinkInformation(editor), 27176 getValidationHandler: () => getValidationHandler(editor), 27177 getUrlPicker: (filetype) => getUrlPicker(editor, filetype) 27178 }); 27179 27180 const init = (lazySinks, editor, lazyAnchorbar, lazyBottomAnchorBar) => { 27181 const contextMenuState = Cell(false); 27182 const toolbar = HeaderBackstage(editor); 27183 const providers = { 27184 icons: () => editor.ui.registry.getAll().icons, 27185 menuItems: () => editor.ui.registry.getAll().menuItems, 27186 translate: global$6.translate, 27187 isDisabled: () => !editor.ui.isEnabled(), 27188 getOption: editor.options.get, 27189 tooltips: TooltipsBackstage(lazySinks.dialog), 27190 checkUiComponentContext: (specContext) => { 27191 if (isDisabled(editor)) { 27192 return { 27193 contextType: 'disabled', 27194 shouldDisable: true 27195 }; 27196 } 27197 const [key, value = ''] = specContext.split(':'); 27198 const contexts = editor.ui.registry.getAll().contexts; 27199 const enabledInContext = get$h(contexts, key) 27200 .fold( 27201 // Fallback to 'mode:design' if key is not found 27202 () => get$h(contexts, 'mode').map((pred) => pred('design')).getOr(false), (pred) => value.charAt(0) === '!' ? !pred(value.slice(1)) : pred(value)); 27203 return { 27204 contextType: key, 27205 shouldDisable: !enabledInContext 27206 }; 27207 } 27208 }; 27209 const urlinput = UrlInputBackstage(editor); 27210 const styles = init$1(editor); 27211 const colorinput = ColorInputBackstage(editor); 27212 const dialogSettings = DialogBackstage(editor); 27213 const isContextMenuOpen = () => contextMenuState.get(); 27214 const setContextMenuState = (state) => contextMenuState.set(state); 27215 const commonBackstage = { 27216 shared: { 27217 providers, 27218 anchors: getAnchors(editor, lazyAnchorbar, lazyBottomAnchorBar, toolbar.isPositionedAtTop), 27219 header: toolbar, 27220 }, 27221 urlinput, 27222 styles, 27223 colorinput, 27224 dialog: dialogSettings, 27225 isContextMenuOpen, 27226 setContextMenuState 27227 }; 27228 const getCompByName = (_name) => Optional.none(); 27229 const popupBackstage = { 27230 ...commonBackstage, 27231 shared: { 27232 ...commonBackstage.shared, 27233 interpreter: (s) => interpretWithoutForm(s, {}, popupBackstage, getCompByName), 27234 getSink: lazySinks.popup 27235 } 27236 }; 27237 const dialogBackstage = { 27238 ...commonBackstage, 27239 shared: { 27240 ...commonBackstage.shared, 27241 interpreter: (s) => interpretWithoutForm(s, {}, dialogBackstage, getCompByName), 27242 getSink: lazySinks.dialog 27243 } 27244 }; 27245 return { 27246 popup: popupBackstage, 27247 dialog: dialogBackstage 27248 }; 27249 }; 27250 27251 const migrationFrom7x = 'https://www.tiny.cloud/docs/tinymce/latest/migration-from-7x/'; 27252 const deprecatedFeatures = { 27253 skipFocus: `ToggleToolbarDrawer skipFocus is deprecated see migration guide: ${migrationFrom7x}`, 27254 }; 27255 const logFeatureDeprecationWarning = (feature) => { 27256 // eslint-disable-next-line no-console 27257 console.warn(deprecatedFeatures[feature], new Error().stack); 27258 }; 27259 27260 const setup$b = (editor, mothership, uiMotherships) => { 27261 const broadcastEvent = (name, evt) => { 27262 each$1([mothership, ...uiMotherships], (m) => { 27263 m.broadcastEvent(name, evt); 27264 }); 27265 }; 27266 const broadcastOn = (channel, message) => { 27267 each$1([mothership, ...uiMotherships], (m) => { 27268 m.broadcastOn([channel], message); 27269 }); 27270 }; 27271 const fireDismissPopups = (evt) => broadcastOn(dismissPopups(), { target: evt.target }); 27272 const fireCloseTooltips = (event) => { 27273 broadcastOn(closeTooltips(), { 27274 closedTooltip: () => { 27275 event.preventDefault(); 27276 } 27277 }); 27278 }; 27279 // Document touch events 27280 const doc = getDocument(); 27281 const onTouchstart = bind$1(doc, 'touchstart', fireDismissPopups); 27282 const onTouchmove = bind$1(doc, 'touchmove', (evt) => broadcastEvent(documentTouchmove(), evt)); 27283 const onTouchend = bind$1(doc, 'touchend', (evt) => broadcastEvent(documentTouchend(), evt)); 27284 // Document mouse events 27285 const onMousedown = bind$1(doc, 'mousedown', fireDismissPopups); 27286 const onMouseup = bind$1(doc, 'mouseup', (evt) => { 27287 if (evt.raw.button === 0) { 27288 broadcastOn(mouseReleased(), { target: evt.target }); 27289 } 27290 }); 27291 // Editor content events 27292 const onContentClick = (raw) => broadcastOn(dismissPopups(), { target: SugarElement.fromDom(raw.target) }); 27293 const onContentMouseup = (raw) => { 27294 if (raw.button === 0) { 27295 broadcastOn(mouseReleased(), { target: SugarElement.fromDom(raw.target) }); 27296 } 27297 }; 27298 const onContentMousedown = () => { 27299 each$1(editor.editorManager.get(), (loopEditor) => { 27300 if (editor !== loopEditor) { 27301 loopEditor.dispatch('DismissPopups', { relatedTarget: editor }); 27302 } 27303 }); 27304 };
27305 // Window events 27306 const onWindowScroll = (evt) => broadcastEvent(windowScroll(), fromRawEvent(evt)); 27307 const onWindowResize = (evt) => { 27308 broadcastOn(repositionPopups(), {}); 27309 broadcastEvent(windowResize(), fromRawEvent(evt)); 27310 }; 27311 // TINY-9425: At the moment, we are only supporting situations where the scrolling container 27312 // is *inside* the shadow root - which is why we bind to the root node, instead of just the outer 27313 // document. However, if we needed to support scrolling containers that *contained* the shadow root, 27314 // we would need to listen to the outer document (or at the least, the root node of the scrolling div in 27315 // the case of muliple layers of shadow roots). 27316 const dos = getRootNode(SugarElement.fromDom(editor.getElement())); 27317 const onElementScroll = capture(dos, 'scroll', (evt) => { 27318 requestAnimationFrame(() => { 27319 const c = editor.getContainer(); 27320 // Because this can fire before the editor is rendered, we need to stop that from happening. 27321 // Some tests can create this situation, and then we get a Node name null or defined error. 27322 if (c !== undefined && c !== null) { 27323 const optScrollingContext = detectWhenSplitUiMode(editor, mothership.element); 27324 const scrollers = optScrollingContext.map((sc) => [sc.element, ...sc.others]).getOr([]); 27325 if (exists(scrollers, (s) => eq(s, evt.target))) { 27326 editor.dispatch('ElementScroll', { target: evt.target.dom }); 27327 broadcastEvent(externalElementScroll(), evt); 27328 } 27329 } 27330 }); 27331 }); 27332 const onEditorResize = () => broadcastOn(repositionPopups(), {}); 27333 const onEditorProgress = (evt) => { 27334 if (evt.state) { 27335 broadcastOn(dismissPopups(), { target: SugarElement.fromDom(editor.getContainer()) }); 27336 } 27337 }; 27338 const onDismissPopups = (event) => { 27339 broadcastOn(dismissPopups(), { target: SugarElement.fromDom(event.relatedTarget.getContainer()) }); 27340 }; 27341 const onFocusIn = (event) => editor.dispatch('focusin', event); 27342 const onFocusOut = (event) => editor.dispatch('focusout', event); 27343 // Don't start listening to events until the UI has rendered 27344 editor.on('PostRender', () => { 27345 editor.on('click', onContentClick); 27346 editor.on('tap', onContentClick); 27347 editor.on('mouseup', onContentMouseup); 27348 editor.on('mousedown', onContentMousedown); 27349 editor.on('ScrollWindow', onWindowScroll); 27350 editor.on('ResizeWindow', onWindowResize); 27351 editor.on('ResizeEditor', onEditorResize); 27352 editor.on('AfterProgressState', onEditorProgress); 27353 editor.on('DismissPopups', onDismissPopups); 27354 editor.on('CloseActiveTooltips', fireCloseTooltips); 27355 each$1([mothership, ...uiMotherships], (gui) => { 27356 gui.element.dom.addEventListener('focusin', onFocusIn); 27357 gui.element.dom.addEventListener('focusout', onFocusOut); 27358 }); 27359 }); 27360 editor.on('remove', () => { 27361 // We probably don't need these unbinds, but it helps to have them if we move this code out. 27362 editor.off('click', onContentClick); 27363 editor.off('tap', onContentClick); 27364 editor.off('mouseup', onContentMouseup); 27365 editor.off('mousedown', onContentMousedown); 27366 editor.off('ScrollWindow', onWindowScroll); 27367 editor.off('ResizeWindow', onWindowResize); 27368 editor.off('ResizeEditor', onEditorResize); 27369 editor.off('AfterProgressState', onEditorProgress); 27370 editor.off('DismissPopups', onDismissPopups); 27371 editor.off('CloseActiveTooltips', fireCloseTooltips); 27372 each$1([mothership, ...uiMotherships], (gui) => { 27373 gui.element.dom.removeEventListener('focusin', onFocusIn); 27374 gui.element.dom.removeEventListener('focusout', onFocusOut); 27375 }); 27376 onMousedown.unbind(); 27377 onTouchstart.unbind(); 27378 onTouchmove.unbind(); 27379 onTouchend.unbind(); 27380 onMouseup.unbind(); 27381 onElementScroll.unbind(); 27382 }); 27383 editor.on('detach', () => { 27384 each$1([mothership, ...uiMotherships], detachSystem); 27385 each$1([mothership, ...uiMotherships], (m) => m.destroy()); 27386 }); 27387 }; 27388 27389 const setup$a = noop; 27390 const isDocked$1 = never; 27391 const getBehaviours$1 = constant$1([]); 27392 27393 var StaticHeader = /*#__PURE__*/Object.freeze({ 27394 __proto__: null, 27395 setup: setup$a, 27396 isDocked: isDocked$1, 27397 getBehaviours: getBehaviours$1 27398 }); 27399 27400 const toolbarHeightChange = constant$1(generate$6('toolbar-height-change')); 27401 27402 const visibility = { 27403 fadeInClass: 'tox-editor-dock-fadein', 27404 fadeOutClass: 'tox-editor-dock-fadeout', 27405 transitionClass: 'tox-editor-dock-transition' 27406 }; 27407 const editorStickyOnClass = 'tox-tinymce--toolbar-sticky-on'; 27408 const editorStickyOffClass = 'tox-tinymce--toolbar-sticky-off'; 27409 const scrollFromBehindHeader = (e, containerHeader) => { 27410 const doc = owner$4(containerHeader); 27411 const win = defaultView(containerHeader);
27412 const viewHeight = win.dom.innerHeight; 27413 const scrollPos = get$b(doc); 27414 const markerElement = SugarElement.fromDom(e.elm); 27415 const markerPos = absolute$2(markerElement); 27416 const markerHeight = get$d(markerElement); 27417 const markerTop = markerPos.y; 27418 const markerBottom = markerTop + markerHeight; 27419 const editorHeaderPos = absolute$3(containerHeader); 27420 const editorHeaderHeight = get$d(containerHeader); 27421 const editorHeaderTop = editorHeaderPos.top; 27422 const editorHeaderBottom = editorHeaderTop + editorHeaderHeight; 27423 // Check to see if the header is docked to the top/bottom of the page (eg is floating) 27424 const editorHeaderDockedAtTop = Math.abs(editorHeaderTop - scrollPos.top) < 2; 27425 const editorHeaderDockedAtBottom = Math.abs(editorHeaderBottom - (scrollPos.top + viewHeight)) < 2; 27426 // If the element is behind the header at the top of the page, then 27427 // scroll the element down by the header height 27428 if (editorHeaderDockedAtTop && markerTop < editorHeaderBottom) { 27429 to(scrollPos.left, markerTop - editorHeaderHeight, doc); 27430 // If the element is behind the header at the bottom of the page, then 27431 // scroll the element up by the header height 27432 } 27433 else if (editorHeaderDockedAtBottom && markerBottom > editorHeaderTop) { 27434 const y = (markerTop - viewHeight) + markerHeight + editorHeaderHeight; 27435 to(scrollPos.left, y, doc); 27436 } 27437 }; 27438 const isDockedMode = (header, mode) => contains$2(Docking.getModes(header), mode); 27439 const updateIframeContentFlow = (header) => { 27440 const getOccupiedHeight = (elm) => getOuter$1(elm) + 27441 (parseInt(get$e(elm, 'margin-top'), 10) || 0) + 27442 (parseInt(get$e(elm, 'margin-bottom'), 10) || 0); 27443 const elm = header.element; 27444 parentElement(elm).each((parentElem) => { 27445 const padding = 'padding-' + Docking.getModes(header)[0]; 27446 if (Docking.isDocked(header)) { 27447 const parentWidth = get$c(parentElem); 27448 set$7(elm, 'width', parentWidth + 'px'); 27449 set$7(parentElem, padding, getOccupiedHeight(elm) + 'px'); 27450 } 27451 else { 27452 remove$6(elm, 'width'); 27453 remove$6(parentElem, padding); 27454 } 27455 }); 27456 }; 27457 const updateSinkVisibility = (sinkElem, visible) => { 27458 if (visible) { 27459 remove$3(sinkElem, visibility.fadeOutClass); 27460 add$1(sinkElem, [visibility.transitionClass, visibility.fadeInClass]); 27461 } 27462 else { 27463 remove$3(sinkElem, visibility.fadeInClass); 27464 add$1(sinkElem, [visibility.fadeOutClass, visibility.transitionClass]); 27465 } 27466 }; 27467 const updateEditorClasses = (editor, docked) => { 27468 const editorContainer = SugarElement.fromDom(editor.getContainer()); 27469 if (docked) { 27470 add$2(editorContainer, editorStickyOnClass); 27471 remove$3(editorContainer, editorStickyOffClass); 27472 } 27473 else { 27474 add$2(editorContainer, editorStickyOffClass); 27475 remove$3(editorContainer, editorStickyOnClass); 27476 } 27477 }; 27478 const restoreFocus = (headerElem, focusedElem) => { 27479 // When the header is hidden, then the element that was focused will be lost 27480 // so we need to restore it if nothing else has already been focused (eg anything other than the body) 27481 const ownerDoc = owner$4(focusedElem); 27482 active$1(ownerDoc).filter((activeElm) => 27483 // Don't try to refocus the same element 27484 !eq(focusedElem, activeElm)).filter((activeElm) => 27485 // Only attempt to refocus if the current focus is the body or is in the header element 27486 eq(activeElm, SugarElement.fromDom(ownerDoc.dom.body)) || contains(headerElem, activeElm)).each(() => focus$4(focusedElem)); 27487 }; 27488 const findFocusedElem = (rootElm, lazySink) => 27489 // Check to see if an element is focused inside the header or inside the sink 27490 // and if so store the element so we can restore it later 27491 search(rootElm).orThunk(() => lazySink().toOptional().bind((sink) => search(sink.element))); 27492 const setup$9 = (editor, sharedBackstage, lazyHeader) => { 27493 if (!editor.inline) { 27494 // If using bottom toolbar then when the editor resizes we need to reset docking 27495 // otherwise it won't know the original toolbar position has moved 27496 if (!sharedBackstage.header.isPositionedAtTop()) { 27497 editor.on('ResizeEditor', () => { 27498 lazyHeader().each(Docking.reset); 27499 }); 27500 } 27501 // No need to update the content flow in inline mode as the header always floats 27502 editor.on('ResizeWindow ResizeEditor', () => { 27503 lazyHeader().each(updateIframeContentFlow); 27504 }); 27505 // Need to reset the docking position on skin loaded as the original position will have 27506 // changed due the skins styles being applied. 27507 // Note: Inline handles it's own skin loading, as it needs to do other initial positioning 27508 editor.on('SkinLoaded', () => { 27509 lazyHeader().each((comp) => { 27510 Docking.isDocked(comp) ? Docking.reset(comp) : Docking.refresh(comp); 27511 }); 27512 }); 27513 // Need to reset when we go fullscreen so that if the header is docked, 27514 // then it'll undock and viceversa 27515 editor.on('FullscreenStateChanged', () => { 27516 lazyHeader().each(Docking.reset); 27517 }); 27518 } 27519 // If inline or sticky toolbars is enabled, then when scrolling into view we may still be 27520 // behind the editor header so we need to adjust the scroll position to account for that 27521 editor.on('AfterScrollIntoView', (e) => { 27522 lazyHeader().each((header) => { 27523 // We need to make sure the header docking has refreshed, otherwise if a large scroll occurred 27524 // the header may have gone off page and need to be docked before doing calculations 27525 Docking.refresh(header); 27526 // If the header element is still visible, then adjust the scroll position if required 27527 const headerElem = header.element; 27528 if (isVisible(headerElem)) { 27529 scrollFromBehindHeader(e, headerElem); 27530 } 27531 }); 27532 }); 27533 // Update the editor classes once initial rendering has completed 27534 editor.on('PostRender', () => { 27535 updateEditorClasses(editor, false); 27536 }); 27537 }; 27538 const isDocked = (lazyHeader) => lazyHeader().map(Docking.isDocked).getOr(false); 27539 const getIframeBehaviours = () => [ 27540 Receiving.config({ 27541 channels: { 27542 [toolbarHeightChange()]: { 27543 onReceive: updateIframeContentFlow 27544 } 27545 } 27546 }) 27547 ]; 27548 const getBehaviours = (editor, sharedBackstage) => { 27549 const focusedElm = value$2(); 27550 const lazySink = sharedBackstage.getSink; 27551 const runOnSinkElement = (f) => { 27552 lazySink().each((sink) => f(sink.element)); 27553 }; 27554 const onDockingSwitch = (comp) => { 27555 if (!editor.inline) { 27556 updateIframeContentFlow(comp); 27557 } 27558 updateEditorClasses(editor, Docking.isDocked(comp)); 27559 // TINY-9223: This will only reposition the popups in the same mothership as the StickyHeader 27560 // and its sink. If we need to reposition the popups in all motherships (in the two sink 27561 // model) then we'll need a reference to all motherships here. 27562 comp.getSystem().broadcastOn([repositionPopups()], {}); 27563 lazySink().each((sink) => sink.getSystem().broadcastOn([repositionPopups()], {})); 27564 }; 27565 const additionalBehaviours = editor.inline ? [] : getIframeBehaviours(); 27566 return [ 27567 Focusing.config({}), 27568 Docking.config({ 27569 contextual: { 27570 lazyContext: (comp) => { 27571 const headerHeight = getOuter$1(comp.element); 27572 const container = editor.inline ? editor.getContentAreaContainer() : editor.getContainer(); 27573 return Optional.from(container).map((c) => { 27574 const box = box$1(SugarElement.fromDom(c)); 27575 const optScrollingContext = detectWhenSplitUiMode(editor, comp.element); 27576 return optScrollingContext.fold(() => { 27577 // Force the header to hide before it overflows outside the container 27578 const boxHeight = box.height - headerHeight; 27579 const topBound = box.y + (isDockedMode(comp, 'top') ? 0 : headerHeight); 27580 return bounds(box.x, topBound, box.width, boxHeight); 27581 }, (scrollEnv) => { 27582 const constrainedBounds = constrain(box, getBoundsFrom(scrollEnv)); 27583 // When the toolbar location is set to the top, y is the top of the container and height is the available container height minus the header height, as the toolbar will be placed at the top of the container 27584 // This is so that as you scroll the scrollable container/the page, it will dock at the top and when there's insufficient height/space (that's the reason of deducting the headerHeight for the available height), it will be hidden. 27585 // When the toolbar location is set to the bottom, y is the top of the container plus the header height, as the toolbar will be placed at the bottom of the container, beyond the container, so that's why we need to add the headerHeight 27586 // When there's insufficient height/space, it will be hidden, and when you scroll past the editor, it will be hidden 27587 const constrainedBoundsY = isDockedMode(comp, 'top') 27588 ? constrainedBounds.y 27589 : constrainedBounds.y + headerHeight; 27590 return bounds(constrainedBounds.x, 27591 // ASSUMPTION: The constrainedBounds removes the need for us to set this to 0px 27592 // for docked mode. Also, docking in a scrolling environment will often be 27593 // at the scroller top, not the window top 27594 constrainedBoundsY, constrainedBounds.width, constrainedBounds.height - headerHeight); 27595 }); 27596 }); 27597 }, 27598 onShow: () => { 27599 runOnSinkElement((elem) => updateSinkVisibility(elem, true)); 27600 }, 27601 onShown: (comp) => { 27602 runOnSinkElement((elem) => remove$2(elem, [visibility.transitionClass, visibility.fadeInClass])); 27603 // Restore focus and reset the stored focused element 27604 focusedElm.get().each((elem) => { 27605 restoreFocus(comp.element, elem); 27606 focusedElm.clear(); 27607 }); 27608 }, 27609 onHide: (comp) => { 27610 findFocusedElem(comp.element, lazySink).fold(focusedElm.clear, focusedElm.set); 27611 runOnSinkElement((elem) => updateSinkVisibility(elem, false)); 27612 }, 27613 onHidden: () => { 27614 runOnSinkElement((elem) => remove$2(elem, [visibility.transitionClass])); 27615 }, 27616 ...visibility 27617 }, 27618 lazyViewport: (comp) => { 27619 const optScrollingContext = detectWhenSplitUiMode(editor, comp.element); 27620 return optScrollingContext.fold(() => { 27621 const boundsWithoutOffset = win(); 27622 const offset = getStickyToolbarOffset(editor); 27623 const top = boundsWithoutOffset.y + (isDockedMode(comp, 'top') && !isFullscreen(editor) ? offset : 0); 27624 const height = boundsWithoutOffset.height - (isDockedMode(comp, 'bottom') ? offset : 0); 27625 // No scrolling context, so just window 27626 return {
27627 bounds: bounds(boundsWithoutOffset.x, top, boundsWithoutOffset.width, height), 27628 optScrollEnv: Optional.none() 27629 }; 27630 }, (sc) => { 27631 // TINY-9411: Implement sticky toolbar offsets in scrollable containers 27632 const combinedBounds = getBoundsFrom(sc); 27633 return { 27634 bounds: combinedBounds, 27635 optScrollEnv: Optional.some({ 27636 currentScrollTop: sc.element.dom.scrollTop, 27637 scrollElmTop: absolute$3(sc.element).top 27638 }) 27639 }; 27640 }); 27641 }, 27642 modes: [sharedBackstage.header.getDockingMode()], 27643 onDocked: onDockingSwitch, 27644 onUndocked: onDockingSwitch 27645 }), 27646 ...additionalBehaviours 27647 ]; 27648 }; 27649 27650 var StickyHeader = /*#__PURE__*/Object.freeze({ 27651 __proto__: null, 27652 setup: setup$9, 27653 isDocked: isDocked, 27654 getBehaviours: getBehaviours 27655 }); 27656 27657 const renderHeader = (spec) => { 27658 const editor = spec.editor; 27659 const getBehaviours$2 = spec.sticky ? getBehaviours : getBehaviours$1; 27660 return { 27661 uid: spec.uid, 27662 dom: spec.dom, 27663 components: spec.components, 27664 behaviours: derive$1(getBehaviours$2(editor, spec.sharedBackstage)) 27665 }; 27666 }; 27667 27668 const factory$3 = (detail, spec) => { 27669 const setMenus = (comp, menus) => { 27670 const newMenus = map$2(menus, (m) => { 27671 const buttonSpec = { 27672 type: 'menubutton', 27673 text: m.text, 27674 fetch: (callback) => { 27675 callback(m.getItems()); 27676 }, 27677 context: 'any' 27678 }; 27679 // Convert to an internal bridge spec 27680 const internal = createMenuButton(buttonSpec).mapError((errInfo) => formatError(errInfo)).getOrDie(); 27681 return renderMenuButton(internal, "tox-mbtn" /* MenuButtonClasses.Button */, spec.backstage, 27682 // https://www.w3.org/TR/wai-aria-practices/examples/menubar/menubar-2/menubar-2.html 27683 Optional.some('menuitem')); 27684 }); 27685 Replacing.set(comp, newMenus); 27686 }; 27687 const apis = { 27688 focus: Keying.focusIn, 27689 setMenus 27690 }; 27691 return { 27692 uid: detail.uid, 27693 dom: detail.dom, 27694 components: [], 27695 behaviours: derive$1([ 27696 Replacing.config({}), 27697 config('menubar-events', [ 27698 runOnAttached((component) => { 27699 detail.onSetup(component); 27700 }), 27701 run$1(mouseover(), (comp, se) => { 27702 // TODO: Use constants 27703 descendant(comp.element, '.' + "tox-mbtn--active" /* MenuButtonClasses.Active */).each((activeButton) => { 27704 closest$3(se.event.target, '.' + "tox-mbtn" /* MenuButtonClasses.Button */).each((hoveredButton) => { 27705 if (!eq(activeButton, hoveredButton)) { 27706 // Now, find the components, and expand the hovered one, and close the active one 27707 comp.getSystem().getByDom(activeButton).each((activeComp) => { 27708 comp.getSystem().getByDom(hoveredButton).each((hoveredComp) => { 27709 Dropdown.expand(hoveredComp); 27710 Dropdown.close(activeComp); 27711 Focusing.focus(hoveredComp); 27712 }); 27713 }); 27714 } 27715 }); 27716 }); 27717 }), 27718 run$1(focusShifted(), (comp, se) => { 27719 se.event.prevFocus.bind((prev) => comp.getSystem().getByDom(prev).toOptional()).each((prev) => { 27720 se.event.newFocus.bind((nu) => comp.getSystem().getByDom(nu).toOptional()).each((nu) => { 27721 if (Dropdown.isOpen(prev)) { 27722 Dropdown.expand(nu); 27723 Dropdown.close(prev); 27724 } 27725 }); 27726 }); 27727 }) 27728 ]), 27729 Keying.config({ 27730 mode: 'flow', 27731 selector: '.' + "tox-mbtn" /* MenuButtonClasse
27731s.Button */, 27732 onEscape: (comp) => { 27733 detail.onEscape(comp); 27734 return Optional.some(true); 27735 } 27736 }), 27737 Tabstopping.config({}) 27738 ]), 27739 apis, 27740 domModification: { 27741 attributes: { 27742 role: 'menubar' 27743 } 27744 } 27745 }; 27746 }; 27747 var SilverMenubar = single({ 27748 factory: factory$3, 27749 name: 'silver.Menubar', 27750 configFields: [ 27751 required$1('dom'), 27752 required$1('uid'), 27753 required$1('onEscape'), 27754 required$1('backstage'), 27755 defaulted('onSetup', noop) 27756 ], 27757 apis: { 27758 focus: (apis, comp) => { 27759 apis.focus(comp); 27760 }, 27761 setMenus: (apis, comp, menus) => { 27762 apis.setMenus(comp, menus); 27763 } 27764 } 27765 }); 27766 27767 const promotionMessage = 'ð Get all features'; 27768 const promotionLink = 'https://www.tiny.cloud/tinymce-upgrade-to-cloud/?utm_campaign=self_hosted_upgrade_promo&utm_source=tiny&utm_medium=referral'; 27769 const renderPromotion = (spec) => { 27770 const components = spec.promotionLink ? [ 27771 { 27772 dom: { 27773 tag: 'a', 27774 attributes: { 27775 'href': promotionLink, 27776 'rel': 'noopener', 27777 'target': '_blank', 27778 'aria-hidden': 'true' 27779 }, 27780 classes: ['tox-promotion-link'], 27781 innerHtml: promotionMessage 27782 } 27783 } 27784 ] : []; 27785 return { 27786 uid: spec.uid, 27787 dom: spec.dom, 27788 components 27789 }; 27790 }; 27791 27792 const setup$8 = (editor) => { 27793 const { sidebars } = editor.ui.registry.getAll(); 27794 // Setup each registered sidebar 27795 each$1(keys(sidebars), (name) => { 27796 const spec = sidebars[name]; 27797 const isActive = () => is$1(Optional.from(editor.queryCommandValue('ToggleSidebar')), name); 27798 editor.ui.registry.addToggleButton(name, { 27799 icon: spec.icon, 27800 tooltip: spec.tooltip, 27801 onAction: (buttonApi) => { 27802 editor.execCommand('ToggleSidebar', false, name); 27803 buttonApi.setActive(isActive()); 27804 }, 27805 onSetup: (buttonApi) => { 27806 buttonApi.setActive(isActive()); 27807 const handleToggle = () => buttonApi.setActive(isActive()); 27808 editor.on('ToggleSidebar', handleToggle); 27809 return () => { 27810 editor.off('ToggleSidebar', handleToggle); 27811 }; 27812 }, 27813 context: 'any' 27814 }); 27815 }); 27816 }; 27817 const getApi = (comp) => ({ 27818 element: () => comp.element.dom 27819 }); 27820 const makePanels = (parts, panelConfigs) => { 27821 const specs = map$2(keys(panelConfigs), (name) => { 27822 const spec = panelConfigs[name]; 27823 const bridged = getOrDie(createSidebar(spec)); 27824 return { 27825 name, 27826 getApi, 27827 onSetup: bridged.onSetup, 27828 onShow: bridged.onShow, 27829 onHide: bridged.onHide 27830 }; 27831 }); 27832 return map$2(specs, (spec) => { 27833 const editorOffCell = Cell(noop); 27834 return parts.slot(spec.name, { 27835 dom: { 27836 tag: 'div', 27837 classes: ['tox-sidebar__pane'] 27838 }, 27839 behaviours: SimpleBehaviours.unnamedEvents([ 27840 onControlAttached(spec, editorOffCell), 27841 onControlDetached(spec, editorOffCell), 27842 run$1(slotVisibility(), (sidepanel, se) => { 27843 const data = se.event; 27844 const optSidePanelSpec = find$5(specs, (config) => config.name === data.name); 27845 optSidePanelSpec.each((sidePanelSpec) => { 27846 const handler = data.visible ? sidePanelSpec.onShow : sidePanelSpec.onHide; 27847 handler(sidePanelSpec.getApi(sidepanel)); 27848 }); 27849 }) 27850 ]) 27851 }); 27852 }); 27853 }; 27854 const makeSidebar = (panelConfigs) =>
27854 SlotContainer.sketch((parts) => ({ 27855 dom: { 27856 tag: 'div', 27857 classes: ['tox-sidebar__pane-container'] 27858 }, 27859 components: makePanels(parts, panelConfigs), 27860 slotBehaviours: SimpleBehaviours.unnamedEvents([ 27861 runOnAttached((slotContainer) => SlotContainer.hideAllSlots(slotContainer)) 27862 ]) 27863 })); 27864 const setSidebar = (sidebar, panelConfigs, showSidebar) => { 27865 const optSlider = Composing.getCurrent(sidebar); 27866 optSlider.each((slider) => { 27867 Replacing.set(slider, [makeSidebar(panelConfigs)]); 27868 // Show the default sidebar 27869 const configKey = showSidebar?.toLowerCase(); 27870 if (isString(configKey) && has$2(panelConfigs, configKey)) { 27871 Composing.getCurrent(slider).each((slotContainer) => { 27872 SlotContainer.showSlot(slotContainer, configKey); 27873 Sliding.immediateGrow(slider); 27874 // TINY-8710: Remove the width as since the skins/styles won't have loaded yet, so it's going to be incorrect 27875 remove$6(slider.element, 'width'); 27876 updateSidebarRoleOnToggle(sidebar.element, "region" /* SidebarStateRoleAttr.Grown */); 27877 }); 27878 } 27879 }); 27880 }; 27881 const updateSidebarRoleOnToggle = (sidebar, sidebarState) => { 27882 set$9(sidebar, 'role', sidebarState); 27883 }; 27884 const toggleSidebar = (sidebar, name) => { 27885 const optSlider = Composing.getCurrent(sidebar); 27886 optSlider.each((slider) => { 27887 const optSlotContainer = Composing.getCurrent(slider); 27888 optSlotContainer.each((slotContainer) => { 27889 if (Sliding.hasGrown(slider)) { 27890 if (SlotContainer.isShowing(slotContainer, name)) { 27891 // close the slider and then hide the slot after the animation finishes 27892 Sliding.shrink(slider); 27893 updateSidebarRoleOnToggle(sidebar.element, "presentation" /* SidebarStateRoleAttr.Shrunk */); 27894 } 27895 else { 27896 SlotContainer.hideAllSlots(slotContainer); 27897 SlotContainer.showSlot(slotContainer, name); 27898 updateSidebarRoleOnToggle(sidebar.element, "region" /* SidebarStateRoleAttr.Grown */); 27899 } 27900 } 27901 else { 27902 // Should already be hidden if the animation has finished but if it has not we hide them 27903 SlotContainer.hideAllSlots(slotContainer); 27904 SlotContainer.showSlot(slotContainer, name); 27905 Sliding.grow(slider); 27906 updateSidebarRoleOnToggle(sidebar.element, "region" /* SidebarStateRoleAttr.Grown */); 27907 } 27908 }); 27909 }); 27910 }; 27911 const whichSidebar = (sidebar) => { 27912 const optSlider = Composing.getCurrent(sidebar); 27913 return optSlider.bind((slider) => { 27914 const sidebarOpen = Sliding.isGrowing(slider) || Sliding.hasGrown(slider); 27915 if (sidebarOpen) { 27916 const optSlotContainer = Composing.getCurrent(slider); 27917 return optSlotContainer.bind((slotContainer) => find$5(SlotContainer.getSlotNames(slotContainer), (name) => SlotContainer.isShowing(slotContainer, name))); 27918 } 27919 else { 27920 return Optional.none(); 27921 } 27922 }); 27923 }; 27924 const fixSize = generate$6('FixSizeEvent'); 27925 const autoSize = generate$6('AutoSizeEvent'); 27926 const renderSidebar = (spec) => ({ 27927 uid: spec.uid, 27928 dom: { 27929 tag: 'div', 27930 classes: ['tox-sidebar'], 27931 attributes: { 27932 role: "presentation" /* SidebarStateRoleAttr.Shrunk */ 27933 } 27934 }, 27935 components: [ 27936 { 27937 dom: { 27938 tag: 'div', 27939 classes: ['tox-sidebar__slider'] 27940 }, 27941 components: [ 27942 // this will be replaced on setSidebar 27943 ], 27944 behaviours: derive$1([ 27945 Tabstopping.config({}), 27946 Focusing.config({}), // TODO use Keying and use focusIn, but need to handle if sidebar contains
27946nothing 27947 Sliding.config({ 27948 dimension: { 27949 property: 'width' 27950 }, 27951 closedClass: 'tox-sidebar--sliding-closed', 27952 openClass: 'tox-sidebar--sliding-open', 27953 shrinkingClass: 'tox-sidebar--sliding-shrinking', 27954 growingClass: 'tox-sidebar--sliding-growing', 27955 onShrunk: (slider) => { 27956 const optSlotContainer = Composing.getCurrent(slider); 27957 optSlotContainer.each(SlotContainer.hideAllSlots); 27958 emit(slider, autoSize); 27959 }, 27960 onGrown: (slider) => { 27961 emit(slider, autoSize); 27962 }, 27963 onStartGrow: (slider) => { 27964 emitWith(slider, fixSize, { width: getRaw(slider.element, 'width').getOr('') }); 27965 }, 27966 onStartShrink: (slider) => { 27967 emitWith(slider, fixSize, { width: get$c(slider.element) + 'px' }); 27968 } 27969 }), 27970 Replacing.config({}), 27971 Composing.config({ 27972 find: (comp) => { 27973 const children = Replacing.contents(comp); 27974 return head(children); 27975 } 27976 }) 27977 ]) 27978 } 27979 ], 27980 behaviours: derive$1([ 27981 ComposingConfigs.childAt(0), 27982 config('sidebar-sliding-events', [ 27983 run$1(fixSize, (comp, se) => { 27984 set$7(comp.element, 'width', se.event.width); 27985 }), 27986 run$1(autoSize, (comp, _se) => { 27987 remove$6(comp.element, 'width'); 27988 }) 27989 ]) 27990 ]) 27991 }); 27992 27993 const getBusySpec$1 = (providerBackstage) => (_root, _behaviours) => ({ 27994 dom: { 27995 tag: 'div', 27996 attributes: { 27997 'aria-label': providerBackstage.translate('Loading...'), 27998 'tabindex': '0' 27999 }, 28000 classes: ['tox-throbber__busy-spinner'] 28001 }, 28002 components: [ 28003 { 28004 dom: fromHtml('<div class="tox-spinner"><div></div><div></div><div></div></div>') 28005 } 28006 ], 28007 }); 28008 const focusBusyComponent = (throbber) => Composing.getCurrent(throbber).each((comp) => focus$4(comp.element, true)); 28009 // When the throbber is enabled, prevent the iframe from being part of the sequential keyboard navigation when Tabbing 28010 // TODO: TINY-7500 Only works for iframe mode at this stage 28011 const toggleEditorTabIndex = (editor, state) => { 28012 const tabIndexAttr = 'tabindex'; 28013 const dataTabIndexAttr = `data-mce-${tabIndexAttr}`; 28014 Optional.from(editor.iframeElement) 28015 .map(SugarElement.fromDom) 28016 .each((iframe) => { 28017 if (state) { 28018 getOpt(iframe, tabIndexAttr).each((tabIndex) => set$9(iframe, dataTabIndexAttr, tabIndex)); 28019 set$9(iframe, tabIndexAttr, -1); 28020 } 28021 else { 28022 remove$8(iframe, tabIndexAttr); 28023 getOpt(iframe, dataTabIndexAttr).each((tabIndex) => { 28024 set$9(iframe, tabIndexAttr, tabIndex); 28025 remove$8(iframe, dataTabIndexAttr); 28026 }); 28027 } 28028 }); 28029 }; 28030 /* 28031 * If the throbber has been toggled on, only focus the throbber if the editor had focus as we don't to steal focus if it is on an input or dialog 28032 * If the throbber has been toggled off, only put focus back on the editor if the throbber had focus. 28033 * The next logical focus transition from the throbber is to put it back on the editor 28034 */ 28035 const toggleThrobber = (editor, comp, state, providerBackstage) => { 28036 const element = comp.element; 28037 toggleEditorTabIndex(editor, state); 28038 if (state) { 28039 Blocking.block(comp, getBusySpec$1(providerBackstage)); 28040 remove$6(element, 'display'); 28041 remove$8(element, 'aria-hidden'); 28042 if (editor.hasFocus()) { 28043 focusBusyComponent(comp); 28044 } 28045 } 28046 else { 28047 // Get the focus of the busy component before it is removed from the DOM 28048 const throbberFocus = Composing.getCurrent(comp).exists((busyComp) => hasFocus(busyComp.element)); 28049 Blocking.unblock(comp); 28050 set$7(element, 'display', 'none'); 28051 set$9(element, 'aria-hidden', 'true'); 28052 if (throbberFocus) { 28053 editor.focus(); 28054 } 28055 } 28056 }; 28057 const renderThrobber = (spec) => ({ 28058 uid: spec.uid, 28059 dom: { 28060 tag: 'div', 28061 attributes: { 28062 'aria-hidden': 'true' 28063 }, 28064 classes: ['tox-throbber'], 28065 styles: { 28066 display: 'none' 28067 } 28068 }, 28069 behaviours: derive$1([ 28070 Replacing.config({}), 28071 Blocking.config({ 28072 focus: false 28073 }), 28074 Composing.config({ 28075 find: (comp) => head(comp.components()) 28076 }) 28077 ]), 28078 components: [] 28079 }); 28080 const isFocusEvent = (event) => event.type === 'focusin'; 28081 const isPasteBinTarget = (event) => { 28082 if (isFocusEvent(event)) { 28083 const node = event.composed ? head(event.composedPath()) : Optional.from(event.target); 28084 return node 28085 .map(SugarElement.fromDom) 28086 .filter(isElement$1) 28087 .exists((targetElm) => has(targetElm, 'mce-pastebin')); 28088 } 28089 else { 28090 return false;
28091 } 28092 }; 28093 const setup$7 = (editor, lazyThrobber, sharedBackstage) => { 28094 const throbberState = Cell(false); 28095 const timer = value$2(); 28096 const stealFocus = (e) => { 28097 if (throbberState.get() && !isPasteBinTarget(e)) { 28098 e.preventDefault(); 28099 focusBusyComponent(lazyThrobber()); 28100 editor.editorManager.setActive(editor); 28101 } 28102 }; 28103 // TODO: TINY-7500 Only worrying about iframe mode at this stage since inline mode has a number of other issues 28104 if (!editor.inline) { 28105 editor.on('PreInit', () => { 28106 // Cover focus when the editor is focused natively 28107 editor.dom.bind(editor.getWin(), 'focusin', stealFocus); 28108 // Cover stealing focus when editor.focus() is called 28109 editor.on('BeforeExecCommand', (e) => { 28110 // If skipFocus is specified as true in the command, don't focus the Throbber 28111 if (e.command.toLowerCase() === 'mcefocus' && e.value !== true) { 28112 stealFocus(e); 28113 } 28114 }); 28115 }); 28116 } 28117 const toggle = (state) => { 28118 if (state !== throbberState.get()) { 28119 throbberState.set(state); 28120 toggleThrobber(editor, lazyThrobber(), state, sharedBackstage.providers); 28121 fireAfterProgressState(editor, state); 28122 } 28123 }; 28124 editor.on('ProgressState', (e) => { 28125 timer.on(clearTimeout); 28126 if (isNumber(e.time)) { 28127 const timerId = global$a.setEditorTimeout(editor, () => toggle(e.state), e.time); 28128 timer.set(timerId); 28129 } 28130 else { 28131 toggle(e.state); 28132 timer.clear(); 28133 } 28134 }); 28135 }; 28136 28137 const renderToolbarGroupCommon = (toolbarGroup) => { 28138 const attributes = toolbarGroup.label.or(toolbarGroup.title).fold(() => ({}), (label) => ({ attributes: { 'aria-label': label } })); 28139 return { 28140 dom: { 28141 tag: 'div', 28142 classes: ['tox-toolbar__group'].concat(toolbarGroup.label.isSome() ? ['tox-toolbar__group_with_label'] : []), 28143 ...attributes 28144 }, 28145 components: [ 28146 ...(toolbarGroup.label.map((label) => { 28147 return { 28148 dom: { 28149 tag: 'span', 28150 classes: ['tox-label', 'tox-label--context-toolbar'], 28151 }, 28152 components: [text$2(label)] 28153 }; 28154 }).toArray()), 28155 ToolbarGroup.parts.items({}) 28156 ], 28157 items: toolbarGroup.items, 28158 markers: { 28159 // nav within a group breaks if disabled buttons are first in their group so skip them 28160 itemSelector: '.tox-tbtn:not([disabled]), ' + 28161 '.tox-toolbar-nav-item:not([disabled]), ' + 28162 '.tox-number-input:not([disabled])' 28163 }, 28164 tgroupBehaviours: derive$1([ 28165 Tabstopping.config({}), 28166 Focusing.config({ 28167 ignore: true 28168 }) 28169 ]) 28170 }; 28171 }; 28172 const renderToolbarGroup = (toolbarGroup) => ToolbarGroup.sketch(renderToolbarGroupCommon(toolbarGroup)); 28173 const getToolbarBehaviours = (toolbarSpec, modeName) => { 28174 const onAttached = runOnAttached((component) => { 28175 const groups = map$2(toolbarSpec.initGroups, renderToolbarGroup); 28176 Toolbar.setGroups(component, groups); 28177 }); 28178 return derive$1([ 28179 DisablingConfigs.toolbarButton(() => toolbarSpec.providers.checkUiComponentContext('any').shouldDisable), 28180 toggleOnReceive(() => toolbarSpec.providers.checkUiComponentContext('any')), 28181 Keying.config({ 28182 // Tabs between groups 28183 mode: modeName, 28184 onEscape: toolbarSpec.onEscape, 28185 visibilitySelector: '.tox-toolbar__overflow', 28186 selector: '.tox-toolbar__group' 28187 }), 28188 config('toolbar-events', [onAttached]) 28189 ]); 28190 }; 28191 const renderMoreToolbarCommon = (toolbarSpec) => { 28192 const modeName = toolbarSpec.cyclicKeying ? 'cyclic' : 'acyclic'; 28193 return { 28194 uid: toolbarSpec.uid, 28195 dom: { 28196 tag: 'div', 28197 classes: ['tox-toolbar-overlord'] 28198 }, 28199 parts: { 28200 // This already knows it is a toolbar group 28201 'overflow-group': renderToolbarGroupCommon({ 28202 title: Optional.none(), 28203 label: Optional.none(), 28204 items: [] 28205 }), 28206 'overflow-button': renderIconButtonSpec({ 28207 context: 'any', 28208 name: 'more', 28209 icon: Optional.some('more-drawer'), 28210 enabled: true, 28211 tooltip: Optional.some('Reveal or hide additional toolbar items'), 28212 primary: false, 28213 buttonType: Optional.none(), 28214 borderless: false 28215 }, Optional.none(), toolbarSpec.providers, [], 'overflow-button') 28216 }, 28217 splitToolbarBehaviours: getToolbarBehaviours(toolbarSpec, modeName) 28218 }; 28219 }; 28220 const renderFloatingMoreToolbar = (toolbarSpec) => { 28221 const baseSpec = renderMoreToolbarCommon(toolbarSpec); 28222 const overflowXOffset = 4; 28223 const primary = SplitFloatingToolbar.parts.primary({ 28224 dom: { 28225 tag: 'div', 28226 classes: ['tox-toolbar__primary'] 28227 } 28228 }); 28229 return SplitFloatingToolbar.sketch({ 28230 ...baseSpec, 28231 lazySink: toolbarSpec.getSink, 28232 getOverflowBounds: () => { 28233 // Restrict the left/right bounds to the editor header width, but don't restrict the top/bottom 28234 const headerElem = toolbarSpec.moreDrawerData.lazyHeader().element; 28235 const headerBounds = absolute$2(headerElem); 28236 const docElem = documentElement(headerElem); 28237 const docBounds = absolute$2(docElem); 28238 const height = Math.max(docElem.dom.scrollHeight, docBounds.height); 28239 return bounds(headerBounds.x + overflowXOffset, docBounds.y, headerBounds.width - overflowXOffset * 2, height); 28240 }, 28241 parts: { 28242 ...baseSpec.parts, 28243 overflow: { 28244 dom: { 28245 tag: 'div', 28246 classes: ['tox-toolbar__overflow'], 28247 attributes: toolbarSpec.attributes 28248 } 28249 } 28250 }, 28251 components: [primary], 28252 markers: { 28253 overflowToggledClass: "tox-tbtn--enabled" /* ToolbarButtonClasse
28253s.Ticked */ 28254 }, 28255 onOpened: (comp) => toolbarSpec.onToggled(comp, true), 28256 onClosed: (comp) => toolbarSpec.onToggled(comp, false) 28257 }); 28258 }; 28259 const renderSlidingMoreToolbar = (toolbarSpec) => { 28260 const primary = SplitSlidingToolbar.parts.primary({ 28261 dom: { 28262 tag: 'div', 28263 classes: ['tox-toolbar__primary'] 28264 } 28265 }); 28266 const overflow = SplitSlidingToolbar.parts.overflow({ 28267 dom: { 28268 tag: 'div', 28269 classes: ['tox-toolbar__overflow'] 28270 } 28271 }); 28272 const baseSpec = renderMoreToolbarCommon(toolbarSpec); 28273 return SplitSlidingToolbar.sketch({ 28274 ...baseSpec, 28275 components: [primary, overflow], 28276 markers: { 28277 openClass: 'tox-toolbar__overflow--open', 28278 closedClass: 'tox-toolbar__overflow--closed', 28279 growingClass: 'tox-toolbar__overflow--growing', 28280 shrinkingClass: 'tox-toolbar__overflow--shrinking', 28281 overflowToggledClass: "tox-tbtn--enabled" /* ToolbarButtonClasses.Ticked */ 28282 }, 28283 onOpened: (comp) => { 28284 // TINY-9223: This will only broadcast to the same mothership as the toolbar 28285 comp.getSystem().broadcastOn([toolbarHeightChange()], { type: 'opened' }); 28286 toolbarSpec.onToggled(comp, true); 28287 }, 28288 onClosed: (comp) => { 28289 // TINY-9223: This will only broadcast to the same mothership as the toolbar 28290 comp.getSystem().broadcastOn([toolbarHeightChange()], { type: 'closed' }); 28291 toolbarSpec.onToggled(comp, false); 28292 } 28293 }); 28294 }; 28295 const renderToolbar = (toolbarSpec) => { 28296 const modeName = toolbarSpec.cyclicKeying ? 'cyclic' : 'acyclic'; 28297 return Toolbar.sketch({ 28298 uid: toolbarSpec.uid, 28299 dom: { 28300 tag: 'div', 28301 classes: ['tox-toolbar'].concat(toolbarSpec.type === ToolbarMode$1.scrolling ? ['tox-toolbar--scrolling'] : []) 28302 }, 28303 components: [ 28304 Toolbar.parts.groups({}) 28305 ], 28306 toolbarBehaviours: getToolbarBehaviours(toolbarSpec, modeName) 28307 }); 28308 }; 28309 28310 const renderButton = (spec, providers) => { 28311 const isToggleButton = spec.type === 'togglebutton'; 28312 const optMemIcon = spec.icon 28313 .map((memIcon) => renderReplaceableIconFromPack(memIcon, providers.icons)) 28314 .map(record); 28315 const getAction = () => (comp) => { 28316 const setIcon = (newIcon) => { 28317 optMemIcon.map((memIcon) => memIcon.getOpt(comp).each((displayIcon) => { 28318 Replacing.set(displayIcon, [ 28319 renderReplaceableIconFromPack(newIcon, providers.icons) 28320 ]); 28321 })); 28322 }; 28323 const setActive = (state) => { 28324 const elm = comp.element; 28325 if (state) { 28326 add$2(elm, "tox-button--enabled" /* ViewButtonClasses.Ticked */); 28327 set$9(elm, 'aria-pressed', true); 28328 } 28329 else { 28330 remove$3(elm, "tox-button--enabled" /* ViewButtonClasses.Ticked */); 28331 remove$8(elm, 'aria-pressed'); 28332 } 28333 }; 28334 const isActive = () => has(comp.element, "tox-button--enabled" /* ViewButtonClasses.Ticked */); 28335 const focus = () => focus$4(comp.element); 28336 if (isToggleButton) { 28337 return spec.onAction({ setIcon, setActive, isActive, focus }); 28338 } 28339 if (spec.type === 'button') { 28340 return spec.onAction({ setIcon }); 28341 } 28342 }; 28343 const action = getAction(); 28344 const buttonSpec = { 28345 ...spec, 28346 name: isToggleButton ? spec.text.getOr(spec.icon.getOr('')) : spec.text ?? spec.icon.getOr(''), 28347 primary: spec.buttonType === 'primary', 28348 buttonType: Optional.from(spec.buttonType), 28349 tooltip: spec.tooltip, 28350 icon: spec.icon, 28351 enabled: true, 28352 borderless: spec.borderless 28353 }; 28354 const buttonTypeClasses = calculateClassesFromButtonType(spec.buttonType ?? 'secondary'); 28355 const optTranslatedText = isToggleButton ? spec.text.map(providers.translate) : Optional.some(prov
28355iders.translate(spec.text)); 28356 const optTranslatedTextComponed = optTranslatedText.map(text$2); 28357 const ariaLabelAttributes = buttonSpec.tooltip.or(optTranslatedText).map((al) => ({ 28358 'aria-label': providers.translate(al), 28359 })).getOr({}); 28360 const optIconSpec = optMemIcon.map((memIcon) => memIcon.asSpec()); 28361 const components = componentRenderPipeline([optIconSpec, optTranslatedTextComponed]); 28362 const hasIconAndText = spec.icon.isSome() && optTranslatedTextComponed.isSome(); 28363 const dom = { 28364 tag: 'button', 28365 classes: buttonTypeClasses 28366 .concat(...spec.icon.isSome() && !hasIconAndText ? ['tox-button--icon'] : []) 28367 .concat(...hasIconAndText ? ['tox-button--icon-and-text'] : []) 28368 .concat(...spec.borderless ? ['tox-button--naked'] : []) 28369 .concat(...spec.type === 'togglebutton' && spec.active ? ["tox-button--enabled" /* ViewButtonClasses.Ticked */] : []), 28370 attributes: ariaLabelAttributes 28371 }; 28372 const extraBehaviours = []; 28373 const iconButtonSpec = renderCommonSpec(buttonSpec, Optional.some(action), extraBehaviours, dom, components, spec.tooltip, providers); 28374 return Button.sketch(iconButtonSpec); 28375 }; 28376 28377 const renderViewButton = (spec, providers) => renderButton(spec, providers); 28378 const renderButtonsGroup = (spec, providers) => { 28379 return { 28380 dom: { 28381 tag: 'div', 28382 classes: ['tox-view__toolbar__group'], 28383 }, 28384 components: map$2(spec.buttons, (button) => renderViewButton(button, providers)) 28385 }; 28386 }; 28387 const deviceDetection = detect$1().deviceType; 28388 const isPhone = deviceDetection.isPhone(); 28389 const isTablet = deviceDetection.isTablet(); 28390 const renderViewHeader = (spec) => { 28391 let hasGroups = false; 28392 const endButtons = map$2(spec.buttons, (btnspec) => { 28393 if (btnspec.type === 'group') { 28394 hasGroups = true; 28395 return renderButtonsGroup(btnspec, spec.providers); 28396 } 28397 else { 28398 return renderViewButton(btnspec, spec.providers); 28399 } 28400 }); 28401 return { 28402 uid: spec.uid, 28403 dom: { 28404 tag: 'div', 28405 classes: [ 28406 !hasGroups ? 'tox-view__header' : 'tox-view__toolbar', 28407 ...(isPhone || isTablet ? ['tox-view--mobile', 'tox-view--scrolling'] : []) 28408 ] 28409 }, 28410 behaviours: derive$1([ 28411 Focusing.config({}), 28412 Keying.config({ 28413 mode: 'flow', 28414 selector: 'button, .tox-button', 28415 focusInside: FocusInsideModes.OnEnterOrSpaceMode 28416 }) 28417 ]), 28418 components: hasGroups ? 28419 endButtons 28420 : [ 28421 Container.sketch({ 28422 dom: { 28423 tag: 'div', 28424 classes: ['tox-view__header-start'] 28425 }, 28426 components: [] 28427 }), 28428 Container.sketch({ 28429 dom: { 28430 tag: 'div', 28431 classes: ['tox-view__header-end'] 28432 }, 28433 components: endButtons 28434 }) 28435 ] 28436 }; 28437 }; 28438 const renderViewPane = (spec) => { 28439 return { 28440 uid: spec.uid, 28441 behaviours: derive$1([ 28442 Focusing.config({}), 28443 Tabstopping.config({}) 28444 ]), 28445 dom: { 28446 tag: 'div', 28447 classes: ['tox-view__pane'] 28448 } 28449 }; 28450 }; 28451 const factory$2 = (detail, components, _spec, _externals) => { 28452 const apis = { 28453 getPane: (comp) => parts$g.getPart(comp, detail, 'pane'), 28454 getOnShow: (_comp) => detail.viewConfig.onShow, 28455 getOnHide: (_comp) => detail.viewConfig.onHide, 28456 }; 28457 return { 28458 uid: detail.uid, 28459 dom: detail.dom, 28460 components, 28461 behaviours: derive$1([ 28462 Focusing.config({}), 28463 Keying.config({ 28464 mode: 'cyclic', 28465 focusInside: FocusInsideModes.OnEnterOrSpaceMode 28466 }) 28467 ]), 28468 apis 28469 }; 28470 }; 28471 var View = composite({ 28472 name: 'silver.View', 28473 configFields: [ 28474 required$1('viewConfig'), 28475 ], 28476 partFields: [ 28477 optional({ 28478 factory: { 28479 sketch: renderViewHeader 28480 }, 28481 schema: [ 28482 required$1('buttons'),
vendor: 4,353 bytes, lines 28483-28589
28483 required$1('providers') 28484 ], 28485 name: 'header' 28486 }), 28487 optional({ 28488 factory: { 28489 sketch: renderViewPane 28490 }, 28491 schema: [], 28492 name: 'pane' 28493 }) 28494 ], 28495 factory: factory$2, 28496 apis: { 28497 getPane: (apis, comp) => apis.getPane(comp), 28498 getOnShow: (apis, comp) => apis.getOnShow(comp), 28499 getOnHide: (apis, comp) => apis.getOnHide(comp) 28500 } 28501 }); 28502 28503 const makeViews = (parts, viewConfigs, providers) => { 28504 return mapToArray(viewConfigs, (config, name) => { 28505 const internalViewConfig = getOrDie(createView(config)); 28506 return parts.slot(name, View.sketch({ 28507 dom: { 28508 tag: 'div', 28509 classes: ['tox-view'] 28510 }, 28511 viewConfig: internalViewConfig, 28512 components: [ 28513 ...internalViewConfig.buttons.length > 0 ? [ 28514 View.parts.header({ 28515 buttons: internalViewConfig.buttons, 28516 providers 28517 }) 28518 ] : [], 28519 View.parts.pane({}) 28520 ] 28521 })); 28522 }); 28523 }; 28524 const makeSlotContainer = (viewConfigs, providers) => SlotContainer.sketch((parts) => ({ 28525 dom: { 28526 tag: 'div', 28527 classes: ['tox-view-wrap__slot-container'] 28528 }, 28529 components: makeViews(parts, viewConfigs, providers), 28530 slotBehaviours: SimpleBehaviours.unnamedEvents([ 28531 runOnAttached((slotContainer) => SlotContainer.hideAllSlots(slotContainer)) 28532 ]) 28533 })); 28534 const getCurrentName = (slotContainer) => { 28535 return find$5(SlotContainer.getSlotNames(slotContainer), (name) => SlotContainer.isShowing(slotContainer, name)); 28536 }; 28537 const hideContainer = (comp) => { 28538 const element = comp.element; 28539 set$7(element, 'display', 'none'); 28540 set$9(element, 'aria-hidden', 'true'); 28541 }; 28542 const showContainer = (comp) => { 28543 const element = comp.element; 28544 remove$6(element, 'display'); 28545 remove$8(element, 'aria-hidden'); 28546 }; 28547 const makeViewInstanceApi = (slot) => ({ 28548 getContainer: constant$1(slot) 28549 }); 28550 const runOnPaneWithInstanceApi = (slotContainer, name, get) => { 28551 SlotContainer.getSlot(slotContainer, name).each((view) => { 28552 View.getPane(view).each((pane) => { 28553 const onCallback = get(view); 28554 onCallback(makeViewInstanceApi(pane.element.dom)); 28555 }); 28556 }); 28557 }; 28558 const runOnShow = (slotContainer, name) => runOnPaneWithInstanceApi(slotContainer, name, View.getOnShow); 28559 const runOnHide = (slotContainer, name) => runOnPaneWithInstanceApi(slotContainer, name, View.getOnHide); 28560 const factory$1 = (detail, spec) => { 28561 const setViews = (comp, viewConfigs) => { 28562 Replacing.set(comp, [makeSlotContainer(viewConfigs, spec.backstage.shared.providers)]); 28563 }; 28564 const whichView = (comp) => { 28565 return Composing.getCurrent(comp).bind(getCurrentName); 28566 }; 28567 const toggleView = (comp, showMainView, hideMainView, name) => { 28568 return Composing.getCurrent(comp).exists((slotContainer) => { 28569 const optCurrentSlotName = getCurrentName(slotContainer); 28570 const isTogglingCurrentView = optCurrentSlotName.exists((current) => name === current); 28571 const exists = SlotContainer.getSlot(slotContainer, name).isSome(); 28572 if (exists) { 28573 SlotContainer.hideAllSlots(slotContainer); 28574 if (!isTogglingCurrentView) { 28575 hideMainView(); 28576 showContainer(comp); 28577 SlotContainer.showSlot(slotContainer, name); 28578 runOnShow(slotContainer, name); 28579 } 28580 else { 28581 hideContainer(comp); 28582 showMainView(); 28583 } 28584 optCurrentSlotName.each((prevName) => runOnHide(slotContainer, prevName)); 28585 } 28586 return exists; 28587 }); 28588 }; 28589 const apis = {
28590 setViews, 28591 whichView, 28592 toggleView 28593 }; 28594 return { 28595 uid: detail.uid, 28596 dom: { 28597 tag: 'div', 28598 classes: ['tox-view-wrap'], 28599 attributes: { 'aria-hidden': 'true' }, 28600 styles: { display: 'none' } 28601 }, 28602 components: [ 28603 // this will be replaced on setViews 28604 ], 28605 behaviours: derive$1([ 28606 Replacing.config({}), 28607 Composing.config({ 28608 find: (comp) => { 28609 const children = Replacing.contents(comp); 28610 return head(children); 28611 } 28612 }) 28613 ]), 28614 apis 28615 }; 28616 }; 28617 var ViewWrapper = single({ 28618 factory: factory$1, 28619 name: 'silver.ViewWrapper', 28620 configFields: [ 28621 required$1('backstage') 28622 ], 28623 apis: { 28624 setViews: (apis, comp, views) => apis.setViews(comp, views), 28625 toggleView: (apis, comp, outerContainer, editorCont, name) => apis.toggleView(comp, outerContainer, editorCont, name), 28626 whichView: (apis, comp) => apis.whichView(comp) 28627 } 28628 }); 28629 28630 const factory = (detail, components, _spec) => { 28631 let toolbarDrawerOpenState = false; 28632 const toggleStatusbar = (editorContainer) => { 28633 sibling(editorContainer, '.tox-statusbar').each((statusBar) => { 28634 if (get$e(statusBar, 'display') === 'none' && get$g(statusBar, 'aria-hidden') === 'true') { 28635 remove$6(statusBar, 'display'); 28636 remove$8(statusBar, 'aria-hidden'); 28637 } 28638 else { 28639 set$7(statusBar, 'display', 'none'); 28640 set$9(statusBar, 'aria-hidden', 'true'); 28641 } 28642 }); 28643 }; 28644 const apis = { 28645 getSocket: (comp) => { 28646 return parts$g.getPart(comp, detail, 'socket'); 28647 }, 28648 setSidebar: (comp, panelConfigs, showSidebar) => { 28649 parts$g.getPart(comp, detail, 'sidebar').each((sidebar) => setSidebar(sidebar, panelConfigs, showSidebar)); 28650 }, 28651 toggleSidebar: (comp, name) => { 28652 parts$g.getPart(comp, detail, 'sidebar').each((sidebar) => toggleSidebar(sidebar, name)); 28653 }, 28654 whichSidebar: (comp) => { 28655 return parts$g.getPart(comp, detail, 'sidebar').bind(w
28655hichSidebar).getOrNull(); 28656 }, 28657 getHeader: (comp) => { 28658 return parts$g.getPart(comp, detail, 'header'); 28659 }, 28660 getToolbar: (comp) => { 28661 return parts$g.getPart(comp, detail, 'toolbar'); 28662 }, 28663 setToolbar: (comp, groups) => { 28664 parts$g.getPart(comp, detail, 'toolbar').each((toolbar) => { 28665 const renderedGroups = map$2(groups, renderToolbarGroup); 28666 toolbar.getApis().setGroups(toolbar, renderedGroups); 28667 }); 28668 }, 28669 setToolbars: (comp, toolbars) => { 28670 parts$g.getPart(comp, detail, 'multiple-toolbar').each((mToolbar) => { 28671 const renderedToolbars = map$2(toolbars, (g) => map$2(g, renderToolbarGroup)); 28672 CustomList.setItems(mToolbar, renderedToolbars); 28673 }); 28674 }, 28675 refreshToolbar: (comp) => { 28676 const toolbar = parts$g.getPart(comp, detail, 'toolbar'); 28677 toolbar.each((toolbar) => toolbar.getApis().refresh(toolbar)); 28678 }, 28679 toggleToolbarDrawer: (comp) => { 28680 parts$g.getPart(comp, detail, 'toolbar').each((toolbar) => { 28681 mapFrom(toolbar.getApis().toggle, (toggle) => toggle(toolbar)); 28682 }); 28683 }, 28684 toggleToolbarDrawerWithoutFocusing: (comp) => { 28685 parts$g.getPart(comp, detail, 'toolbar').each((toolbar) => { 28686 mapFrom(toolbar.getApis().toggleWithoutFocusing, (toggleWithoutFocusing) => toggleWithoutFocusing(toolbar)); 28687 }); 28688 }, 28689 isToolbarDrawerToggled: (comp) => { 28690 // isOpen may not be defined on all toolbars e.g. 'scrolling' and 'wrap' 28691 return parts$g.getPart(comp, detail, 'toolbar') 28692 .bind((toolbar) => Optional.from(toolbar.getApis().isOpen).map((isOpen) => isOpen(toolbar))) 28693 .getOr(false); 28694 }, 28695 getThrobber: (comp) => { 28696 return parts$g.getPart(comp, detail, 'throbber'); 28697 }, 28698 focusToolbar: (comp) => { 28699 const optToolbar = parts$g.getPart(comp, detail, 'toolbar').orThunk(() => parts$g.getPart(comp, detail, 'multiple-toolbar')); 28700 optToolbar.each((toolbar) => { 28701 Keying.focusIn(toolbar); 28702 }); 28703 }, 28704 setMenubar: (comp, menus) => { 28705 parts$g.getPart(comp, detail, 'menubar').each((menubar) => { 28706 SilverMenubar.setMenus(menubar, menus); 28707 }); 28708 }, 28709 focusMenubar: (comp) => { 28710 parts$g.getPart(comp, detail, 'menubar').each((menubar) => { 28711 SilverMenubar.focus(menubar); 28712 }); 28713 }, 28714 setViews: (comp, viewConfigs, showView) => { 28715 parts$g.getPart(comp, detail, 'viewWrapper').each((wrapper) => { 28716 ViewWrapper.setViews(wrapper, viewConfigs); 28717 }); 28718 const configKey = showView?.toLowerCase(); 28719 if (isString(configKey) && has$2(viewConfigs, configKey)) { 28720 apis.toggleView(comp, configKey); 28721 } 28722 }, 28723 toggleView: (comp, name) => { 28724 return parts$g.getPart(comp, detail, 'viewWrapper').exists((wrapper) => ViewWrapper.toggleView(wrapper, () => apis.showMainView(comp), () => apis.hideMainView(comp), name)); 28725 }, 28726 whichView: (comp) => { 28727 return parts$g.getPart(comp, detail, 'viewWrapper').bind(ViewWrapper.whichView).getOrNull(); 28728 }, 28729 hideMainView: (comp) => { 28730 toolbarDrawerOpenState = apis.isToolbarDrawerToggled(comp); 28731 if (toolbarDrawerOpenState) { 28732 apis.toggleToolbarDrawer(comp); 28733 } 28734 parts$g.getPart(comp, detail, 'editorContainer').each((editorContainer) => { 28735 const element = editorContainer.element; 28736 toggleStatusbar(element); 28737 set$7(element, 'display', 'none'); 28738 set$9(element, 'aria-hidden', 'true'); 28739 }); 28740 }, 28741 showMainView: (comp) => { 28742 if (toolbarDrawerOpenState) { 28743 apis.toggleToolbarDrawer(comp); 28744 } 28745 parts$g.getPart(comp, detail, 'editorContainer').each((editorContainer) => { 28746 const element = editorContainer.element; 28747 toggleStatusbar(element); 28748 remove$6(element, 'display'); 28749 remove$8(element, 'aria-hidden'); 28750 }); 28751 } 28752 }; 28753 return { 28754 uid: detail.uid, 28755 dom: detail.dom, 28756 components, 28757 apis, 28758 behaviours: detail.behaviours 28759 }; 28760 }; 28761 const partMenubar = partType$1.optional({ 28762 factory: SilverMenubar, 28763 name: 'menubar', 28764 schema: [ 28765 required$1('backstage') 28766 ] 28767 }); 28768 const toolbarFactory = (spec) => { 28769 if (spec.type === ToolbarMode$1.sliding) { 28770 return renderSlidingMoreToolbar; 28771 } 28772 else if (spec.type === ToolbarMode$1.floating) { 28773 return renderFloatingMoreToolbar; 28774 } 28775 else { 28776 return renderToolbar; 28777 } 28778 }; 28779 const partMultipleToolbar = partType$1.optional({ 28780 factory: { 28781 sketch: (spec) => CustomList.sketch({ 28782 uid: spec.uid, 28783 dom: spec.dom, 28784 listBehaviours: derive$1([ 28785 Keying.config({ 28786 mode: 'acyclic', 28787 selector: '.tox-toolbar' 28788 }) 28789 ]), 28790 makeItem: () => renderToolbar({ 28791 type: spec.type, 28792 uid: generate$6('multiple-toolbar-item'), 28793 cyclicKeying: false, 28794 initGroups: [], 28795 providers: spec.providers, 28796 onEscape: () => { 28797 spec.onEscape(); 28798 return Optional.some(true); 28799 } 28800 }), 28801 setupItem: (_mToolbar, tc, data, _index) => { 28802 Toolbar.setGroups(tc, data); 28803 }, 28804 shell: true 28805 }) 28806 }, 28807 name: 'multiple-toolbar', 28808 schema: [ 28809 required$1('dom'), 28810 required$1('onEscape') 28811 ] 28812 }); 28813 const partToolbar = partType$1.optional({ 28814 factory: { 28815 sketch: (spec) => { 28816 const renderer = toolbarFactory(spec); 28817 const toolbarSpec = { 28818 type: spec.type, 28819 uid: spec.uid, 28820 onEscape: () => { 28821 spec.onEscape(); 28822 return Optional.some(true); 28823 }, 28824 onToggled: (_comp, state) => spec.onToolbarToggled(state), 28825 cyclicKeying: false, 28826 initGroups: [], 28827 getSink: spec.getSink, 28828 providers: spec.providers, 28829 moreDrawerData: { 28830 lazyToolbar: spec.lazyToolbar, 28831 lazyMoreButton: spec.lazyMoreButton, 28832 lazyHeader: spec.lazyHeader 28833 }, 28834 attributes: spec.attributes 28835 }; 28836 return renderer(toolbarSpec); 28837 } 28838 }, 28839 name: 'toolbar', 28840 schema: [ 28841 required$1('dom'), 28842 required$1('onEscape'), 28843 required$1('getSink') 28844 ] 28845 }); 28846 const partHeader = partType$1.optional({ 28847 factory: { 28848 sketch: renderHeader 28849 }, 28850 name: 'header', 28851 schema: [ 28852 required$1('dom') 28853 ] 28854 }); 28855 const partPromotion = partType$1.optional({ 28856 factory: { 28857 sketch: renderPromotion 28858 }, 28859 name: 'promotion', 28860 schema: [ 28861 required$1('dom'), 28862 required$1('promotionLink') 28863 ] 28864 }); 28865 const partSocket = partType$1.optional({ 28866 // factory: Fun.identity, 28867 name: 'socket', 28868 schema: [ 28869 required$1('dom') 28870 ] 28871 }); 28872 const partSidebar = partType$1.optional({ 28873 factory: {
vendor: 5,230 bytes, lines 28874-29008
28874 sketch: renderSidebar 28875 }, 28876 name: 'sidebar', 28877 schema: [ 28878 required$1('dom') 28879 ] 28880 }); 28881 const partThrobber = partType$1.optional({ 28882 factory: { 28883 sketch: renderThrobber 28884 }, 28885 name: 'throbber', 28886 schema: [ 28887 required$1('dom') 28888 ] 28889 }); 28890 const partViewWrapper = partType$1.optional({ 28891 factory: ViewWrapper, 28892 name: 'viewWrapper', 28893 schema: [ 28894 required$1('backstage') 28895 ] 28896 }); 28897 const renderEditorContainer = (spec) => ({ 28898 uid: spec.uid, 28899 dom: { 28900 tag: 'div', 28901 classes: ['tox-editor-container'] 28902 }, 28903 components: spec.components 28904 }); 28905 const partEditorContainer = partType$1.optional({ 28906 factory: { 28907 sketch: renderEditorContainer 28908 }, 28909 name: 'editorContainer', 28910 schema: [] 28911 }); 28912 var OuterContainer = composite({ 28913 name: 'OuterContainer', 28914 factory, 28915 configFields: [ 28916 required$1('dom'), 28917 required$1('behaviours') 28918 ], 28919 partFields: [ 28920 partHeader, 28921 partMenubar, 28922 partToolbar, 28923 partMultipleToolbar, 28924 partSocket, 28925 partSidebar, 28926 partPromotion, 28927 partThrobber, 28928 partViewWrapper, 28929 partEditorContainer 28930 ], 28931 apis: { 28932 getSocket: (apis, comp) => { 28933 return apis.getSocket(comp); 28934 }, 28935 setSidebar: (apis, comp, panelConfigs, showSidebar) => { 28936 apis.setSidebar(comp, panelConfigs, showSidebar); 28937 }, 28938 toggleSidebar: (apis, comp, name) => { 28939 apis.toggleSidebar(comp, name); 28940 }, 28941 whichSidebar: (apis, comp) => { 28942 return apis.whichSidebar(comp); 28943 }, 28944 getHeader: (apis, comp) => { 28945 return apis.getHeader(comp); 28946 }, 28947 getToolbar: (apis, comp) => { 28948 return apis.getToolbar(comp); 28949 }, 28950 setToolbar: (apis, comp, groups) => { 28951 apis.setToolbar(comp, groups); 28952 }, 28953 setToolbars: (apis, comp, toolbars) => { 28954 apis.setToolbars(comp, toolbars); 28955 }, 28956 refreshToolbar: (apis, comp) => { 28957 return apis.refreshToolbar(comp); 28958 }, 28959 toggleToolbarDrawer: (apis, comp) => { 28960 apis.toggleToolbarDrawer(comp); 28961 }, 28962 toggleToolbarDrawerWithoutFocusing: (apis, comp) => { 28963 apis.toggleToolbarDrawerWithoutFocusing(comp); 28964 }, 28965 isToolbarDrawerToggled: (apis, comp) => { 28966 return apis.isToolbarDrawerToggled(comp); 28967 }, 28968 getThrobber: (apis, comp) => { 28969 return apis.getThrobber(comp); 28970 }, 28971 // FIX: Dupe 28972 setMenubar: (apis, comp, menus) => { 28973 apis.setMenubar(comp, menus); 28974 }, 28975 focusMenubar: (apis, comp) => { 28976 apis.focusMenubar(comp); 28977 }, 28978 focusToolbar: (apis, comp) => { 28979 apis.focusToolbar(comp); 28980 }, 28981 setViews: (apis, comp, views, showView) => { 28982 apis.setViews(comp, views, showView); 28983 }, 28984 toggleView: (apis, comp, name) => { 28985 return apis.toggleView(comp, name); 28986 }, 28987 whichView: (apis, comp) => { 28988 return apis.whichView(comp); 28989 }, 28990 } 28991 }); 28992 28993 const defaultMenubar = 'file edit view insert format tools table help'; 28994 const defaultMenus = { 28995 file: { title: 'File', items: 'newdocument restoredraft | preview | importword exportpdf exportword | export print | deleteallconversations' }, 28996 edit: { title: 'Edit', items: 'undo redo | cut copy paste pastetext | selectall | searchreplace' }, 28997 view: { title: 'View', items: 'code suggestededits revisionhistory | visualaid visualchars visualblocks | spellchecker | preview fullscreen | showcomments' }, 28998 insert: { title: 'Insert', items: 'image video link media addcomment pageembed inserttemplate codesample inserttable accordion math | charmap emoticons hr | pagebreak nonbreaking anchor tableofcontents footnotes | mergetags | insertdatetime' }, 28999 format: { title: 'Format', items: 'bold italic underline strikethrough superscript subscript codeformat | styles blocks fontfamily fontsize align lineheight | forecolor backcolor | language | removeformat' }, 29000 tools: { title: 'Tools', items: 'tinymceai-chat tinymceai-review tinymceai-quickactions aidialog aishortcuts | spellchecker spellcheckerlanguage | autocorrect capitalization | a11ycheck code typography wordcount addtemplate' }, 29001 table: { title: 'Table', items: 'inserttable | cell row column | advtablesort | tableprops deletetable' }, 29002 help: { title: 'Help', items: 'help' } 29003 }; 29004 const make = (menu, registry, editor) => { 29005 const removedMenuItems = getRemovedMenuItems(editor).split(/[ ,]/); 29006 return { 29007 text: menu.title, 29008 getItems: () =>
29008 bind$3(menu.items, (i) => { 29009 const itemName = i.toLowerCase(); 29010 if (itemName.trim().length === 0) { 29011 return []; 29012 } 29013 else if (exists(removedMenuItems, (removedMenuItem) => removedMenuItem === itemName)) { 29014 return []; 29015 } 29016 else if (itemName === 'separator' || itemName === '|') { 29017 return [{ 29018 type: 'separator' 29019 }]; 29020 } 29021 else if (registry.menuItems[itemName]) { 29022 return [registry.menuItems[itemName]]; 29023 } 29024 else { 29025 return []; 29026 } 29027 }) 29028 }; 29029 }; 29030 const parseItemsString = (items) => { 29031 return items.split(' '); 29032 }; 29033 const identifyMenus = (editor, registry) => { 29034 const rawMenuData = { ...defaultMenus, ...registry.menus }; 29035 const userDefinedMenus = keys(registry.menus).length > 0; 29036 const menubar = registry.menubar === undefined || registry.menubar === true ? parseItemsString(defaultMenubar) : parseItemsString(registry.menubar === false ? '' : registry.menubar); 29037 const validMenus = filter$2(menubar, (menuName) => { 29038 const isDefaultMenu = has$2(defaultMenus, menuName); 29039 if (userDefinedMenus) { 29040 return isDefaultMenu || get$h(registry.menus, menuName).exists((menu) => has$2(menu, 'items')); 29041 } 29042 else { 29043 return isDefaultMenu; 29044 } 29045 }); 29046 const menus = map$2(validMenus, (menuName) => { 29047 const menuData = rawMenuData[menuName]; 29048 return make({ title: menuData.title, items: parseItemsString(menuData.items) }, registry, editor); 29049 }); 29050 return filter$2(menus, (menu) => { 29051 // Filter out menus that have no items, or only separators 29052 const isNotSeparator = (item) => isString(item) || item.type !== 'separator'; 29053 return menu.getItems().length > 0 && exists(menu.getItems(), isNotSeparator); 29054 }); 29055 }; 29056 29057 const fireSkinLoaded = (editor) => { 29058 const done = () => { 29059 editor._skinLoaded = true; 29060 fireSkinLoaded$1(editor); 29061 }; 29062 return () => { 29063 if (editor.initialized) { 29064 done(); 29065 } 29066 else { 29067 editor.on('init', done); 29068 } 29069 }; 29070 }; 29071 const fireSkinLoadError = (editor, err) => () => fireSkinLoadError$1(editor, { message: err }); 29072 29073 const getSkinResourceIdentifier = (editor) => { 29074 const skin = getSkin(editor); 29075 // Use falsy check to cover false, undefined/null and empty string 29076 if (!skin) { 29077 return Optional.none(); 29078 } 29079 else { 29080 return Optional.from(skin); 29081 } 29082 }; 29083 const loadStylesheet = (editor, stylesheetUrl, styleSheetLoader) => { 29084 // Ensure the stylesheet is cleaned up when the editor is destroyed 29085 editor.on('remove', () => styleSheetLoader.unload(stylesheetUrl)); 29086 return styleSheetLoader.load(stylesheetUrl); 29087 }; 29088 const loadRawCss = (editor, key, css, styleSheetLoader) => { 29089 // Ensure the stylesheet is cleaned up when the editor is destroyed 29090 editor.on('remove', () => styleSheetLoader.unloadRawCss(key)); 29091 return styleSheetLoader.loadRawCss(key, css); 29092 }; 29093 const skinIdentifierToResourceKey = (identifier, filename) => 'ui/' + identifier + '/' + filename; 29094 const getResourceValue = (resourceKey) => Optional.from(tinymce.Resource.get(resourceKey)).filter(isString); 29095 const determineCSSDecision = (editor, filenameBase, skinUrl = '') => { 29096 const resourceKey = getSkinResourceIdentifier(editor) 29097 .map((identifier) => skinIdentifierToResourceKey(identifier, `${filenameBase}.css`)); 29098 const resourceValue = resourceKey.bind(getResourceValue); 29099 return lift2(resourceKey, resourceValue, (key, css) => { 29100 return { _kind: 'load-raw', key, css }; 29101 }).getOrThunk(() => { 29102 const suffix = editor.editorManager.suffix; 29103 const skinUiCssUrl = skinUrl + `/${filenameBase}${suffix}.css`; 29104 return { _kind: 'load-stylesheet', url: skinUiCssUrl }; 29105 }); 29106 }; 29107 const loadUiSkins = (editor, skinUrl) => { 29108 const loader = editor.ui.styleSheetLoader; 29109 const decision = determineCSSDecision(editor, 'skin', skinUrl); 29110 switch (decision._kind) { 29111 case 'load-raw': 29112 const { key, css } = decision; 29113 loadRawCss(editor, key, css, loader); 29114 return Promise.resolve(); 29115 case 'load-stylesheet': 29116 const { url } = decision; 29117 return loadStylesheet(editor, url, loader); 29118 default: 29119 return Promise.resolve(); 29120 } 29121 }; 29122 const loadShadowDomUiSkins = (editor, skinUrl) => { 29123 const isInShadowRoot$1 = isInShadowRoot(SugarElement.fromDom(editor.getElement())); 29124 if (!isInShadowRoot$1) { 29125 return Promise.resolve(); 29126 } 29127 else { 29128 const loader = global$9.DOM.styleSheetLoader; 29129 const decision = determineCSSDecision(editor, 'skin.shadowdom', skinUrl); 29130 switch (decision._kind) { 29131 case 'load-raw': 29132 const { key, css } = decision; 29133 loadRawCss(editor, key, css, loader); 29134 return Promise.resolve(); 29135 case 'load-stylesheet': 29136 const { url } = decision; 29137 return loadStylesheet(editor, url, loader); 29138 default: 29139 return Promise.resolve(); 29140 } 29141 } 29142 }; 29143 const loadUiContentCSS = (editor, isInline, skinUrl) => { 29144 const filenameBase = isInline ? 'content.inline' : 'content'; 29145 const decision = determineCSSDecision(editor, filenameBase, skinUrl); 29146 if (!skinUrl) { 29147 return Promise.resolve(); 29148 } 29149 switch (decision._kind) { 29150 case 'load-raw': 29151 const { key } = decision; 29152 editor.contentCSS.push(key); 29153 return Promise.resolve(); 29154 case 'load-stylesheet': 29155 const { url } = decision; 29156 editor.contentCSS.push(url); 29157 return Promise.resolve(); 29158 default: 29159 return Promise.resolve(); 29160 } 29161 }; 29162 const loadUrlSkin = async (isInline, editor) => { 29163 const skinUrl = getSkinUrl(editor); 29164 await loadUiContentCSS(editor, isInline, skinUrl);
29165 // In Modern Inline, this is explicitly called in editor.on('focus', ...) as well as in render(). 29166 // Seems to work without, but adding a note in case things break later 29167 if (!isSkinDisabled(editor) && isString(skinUrl)) { 29168 return Promise.all([ 29169 loadUiSkins(editor, skinUrl), 29170 loadShadowDomUiSkins(editor, skinUrl) 29171 ]).then(); 29172 } 29173 }; 29174 const loadSkin = (isInline, editor) => { 29175 return loadUrlSkin(isInline, editor).then(fireSkinLoaded(editor), fireSkinLoadError(editor, 'Skin could not be loaded')); 29176 }; 29177 const iframe = curry(loadSkin, false); 29178 const inline = curry(loadSkin, true); 29179 29180 const getButtonApi = (component) => ({ 29181 isEnabled: () => !Disabling.isDisabled(component), 29182 setEnabled: (state) => Disabling.set(component, !state), 29183 setText: (text) => emitWith(component, updateMenuText, { 29184 text 29185 }), 29186 setIcon: (icon) => emitWith(component, updateMenuIcon, { 29187 icon 29188 }) 29189 }); 29190 const getToggleApi = (component) => ({ 29191 setActive: (state) => { 29192 Toggling.set(component, state); 29193 }, 29194 isActive: () => Toggling.isOn(component), 29195 isEnabled: () => !Disabling.isDisabled(component), 29196 setEnabled: (state) => Disabling.set(component, !state), 29197 setText: (text) => emitWith(component, updateMenuText, { 29198 text 29199 }), 29200 setIcon: (icon) => emitWith(component, updateMenuIcon, { 29201 icon 29202 }) 29203 }); 29204 const getTooltipAttributes = (tooltip, providersBackstage) => tooltip.map((tooltip) => ({ 29205 'aria-label': providersBackstage.translate(tooltip), 29206 })).getOr({}); 29207 const focusButtonEvent = generate$6('focus-button'); 29208 const renderCommonStructure = (optIcon, optText, tooltip, behaviours, providersBackstage, context, btnName) => { 29209 const optMemDisplayText = optText.map((text) => record(renderLabel$1(text, "tox-tbtn" /* ToolbarButtonClasses.Button */, providersBackstage))); 29210 const optMemDisplayIcon = optIcon.map((icon) => record(renderReplaceableIconFromPack(icon, providersBackstage.icons))); 29211 return { 29212 dom: { 29213 tag: 'button', 29214 classes: ["tox-tbtn" /* ToolbarButtonClasses.Button */].concat(optText.isSome() ? ["tox-tbtn--select" /* ToolbarButtonClasses.MatchWidth */] : []), 29215 attributes: { 29216 ...getTooltipAttributes(tooltip, providersBackstage), 29217 ...(isNonNullable(btnName) ? { 'data-mce-name': btnName } : {}) 29218 } 29219 }, 29220 components: componentRenderPipeline([ 29221 optMemDisplayIcon.map((mem) => mem.asSpec()), 29222 optMemDisplayText.map((mem) => mem.asSpec()), 29223 ]), 29224 eventOrder: { 29225 [mousedown()]: [ 29226 'focusing', 29227 'alloy.base.behaviour', 29228 commonButtonDisplayEvent 29229 ], 29230 [attachedToDom()]: [commonButtonDisplayEvent, 'toolbar-group-button-events'], 29231 [detachedFromDom()]: [commonButtonDisplayEvent, 'toolbar-group-button-events', 'tooltipping'] 29232 }, 29233 buttonBehaviours: derive$1([ 29234 DisablingConfigs.toolbarButton(() => providersBackstage.checkUiComponentContext(context).shouldDisable), 29235 toggleOnReceive(() => providersBackstage.checkUiComponentContext(context)), 29236 config(commonButtonDisplayEvent, [ 29237 runOnAttached((comp, _se) => forceInitialSize(comp)), 29238 run$1(updateMenuText, (comp, se) => { 29239 optMemDisplayText.bind((mem) => mem.getOpt(comp)).each((displayText) => { 29240 Replacing.set(displayText, [text$2(providersBackstage.translate(se.event.text))]); 29241 }); 29242 }), 29243 run$1(updateMenuIcon, (comp, se) => { 29244 optMemDisplayIcon.bind((mem) => mem.getOpt(comp)).each((displayIcon) => { 29245 Replacing.set(displayIcon, [renderReplaceableIconFromPack(se.event.icon, providersBackstage.icons)]); 29246 }); 29247 }), 29248 run$1(mousedown(), (button, se) => { 29249 se.event.prevent(); 29250 emit(button, focusButtonEvent); 29251 }) 29252 ])
29253 ].concat(behaviours.getOr([]))) 29254 }; 29255 }; 29256 const renderFloatingToolbarButton = (spec, backstage, identifyButtons, attributes, btnName) => { 29257 const sharedBackstage = backstage.shared; 29258 const editorOffCell = Cell(noop); 29259 const specialisation = { 29260 toolbarButtonBehaviours: [], 29261 getApi: getButtonApi, 29262 onSetup: spec.onSetup 29263 }; 29264 const behaviours = [ 29265 config('toolbar-group-button-events', [ 29266 onControlAttached(specialisation, editorOffCell), 29267 onControlDetached(specialisation, editorOffCell) 29268 ]), 29269 ...(spec.tooltip.map((t) => Tooltipping.config(backstage.shared.providers.tooltips.getConfig({ 29270 tooltipText: backstage.shared.providers.translate(t), 29271 })))).toArray() 29272 ]; 29273 return FloatingToolbarButton.sketch({ 29274 lazySink: sharedBackstage.getSink, 29275 fetch: () => Future.nu((resolve) => { 29276 resolve(map$2(identifyButtons(spec.items), renderToolbarGroup)); 29277 }), 29278 markers: { 29279 toggledClass: "tox-tbtn--enabled" /* ToolbarButtonClasses.Ticked */ 29280 }, 29281 parts: { 29282 button: renderCommonStructure(spec.icon, spec.text, spec.tooltip, Optional.some(behaviours), sharedBackstage.providers, spec.context, btnName), 29283 toolbar: { 29284 dom: { 29285 tag: 'div', 29286 classes: ['tox-toolbar__overflow'], 29287 attributes 29288 } 29289 } 29290 } 29291 }); 29292 }; 29293 const renderCommonToolbarButton = (spec, specialisation, providersBackstage, btnName) => { 29294 const editorOffCell = Cell(noop); 29295 const structure = renderCommonStructure(spec.icon, spec.text, spec.tooltip, Optional.none(), providersBackstage, spec.context, btnName); 29296 return Button.sketch({ 29297 dom: structure.dom, 29298 components: structure.components, 29299 eventOrder: toolbarButtonEventOrder, 29300 buttonBehaviours: { 29301 ...derive$1([ 29302 config('toolbar-button-events', [ 29303 onToolbarButtonExecute({ 29304 onAction: spec.onAction, 29305 getApi: specialisation.getApi 29306 }), 29307 onControlAttached(specialisation, editorOffCell), 29308 onControlDetached(specialisation, editorOffCell) 29309 ]), 29310 ...(spec.tooltip.map((t) => Tooltipping.config(providersBackstage.tooltips.getConfig({ 29311 tooltipText: providersBackstage.translate(t) + spec.shortcut.map((shortcut) => ` (${convertText(shortcut)})`).getOr(''), 29312 })))).toArray(), 29313 // Enable toolbar buttons by default 29314 DisablingConfigs.toolbarButton(() => !spec.enabled || providersBackstage.checkUiComponentContext(spec.context).shouldDisable), 29315 toggleOnReceive(() => providersBackstage.checkUiComponentContext(spec.context)) 29316 ].concat(specialisation.toolbarButtonBehaviours)), 29317 // Here we add the commonButtonDisplayEvent behaviour from the structure so we can listen 29318 // to updateMenuIcon and updateMenuText events and run the defined callbacks as they are 29319 // defined in the renderCommonStructure function and fix the size of the button onAttached. 29320 [commonButtonDisplayEvent]: structure.buttonBehaviours?.[commonButtonDisplayEvent], 29321 } 29322 }); 29323 }; 29324 const renderToolbarButton = (spec, providersBackstage, btnName) => renderToolbarButtonWith(spec, providersBackstage, [], btnName); 29325 const renderToolbarButtonWith = (spec, providersBackstage, bonusEvents, btnName) => renderCommonToolbarButton(spec, { 29326 toolbarButtonBehaviours: (bonusEvents.length > 0 ? [ 29327 // TODO: May have to pass through eventOrder if events start clashing 29328 config('toolbarButtonWith', bonusEvents) 29329 ] : []), 29330 getApi: getButtonApi, 29331 onSetup: spec.onSetup 29332 }, providersBackstage, btnName); 29333 const renderToolbarToggleButton = (spec, providersBackstage, btnName) => renderToolbarToggleButtonWith(spec, providersBackstage, [], btnName); 29334 const renderToolbarToggleButtonWith = (spec, providersBackstage, bonusEvents, btnName) => renderCommonToolbarButton(spec, { 29335 toolbarButtonBehaviours: [ 29336 Replacing.config({}), 29337 Toggling.config({ toggleClass: "tox-tbtn--enabled" /* ToolbarButtonClasse
29337s.Ticked */, aria: { mode: 'pressed' }, toggleOnExecute: false }) 29338 ].concat(bonusEvents.length > 0 ? [ 29339 // TODO: May have to pass through eventOrder if events start clashing 29340 config('toolbarToggleButtonWith', bonusEvents) 29341 ] : []), 29342 getApi: getToggleApi, 29343 onSetup: spec.onSetup 29344 }, providersBackstage, btnName); 29345 const fetchChoices = (getApi, spec, providersBackstage) => (comp) => Future.nu((callback) => spec.fetch(callback)) 29346 .map((items) => Optional.from(createTieredDataFrom(deepMerge(createPartialChoiceMenu(generate$6('menu-value'), items, (value) => { 29347 spec.onItemAction(getApi(comp), value); 29348 }, spec.columns, spec.presets, ItemResponse$1.CLOSE_ON_EXECUTE, spec.select.getOr(never), providersBackstage), { 29349 movement: deriveMenuMovement(spec.columns, spec.presets), 29350 menuBehaviours: SimpleBehaviours.unnamedEvents(spec.columns !== 'auto' ? [] : [ 29351 runOnAttached((comp, _se) => { 29352 detectSize(comp, 4, classForPreset(spec.presets)).each(({ numRows, numColumns }) => { 29353 Keying.setGridSize(comp, numRows, numColumns); 29354 }); 29355 }) 29356 ]) 29357 })))); 29358 const makeSplitButtonApi = (tooltipString, sharedBackstage, spec) => (component) => { 29359 const system = component.getSystem(); 29360 const element = component.element; 29361 const getComponents = () => { 29362 const isChevron = has(element, 'tox-split-button__chevron'); 29363 const mainOpt = isChevron ? 29364 prevSibling(element).bind((el) => system.getByDom(el).toOptional()) : 29365 Optional.some(component); 29366 const chevronOpt = isChevron ? 29367 Optional.some(component) : 29368 nextSibling(element).bind((el) => system.getByDom(el).toOptional().filter((comp) => has(comp.element, 'tox-split-button__chevron'))); 29369 return { mainOpt, chevronOpt }; 29370 }; 29371 const applyBoth = (f) => { 29372 const { mainOpt, chevronOpt } = getComponents(); 29373 mainOpt.each(f); 29374 chevronOpt.each(f); 29375 }; 29376 return { 29377 isEnabled: () => { 29378 const { mainOpt } = getComponents(); 29379 return mainOpt.exists((c) => !Disabling.isDisabled(c)); 29380 }, 29381 setEnabled: (state) => applyBoth((c) => Disabling.set(c, !state)), 29382 setText: (text) => { 29383 const { mainOpt } = getComponents(); 29384 mainOpt.each((c) => emitWith(c, updateMenuText, { text })); 29385 }, 29386 setIcon: (icon) => { 29387 const { mainOpt } = getComponents(); 29388 mainOpt.each((c) => emitWith(c, updateMenuIcon, { icon })); 29389 }, 29390 setIconFill: (id, value) => applyBoth((c) => { 29391 descendant(c.element, `svg path[class="${id}"], rect[class="${id}"]`).each((underlinePath) => { 29392 set$9(underlinePath, 'fill', value); 29393 }); 29394 }), 29395 isActive: () => { 29396 const { mainOpt } = getComponents(); 29397 return mainOpt.exists((c) => Toggling.isOn(c)); 29398 },
29399 setActive: (state) => { 29400 const { mainOpt } = getComponents(); 29401 mainOpt.each((c) => Toggling.set(c, state)); 29402 }, 29403 setTooltip: (tooltip) => { 29404 tooltipString.set(tooltip); 29405 const { mainOpt, chevronOpt } = getComponents(); 29406 mainOpt.each((c) => set$9(c.element, 'aria-label', sharedBackstage.providers.translate(tooltip))); 29407 // For chevron, use the explicit chevronTooltip if provided, otherwise fall back to default behavior 29408 const chevronTooltipText = spec.chevronTooltip 29409 .map((chevronTooltip) => sharedBackstage.providers.translate(chevronTooltip)) 29410 .getOr(sharedBackstage.providers.translate(tooltip)); 29411 chevronOpt.each((c) => set$9(c.element, 'aria-label', chevronTooltipText)); 29412 } 29413 }; 29414 }; 29415 const renderSplitButton = (spec, sharedBackstage, btnName) => { 29416 const editorOffCell = Cell(noop); 29417 const tooltipString = Cell(spec.tooltip.getOr('')); 29418 const getApi = makeSplitButtonApi(tooltipString, sharedBackstage, spec); 29419 const menuId = generate$6('tox-split-menu'); 29420 const expandedCell = Cell(false); 29421 const getAriaAttributes = () => ({ 29422 'aria-haspopup': 'menu', 29423 'aria-expanded': String(expandedCell.get()), 29424 'aria-controls': menuId 29425 }); 29426 // Helper to get ARIA label for the main button 29427 const getMainButtonAriaLabel = () => { 29428 return spec.tooltip.map((tooltip) => sharedBackstage.providers.translate(tooltip)) 29429 .getOr(sharedBackstage.providers.translate('Text color')); 29430 }; 29431 // Helper to get ARIA label and tooltip for the chevron/dropdown button 29432 const getChevronTooltip = () => { 29433 return spec.chevronTooltip 29434 .map((tooltip) => sharedBackstage.providers.translate(tooltip)) 29435 .getOrThunk(() => { 29436 const mainLabel = getMainButtonAriaLabel(); 29437 return sharedBackstage.providers.translate(['{0} menu', mainLabel]); 29438 }); 29439 }; 29440 const updateAriaExpanded = (expanded, comp) => { 29441 expandedCell.set(expanded); 29442 set$9(comp.element, 'aria-expanded', String(expanded)); 29443 }; 29444 const arrow = Dropdown.sketch({ 29445 dom: { 29446 tag: 'button', 29447 classes: ["tox-tbtn" /* ToolbarButtonClasses.Button */, 'tox-split-button__chevron'], 29448 innerHtml: get('chevron-down', sharedBackstage.providers.icons), 29449 attributes: { 29450 'aria-label': getChevronTooltip(), 29451 ...(isNonNullable(btnName) ? { 'data-mce-name': btnName + '-chevron' } : {}), 29452 ...getAriaAttributes() 29453 } 29454 }, 29455 components: [], 29456 toggleClass: "tox-tbtn--enabled" /* ToolbarButtonClasses.Ticked */, 29457 dropdownBehaviours: derive$1([ 29458 config('split-dropdown-events', [ 29459 runOnAttached((comp, _se) => forceInitialSize(comp)), 29460 onControlAttached({ getApi, onSetup: spec.onSetup }, editorOffCell), 29461 run$1('alloy-dropdown-open', (comp) => updateAriaExpanded(true, comp)), 29462 run$1('alloy-dropdown-close', (comp) => updateAriaExpanded(false, comp)), 29463 ]), 29464 DisablingConfigs.toolbarButton(() => sharedBackstage.providers.checkUiComponentContext(spec.context).shouldDisable), 29465 toggleOnReceive(() => sharedBackstage.providers.checkUiComponentContext(spec.context)), 29466 Unselecting.config({}), 29467 Tooltipping.config(sharedBackstage.providers.tooltips.getConfig({ 29468 tooltipText: getChevronTooltip(), 29469 onShow: (comp) => { 29470 if (tooltipString.get() !== spec.tooltip.getOr('')) { 29471 const chevronTooltipText = spec.chevronTooltip 29472 .map((chevronTooltip) => sharedBackstage.providers.translate(chevronTooltip)) 29473 .getOr(`${sharedBackstage.providers.translate(tooltipString.get())} menu`); 29474 Tooltipping.setComponents(comp, sharedBackstage.providers.tooltips.getComponents({ tooltipText: chevronTooltipText })); 29475 } 29476 } 29477 })) 29478 ]), 29479 lazySink: sharedBackstage.getSink, 29480 fetch: fetchChoices(getApi, spec, sharedBackstage.providers), 29481 getHotspot: (comp) => prevSibling(comp.element).bind((el) => comp.getSystem().getByDom(el).toOptional()), 29482 onOpen: (_anchor, _comp, menu) => { 29483 Highlighting.highlightBy(menu, (item) => has(item.element, 'tox-collection__item--active')); 29484 Highlighting.getHighlighted(menu).each(Keying.focusIn); 29485 }, 29486 parts: { 29487 menu: { 29488 ...part(false, spec.columns, spec.presets), 29489 dom: { 29490 ...part(false, spec.columns, spec.presets).dom, 29491 tag: 'div', 29492 attributes: { 29493 id: menuId 29494 } 29495 } 29496 } 29497 } 29498 }); 29499 const structure = renderCommonStructure(spec.icon, spec.text, Optional.none(), Optional.some([ 29500 Toggling.config({ 29501 toggleClass: "tox-tbtn--enabled" /* ToolbarButtonClasse
29501s.Ticked */, 29502 aria: spec.presets === 'color' ? { mode: 'none' } : { mode: 'pressed' }, 29503 toggleOnExecute: false 29504 }), 29505 ...(spec.tooltip.isSome() ? [ 29506 Tooltipping.config(sharedBackstage.providers.tooltips.getConfig({ 29507 tooltipText: sharedBackstage.providers.translate(spec.tooltip.getOr('')), 29508 onShow: (comp) => { 29509 if (tooltipString.get() !== spec.tooltip.getOr('')) { 29510 const translated = sharedBackstage.providers.translate(tooltipString.get()); 29511 Tooltipping.setComponents(comp, sharedBackstage.providers.tooltips.getComponents({ tooltipText: translated })); 29512 } 29513 } 29514 })) 29515 ] : []) 29516 ]), sharedBackstage.providers, spec.context, btnName); 29517 const mainButton = Button.sketch({ 29518 dom: { 29519 ...structure.dom, 29520 classes: [ 29521 "tox-tbtn" /* ToolbarButtonClasses.Button */, 29522 'tox-split-button__main' 29523 ].concat(spec.text.isSome() ? ["tox-tbtn--select" /* ToolbarButtonClasses.MatchWidth */] : []), 29524 attributes: { 29525 'aria-label': getMainButtonAriaLabel(), 29526 ...(isNonNullable(btnName) ? { 'data-mce-name': btnName } : {}) 29527 } 29528 }, 29529 components: structure.components, 29530 eventOrder: structure.eventOrder, 29531 buttonBehaviours: structure.buttonBehaviours, 29532 action: (button) => { 29533 if (spec.onAction) { 29534 const api = getApi(button); 29535 if (api.isEnabled()) { 29536 spec.onAction(api); 29537 } 29538 } 29539 } 29540 }); 29541 return [mainButton, arrow]; 29542 }; 29543 29544 const contextFormInputSelector = '.tox-toolbar-slider__input,.tox-toolbar-textfield'; 29545 const focusIn = (contextbar) => { 29546 InlineView.getContent(contextbar).each((comp) => { 29547 descendant(comp.element, contextFormInputSelector).fold(() => Keying.focusIn(comp), focus$4); 29548 }); 29549 }; 29550 // TODO: Is this really the best way to move focus out of the input when it gets disabled #TINY-11527 29551 const focusParent = (comp) => search(comp.element).each((focus) => { 29552 ancestor$1(focus, '[tabindex="-1"]').each((parent) => { 29553 focus$4(parent); 29554 }); 29555 }); 29556 29557 const forwardSlideEvent = generate$6('forward-slide'); 29558 const backSlideEvent = generate$6('backward-slide'); 29559 const changeSlideEvent = generate$6('change-slide-event'); 29560 const resizingClass = 'tox-pop--resizing'; 29561 const renderContextToolbar = (spec) => { 29562 const stack = Cell([]); 29563 const sketch = InlineView.sketch({ 29564 dom: { 29565 tag: 'div', 29566 classes: ['tox-pop'] 29567 }, 29568 fireDismissalEventInstead: { 29569 event: 'doNotDismissYet' 29570 }, 29571 onShow: (comp) => { 29572 stack.set([]); 29573 InlineView.getContent(comp).each((c) => { 29574 remove$6(c.element, 'visibility'); 29575 }); 29576 remove$3(comp.element, resizingClass); 29577 remove$6(comp.element, 'width'); 29578 }, 29579 onHide: () => { 29580 stack.set([]); 29581 spec.onHide(); 29582 }, 29583 inlineBehaviours: derive$1([ 29584 config('context-toolbar-events', [ 29585 runOnSource(transitionend(), (comp, se) => { 29586 if (se.event.raw.propertyName === 'width') { 29587 remove$3(comp.element, resizingClass); 29588 remove$6(comp.element, 'width'); 29589 } 29590 }), 29591 run$1(changeSlideEvent, (comp, se) => { 29592 const elem = comp.element; 29593 // If it was partially through a slide, clear that and measure afresh 29594 remove$6(elem, 'width'); 29595 const currentWidth = get$c(elem); 29596 const hadFocus = search(comp.element).isSome(); 29597 // Remove these so that we can property measure the width of the context form content 29598 remove$6(elem, 'left'); 29599 remove$6(elem, 'right'); 29600 remove$6(elem, 'max-width'); 29601 InlineView.setContent(comp, se.event.contents); 29602 add$2(elem, resizingClass); 29603 const newWidth = get$c(elem); 29604 // Reposition without transition to avoid it from being animated from previous position 29605 set$7(elem, 'transition', 'none'); 29606 InlineView.reposition(comp); 29607 remove$6(elem, 'transition'); 29608 set$7(elem, 'width', currentWidth + 'px'); 29609 se.event.focus.fold(() => { 29610 if (hadFocus) { 29611 focusIn(comp); 29612 } 29613 }, (f) => { 29614 active$1(getRootNode(comp.element)).fold(() => focus$4(f), (active) => {
29615 // We need this extra check since if the focus is aleady on the iframe we don't want to call focus on it again since that closes the context toolbar 29616 if (!eq(active, f)) { 29617 spec.focusElement(f); 29618 } 29619 }); 29620 }); 29621 setTimeout(() => { 29622 set$7(comp.element, 'width', newWidth + 'px'); 29623 }, 0); 29624 }), 29625 run$1(forwardSlideEvent, (comp, se) => { 29626 InlineView.getContent(comp).each((oldContents) => { 29627 stack.set(stack.get().concat([ 29628 { 29629 bar: oldContents, 29630 focus: active$1(getRootNode(comp.element)) 29631 } 29632 ])); 29633 }); 29634 emitWith(comp, changeSlideEvent, { 29635 contents: se.event.forwardContents, 29636 focus: Optional.none() 29637 }); 29638 }), 29639 run$1(backSlideEvent, (comp, _se) => { 29640 spec.onBack(); 29641 last$1(stack.get()).each((last) => { 29642 stack.set(stack.get().slice(0, stack.get().length - 1)); 29643 emitWith(comp, changeSlideEvent, { 29644 // Because we are using premade, we should have access to the same element 29645 // to give focus (although it isn't working) 29646 contents: premade(last.bar), 29647 focus: last.focus 29648 }); 29649 }); 29650 }) 29651 ]), 29652 Keying.config({ 29653 mode: 'special', 29654 onEscape: (comp) => last$1(stack.get()).fold(() => 29655 // Escape just focuses the content. It no longer closes the toolbar. 29656 spec.onEscape(), (_) => { 29657 emit(comp, backSlideEvent); 29658 return Optional.some(true); 29659 }) 29660 }) 29661 ]), 29662 lazySink: () => Result.value(spec.sink) 29663 }); 29664 return { 29665 sketch, 29666 inSubtoolbar: () => stack.get().length > 0 29667 }; 29668 }; 29669 29670 const createNavigateBackButton = (editor, backstage) => { 29671 const bridged = getOrDie(createToolbarButton({ 29672 type: 'button', 29673 icon: 'chevron-left', 29674 tooltip: 'Back', 29675 onAction: noop 29676 })); 29677 return renderToolbarButtonWith(bridged, backstage.shared.providers, [ 29678 run$1(internalToolbarButtonExecute, (comp) => { 29679 emit(comp, backSlideEvent); 29680 }) 29681 ]); 29682 }; 29683 29684 const makeTooltipText = (editor, labelWithPlaceholder, value) => isEmpty(value) ? editor.translate(labelWithPlaceholder) : editor.translate([labelWithPlaceholder, editor.translate(value)]); 29685 29686 const generateSelectItems = (backstage, spec) => { 29687 const generateItem = (rawItem, response, invalid, value) => { 29688 const translatedText = backstage.shared.providers.translate(rawItem.title); 29689 if (rawItem.type === 'separator') { 29690 return Optional.some({ 29691 type: 'separator', 29692 text: translatedText 29693 }); 29694 } 29695 else if (rawItem.type === 'submenu') { 29696 const items = bind$3(rawItem.getStyleItems(), (si) => validate(si, response, value)); 29697 if (response === 0 /* IrrelevantStyleItemResponse.Hide */ && items.length <= 0) { 29698 return Optional.none(); 29699 } 29700 else { 29701 return Optional.some({ 29702 type: 'nestedmenuitem', 29703 text: translatedText, 29704 enabled: items.length > 0, 29705 getSubmenuItems: () =>
29705 bind$3(rawItem.getStyleItems(), (si) => validate(si, response, value)) 29706 }); 29707 } 29708 } 29709 else { 29710 return Optional.some({ 29711 // ONLY TOGGLEMENUITEMS HANDLE STYLE META. 29712 // See ToggleMenuItem and ItemStructure for how it's handled. 29713 // If this type ever changes, we'll need to change that too 29714 type: 'togglemenuitem', 29715 text: translatedText, 29716 icon: rawItem.icon, 29717 active: rawItem.isSelected(value), 29718 enabled: !invalid, 29719 onAction: spec.onAction(rawItem), 29720 ...rawItem.getStylePreview().fold(() => ({}), (preview) => ({ meta: { style: preview } })) 29721 }); 29722 } 29723 }; 29724 const validate = (item, response, value) => { 29725 const invalid = item.type === 'formatter' && spec.isInvalid(item); 29726 // If we are making them disappear based on some setting 29727 if (response === 0 /* IrrelevantStyleItemResponse.Hide */) { 29728 return invalid ? [] : generateItem(item, response, false, value).toArray(); 29729 } 29730 else { 29731 return generateItem(item, response, invalid, value).toArray(); 29732 } 29733 }; 29734 const validateItems = (preItems) => { 29735 const value = spec.getCurrentValue(); 29736 const response = spec.shouldHide ? 0 /* IrrelevantStyleItemResponse.Hide */ : 1 /* IrrelevantStyleItemResponse.Disable */; 29737 return bind$3(preItems, (item) => validate(item, response, value)); 29738 }; 29739 const getFetch = (backstage, getStyleItems) => (comp, callback) => { 29740 const preItems = getStyleItems(); 29741 const items = validateItems(preItems); 29742 const menu = build(items, ItemResponse$1.CLOSE_ON_EXECUTE, backstage, { 29743 isHorizontalMenu: false, 29744 search: Optional.none() 29745 }); 29746 callback(menu); 29747 }; 29748 return { 29749 validateItems, 29750 getFetch 29751 }; 29752 }; 29753 const createMenuItems = (backstage, spec) => { 29754 const dataset = spec.dataset; // needs to be a var for tsc to understand the ternary 29755 const getStyleItems = dataset.type === 'basic' ? 29756 () => map$2(dataset.data, (d) => processBasic(d, spec.isSelectedFor, spec.getPreviewFor)) : 29757 dataset.getData; 29758 return { 29759 items: generateSelectItems(backstage, spec), 29760 getStyleItems 29761 }; 29762 }; 29763 const createSelectButton = (editor, backstage, spec, getTooltip, textUpdateEventName, btnName) => { 29764 const { items, getStyleItems } = createMenuItems(backstage, spec); 29765 const tooltipString = Cell(spec.tooltip); 29766 const getApi = (comp) => ({ 29767 getComponent: constant$1(comp), 29768 setTooltip: (tooltip) => { 29769 const translatedTooltip = backstage.shared.providers.translate(tooltip); 29770 set$9(comp.element, 'aria-label', translatedTooltip); 29771 tooltipString.set(tooltip); 29772 } 29773 }); 29774 // Set the initial text when the component is attached and then update on node changes 29775 const onSetup = (api) => { 29776 const handler = (e) => api.setTooltip(makeTooltipText(editor, getTooltip(e.value), e.value)); 29777 editor.on(textUpdateEventName, handler); 29778 return composeUnbinders(onSetupEvent(editor, 'NodeChange', (api) => { 29779 const comp = api.getComponent(); 29780 spec.updateText(comp); 29781 Disabling.set(api.getComponent(), (!editor.selection.isEditable() || getStyleItems().length === 0)); 29782 })(api), () => editor.off(textUpdateEventName, handler)); 29783 }; 29784 return renderCommonDropdown({ 29785 context: 'mode:design', 29786 text: spec.icon.isSome() ? Optional.none() : spec.text, 29787 icon: spec.icon, 29788 ariaLabel: Optional.some(spec.tooltip), 29789 tooltip: Optional.none(), // TINY-10474 - Using own tooltip config 29790 role: Optional.none(), 29791 fetch: items.getFetch(backstage, getStyleItems), 29792 onSetup, 29793 getApi, 29794 columns: 1, 29795 presets: 'normal', 29796 classes: spec.icon.isSome() ? [] : ['bespoke'], 29797 dropdownBehaviours: [ 29798 Tooltipping.config({ 29799 ...backstage.shared.providers.tooltips.getConfig({ 29800 tooltipText: backstage.shared.providers.translate(spec.tooltip), 29801 onShow: (comp) => { 29802 if (spec.tooltip !== tooltipString.get()) { 29803 const translatedTooltip = backstage.shared.providers.translate(tooltipString.get()); 29804 Tooltipping.setComponents(comp, backstage.shared.providers.tooltips.getComponents({ tooltipText: translatedTooltip })); 29805 } 29806 } 29807 }), 29808 }) 29809 ] 29810 }
29810, "tox-tbtn" /* ToolbarButtonClasses.Button */, backstage.shared, btnName); 29811 }; 29812 29813 const process = (rawFormats) => map$2(rawFormats, (item) => { 29814 let title = item, format = item; 29815 // Allow text=value block formats 29816 const values = item.split('='); 29817 if (values.length > 1) { 29818 title = values[0]; 29819 format = values[1]; 29820 } 29821 return { title, format }; 29822 }); 29823 const buildBasicStaticDataset = (data) => ({ 29824 type: 'basic', 29825 data 29826 }); 29827 var Delimiter; 29828 (function (Delimiter) { 29829 Delimiter[Delimiter["SemiColon"] = 0] = "SemiColon"; 29830 Delimiter[Delimiter["Space"] = 1] = "Space"; 29831 })(Delimiter || (Delimiter = {})); 29832 const split = (rawFormats, delimiter) => { 29833 if (delimiter === Delimiter.SemiColon) { 29834 return rawFormats.replace(/;$/, '').split(';'); 29835 } 29836 else { 29837 return rawFormats.split(' '); 29838 } 29839 }; 29840 const buildBasicSettingsDataset = (editor, settingName, delimiter) => { 29841 // eslint-disable-next-line @tinymce/no-direct-editor-options 29842 const rawFormats = editor.options.get(settingName); 29843 const data = process(split(rawFormats, delimiter)); 29844 return { 29845 type: 'basic', 29846 data 29847 }; 29848 }; 29849 29850 const menuTitle$4 = 'Align'; 29851 const getTooltipPlaceholder$4 = constant$1('Alignment {0}'); 29852 const fallbackAlignment = 'left'; 29853 const alignMenuItems = [ 29854 { title: 'Left', icon: 'align-left', format: 'alignleft', command: 'JustifyLeft' }, 29855 { title: 'Center', icon: 'align-center', format: 'aligncenter', command: 'JustifyCenter' }, 29856 { title: 'Right', icon: 'align-right', format: 'alignright', command: 'JustifyRight' }, 29857 { title: 'Justify', icon: 'align-justify', format: 'alignjustify', command: 'JustifyFull' } 29858 ]; 29859 const getSpec$4 = (editor) => { 29860 const getMatchingValue = () => find$5(alignMenuItems, (item) => editor.formatter.match(item.format)); 29861 const isSelectedFor = (format) => () => editor.formatter.match(format); 29862 const getPreviewFor = (_format) => Optional.none; 29863 const updateSelectMenuIcon = (comp) => { 29864 const match = getMatchingValue(); 29865 const alignment = match.fold(constant$1(fallbackAlignment), (item) => item.title.toLowerCase()); 29866 emitWith(comp, updateMenuIcon, { 29867 icon: `align-${alignment}` 29868 }); 29869 fireAlignTextUpdate(editor, { value: alignment }); 29870 }; 29871 const dataset = buildBasicStaticDataset(alignMenuItems); 29872 const onAction = (rawItem) => () => find$5(alignMenuItems, (item) => item.format === rawItem.format) 29873 .each((item) => editor.execCommand(item.command)); 29874 return { 29875 tooltip: makeTooltipText(editor, getTooltipPlaceholder$4(), fallbackAlignment), 29876 text: Optional.none(), 29877 icon: Optional.some('align-left'), 29878 isSelectedFor, 29879 getCurrentValue: Optional.none, 29880 getPreviewFor, 29881 onAction, 29882 updateText: updateSelectMenuIcon, 29883 dataset, 29884 shouldHide: false, 29885 isInvalid: (item) => !editor.formatter.canApply(item.format) 29886 }; 29887 }; 29888 const createAlignButton = (editor, backstage) => createSelectButton(editor, backstage, getSpec$4(editor), getTooltipPlaceholder$4, 'AlignTextUpdate', 'align'); 29889 const createAlignMenu = (editor, backstage) => { 29890 const menuItems = createMenuItems(backstage, getSpec$4(editor)); 29891 editor.ui.registry.addNestedMenuItem('align', { 29892 text: backstage.shared.providers.translate(menuTitle$4), 29893 onSetup: onSetupEditableToggle(editor), 29894 getSubmenuItems: () => menuItems.items.validateItems(menuItems.getStyleItems()) 29895 }); 29896 }; 29897 29898 const findNearest = (editor, getStyles) => { 29899 const styles = getStyles(); 29900 const formats = map$2(styles, (style) => style.format); 29901 return Optional.from(editor.formatter.closest(formats)).bind((fmt) => find$5(styles, (data) => data.format === fmt)); 29902 }; 29903 29904 const menuTitle$3 = 'Blocks'; 29905 const getTooltipPlaceholder$3 = constant$1('Block {0}'); 29906 const fallbackFormat = 'Paragraph'; 29907 const getSpec$3 = (editor) => { 29908 const isSelectedFor = (format) => () => editor.formatter.match(format); 29909 const getPreviewFor = (format) => () => { 29910 const fmt = editor.formatter.get(format); 29911 if (fmt) { 29912 return Optional.some({ 29913 tag: fmt.length > 0 ? fmt[0].inline || fmt[0].block || 'div' : 'div', 29914 styles: editor.dom.parseStyle(editor.formatter.getCssText(format)) 29915 }); 29916 } 29917 else { 29918 return Optional.none(); 29919 } 29920 }; 29921 const updateSelectMenuText = (comp) => { 29922 const detectedFormat = findNearest(editor, () => dataset.data); 29923 const text = detectedFormat.fold(constant$1(fallbackFormat), (fmt) => fmt.title); 29924 emitWith(comp, updateMenuText, { 29925 text 29926 }); 29927 fireBlocksTextUpdate(editor, { value: text }); 29928 }; 29929 const dataset = buildBasicSettingsDataset(editor, 'block_formats', Delimiter.SemiColon); 29930 return { 29931 tooltip: makeTooltipText(editor, getTooltipPlaceholder$3(), fallbackFormat), 29932 text: Optional.some(fallbackFormat), 29933 icon: Optional.none(), 29934 isSelectedFor, 29935 getCurrentValue: Optional.none, 29936 getPreviewFor, 29937 onAction: onActionToggleFormat$1(editor), 29938 updateText: updateSelectMenuText, 29939 dataset, 29940 shouldHide: false, 29941 isInvalid: (item) => !editor.formatter.canApply(item.format) 29942 }; 29943 }; 29944 const createBlocksButton = (editor, backstage) => createSelectButton(editor, backstage, getSpec$3(editor), getTooltipPlaceholder$3, 'BlocksTextUpdate', 'blocks'); 29945 // FIX: Test this! 29946 const createBlocksMenu = (editor, backstage) => { 29947 const menuItems = createMenuItems(backstage, getSpec$3(editor)); 29948 editor.ui.registry.addNestedMenuItem('blocks', { 29949 text: menuTitle$3, 29950 onSetup: onSetupEditableToggle(editor), 29951 getSubmenuItems: () =>
29951 menuItems.items.validateItems(menuItems.getStyleItems()) 29952 }); 29953 }; 29954 29955 const menuTitle$2 = 'Fonts'; 29956 const getTooltipPlaceholder$2 = constant$1('Font {0}'); 29957 const systemFont = 'System Font'; 29958 // A list of fonts that must be in a font family for the font to be recognised as the system stack 29959 // Note: Don't include 'BlinkMacSystemFont', as Chrome on Mac converts it to different names 29960 // The system font stack will be similar to the following. (Note: each has minor variants) 29961 // Oxide: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 29962 // Bootstrap: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 29963 // Wordpress: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; 29964 const systemStackFonts = ['-apple-system', 'Segoe UI', 'Roboto', 'Helvetica Neue', 'sans-serif']; 29965 // Split the fonts into an array and strip away any start/end quotes 29966 const splitFonts = (fontFamily) => { 29967 const fonts = fontFamily.split(/\s*,\s*/); 29968 return map$2(fonts, (font) => font.replace(/^['"]+|['"]+$/g, '')); 29969 }; 29970 const matchesStack = (fonts, stack) => stack.length > 0 && forall(stack, (font) => fonts.indexOf(font.toLowerCase()) > -1); 29971 const isSystemFontStack = (fontFamily, userStack) => { 29972 if (fontFamily.indexOf('-apple-system') === 0 || userStack.length > 0) { 29973 const fonts = splitFonts(fontFamily.toLowerCase()); 29974 return matchesStack(fonts, systemStackFonts) || matchesStack(fonts, userStack); 29975 } 29976 else { 29977 return false; 29978 } 29979 }; 29980 const getSpec$2 = (editor) => { 29981 const getMatchingValue = () => { 29982 const getFirstFont = (fontFamily) => fontFamily ? splitFonts(fontFamily)[0] : ''; 29983 const fontFamily = editor.queryCommandValue('FontName'); 29984 const items = dataset.data; 29985 const font = fontFamily ? fontFamily.toLowerCase() : ''; 29986 const userStack = getDefaultFontStack(editor); 29987 const matchOpt = find$5(items, (item) => { 29988 const format = item.format; 29989 return (format.toLowerCase() === font) || (getFirstFont(format).toLowerCase() === getFirstFont(font).toLowerCase()); 29990 }).orThunk(() => { 29991 return someIf(isSystemFontStack(font, userStack), { 29992 title: systemFont, 29993 format: font 29994 }); 29995 }); 29996 return { matchOpt, font: fontFamily }; 29997 }; 29998 const isSelectedFor = (item) => (valueOpt) => valueOpt.exists((value) => value.format === item); 29999 const getCurrentValue = () => { 30000 const { matchOpt } = getMatchingValue(); 30001 return matchOpt; 30002 }; 30003 const getPreviewFor = (item) => () => Optional.some({ 30004 tag: 'div', 30005 styles: item.indexOf('dings') === -1 ? { 'font-family': item } : {} 30006 }); 30007 const onAction = (rawItem) => () => { 30008 editor.undoManager.transact(() => { 30009 editor.focus(); 30010 editor.execCommand('FontName', false, rawItem.format); 30011 }); 30012 }; 30013 const updateSelectMenuText = (comp) => { 30014 const { matchOpt, font } = getMatchingValue(); 30015 const text = matchOpt.fold(constant$1(font), (item) => item.title); 30016 emitWith(comp, updateMenuText, { 30017 text 30018 }); 30019 fireFontFamilyTextUpdate(editor, { value: text }); 30020 }; 30021 const dataset = buildBasicSettingsDataset(editor, 'font_family_formats', Delimiter.SemiColon); 30022 return { 30023 tooltip: makeTooltipText(editor, getTooltipPlaceholder$2(), systemFont), 30024 text: Optional.some(systemFont), 30025 icon: Optional.none(), 30026 isSelectedFor, 30027 getCurrentValue, 30028 getPreviewFor, 30029 onAction, 30030 updateText: updateSelectMenuText, 30031 dataset, 30032 shouldHide: false, 30033 isInvalid: never 30034 }; 30035 }; 30036 const createFontFamilyButton = (editor, backstage) => createSelectButton(editor, backstage, getSpec$2(editor), getTooltipPlaceholder$2, 'FontFamilyTextUpdate', 'fontfamily'); 30037 // TODO: Test this! 30038 const createFontFamilyMenu = (editor, backstage) => { 30039 const menuItems = createMenuItems(backstage, getSpec$2(editor)); 30040 editor.ui.registry.addNestedMenuItem('fontfamily', { 30041 text: backstage.shared.providers.translate(menuTitle$2), 30042 onSetup: onSetupEditableToggle(editor), 30043 getSubmenuItems: () =>
30043 menuItems.items.validateItems(menuItems.getStyleItems()) 30044 }); 30045 }; 30046 30047 var global$1 = tinymce.util.Tools.resolve('tinymce.util.VK'); 30048 30049 const createBespokeNumberInput = (editor, backstage, spec, btnName) => { 30050 let currentComp = Optional.none(); 30051 const getValueFromCurrentComp = (comp) => comp.map((alloyComp) => Representing.getValue(alloyComp)).getOr(''); 30052 const onSetup = onSetupEvent(editor, 'NodeChange SwitchMode DisabledStateChange', (api) => { 30053 const comp = api.getComponent(); 30054 currentComp = Optional.some(comp); 30055 spec.updateInputValue(comp); 30056 Disabling.set(comp, !editor.selection.isEditable() || isDisabled(editor)); 30057 }); 30058 const getApi = (comp) => ({ getComponent: constant$1(comp) }); 30059 const editorOffCell = Cell(noop); 30060 const customEvents = generate$6('custom-number-input-events'); 30061 const changeValue = (f, fromInput, focusBack) => { 30062 const text = getValueFromCurrentComp(currentComp); 30063 const newValue = spec.getNewValue(text, f); 30064 const lenghtDelta = text.length - `${newValue}`.length; 30065 const oldStart = currentComp.map((comp) => comp.element.dom.selectionStart - lenghtDelta); 30066 const oldEnd = currentComp.map((comp) => comp.element.dom.selectionEnd - lenghtDelta); 30067 spec.onAction(newValue, focusBack); 30068 currentComp.each((comp) => { 30069 Representing.setValue(comp, newValue); 30070 if (fromInput) { 30071 oldStart.each((oldStart) => comp.element.dom.selectionStart = oldStart); 30072 oldEnd.each((oldEnd) => comp.element.dom.selectionEnd = oldEnd); 30073 } 30074 }); 30075 }; 30076 const decrease = (fromInput, focusBack) => changeValue((n, s) => n - s, fromInput, focusBack); 30077 const increase = (fromInput, focusBack) => changeValue((n, s) => n + s, fromInput, focusBack); 30078 const goToParent = (comp) => parentElement(comp.element).fold(Optional.none, (parent) => { 30079 focus$4(parent); 30080 return Optional.some(true); 30081 }); 30082 const focusInput = (comp) => { 30083 if (hasFocus(comp.element)) { 30084 firstChild(comp.element).each((input) => focus$4(input)); 30085 return Optional.some(true); 30086 } 30087 else { 30088 return Optional.none(); 30089 } 30090 }; 30091 const makeStepperButton = (action, title, tooltip, classes) => { 30092 const editorOffCellStepButton = Cell(noop); 30093 const translatedTooltip = backstage.shared.providers.translate(tooltip); 30094 const altExecuting = generate$6('altExecuting'); 30095 const onSetup = onSetupEvent(editor, 'NodeChange SwitchMode DisabledStateChange', (api) => { 30096 Disabling.set(api.getComponent(), !editor.selection.isEditable() || isDisabled(editor)); 30097 }); 30098 const onClick = (comp) => { 30099 if (!Disabling.isDisabled(comp)) { 30100 action(true); 30101 } 30102 }; 30103 return Button.sketch({ 30104 dom: { 30105 tag: 'button', 30106 attributes: { 30107 'aria-label': translatedTooltip, 30108 'data-mce-name': title 30109 }, 30110 classes: classes.concat(title) 30111 }, 30112 components: [ 30113 renderIconFromPack$1(title, backstage.shared.providers.icons) 30114 ], 30115 buttonBehaviours: derive$1([ 30116 Disabling.config({}), 30117 Tooltipping.config(backstage.shared.providers.tooltips.getConfig({ 30118 tooltipText: translatedTooltip 30119 })), 30120 config(altExecuting, [ 30121 onControlAttached({ onSetup, getApi }, editorOffCellStepButton), 30122 onControlDetached({ getApi }, editorOffCellStepButton), 30123 run$1(keydown(), (comp, se) => { 30124 if (se.event.raw.keyCode === global$1.SPACEBAR || se.event.raw.keyCode === global$1.ENTER) { 30125 if (!Disabling.isDisabled(comp)) { 30126 action(false); 30127 } 30128 } 30129 }), 30130 run$1(click(), onClick), 30131 run$1(touchend(), onClick) 30132 ]) 30133 ]), 30134 eventOrder: {
30135 [keydown()]: [altExecuting, 'keying'], 30136 [click()]: [altExecuting, 'alloy.base.behaviour'], 30137 [touchend()]: [altExecuting, 'alloy.base.behaviour'], 30138 [attachedToDom()]: ['alloy.base.behaviour', altExecuting, 'tooltipping'], 30139 [detachedFromDom()]: [altExecuting, 'tooltipping'] 30140 } 30141 }); 30142 }; 30143 const memMinus = record(makeStepperButton((focusBack) => decrease(false, focusBack), 'minus', 'Decrease font size', [])); 30144 const memPlus = record(makeStepperButton((focusBack) => increase(false, focusBack), 'plus', 'Increase font size', [])); 30145 const memInput = record({ 30146 dom: { 30147 tag: 'div', 30148 classes: ['tox-input-wrapper'] 30149 }, 30150 components: [ 30151 Input.sketch({ 30152 inputBehaviours: derive$1([ 30153 Disabling.config({}), 30154 config(customEvents, [ 30155 onControlAttached({ onSetup, getApi }, editorOffCell), 30156 onControlDetached({ getApi }, editorOffCell) 30157 ]), 30158 config('input-update-display-text', [ 30159 run$1(updateMenuText, (comp, se) => { 30160 Representing.setValue(comp, se.event.text); 30161 }), 30162 run$1(focusout(), (comp) => { 30163 spec.onAction(Representing.getValue(comp)); 30164 }), 30165 run$1(change(), (comp) => { 30166 spec.onAction(Representing.getValue(comp)); 30167 }) 30168 ]), 30169 Keying.config({ 30170 mode: 'special', 30171 onEnter: (_comp) => { 30172 changeValue(identity, true, true); 30173 return Optional.some(true); 30174 }, 30175 onEscape: goToParent, 30176 onUp: (_comp) => { 30177 increase(true, false); 30178 return Optional.some(true); 30179 }, 30180 onDown: (_comp) => { 30181 decrease(true, false); 30182 return Optional.some(true); 30183 }, 30184 onLeft: (_comp, se) => { 30185 se.cut(); 30186 return Optional.none(); 30187 }, 30188 onRight: (_comp, se) => { 30189 se.cut(); 30190 return Optional.none(); 30191 } 30192 }) 30193 ]) 30194 }) 30195 ], 30196 behaviours: derive$1([ 30197 Focusing.config({}), 30198 Keying.config({ 30199 mode: 'special', 30200 onEnter: focusInput, 30201 onSpace: focusInput, 30202 onEscape: goToParent 30203 }), 30204 config('input-wrapper-events', [ 30205 run$1(mouseover(), (comp) => { 30206 each$1([memMinus, memPlus], (button) => { 30207 const buttonNode = SugarElement.fromDom(button.get(comp).element.dom); 30208 if (hasFocus(buttonNode)) { 30209 blur$1(buttonNode); 30210 } 30211 }); 30212 }) 30213 ]) 30214 ]) 30215 }); 30216 return { 30217 dom: { 30218 tag: 'div', 30219 classes: ['tox-number-input'], 30220 attributes: { 30221 ...(isNonNullable(btnName) ? { 'data-mce-name': btnName } : {}) 30222 } 30223 }, 30224 components: [ 30225 memMinus.asSpec(), 30226 memInput.asSpec(), 30227 memPlus.asSpec() 30228 ], 30229 behaviours: derive$1([ 30230 Focusing.config({}), 30231 Keying.config({ 30232 mode: 'flow', 30233 focusInside: FocusInsideModes.OnEnterOrSpaceMode, 30234 cycles: false, 30235 selector: 'button, .tox-input-wrapper', 30236 onEscape: (wrapperComp) => { 30237 if (hasFocus(wrapperComp.element)) { 30238 return Optional.none(); 30239 } 30240 else { 30241 focus$4(wrapperComp.element); 30242 return Optional.some(true); 30243 } 30244 }, 30245 }) 30246 ]) 30247 }; 30248 }; 30249 30250 const menuTitle$1 = 'Font sizes'; 30251 const getTooltipPlaceholder$1 = constant$1('Font size {0}'); 30252 const fallbackFontSize = '12pt'; 30253 // See https://websemantics.uk/articles/font-size-conversion/ for conversions 30254 const legacyFontSizes = { 30255 '8pt': '1', 30256 '10pt': '2', 30257 '12pt': '3', 30258 '14pt': '4', 30259 '18pt': '5', 30260 '24pt': '6', 30261 '36pt': '7' 30262 }; 30263 // Note: 'xx-small', 'x-small' and 'large' are rounded up to nearest whole pt
30264 const keywordFontSizes = { 30265 'xx-small': '7pt', 30266 'x-small': '8pt', 30267 'small': '10pt', 30268 'medium': '12pt', 30269 'large': '14pt', 30270 'x-large': '18pt', 30271 'xx-large': '24pt' 30272 }; 30273 const round = (number, precision) => { 30274 const factor = Math.pow(10, precision); 30275 return Math.round(number * factor) / factor; 30276 }; 30277 const toPt = (fontSize, precision) => { 30278 if (/[0-9.]+px$/.test(fontSize)) { 30279 // Round to the nearest 0.5 30280 return round(parseInt(fontSize, 10) * 72 / 96, precision || 0) + 'pt'; 30281 } 30282 else { 30283 return get$h(keywordFontSizes, fontSize).getOr(fontSize); 30284 } 30285 }; 30286 const toLegacy = (fontSize) => get$h(legacyFontSizes, fontSize).getOr(''); 30287 const getSpec$1 = (editor) => { 30288 const getMatchingValue = () => { 30289 let matchOpt = Optional.none(); 30290 const items = dataset.data; 30291 const fontSize = editor.queryCommandValue('FontSize'); 30292 if (fontSize) { 30293 // checking for three digits after decimal point, should be precise enough 30294 for (let precision = 3; matchOpt.isNone() && precision >= 0; precision--) { 30295 const pt = toPt(fontSize, precision); 30296 const legacy = toLegacy(pt); 30297 matchOpt = find$5(items, (item) => item.format === fontSize || item.format === pt || item.format === legacy); 30298 } 30299 } 30300 return { matchOpt, size: fontSize }; 30301 }; 30302 const isSelectedFor = (item) => (valueOpt) => valueOpt.exists((value) => value.format === item); 30303 const getCurrentValue = () => { 30304 const { matchOpt } = getMatchingValue(); 30305 return matchOpt; 30306 }; 30307 const getPreviewFor = constant$1(Optional.none); 30308 const onAction = (rawItem) => () => { 30309 editor.undoManager.transact(() => { 30310 editor.focus(); 30311 editor.execCommand('FontSize', false, rawItem.format); 30312 }); 30313 }; 30314 const updateSelectMenuText = (comp) => { 30315 const { matchOpt, size } = getMatchingValue(); 30316 const text = matchOpt.fold(constant$1(size), (match) => match.title); 30317 emitWith(comp, updateMenuText, { 30318 text 30319 }); 30320 fireFontSizeTextUpdate(editor, { value: text }); 30321 }; 30322 const dataset = buildBasicSettingsDataset(editor, 'font_size_formats', Delimiter.Space); 30323 return { 30324 tooltip: makeTooltipText(editor, getTooltipPlaceholder$1(), fallbackFontSize), 30325 text: Optional.some(fallbackFontSize), 30326 icon: Optional.none(), 30327 isSelectedFor, 30328 getPreviewFor, 30329 getCurrentValue, 30330 onAction, 30331 updateText: updateSelectMenuText, 30332 dataset, 30333 shouldHide: false, 30334 isInvalid: never 30335 }; 30336 }; 30337 const createFontSizeButton = (editor, backstage) => createSelectButton(editor, backstage, getSpec$1(editor), getTooltipPlaceholder$1, 'FontSizeTextUpdate', 'fontsize'); 30338 const getConfigFromUnit = (unit) => { 30339 const baseConfig = { step: 1 }; 30340 const configs = { 30341 em: { step: 0.1 }, 30342 cm: { step: 0.1 }, 30343 in: { step: 0.1 }, 30344 pc: { step: 0.1 }, 30345 ch: { step: 0.1 }, 30346 rem: { step: 0.1 } 30347 }; 30348 return configs[unit] ?? baseConfig; 30349 }; 30350 const defaultValue = 16; 30351 const isValidValue = (value) => value >= 0; 30352 const getNumberInputSpec = (editor) => { 30353 const getCurrentValue = () => editor.queryCommandValue('FontSize'); 30354 const updateInputValue = (comp) => emitWith(comp, updateMenuText, { 30355 text: getCurrentValue() 30356 }); 30357 return { 30358 updateInputValue, 30359 onAction: (format, focusBack) => editor.execCommand('FontSize', false, format, { skip_focus: !focusBack }), 30360 getNewValue: (text, updateFunction) => { 30361 parse(text, ['unsupportedLength', 'empty']); 30362 const currentValue = getCurrentValue(); 30363 const parsedText = parse(text, ['unsupportedLength', 'empty']).or(parse(currentValue, ['unsupportedLength', 'empty'])); 30364 const value = parsedText.map((res) => res.value).getOr(defaultValue); 30365 const defaultUnit = getFontSizeInputDefaultUnit(editor); 30366 const unit = parsedText.map((res) => res.unit).filter((u) => u !== '').getOr(defaultUnit); 30367 const newValue = updateFunction(value, getConfigFromUnit(unit).step); 30368 const res = `${isValidValue(newValue) ? newValue : value}${unit}`; 30369 if (res !== currentValue) { 30370 fireFontSizeInputTextUpdate(editor, { value: res }); 30371 } 30372 return res; 30373 } 30374 }; 30375 }; 30376 const createFontSizeInputButton = (editor, backstage) => createBespokeNumberInput(editor, backstage, getNumberInputSpec(editor), 'fontsizeinput'); 30377 // TODO: Test this! 30378 const createFontSizeMenu = (editor, backstage) => { 30379 const menuItems = createMenuItems(backstage, getSpec$1(editor)); 30380 editor.ui.registry.addNestedMenuItem('fontsize', { 30381 text: menuTitle$1, 30382 onSetup: onSetupEditableToggle(editor), 30383 getSubmenuItems: () =>
30383 menuItems.items.validateItems(menuItems.getStyleItems()) 30384 }); 30385 }; 30386 30387 const menuTitle = 'Formats'; 30388 const getTooltipPlaceholder = (value) => isEmpty(value) ? 'Formats' : 'Format {0}'; 30389 const getSpec = (editor, dataset) => { 30390 const fallbackFormat = 'Formats'; 30391 const isSelectedFor = (format) => () => editor.formatter.match(format); 30392 const getPreviewFor = (format) => () => { 30393 const fmt = editor.formatter.get(format); 30394 return fmt !== undefined ? Optional.some({ 30395 tag: fmt.length > 0 ? fmt[0].inline || fmt[0].block || 'div' : 'div', 30396 styles: editor.dom.parseStyle(editor.formatter.getCssText(format)) 30397 }) : Optional.none(); 30398 }; 30399 const updateSelectMenuText = (comp) => { 30400 const getFormatItems = (fmt) => { 30401 if (isNestedFormat(fmt)) { 30402 return bind$3(fmt.items, getFormatItems); 30403 } 30404 else if (isFormatReference(fmt)) { 30405 return [{ title: fmt.title, format: fmt.format }]; 30406 } 30407 else { 30408 return []; 30409 } 30410 }; 30411 const flattenedItems = bind$3(getStyleFormats(editor), getFormatItems); 30412 const detectedFormat = findNearest(editor, constant$1(flattenedItems)); 30413 const text = detectedFormat.fold(constant$1({ 30414 title: fallbackFormat, 30415 tooltipLabel: '' 30416 }), (fmt) => ({ 30417 title: fmt.title, 30418 tooltipLabel: fmt.title 30419 })); 30420 emitWith(comp, updateMenuText, { 30421 text: text.title 30422 }); 30423 fireStylesTextUpdate(editor, { value: text.tooltipLabel }); 30424 }; 30425 return { 30426 tooltip: makeTooltipText(editor, getTooltipPlaceholder(''), ''), 30427 text: Optional.some(fallbackFormat), 30428 icon: Optional.none(), 30429 isSelectedFor, 30430 getCurrentValue: Optional.none, 30431 getPreviewFor, 30432 onAction: onActionToggleFormat$1(editor), 30433 updateText: updateSelectMenuText, 30434 shouldHide: shouldAutoHideStyleFormats(editor), 30435 isInvalid: (item) => !editor.formatter.canApply(item.format), 30436 dataset 30437 }; 30438 }; 30439 const createStylesButton = (editor, backstage) => { 30440 const dataset = { type: 'advanced', ...backstage.styles }; 30441 return createSelectButton(editor, backstage, getSpec(editor, dataset), getTooltipPlaceholder, 'StylesTextUpdate', 'styles'); 30442 }; 30443 const createStylesMenu = (editor, backstage) => { 30444 const dataset = { type: 'advanced', ...backstage.styles }; 30445 const menuItems = createMenuItems(backstage, getSpec(editor, dataset)); 30446 editor.ui.registry.addNestedMenuItem('styles', { 30447 text: menuTitle, 30448 onSetup: onSetupEditableToggle(editor, () => menuItems.getStyleItems().length > 0), 30449 getSubmenuItems: () => menuItems.items.validateItems(menuItems.getStyleItems()) 30450 }); 30451 }; 30452 30453 const defaultToolbar = [ 30454 { 30455 name: 'history', items: ['undo', 'redo'] 30456 }, 30457 { 30458 name: 'ai', items: ['tinymceai-chat', 'tinymceai-review', 'tinymceai-quickactions', 'aidialog', 'aishortcuts'] 30459 }, 30460 { 30461 name: 'styles', items: ['styles'] 30462 }, 30463 { 30464 name: 'formatting', items: ['bold', 'italic'] 30465 }, 30466 { 30467 name: 'alignment', items: ['alignleft', 'aligncenter', 'alignright', 'alignjustify'] 30468 }, 30469 { 30470 name: 'indentation', items: ['outdent', 'indent'] 30471 }, 30472 { 30473 name: 'permanent pen', items: ['permanentpen'] 30474 }, 30475 { 30476 name: 'comments', items: ['addcomment'] 30477 } 30478 ]; 30479 const renderFromBridge = (bridgeBuilder, render) => (spec, backstage, editor, btnName) => { 30480 const internal = bridgeBuilder(spec).mapError((errInfo) => formatError(errInfo)).getOrDie(); 30481 return render(internal, backstage, editor, btnName); 30482 }; 30483 const types = { 30484 button: renderFromBridge(createToolbarButton, (s, backstage, _, btnName) => renderToolbarButton(s, backstage.shared.providers, btnName)), 30485 togglebutton: renderFromBridge(createToggleButton, (s, backstage, _, btnName) => renderToolbarToggleButton(s, backstage.shared.providers, btnName)), 30486 menubutton: renderFromBridge(createMenuButton, (s, backstage, _, btnName) =>
30486 renderMenuButton(s, "tox-tbtn" /* ToolbarButtonClasses.Button */, backstage, Optional.none(), false, btnName)), 30487 splitbutton: renderFromBridge(createSplitButton, (s, backstage, _, btnName) => renderSplitButton(s, backstage.shared, btnName)), 30488 grouptoolbarbutton: renderFromBridge(createGroupToolbarButton, (s, backstage, editor, btnName) => { 30489 const buttons = editor.ui.registry.getAll().buttons; 30490 const identify = (toolbar) => identifyButtons(editor, { buttons, toolbar, allowToolbarGroups: false }, backstage, Optional.none()); 30491 const attributes = { 30492 [Attribute]: backstage.shared.header.isPositionedAtTop() ? AttributeValue.TopToBottom : AttributeValue.BottomToTop 30493 }; 30494 switch (getToolbarMode(editor)) { 30495 case ToolbarMode$1.floating: 30496 return renderFloatingToolbarButton(s, backstage, identify, attributes, btnName); 30497 default: 30498 // TODO change this message and add a case when sliding is available 30499 throw new Error('Toolbar groups are only supported when using floating toolbar mode'); 30500 } 30501 }) 30502 }; 30503 const extractFrom = (spec, backstage, editor, btnName) => get$h(types, spec.type).fold(() => { 30504 // eslint-disable-next-line no-console 30505 console.error('skipping button defined by', spec); 30506 return Optional.none(); 30507 }, (render) => Optional.some(render(spec, backstage, editor, btnName))); 30508 const bespokeButtons = { 30509 styles: createStylesButton, 30510 fontsize: createFontSizeButton, 30511 fontsizeinput: createFontSizeInputButton, 30512 fontfamily: createFontFamilyButton, 30513 blocks: createBlocksButton, 30514 align: createAlignButton, 30515 navigateback: createNavigateBackButton 30516 }; 30517 const removeUnusedDefaults = (buttons) => { 30518 const filteredItemGroups = map$2(defaultToolbar, (group) => { 30519 const items = filter$2(group.items, (subItem) => has$2(buttons, subItem) || has$2(bespokeButtons, subItem)); 30520 return { 30521 name: group.name, 30522 items 30523 }; 30524 }); 30525 return filter$2(filteredItemGroups, (group) => group.items.length > 0); 30526 }; 30527 const convertStringToolbar = (strToolbar) => { 30528 const groupsStrings = strToolbar.split('|'); 30529 return map$2(groupsStrings, (g) => ({ 30530 items: g.trim().split(' ') 30531 })); 30532 }; 30533 const isToolbarGroupSettingArray = (toolbar) => isArrayOf(toolbar, (t) => (has$2(t, 'name') || has$2(t, 'label')) && has$2(t, 'items')); 30534 // Toolbar settings 30535 // false = disabled 30536 // undefined or true = default 30537 // string = enabled with specified buttons and groups 30538 // string array = enabled with specified buttons and groups 30539 // object array = enabled with specified buttons, groups and group titles 30540 const createToolbar = (toolbarConfig) => { 30541 const toolbar = toolbarConfig.toolbar; 30542 const buttons = toolbarConfig.buttons; 30543 if (toolbar === false) { 30544 return []; 30545 } 30546 else if (toolbar === undefined || toolbar === true) { 30547 return removeUnusedDefaults(buttons); 30548 } 30549 else if (isString(toolbar)) { 30550 return convertStringToolbar(toolbar); 30551 } 30552 else if (isToolbarGroupSettingArray(toolbar)) { 30553 return toolbar; 30554 } 30555 else { 30556 // eslint-disable-next-line no-console 30557 console.error('Toolbar type should be string, string[], boolean or ToolbarGroup[]'); 30558 return []; 30559 } 30560 }; 30561 const lookupButton = (editor, buttons, toolbarItem, allowToolbarGroups, backstage, prefixes) => get$h(buttons, toolbarItem.toLowerCase()) 30562 .orThunk(() => prefixes.bind((ps) => findMap(ps, (prefix) => get$h(buttons, prefix + toolbarItem.toLowerCase())))) 30563 .fold(() => get$h(bespokeButtons, toolbarItem.toLowerCase()).map((r) => r(editor, backstage)), 30564 // TODO: Add back after TINY-3232 is implemented 30565 // .orThunk(() => { 30566 // console.error('No representation for toolbarItem: ' + toolbarItem); 30567 // return Optional.none(); 30568 // ), 30569 (spec) => { 30570 if (spec.type === 'grouptoolbarbutton' && !allowToolbarGroups) { 30571 // TODO change this message when sliding is available 30572 // eslint-disable-next-line no-console 30573 console.warn(`Ignoring the '${toolbarItem}' toolbar button. Group toolbar buttons are only supported when using floating toolbar mode and cannot be nested.`); 30574 return Optional.none(); 30575 } 30576 else { 30577 return extractFrom(spec, backstage, editor, toolbarItem.toLowerCase()); 30578 } 30579 }); 30580 const identifyButtons = (editor, toolbarConfig, backstage, prefixes) => { 30581 const toolbarGroups = createToolbar(toolbarConfig); 30582 const groups = map$2(toolbarGroups, (group) => { 30583 const items = bind$3(group.items, (toolbarItem) => { 30584 if (toolbarItem.trim().length === 0) { 30585 return []; 30586 } 30587 return lookupButton(editor, toolbarConfig.buttons, toolbarItem, toolbarConfig.allowToolbarGroups, backstage, prefixes) 30588 .map((spec) => Array.isArray(spec) ? spec : [spec]) 30589 .getOr([]); 30590 }); 30591 return { 30592 title: Optional.from(editor.translate(group.name)),
vendor: 8,313 bytes, lines 30593-30746
30593 label: someIf(group.label !== undefined, editor.translate(group.label)), 30594 items 30595 }; 30596 }); 30597 return filter$2(groups, (group) => group.items.length > 0); 30598 }; 30599 30600 // Set toolbar(s) depending on if multiple toolbars is configured or not 30601 const setToolbar = (editor, uiRefs, rawUiConfig, backstage) => { 30602 const outerContainer = uiRefs.mainUi.outerContainer; 30603 const toolbarConfig = rawUiConfig.toolbar; 30604 const toolbarButtonsConfig = rawUiConfig.buttons; 30605 // Check if toolbar type is a non-empty string array 30606 if (isArrayOf(toolbarConfig, isString)) { 30607 const toolbars = toolbarConfig.map((t) => { 30608 const config = { toolbar: t, buttons: toolbarButtonsConfig, allowToolbarGroups: rawUiConfig.allowToolbarGroups }; 30609 return identifyButtons(editor, config, backstage, Optional.none()); 30610 }); 30611 OuterContainer.setToolbars(outerContainer, toolbars); 30612 } 30613 else { 30614 OuterContainer.setToolbar(outerContainer, identifyButtons(editor, rawUiConfig, backstage, Optional.none())); 30615 } 30616 }; 30617 30618 const detection = detect$1(); 30619 const isiOS12 = detection.os.isiOS() && detection.os.version.major <= 12; 30620 const setupEvents$1 = (editor, uiRefs) => { 30621 const { uiMotherships } = uiRefs; 30622 const dom = editor.dom; 30623 let contentWindow = editor.getWin(); 30624 const initialDocEle = editor.getDoc().documentElement; 30625 const lastWindowDimensions = Cell(SugarPosition(contentWindow.innerWidth, contentWindow.innerHeight)); 30626 const lastDocumentDimensions = Cell(SugarPosition(initialDocEle.offsetWidth, initialDocEle.offsetHeight)); 30627 const resizeWindow = () => { 30628 // Check if the window dimensions have changed and if so then trigger a content resize event 30629 const outer = lastWindowDimensions.get(); 30630 if (outer.left !== contentWindow.innerWidth || outer.top !== contentWindow.innerHeight) { 30631 lastWindowDimensions.set(SugarPosition(contentWindow.innerWidth, contentWindow.innerHeight)); 30632 fireResizeContent(editor); 30633 } 30634 }; 30635 const resizeDocument = () => { 30636 // Don't use the initial doc ele, as there's a small chance it may have changed 30637 const docEle = editor.getDoc().documentElement; 30638 // Check if the document dimensions have changed and if so then trigger a content resize event 30639 const inner = lastDocumentDimensions.get(); 30640 if (inner.left !== docEle.offsetWidth || inner.top !== docEle.offsetHeight) { 30641 lastDocumentDimensions.set(SugarPosition(docEle.offsetWidth, docEle.offsetHeight)); 30642 fireResizeContent(editor); 30643 } 30644 }; 30645 const scroll = (e) => { 30646 fireScrollContent(editor, e); 30647 }; 30648 dom.bind(contentWindow, 'resize', resizeWindow); 30649 dom.bind(contentWindow, 'scroll', scroll); 30650 // Bind to async load events and trigger a content resize event if the size has changed 30651 const elementLoad = capture(SugarElement.fromDom(editor.getBody()), 'load', resizeDocument); 30652 // We want to hide ALL UI motherships here. 30653 editor.on('hide', () => { 30654 each$1(uiMotherships, (m) => { 30655 set$7(m.element, 'display', 'none'); 30656 }); 30657 }); 30658 editor.on('show', () => { 30659 each$1(uiMotherships, (m) => { 30660 remove$6(m.element, 'display'); 30661 }); 30662 }); 30663 editor.on('NodeChange', resizeDocument); 30664 editor.on('remove', () => { 30665 elementLoad.unbind(); 30666 dom.unbind(contentWindow, 'resize', resizeWindow); 30667 dom.unbind(contentWindow, 'scroll', scroll); 30668 // Clean memory for IE 30669 contentWindow = null; 30670 }); 30671 }; 30672 // TINY-9226: When set, the `ui_mode: split` option will create two different sinks (one for popups and one for sinks) 30673 // and the popup sink will be placed adjacent to the editor. This will make it having the same scrolling ancestry. 30674 const attachUiMotherships = (editor, uiRoot, uiRefs) => { 30675 if (isSplitUiMode(editor)) { 30676 attachSystemAfter(uiRefs.mainUi.mothership.element, uiRefs.popupUi.mothership); 30677 } 30678 // In UiRefs, dialogUi and popupUi refer to the same thing if ui_mode: combined 30679 attachSystem(uiRoot, uiRefs.dialogUi.mothership); 30680 }; 30681 const render$1 = (editor, uiRefs, rawUiConfig, backstage, args) => { 30682 const { mainUi, uiMotherships } = uiRefs; 30683 const lastToolbarWidth = Cell(0); 30684 const outerContainer = mainUi.outerContainer; 30685 // eslint-disable-next-line @typescript-eslint/no-floating-promises 30686 iframe(editor); 30687 const eTargetNode = SugarElement.fromDom(args.targetNode); 30688 const uiRoot = getContentContainer(getRootNode(eTargetNode)); 30689 attachSystemAfter(eTargetNode, mainUi.mothership); 30690 attachUiMotherships(editor, uiRoot, uiRefs); 30691 editor.on('PostRender', () => { 30692 OuterContainer.setSidebar(outerContainer, rawUiConfig.sidebar, getSidebarShow(editor)); 30693 OuterContainer.setViews(outerContainer, rawUiConfig.views, getViewShow(editor)); 30694 }, true); 30695 // TINY-10343: Using `SkinLoaded` instead of `PostRender` because if the skin loading takes too long you run in to rendering problems since things are measured before the CSS is being applied 30696 editor.on('SkinLoaded', () => { 30697 // Set the sidebar before the toolbar and menubar 30698 // - each sidebar has an associated toggle toolbar button that needs to check the 30699 // sidebar that is set to determine its active state on setup 30700 setToolbar(editor, uiRefs, rawUiConfig, backstage); 30701 lastToolbarWidth.set(editor.getWin().innerWidth); 30702 OuterContainer.setMenubar(outerContainer, identifyMenus(editor, rawUiConfig)); 30703 setupEvents$1(editor, uiRefs); 30704 }); 30705 const socket = OuterContainer.getSocket(outerContainer).getOrDie('Could not find expected socket element'); 30706 if (isiOS12) { 30707 setAll(socket.element, { 30708 'overflow': 'scroll', 30709 '-webkit-overflow-scrolling': 'touch' // required for ios < 13 content scrolling 30710 }); 30711 const limit = first$1(() => { 30712 editor.dispatch('ScrollContent'); 30713 }, 20); 30714 const unbinder = bind$1(socket.element, 'scroll', limit.throttle); 30715 editor.on('remove', unbinder.unbind); 30716 } 30717 setupEventsForUi(editor, uiRefs); 30718 editor.addCommand('ToggleSidebar', (_ui, value) => { 30719 OuterContainer.toggleSidebar(outerContainer, value); 30720 fireToggleSidebar(editor); 30721 }); 30722 editor.addQueryValueHandler('ToggleSidebar', () => OuterContainer.whichSidebar(outerContainer) ?? ''); 30723 editor.addCommand('ToggleView', (_ui, value) => { 30724 if (OuterContainer.toggleView(outerContainer, value)) { 30725 const target = outerContainer.element; 30726 mainUi.mothership.broadcastOn([dismissPopups()], { target }); 30727 each$1(uiMotherships, (m) => { 30728 m.broadcastOn([dismissPopups()], { target }); 30729 }); 30730 // Switching back to main view should focus the editor and update any UIs 30731 if (isNull(OuterContainer.whichView(outerContainer))) { 30732 editor.focus(); 30733 editor.nodeChanged(); 30734 OuterContainer.refreshToolbar(outerContainer); 30735 } 30736 fireToggleView(editor); 30737 } 30738 }); 30739 editor.addQueryValueHandler('ToggleView', () => OuterContainer.whichView(outerContainer) ?? ''); 30740 const toolbarMode = getToolbarMode(editor); 30741 const refreshDrawer = () => { 30742 OuterContainer.refreshToolbar(uiRefs.mainUi.outerContainer); 30743 }; 30744 if (toolbarMode === ToolbarMode$1.sliding || toolbarMode === ToolbarMode$1.floating) { 30745 editor.on('ResizeWindow ResizeEditor ResizeContent', () => { 30746 // Check if the width has changed, if so then refresh the toolbar drawer. We don't care if height changes.
30747 const width = editor.getWin().innerWidth; 30748 if (width !== lastToolbarWidth.get()) { 30749 refreshDrawer(); 30750 lastToolbarWidth.set(width); 30751 } 30752 }); 30753 } 30754 const api = { 30755 setEnabled: (state) => { 30756 const eventType = state ? 'setEnabled' : 'setDisabled'; 30757 broadcastEvents(uiRefs, eventType); 30758 }, 30759 isEnabled: () => !Disabling.isDisabled(outerContainer) 30760 }; 30761 return { 30762 iframeContainer: socket.element.dom, 30763 editorContainer: outerContainer.element.dom, 30764 api 30765 }; 30766 }; 30767 30768 var Iframe = /*#__PURE__*/Object.freeze({ 30769 __proto__: null, 30770 render: render$1 30771 }); 30772 30773 const parseToInt = (val) => { 30774 // if size is a number or '_px', will return the number 30775 const re = /^[0-9\.]+(|px)$/i; 30776 if (re.test('' + val)) { 30777 return Optional.some(parseInt('' + val, 10)); 30778 } 30779 return Optional.none(); 30780 }; 30781 const numToPx = (val) => isNumber(val) ? val + 'px' : val; 30782 const calcCappedSize = (size, minSize, maxSize) => { 30783 const minOverride = minSize.filter((min) => size < min); 30784 const maxOverride = maxSize.filter((max) => size > max); 30785 return minOverride.or(maxOverride).getOr(size); 30786 }; 30787 const convertValueToPx = (element, value) => { 30788 if (typeof value === 'number') { 30789 return Optional.from(value); 30790 } 30791 const splitValue = /^([0-9.]+)(pt|em|px)$/.exec(value.trim()); 30792 if (splitValue) { 30793 const type = splitValue[2]; 30794 const parsed = Number.parseFloat(splitValue[1]); 30795 if (Number.isNaN(parsed) || parsed < 0) { 30796 return Optional.none(); 30797 } 30798 else if (type === 'em') { 30799 return Optional.from(parsed * Number.parseFloat(window.getComputedStyle(element.dom).fontSize)); 30800 } 30801 else if (type === 'pt') { 30802 return Optional.from(parsed * (72 / 96)); 30803 } 30804 else if (type === 'px') { 30805 return Optional.from(parsed); 30806 } 30807 } 30808 return Optional.none(); 30809 }; 30810 30811 const getHeight = (editor) => { 30812 const baseHeight = convertValueToPx(SugarElement.fromDom(editor.targetElm), getHeightOption(editor)); 30813 const minHeight = getMinHeightOption(editor); 30814 const maxHeight = getMaxHeightOption(editor); 30815 return baseHeight.map((height) => calcCappedSize(height, minHeight, maxHeight)); 30816 }; 30817 const getHeightWithFallback = (editor) => { 30818 return getHeight(editor).getOr(getHeightOption(editor)); // If we can't parse, set the height while ignoring min/max values. 30819 }; 30820 const getWidth = (editor) => { 30821 const baseWidth = getWidthOption(editor); 30822 const minWidth = getMinWidthOption(editor); 30823 const maxWidth = getMaxWidthOption(editor); 30824 return parseToInt(baseWidth).map((width) => calcCappedSize(width, minWidth, maxWidth)); 30825 }; 30826 const getWidthWithFallback = (editor) => { 30827 const width = getWidth(editor); 30828 return width.getOr(getWidthOption(editor)); 30829 }; 30830 30831 const { ToolbarLocation, ToolbarMode } = Options; 30832 const maximumDistanceToEdge = 40; 30833 const InlineHeader = (editor, targetElm, uiRefs, backstage, floatContainer) => { 30834 const { mainUi, uiMotherships } = uiRefs; 30835 const DOM = global$9.DOM; 30836 const useFixedToolbarContainer = useFixedContainer(editor); 30837 const isSticky = isStickyToolbar(editor); 30838 const editorMaxWidthOpt = getMaxWidthOption(editor).or(getWidth(editor)); 30839 const headerBackstage = backstage.shared.header; 30840 const isPositionedAtTop = headerBackstage.isPositionedAtTop; 30841 const minimumToolbarWidth = 150; // Value is arbitrary. 30842 const toolbarMode = getToolbarMode(editor); 30843 const isSplitToolbar = toolbarMode === ToolbarMode.sliding || toolbarMode === ToolbarMode.floating; 30844 const visible = Cell(false); 30845 const isVisible = () => visible.get() && !editor.removed; 30846 // Calculate the toolbar offset when using a split toolbar drawer 30847 const calcToolbarOffset = (toolbar) => isSplitToolbar ? 30848 toolbar.fold(constant$1(0), (tbar) => 30849 // If we have an overflow toolbar, we need to offset the positioning by the height of the overflow toolbar 30850 tbar.components().length > 1 ? get$d(tbar.components()[1].element) : 0) : 0; 30851 const calcMode = (container) => { 30852 switch (getToolbarLocation(editor)) { 30853 case ToolbarLocation.auto: 30854 const toolbar = OuterContainer.getToolbar(mainUi.outerContainer); 30855 const offset = calcToolbarOffset(toolbar); 30856 const toolbarHeight = get$d(container.element) - offset; 30857 const targetBounds = box$1(targetElm); 30858 // Determine if the toolbar has room to render at the top/bottom of the document 30859 const roomAtTop = targetBounds.y > toolbarHeight; 30860 if (roomAtTop) { 30861 return 'top'; 30862 } 30863 else { 30864 const doc = documentElement(targetElm); 30865 const docHeight = Math.max(doc.dom.scrollHeight, get$d(doc)); 30866 const roomAtBottom = targetBounds.bottom < doc
30866Height - toolbarHeight; 30867 // If there isn't ever room to add the toolbar above the target element, then place the toolbar at the bottom. 30868 // Likewise if there's no room at the bottom, then we should show at the top. If there's no room at the bottom 30869 // or top, then prefer the bottom except when it'll prevent accessing the content at the bottom. 30870 // Make sure to exclude scroll position, as we want to still show at the top if the user can scroll up to undock 30871 if (roomAtBottom) { 30872 return 'bottom'; 30873 } 30874 else { 30875 const winBounds = win(); 30876 const isRoomAtBottomViewport = winBounds.bottom < targetBounds.bottom - toolbarHeight; 30877 return isRoomAtBottomViewport ? 'bottom' : 'top'; 30878 } 30879 } 30880 case ToolbarLocation.bottom: 30881 return 'bottom'; 30882 case ToolbarLocation.top: 30883 default: 30884 return 'top'; 30885 } 30886 }; 30887 const setupMode = (mode) => { 30888 // Update the docking mode 30889 floatContainer.on((container) => { 30890 Docking.setModes(container, [mode]); 30891 headerBackstage.setDockingMode(mode); 30892 // Update the vertical menu direction 30893 const verticalDir = isPositionedAtTop() ? AttributeValue.TopToBottom : AttributeValue.BottomToTop; 30894 set$9(container.element, Attribute, verticalDir); 30895 }); 30896 }; 30897 const updateChromeWidth = () => { 30898 floatContainer.on((container) => { 30899 // Update the max width of the inline toolbar 30900 const maxWidth = editorMaxWidthOpt.getOrThunk(() => { 30901 // Adding 10px of margin so that the toolbar won't try to wrap 30902 return getBounds$1().width - viewport$1(targetElm).left - 10; 30903 }); 30904 set$7(container.element, 'max-width', maxWidth + 'px'); 30905 }); 30906 }; 30907 const updateChromePosition = (isOuterContainerWidthRestored, prevScroll) => { 30908 floatContainer.on((container) => { 30909 const toolbar = OuterContainer.getToolbar(mainUi.outerContainer); 30910 const offset = calcToolbarOffset(toolbar); 30911 // The float container/editor may not have been rendered yet, which will cause it to have a non integer based positions 30912 // so we need to round this to account for that. 30913 const targetBounds = box$1(targetElm); 30914 const offsetParent = getOffsetParent$1(editor, mainUi.outerContainer.element); 30915 const getLeft = () => offsetParent.fold(() => targetBounds.x, (offsetParent) => { 30916 // Because for ui_mode: split, the main mothership (which includes the toolbar) is moved and added as a sibling 30917 // If there's any relative position div set as the parent and the offsetParent is no longer the body, 30918 // the absolute top/left positions would no longer be correct 30919 // When there's a relative div and the position is the same as the toolbar container 30920 // then it would produce a negative top as it needs to be positioned on top of the offsetParent 30921 const offsetBox = box$1(offsetParent); 30922 const isOffsetParentBody = eq(offsetParent, body()); 30923 return isOffsetParentBody 30924 ? targetBounds.x 30925 : targetBounds.x - offsetBox.x; 30926 }); 30927 const getTop = () => offsetParent.fold(() => isPositionedAtTop() 30928 ? Math.max(targetBounds.y - get$d(container.element) + offset, 0) 30929 : targetBounds.bottom, (offsetParent) => { 30930 // Because for ui_mode: split, the main mothership (which includes the toolbar) is moved and added as a sibling 30931 // If there's any relative position div set as the parent and the offsetParent is no longer the body, 30932 // the absolute top/left positions would no longer be correct 30933 // When there's a relative div and the position is the same as the toolbar container 30934 // then it would produce a negative top as it needs to be positioned on top of the offsetParent 30935 const offsetBox = box$1(offsetParent); 30936 const scrollDelta = offsetParent.dom.scrollTop ?? 0; 30937 const isOffsetParentBody = eq(offsetParent, body()); 30938 const topValue = isOffsetParentBody 30939 ? Math.max(targetBounds.y - get$d(container.element) + offset, 0) 30940 : targetBounds.y - offsetBox.y + scrollDelta - get$d(container.element) + offset; 30941 return isPositionedAtTop() 30942 ? topValue 30943 : targetBounds.bottom; 30944 }); 30945 const left = getLeft(); 30946 const widthProperties = someIf(isOuterContainerWidthRestored, 30947 // This width can be used for calculating the "width" when resolving issues with flex-wrapping being triggered at the window width, despite scroll space being available to the right. 30948 Math.ceil(mainUi.outerContainer.element.dom.getBoundingClientRect().width)) 30949 // this check is needed because if the toolbar is rendered outside of the `outerContainer` because the toolbar have `position: "fixed"` 30950 // the calculate width isn't correct 30951 .filter((w) => w > minimumToolbarWidth).map((toolbarWidth) => { 30952 const scroll = prevScroll.getOr(get$b()); 30953 /* 30954 As the editor container can wrap its elements (due to flex-wrap), the width of the container impacts also its height. Adding a minimum width works around two problems: 30955 30956 a) The docking behaviour (e.g. lazyContext) does not handle the situation of a very thin component near the edge of the screen very well, and actually has no concept of horizontal scroll - it only checks y values. 30957 30958 b) A very small toolbar is essentially unusable. On scrolling of X, we keep updating the width of the toolbar so that it can grow to fit the available space. 30959 30960 Note: this is entirely determined on the number of items in the menu and the toolbar, because when they wrap, that's what causes the height. Also, having multiple toolbars can also make it higher. 30961 */ 30962 const availableWidth = window.innerWidth - (left - scroll.left); 30963 const width = Math.max(Math.min(toolbarWidth, availableWidth), minimumToolbarWidth); 30964 if (availableWidth < toolbarWidth) { 30965 set$7(mainUi.outerContainer.element, 'width', width + 'px'); 30966 } 30967 return { 30968 width: width + 'px' 30969 }; 30970 }).getOr({ width: 'max-content' }); 30971 const baseProperties = { 30972 position: 'absolute', 30973 left: Math.round(left) + 'px', 30974 top: getTop() + 'px' 30975 }; 30976 setAll(mainUi.outerContainer.element, { 30977 ...baseProperties, 30978 ...widthProperties 30979 }); 30980 }); 30981 }; 30982 // This would return Optional.none, for ui_mode: combined, which will fallback to the default co
30982de block 30983 // For ui_mode: split, the offsetParent would be the body if there were no relative div set as parent 30984 const getOffsetParent$1 = (editor, element) => isSplitUiMode(editor) ? getOffsetParent(element) : Optional.none(); 30985 const repositionPopups$1 = () => { 30986 each$1(uiMotherships, (m) => { 30987 m.broadcastOn([repositionPopups()], {}); 30988 }); 30989 }; 30990 const restoreOuterContainerWidth = () => { 30991 /* 30992 Editors can be placed so far to the right that their left position is beyond the window width. This causes problems with flex-wrap. To solve this, set a width style on the container. 30993 Natural width of the container needs to be calculated first. 30994 */ 30995 if (!useFixedToolbarContainer) { 30996 const toolbarCurrentRightsidePosition = absolute$3(mainUi.outerContainer.element).left + getOuter(mainUi.outerContainer.element); 30997 /* 30998 Check the width if we are within X number of pixels to the edge ( or above ). Also check if we have the width-value set. 30999 This helps handling the issue where it goes from having a width set ( because it's too wide ) to going so far from the edge it no longer triggers the problem. Common when the width is changed by test. 31000 */ 31001 if (toolbarCurrentRightsidePosition >= window.innerWidth - maximumDistanceToEdge || getRaw(mainUi.outerContainer.element, 'width').isSome()) { 31002 set$7(mainUi.outerContainer.element, 'position', 'absolute'); 31003 set$7(mainUi.outerContainer.element, 'left', '0px'); 31004 remove$6(mainUi.outerContainer.element, 'width'); 31005 return true; 31006 } 31007 } 31008 return false; 31009 }; 31010 const update = (stickyAction) => { 31011 // Skip updating the ui if it's hidden 31012 if (!isVisible()) { 31013 return; 31014 } 31015 // Handles positioning, docking and SplitToolbar (more drawer) behaviour. Modes: 31016 // 1. Basic inline: does positioning and docking 31017 // 2. Inline + more drawer: does positioning, docking and SplitToolbar 31018 // 3. Inline + fixed_toolbar_container: does nothing 31019 // 4. Inline + fixed_toolbar_container + more drawer: does SplitToolbar 31020 // Update the max width, as the body width may have changed 31021 if (!useFixedToolbarContainer) { 31022 updateChromeWidth(); 31023 } 31024 const prevScroll = get$b(); 31025 const isOuterContainerWidthRestored = useFixedToolbarContainer ? false : restoreOuterContainerWidth(); 31026 /* 31027 Refresh split toolbar. Before calling refresh, we need to make sure that we have the full width (through restoreOuterContainerWidth above), otherwise too much will be put in the overflow drawer. 31028 A split toolbar requires a calculation to see what ends up in the "more drawer". When we don't have a split toolbar, then there is no reason to refresh the toolbar when the size changes. 31029 */ 31030 if (isSplitToolbar) { 31031 OuterContainer.refreshToolbar(mainUi.outerContainer); 31032 } 31033 // Positioning 31034 if (!useFixedToolbarContainer) { 31035 // Getting the current scroll as the previous step may have reset the scroll, 31036 // We also want calculation based on the previous scroll, then restoring the scroll when everything is set. 31037 const currentScroll = get$b(); 31038 const optScroll = someIf(prevScroll.left !== currentScroll.left, prevScroll); 31039 // This will position the container in the right spot. 31040 updateChromePosition(isOuterContainerWidthRestored, optScroll); 31041 // Restore scroll left position only if they are different, keeping the current scroll top, that shouldn't be changed 31042 optScroll.each((scroll) => { 31043 to(scroll.left, currentScroll.top); 31044 }); 31045 } 31046 // Docking 31047 if (isSticky) { 31048 floatContainer.on(stickyAction); 31049 } 31050 // Floating toolbar 31051 repositionPopups$1(); 31052 }; 31053 const doUpdateMode = () => { 31054 // Skip updating the mode if the toolbar is hidden, is 31055 // using a fixed container or has sticky toolbars disabled 31056 if (useFixedToolbarContainer || !isSticky || !isVisible()) { 31057 return false;
vendor: 10,956 bytes, lines 31058-31257
31058 } 31059 return floatContainer.get().exists((fc) => { 31060 const currentMode = headerBackstage.getDockingMode(); 31061 const newMode = calcMode(fc); 31062 // Note: the docking mode will only be able to change when the `toolbar_location` 31063 // is set to "auto". 31064 if (newMode !== currentMode) { 31065 setupMode(newMode); 31066 return true; 31067 } 31068 else { 31069 return false; 31070 } 31071 }); 31072 }; 31073 const show = () => { 31074 visible.set(true); 31075 set$7(mainUi.outerContainer.element, 'display', 'flex'); 31076 DOM.addClass(editor.getBody(), 'mce-edit-focus'); 31077 each$1(uiMotherships, (m) => { 31078 // We remove the display style when showing, because when hiding, we set it to "none" 31079 remove$6(m.element, 'display'); 31080 }); 31081 doUpdateMode(); 31082 if (isSplitUiMode(editor)) { 31083 // When the toolbar is shown, then hidden and when the page is then scrolled, 31084 // the toolbar is set to docked, which shouldn't be as it should be static position 31085 // calling reset here, to reset the state. 31086 // Another case would be when the toolbar is shown initially (with location_bottom) 31087 // we don't want to dock the toolbar, calling Docking.refresh 31088 update((elem) => Docking.isDocked(elem) ? Docking.reset(elem) : Docking.refresh(elem)); 31089 } 31090 else { 31091 // Even if we aren't updating the docking mode, we still want to reposition 31092 // the Ui. NOTE: We are using Docking.refresh here, rather than Docking.reset. This 31093 // means it should keep whatever its "previous" coordinates were, and will just 31094 // behave like the window was scrolled again, and Docking needs to work out if it 31095 // is going to dock / undock 31096 update(Docking.refresh); 31097 } 31098 }; 31099 const hide = () => { 31100 visible.set(false); 31101 set$7(mainUi.outerContainer.element, 'display', 'none'); 31102 DOM.removeClass(editor.getBody(), 'mce-edit-focus'); 31103 each$1(uiMotherships, (m) => { 31104 set$7(m.element, 'display', 'none'); 31105 }); 31106 }; 31107 const updateMode = () => { 31108 const changedMode = doUpdateMode(); 31109 // If the docking mode has changed due to the update, we want to reset 31110 // docking. This will clear any prior stored positions 31111 if (changedMode) { 31112 update(Docking.reset); 31113 } 31114 }; 31115 return { 31116 isVisible, 31117 isPositionedAtTop, 31118 show, 31119 hide, 31120 update, 31121 updateMode, 31122 repositionPopups: repositionPopups$1 31123 }; 31124 }; 31125 31126 const getTargetPosAndBounds = (targetElm, isToolbarTop) => { 31127 const bounds = box$1(targetElm); 31128 return { 31129 pos: isToolbarTop ? bounds.y : bounds.bottom, 31130 bounds 31131 }; 31132 }; 31133 const setupEvents = (editor, targetElm, ui, toolbarPersist) => { 31134 const prevPosAndBounds = Cell(getTargetPosAndBounds(targetElm, ui.isPositionedAtTop())); 31135 const resizeContent = (e) => { 31136 const { pos, bounds } = getTargetPosAndBounds(targetElm, ui.isPositionedAtTop()); 31137 const { pos: prevPos, bounds: prevBounds } = prevPosAndBounds.get(); 31138 const hasResized = bounds.height !== prevBounds.height || bounds.width !== prevBounds.width; 31139 prevPosAndBounds.set({ pos, bounds }); 31140 if (hasResized) { 31141 fireResizeContent(editor, e); 31142 } 31143 if (ui.isVisible()) { 31144 if (prevPos !== pos) { 31145 // The proposed toolbar location has moved, so we need to reposition the Ui. This might 31146 // include things like refreshing any Docking / stickiness for the toolbars 31147 ui.update(Docking.reset); 31148 } 31149 else if (hasResized) { 31150 // The proposed toolbar location hasn't moved, but the dimensions of the editor have changed. 31151 // We use "updateMode" here instead of "update". The primary reason is that "updateMode" 31152 // only repositions the Ui if it has detected that the docking mode needs to change, which 31153 // will only happen with `toolbar_location` is set to `auto`. 31154 ui.updateMode(); 31155 // NOTE: This repositionPopups call is going to be a duplicate if "updateMode" identifies 31156 // that the mode has changed. We probably need to make it a bit more granular .. so 31157 // that we can just query if the mode has changed. Otherwise, we're going to end up with 31158 // situations like this where we are doing a potentially expensive operation 31159 // (repositionPopups) more than once. 31160 ui.repositionPopups(); 31161 } 31162 } 31163 }; 31164 if (!toolbarPersist) { 31165 editor.on('activate', ui.show); 31166 editor.on('deactivate', ui.hide); 31167 } 31168 // For both the initial load (SkinLoaded) and any resizes (ResizeWindow), we want to 31169 // update the positions of the Ui elements (and reset Docking / stickiness) 31170 editor.on('SkinLoaded ResizeWindow', () => ui.update(Docking.reset)); 31171 editor.on('NodeChange keydown', (e) => { 31172 requestAnimationFrame(() => resizeContent(e)); 31173 }); 31174 // When the page has been scrolled, we need to update any docking positions. We also 31175 // want to reposition all the Ui elements if required. 31176 let lastScrollX = 0; 31177 const updateUi = last(() => ui.update(Docking.refresh), 33); 31178 editor.on('ScrollWindow', () => { 31179 const newScrollX = get$b().left; 31180 if (newScrollX !== lastScrollX) { 31181 lastScrollX = newScrollX; 31182 updateUi.throttle(); 31183 } 31184 ui.updateMode(); 31185 }); 31186 if (isSplitUiMode(editor)) { 31187 editor.on('ElementScroll', (_args) => { 31188 // When the scroller containing the editor scrolls, update the Ui positions 31189 ui.update(Docking.refresh); 31190 }); 31191 } 31192 // Bind to async load events and trigger a content resize event if the size has changed 31193 // This is handling resizing based on anything loading inside the content (e.g. img tags) 31194 const elementLoad = unbindable(); 31195 elementLoad.set(capture(SugarElement.fromDom(editor.getBody()), 'load', (e) => resizeContent(e.raw))); 31196 editor.on('remove', () => { 31197 elementLoad.clear(); 31198 }); 31199 }; 31200 const render = (editor, uiRefs, rawUiConfig, backstage, args) => { 31201 const { mainUi } = uiRefs; 31202 // This is used to store the reference to the header part of OuterContainer, which is 31203 // *not* created by this module. This reference is used to make sure that we only bind 31204 // events for an inline container *once* ... because our show function is just the 31205 // InlineHeader's show function if this reference is already set. We pass it through to 31206 // InlineHeader because InlineHeader will depend on it. 31207 const floatContainer = value$2(); 31208 const targetElm = SugarElement.fromDom(args.targetNode); 31209 const ui = InlineHeader(editor, targetElm, uiRefs, backstage, floatContainer); 31210 const toolbarPersist = isToolbarPersist(editor); 31211 // eslint-disable-next-line @typescript-eslint/no-floating-promises 31212 inline(editor); 31213 const render = () => { 31214 // Because we set the floatContainer immediately afterwards, this is just telling us 31215 // if we have already called this code (e.g. show, hide, show) - then don't do anything 31216 // more than show. It's a pretty messy way of ensuring that all the code that follows 31217 // this `if` block is only executed once (setting up events etc.). So the first call 31218 // to `render` will execute it, but the second call won't. This `render` function is 31219 // used for most of the "show" handlers here, so the function can be invoked either 31220 // for the first time, or or just because something is being show again, after being 31221 // toggled to hidden earlier. 31222 if (floatContainer.isSet()) { 31223 ui.show(); 31224 return; 31225 } 31226 // Set up the header part of OuterContainer. Once configured, the `InlineHeader` code 31227 // will use it when setting up and updating the Ui. This module uses it mainly just to 31228 // allow us to call `render` multiple times, but only have it execute the setup code once. 31229 floatContainer.set(OuterContainer.getHeader(mainUi.outerContainer).getOrDie()); 31230 // `uiContainer` handles *where* the motherhips get added by default. Currently, uiContainer 31231 // will mostly be the <body> of the document (unless it's a ShadowRoot). When using ui_mode: split, 31232 // the main mothership (which includes the toolbar) and popup sinks will be added as siblings of 31233 // the target element, so that they have the same scrolling context / environment 31234 const uiContainer = getUiContainer(editor); 31235 // Position the motherships based on the editor Ui options. 31236 if (isSplitUiMode(editor)) { 31237 attachSystemAfter(targetElm, mainUi.mothership); 31238 // Only in ui_mode: split, do we have a separate popup sink 31239 attachSystemAfter(targetElm, uiRefs.popupUi.mothership); 31240 } 31241 else { 31242 attachSystem(uiContainer, mainUi.mothership); 31243 } 31244 // NOTE: In UiRefs, dialogUi and popupUi refer to the same thing if ui_mode: combined 31245 attachSystem(uiContainer, uiRefs.dialogUi.mothership); 31246 const setup = () => { 31247 // Unlike menubar below which uses OuterContainer directly, this level of abstraction is 31248 // required because of the different types of toolbars available (e.g. multiple vs single) 31249 setToolbar(editor, uiRefs, rawUiConfig, backstage); 31250 OuterContainer.setMenubar(mainUi.outerContainer, identifyMenus(editor, rawUiConfig)); 31251 // Initialise the toolbar - set initial positioning then show 31252 ui.show(); 31253 setupEvents(editor, targetElm, ui, toolbarPersist); 31254 editor.nodeChanged(); 31255 }; 31256 if (toolbarPersist) { 31257 // TINY-10482: for `toolbar_persist: true` we need to wait for the skin to be loaded before showing the toolbar/menubar.
31258 // Without this, there's the occasional chance that the toolbar/menubar could be set/shown before the skin has finished 31259 // loading, which causes CSS issues. 31260 editor.once('SkinLoaded', setup); 31261 } 31262 else { 31263 setup(); 31264 } 31265 }; 31266 editor.on('show', render); 31267 editor.on('hide', ui.hide); 31268 if (!toolbarPersist) { 31269 editor.on('focus', render); 31270 editor.on('blur', ui.hide); 31271 } 31272 editor.on('init', () => { 31273 if (editor.hasFocus() || toolbarPersist) { 31274 render(); 31275 } 31276 }); 31277 setupEventsForUi(editor, uiRefs); 31278 const api = { 31279 show: render, 31280 hide: ui.hide, 31281 setEnabled: (state) => { 31282 const eventType = state ? 'setEnabled' : 'setDisabled'; 31283 broadcastEvents(uiRefs, eventType); 31284 }, 31285 isEnabled: () => !Disabling.isDisabled(mainUi.outerContainer) 31286 }; 31287 return { 31288 editorContainer: mainUi.outerContainer.element.dom, 31289 api 31290 }; 31291 }; 31292 31293 var Inline = /*#__PURE__*/Object.freeze({ 31294 __proto__: null, 31295 render: render 31296 }); 31297 31298 const LazyUiReferences = () => { 31299 const dialogUi = value$2(); 31300 const popupUi = value$2(); 31301 const mainUi = value$2(); 31302 const lazyGetInOuterOrDie = (label, f) => () => mainUi.get().bind((oc) => f(oc.outerContainer)).getOrDie(`Could not find ${label} element in OuterContainer`); 31303 // TINY-9226: If the motherships are the same, return just the dialog Ui of them (ui_mode: combined mode) 31304 const getUiMotherships = () => { 31305 const optDialogMothership = dialogUi.get().map((ui) => ui.mothership); 31306 const optPopupMothership = popupUi.get().map((ui) => ui.mothership); 31307 return optDialogMothership.fold(() => optPopupMothership.toArray(), (dm) => optPopupMothership.fold(() => [dm], (pm) => eq(dm.element, pm.element) ? [dm] : [dm, pm])); 31308 }; 31309 return { 31310 dialogUi, 31311 popupUi, 31312 mainUi, 31313 getUiMotherships, 31314 lazyGetInOuterOrDie 31315 }; 31316 }; 31317 31318 // TODO: Find a better way of doing this. We probably don't want to just listen to 31319 // editor events. Having an API available like WindowManager would be the best option 31320 const showContextToolbarEvent = 'contexttoolbar-show'; 31321 const hideContextToolbarEvent = 'contexttoolbar-hide'; 31322 31323 const getFormApi = (input, valueState, focusfallbackElement) => { 31324 return ({ 31325 setInputEnabled: (state) => { 31326 if (!state && focusfallbackElement) { 31327 focus$4(focusfallbackElement); 31328 } 31329 Disabling.set(input, !state); 31330 }, 31331 isInputEnabled: () => !Disabling.isDisabled(input), 31332 hide: () => { 31333 emit(input, sandboxClose()); 31334 }, 31335 back: () => { 31336 emit(input, backSlideEvent); 31337 }, 31338 getValue: () => { 31339 return valueState.get().getOrThunk(() => Representing.getValue(input)); 31340 }, 31341 setValue: (value) => { 31342 if (input.getSystem().isConnected()) { 31343 Representing.setValue(input, value); 31344 } 31345 else { 31346 valueState.set(value); 31347 } 31348 } 31349 }); 31350 }; 31351 const getFormParentApi = (comp, valueState, focusfallbackElement) => { 31352 const parent$1 = parent(comp.element); 31353 const parentCompOpt = parent$1.bind((parent) => comp.getSystem().getByDom(parent).toOptional()); 31354 return getFormApi(parentCompOpt.getOr(comp), valueState, focusfallbackElement); 31355 }; 31356 31357 const runOnExecute = (memInput, original, valueState) => run$1(internalToolbarButtonExecute, (comp, se) => { 31358 const input = memInput.get(comp); 31359 const formApi = getFormApi(input, valueState, comp.element); 31360 original.onAction(formApi, se.event.buttonApi); 31361 }); 31362 const renderContextButton = (memInput, button, providers, valueState) => { 31363 const { primary, ...rest } = button.original; 31364 const bridged = getOrDie(createToolbarButton({ 31365 ...rest, 31366 type: 'button', 31367 onAction: noop 31368 }));
31369 return renderToolbarButtonWith(bridged, providers, [ 31370 runOnExecute(memInput, button, valueState) 31371 ]); 31372 }; 31373 const renderContextToggleButton = (memInput, button, providers, valueState) => { 31374 const { primary, ...rest } = button.original; 31375 const bridged = getOrDie(createToggleButton({ 31376 ...rest, 31377 type: 'togglebutton', 31378 onAction: noop 31379 })); 31380 return renderToolbarToggleButtonWith(bridged, providers, [ 31381 runOnExecute(memInput, button, valueState) 31382 ]); 31383 }; 31384 const isToggleButton = (button) => button.type === 'contextformtogglebutton'; 31385 const generateOne = (memInput, button, providersBackstage, valueState) => { 31386 if (isToggleButton(button)) { 31387 return renderContextToggleButton(memInput, button, providersBackstage, valueState); 31388 } 31389 else { 31390 return renderContextButton(memInput, button, providersBackstage, valueState); 31391 } 31392 }; 31393 const generate = (memInput, buttons, providersBackstage, valueState) => { 31394 const mementos = map$2(buttons, (button) => record(generateOne(memInput, button, providersBackstage, valueState))); 31395 const asSpecs = () => map$2(mementos, (mem) => mem.asSpec()); 31396 const findPrimary = (compInSystem) => findMap(buttons, (button, i) => { 31397 if (button.primary) { 31398 return Optional.from(mementos[i]).bind((mem) => mem.getOpt(compInSystem)).filter(not(Disabling.isDisabled)); 31399 } 31400 else { 31401 return Optional.none(); 31402 } 31403 }); 31404 return { 31405 asSpecs, 31406 findPrimary 31407 }; 31408 }; 31409 31410 const renderContextFormSizeInput = (ctx, providersBackstage, onEnter, valueState) => { 31411 const { width, height } = ctx.initValue(); 31412 let converter = noSizeConversion; 31413 const enabled = true; 31414 const ratioEvent = generate$6('ratio-event'); 31415 const getApi = (comp) => getFormApi(comp, valueState); 31416 const makeIcon = (iconName) => render$4(iconName, { tag: 'span', classes: ['tox-icon', 'tox-lock-icon__' + iconName] }, providersBackstage.icons); 31417 const disabled = () => !enabled; 31418 const label = ctx.label.getOr('Constrain proportions'); 31419 const translatedLabel = providersBackstage.translate(label); 31420 const pLock = FormCoupledInputs.parts.lock({ 31421 dom: { 31422 tag: 'button', 31423 classes: ['tox-lock', 'tox-lock-context-form-size-input', 'tox-button', 'tox-button--naked', 'tox-button--icon'], 31424 attributes: { 31425 'aria-label': translatedLabel, 31426 'data-mce-name': label 31427 } 31428 }, 31429 components: [ 31430 makeIcon('lock'), 31431 makeIcon('unlock') 31432 ], 31433 buttonBehaviours: derive$1([ 31434 Disabling.config({ disabled }), 31435 Tabstopping.config({}), 31436 Tooltipping.config(providersBackstage.tooltips.getConfig({ 31437 tooltipText: translatedLabel 31438 })) 31439 ]) 31440 }); 31441 const formGroup = (components) => ({ 31442 dom: { 31443 tag: 'div', 31444 classes: ['tox-context-form__group'] 31445 }, 31446 components 31447 }); 31448 const goToParent = (comp) => { 31449 const focussableWrapperOpt = ancestor$1(comp.element, 'div.tox-focusable-wrapper'); 31450 return focussableWrapperOpt.fold(Optional.none, (focussableWrapper) => { 31451 focus$4(focussableWrapper); 31452 return Optional.some(true); 31453 }); 31454 }; 31455 const getFieldPart = (isField1) => FormField.parts.field({ 31456 factory: Input, 31457 inputClasses: ['tox-textfield', 'tox-toolbar-textfield', 'tox-textfield-size'], 31458 data: isField1 ? width : height, 31459 inputBehaviours: derive$1([ 31460 Disabling.config({ disabled }), 31461 Tabstopping.config({}), 31462 config('size-input-toolbar-events', [ 31463 run$1(focusin(), (component, _simulatedEvent) => { 31464 emitWith(component, ratioEvent, { isField1 }); 31465 }) 31466 ]),
31467 Keying.config({ mode: 'special', onEnter, onEscape: goToParent }) 31468 ]), 31469 selectOnFocus: false 31470 }); 31471 const getLabel = (label) => ({ 31472 dom: { 31473 tag: 'label', 31474 classes: ['tox-label'] 31475 }, 31476 components: [ 31477 text$2(providersBackstage.translate(label)) 31478 ] 31479 }); 31480 const focusableWrapper = (field) => ({ 31481 dom: { 31482 tag: 'div', 31483 classes: ['tox-focusable-wrapper', 'tox-toolbar-nav-item'], 31484 }, 31485 components: [field], 31486 behaviours: derive$1([ 31487 Tabstopping.config({}), 31488 Focusing.config({}), 31489 Keying.config({ 31490 mode: 'special', 31491 onEnter: (comp) => { 31492 const focussableInputOpt = descendant(comp.element, 'input'); 31493 return focussableInputOpt.fold(Optional.none, (focussableInput) => { 31494 focus$4(focussableInput); 31495 return Optional.some(true); 31496 }); 31497 } 31498 }) 31499 ]) 31500 }); 31501 const widthField = focusableWrapper(FormCoupledInputs.parts.field1(formGroup([FormField.parts.label(getLabel('Width:')), getFieldPart(true)]))); 31502 const heightField = focusableWrapper(FormCoupledInputs.parts.field2(formGroup([FormField.parts.label(getLabel('Height:')), getFieldPart(false)]))); 31503 const editorOffCell = Cell(noop);
31504 const controlLifecycleHandlers = [ 31505 onControlAttached({ 31506 onBeforeSetup: (comp) => descendant(comp.element, 'input').each(focus$4), 31507 onSetup: ctx.onSetup, 31508 getApi 31509 }, editorOffCell), 31510 onContextFormControlDetached({ getApi }, editorOffCell, valueState), 31511 ]; 31512 return FormCoupledInputs.sketch({ 31513 dom: { 31514 tag: 'div', 31515 classes: ['tox-context-form__group'] 31516 }, 31517 components: [ 31518 // NOTE: Form coupled inputs to the FormField.sketch themselves. 31519 widthField, 31520 formGroup([ 31521 pLock 31522 ]), 31523 heightField 31524 ], 31525 field1Name: 'width', 31526 field2Name: 'height', 31527 locked: true, 31528 markers: { 31529 lockClass: 'tox-locked' 31530 }, 31531 onLockedChange: (current, other, _lock) => { 31532 parseSize(Representing.getValue(current)).each((size) => { 31533 converter(size).each((newSize) => { 31534 Representing.setValue(other, formatSize(newSize)); 31535 }); 31536 }); 31537 }, 31538 onInput: (current) => emit(current, formInputEvent), 31539 coupledFieldBehaviours: derive$1([ 31540 Focusing.config({}), 31541 Keying.config({ 31542 mode: 'flow', 31543 focusInside: FocusInsideModes.OnEnterOrSpaceMode, 31544 cycles: false, 31545 selector: 'button, .tox-focusable-wrapper', 31546 }), 31547 Disabling.config({ 31548 disabled, 31549 onDisabled: (comp) => { 31550 FormCoupledInputs.getField1(comp).bind(FormField.getField).each(Disabling.disable); 31551 FormCoupledInputs.getField2(comp).bind(FormField.getField).each(Disabling.disable); 31552 FormCoupledInputs.getLock(comp).each(Disabling.disable); 31553 }, 31554 onEnabled: (comp) => { 31555 FormCoupledInputs.getField1(comp).bind(FormField.getField).each(Disabling.enable); 31556 FormCoupledInputs.getField2(comp).bind(FormField.getField).each(Disabling.enable); 31557 FormCoupledInputs.getLock(comp).each(Disabling.enable); 31558 } 31559 }), 31560 toggleOnReceive(() => providersBackstage.checkUiComponentContext('mode:design')), 31561 config('size-input-toolbar-events2', [ 31562 run$1(ratioEvent, (component, simulatedEvent) => { 31563 const isField1 = simulatedEvent.event.isField1; 31564 const optCurrent = isField1 ? FormCoupledInputs.getField1(component) : FormCoupledInputs.getField2(component); 31565 const optOther = isField1 ? FormCoupledInputs.getField2(component) : FormCoupledInputs.getField1(component); 31566 const value1 = optCurrent.map(Representing.getValue).getOr(''); 31567 const value2 = optOther.map(Representing.getValue).getOr(''); 31568 converter = makeRatioConverter(value1, value2); 31569 }), 31570 run$1(formInputEvent, (input) => ctx.onInput(getApi(input))), 31571 ...controlLifecycleHandlers, 31572 ]) 31573 ]) 31574 }); 31575 }; 31576 31577 const createContextFormFieldFromParts = (pLabel, pField, providers) => FormField.sketch({ 31578 dom: { 31579 tag: 'div', 31580 classes: ['tox-context-form__group'] 31581 }, 31582 components: [...pLabel.toArray(), pField], 31583 fieldBehaviours: derive$1([ 31584 Disabling.config({ 31585 disabled: () => providers.checkUiComponentContext('mode:design').shouldDisable, 31586 onDisabled: (comp) => { 31587 focusParent(comp); 31588 FormField.getField(comp).each(Disabling.disable); 31589 }, 31590 onEnabled: (comp) => { 31591 FormField.getField(comp).each(Disabling.enable); 31592 } 31593 }), 31594 ]) 31595 }); 31596 31597 const renderContextFormSliderInput = (ctx, providers, onEnter, valueState) => { 31598 const editorOffCell = Cell(noop); 31599 const getApi = (comp) => getFormParentApi(comp, valueState); 31600 const pLabel = ctx.label.map((label) => FormField.parts.label({ 31601 dom: { tag: 'label', classes: ['tox-label'] }, 31602 components: [text$2(providers.translate(label))] 31603 })); 31604 const pField = FormField.parts.field({ 31605 factory: Input, 31606 type: 'range', 31607 inputClasses: ['tox-toolbar-slider__input', 'tox-toolbar-nav-item'], 31608 inputAttributes: { 31609 min: String(ctx.min()), 31610 max: String(ctx.max()) 31611 }, 31612 data: ctx.initValue().toString(), 31613 fromInputValue: (value) => toFloat(value).getOr(ctx.min()), 31614 toInputValue: (value) => String(value), 31615 inputBehaviours: derive$1([ 31616 Disabling.config({ 31617 disabled: () => providers.checkUiComponentContext('mode:design').shouldDisable 31618 }), 31619 toggleOnReceive(() => providers.checkUiComponentContext('mode:design')), 31620 Keying.config({
31621 mode: 'special', 31622 onEnter, 31623 // These two lines need to be tested. They are about left and right bypassing 31624 // any keyboard handling, and allowing left and right to be processed by the input 31625 // Maybe this should go in an alloy sketch for Input? 31626 onLeft: (comp, se) => { 31627 se.cut(); 31628 return Optional.none(); 31629 }, 31630 onRight: (comp, se) => { 31631 se.cut(); 31632 return Optional.none(); 31633 } 31634 }), 31635 config('slider-events', [ 31636 onControlAttached({ 31637 onSetup: ctx.onSetup, 31638 getApi, 31639 onBeforeSetup: Keying.focusIn 31640 }, editorOffCell), 31641 onContextFormControlDetached({ getApi }, editorOffCell, valueState), 31642 run$1(input(), (comp) => { 31643 ctx.onInput(getApi(comp)); 31644 }) 31645 ]) 31646 ]) 31647 }); 31648 return createContextFormFieldFromParts(pLabel, pField, providers); 31649 }; 31650 31651 const renderContextFormTextInput = (ctx, providers, onEnter, valueState) => { 31652 const editorOffCell = Cell(noop); 31653 const getFormApi = (comp) => getFormParentApi(comp, valueState); 31654 const pLabel = ctx.label.map((label) => FormField.parts.label({ 31655 dom: { tag: 'label', classes: ['tox-label'] }, 31656 components: [text$2(providers.translate(label))] 31657 })); 31658 const placeholder = ctx.placeholder.map((p) => ({ placeholder: providers.translate(p) })).getOr({}); 31659 const inputAttributes = { 31660 ...placeholder, 31661 }; 31662 const pField = FormField.parts.field({ 31663 factory: Input, 31664 inputClasses: ['tox-toolbar-textfield', 'tox-toolbar-nav-item'], 31665 inputAttributes, 31666 data: ctx.initValue(), 31667 selectOnFocus: true, 31668 inputBehaviours: derive$1([ 31669 Disabling.config({ 31670 disabled: () => providers.checkUiComponentContext('mode:design').shouldDisable 31671 }), 31672 toggleOnReceive(() => providers.checkUiComponentContext('mode:design')), 31673 Keying.config({ 31674 mode: 'special', 31675 onEnter, 31676 // These two lines need to be tested. They are about left and right bypassing 31677 // any keyboard handling, and allowing left and right to be processed by the input 31678 // Maybe this should go in an alloy sketch for Input? 31679 onLeft: (comp, se) => { 31680 se.cut(); 31681 return Optional.none(); 31682 }, 31683 onRight: (comp, se) => { 31684 se.cut(); 31685 return Optional.none(); 31686 } 31687 }), 31688 config('input-events', [ 31689 onControlAttached({ 31690 onSetup: ctx.onSetup, 31691 getApi: (comp) => { 31692 const closestFocussableOpt = ancestor$1(comp.element, '.tox-toolbar').bind((toolbar) => descendant(toolbar, 'button:enabled')); 31693 return closestFocussableOpt.fold(() => getFormParentApi(comp, valueState), (closestFocussable) => getFormParentApi(comp, valueState, closestFocussable)); 31694 }, 31695 onBeforeSetup: Keying.focusIn 31696 }, editorOffCell), 31697 onContextFormControlDetached({ getApi: getFormApi }, editorOffCell, valueState), 31698 run$1(input(), (comp) => { 31699 ctx.onInput(getFormApi(comp)); 31700 }) 31701 ]) 31702 ]) 31703 }); 31704 return createContextFormFieldFromParts(pLabel, pField, providers); 31705 }; 31706 31707 const buildInitGroup = (f, ctx, providers) => { 31708 const valueState = value$2(); 31709 const onEnter = (input) => { 31710 return startCommands.findPrimary(input).orThunk(() => endCommands.findPrimary(input)).map((primary) => { 31711 emitExecute(primary); 31712 return true; 31713 }); 31714 }; 31715 const memInput = record(f(providers, onEnter, valueState)); 31716 const commandParts = partition$3(ctx.commands, (command) => command.align === 'start'); 31717 const startCommands = generate(memInput, commandParts.pass, providers, valueState); 31718 const endCommands = generate(memInput, commandParts.fail, providers, valueState); 31719 return filter$2([ 31720 { 31721 title: Optional.none(), 31722 label: Optional.none(), 31723 items: startCommands.asSpecs() 31724 }, 31725 { 31726 title: Optional.none(), 31727 label: Optional.none(), 31728 items: [memInput.asSpec()] 31729 }, 31730 { 31731 title: Optional.none(), 31732 label: Optional.none(), 31733 items: endCommands.asSpecs() 31734 } 31735 ], (group) => group.items.length > 0); 31736 }; 31737 const buildInitGroups = (ctx, providers) => { 31738 switch (ctx.type) { 31739 case 'contextform': return buildInitGroup(curry(renderContextFormTextInput, ctx), ctx, providers); 31740 case 'contextsliderform': return buildInitGroup(curry(renderContextFormSliderInput, ctx), ctx, providers); 31741 case 'contextsizeinputform': return buildInitGroup(curry(renderContextFormSizeInput, ctx), ctx, providers); 31742 } 31743 }; 31744 const renderContextForm = (toolbarType, ctx, providers) => renderToolbar({ 31745 type: toolbarType, 31746 uid: generate$6('context-toolbar'), 31747 initGroups: buildInitGroups(ctx, providers), 31748 onEscape: Optional.none, 31749 cyclicKeying: true, 31750 providers 31751 }); 31752 const ContextForm = { 31753 renderContextForm, 31754 buildInitGroups 31755 }; 31756 31757 // The "threshold" here is the amount of overlap. To make the overlap check 31758 // be more permissive (return true for 'almost' an overlap), use a negative 31759 // threshold value 31760 const isVerticalOverlap = (a, b, threshold) => b.bottom - a.y >= threshold && a.bottom - b.y >= threshold; 31761 const getRangeRect = (rng) => { 31762 const rect = rng.getBoundingClientRect(); 31763 // Some ranges (eg <td><br></td>) will return a 0x0 rect, so we'll need to calculate it from the leaf instead 31764 if (rect.height <= 0 && rect.width <= 0) { 31765 const leaf$1 = leaf(SugarElement.fromDom(rng.startContainer), rng.startOffset).element; 31766 const elm = isText(leaf$1) ? parent(leaf$1) : Optional.some(leaf$1); 31767 return elm.filter(isElement$1) 31768 .map((e) => e.dom.getBoundingClientRect()) 31769 // We have nothing valid, so just fallback to the original rect 31770 .getOr(rect); 31771 } 31772 else { 31773 return rect; 31774 } 31775 }; 31776 const getSelectionBounds = (editor) => { 31777 const rng = editor.selection.getRng();
vendor: 5,577 bytes, lines 31778-31882
31778 const rect = getRangeRect(rng); 31779 if (editor.inline) { 31780 const scroll = get$b(); 31781 return bounds(scroll.left + rect.left, scroll.top + rect.top, rect.width, rect.height); 31782 } 31783 else { 31784 // Translate to the top level document, as rect is relative to the iframe viewport 31785 const bodyPos = absolute$2(SugarElement.fromDom(editor.getBody())); 31786 return bounds(bodyPos.x + rect.left, bodyPos.y + rect.top, rect.width, rect.height); 31787 } 31788 }; 31789 const getAnchorElementBounds = (editor, lastElement) => lastElement 31790 .filter((elem) => inBody(elem) && isHTMLElement(elem)) 31791 .map(absolute$2) 31792 .getOrThunk(() => getSelectionBounds(editor)); 31793 const getHorizontalBounds = (contentAreaBox, viewportBounds, margin) => { 31794 const x = Math.max(contentAreaBox.x + margin, viewportBounds.x); 31795 const right = Math.min(contentAreaBox.right - margin, viewportBounds.right); 31796 return { x, width: right - x }; 31797 }; 31798 const getVerticalBounds = (editor, contentAreaBox, viewportBounds, isToolbarLocationTop, toolbarType, margin) => { 31799 const container = SugarElement.fromDom(editor.getContainer()); 31800 const header = descendant(container, '.tox-editor-header').getOr(container); 31801 const headerBox = box$1(header); 31802 const isToolbarBelowContentArea = headerBox.y >= contentAreaBox.bottom; 31803 const isToolbarAbove = isToolbarLocationTop && !isToolbarBelowContentArea; 31804 // Scenario toolbar top & inline: Bottom of the header -> Bottom of the viewport 31805 if (editor.inline && isToolbarAbove) { 31806 return { 31807 y: Math.max(headerBox.bottom + margin, viewportBounds.y), 31808 bottom: viewportBounds.bottom 31809 }; 31810 } 31811 // Scenario toolbar top & inline: Top of the viewport -> Top of the header 31812 if (editor.inline && !isToolbarAbove) { 31813 return { 31814 y: viewportBounds.y, 31815 bottom: Math.min(headerBox.y - margin, viewportBounds.bottom) 31816 }; 31817 } 31818 // Allow line based context toolbar to overlap the statusbar 31819 const containerBounds = toolbarType === 'line' ? box$1(container) : contentAreaBox; 31820 // Scenario toolbar bottom & Iframe: Bottom of the header -> Bottom of the editor container 31821 if (isToolbarAbove) { 31822 return { 31823 y: Math.max(headerBox.bottom + margin, viewportBounds.y), 31824 bottom: Math.min(containerBounds.bottom - margin, viewportBounds.bottom) 31825 }; 31826 } 31827 // Scenario toolbar bottom & Iframe: Top of the editor container -> Top of the header 31828 return { 31829 y: Math.max(containerBounds.y + margin, viewportBounds.y), 31830 bottom: Math.min(headerBox.y - margin, viewportBounds.bottom) 31831 }; 31832 }; 31833 const getContextToolbarBounds = (editor, sharedBackstage, toolbarType, margin = 0) => { 31834 const viewportBounds = getBounds$1(window); 31835 const contentAreaBox = box$1(SugarElement.fromDom(editor.getContentAreaContainer())); 31836 const toolbarOrMenubarEnabled = isMenubarEnabled(editor) || isToolbarEnabled(editor) || isMultipleToolbars(editor); 31837 const { x, width } = getHorizontalBounds(contentAreaBox, viewportBounds, margin); 31838 // Create bounds that lets the context toolbar overflow outside the content area, but remains in the viewport 31839 if (editor.inline && !toolbarOrMenubarEnabled) { 31840 return bounds(x, viewportBounds.y, width, viewportBounds.height); 31841 } 31842 else { 31843 const isToolbarTop = sharedBackstage.header.isPositionedAtTop(); 31844 const { y, bottom } = getVerticalBounds(editor, contentAreaBox, viewportBounds, isToolbarTop, toolbarType, margin); 31845 return bounds(x, y, width, bottom - y); 31846 } 31847 }; 31848 31849 const bubbleSize$1 = 12; 31850 const bubbleAlignments$1 = { 31851 valignCentre: [], 31852 alignCentre: [], 31853 alignLeft: ['tox-pop--align-left'], 31854 alignRight: ['tox-pop--align-right'], 31855 right: ['tox-pop--right'], 31856 left: ['tox-pop--left'], 31857 bottom: ['tox-pop--bottom'], 31858 top: ['tox-pop--top'], 31859 inset: ['tox-pop--inset'] 31860 }; 31861 const anchorOverrides = { 31862 maxHeightFunction: expandable$1(), 31863 maxWidthFunction: expandable() 31864 }; 31865 const isEntireElementSelected = (editor, elem) => { 31866 const rng = editor.selection.getRng(); 31867 const leaf$1 = leaf(SugarElement.fromDom(rng.startContainer), rng.startOffset); 31868 return rng.startContainer === rng.endContainer && rng.startOffset === rng.endOffset - 1 && eq(leaf$1.element, elem); 31869 }; 31870 const preservePosition = (elem, position, f) => { 31871 const currentPosition = getRaw(elem, 'position'); 31872 set$7(elem, 'position', position); 31873 const result = f(elem); 31874 currentPosition.each((pos) => set$7(elem, 'position', pos)); 31875 return result; 31876 }; 31877 // Don't use an inset layout when using a selection/line based anchors as it'll cover the content and can't be moved out the way 31878 const shouldUseInsetLayouts = (position) => position === 'node'; 31879 /** 31880 * This function is designed to attempt to intelligently detect where the contextbar should be anchored when using an inside 31881 * layout. It will attempt to preserve the previous outside placement when anchoring to the same element. However, when the 31882 * placement is re-triggered (e.g. not triggered by a reposition) and the current editor selection overlaps with the contextbar,
31883 * then the anchoring should flip from the previous position to avoid conflicting with the selection. 31884 */ 31885 const determineInsetLayout = (editor, contextbar, elem, data, bounds) => { 31886 const selectionBounds = getSelectionBounds(editor); 31887 const isSameAnchorElement = data.lastElement().exists((prev) => eq(elem, prev)); 31888 if (isEntireElementSelected(editor, elem)) { 31889 // The entire anchor element is selected so it'll always overlap with the selection, in which case just 31890 // preserve or show at the top for a new anchor element. 31891 return isSameAnchorElement ? preserve$1 : north$1; 31892 } 31893 else if (isSameAnchorElement) { 31894 // Preserve the position, get the bounds and then see if we have an overlap. 31895 // If overlapping and this wasn't triggered by a reposition then flip the placement 31896 return preservePosition(contextbar, data.getMode(), () => { 31897 // TINY-8890: The negative 20px threshold here was arrived at by considering the use 31898 // case of a table with default heights for the rows. The threshold had to be 31899 // large enough so that the context toolbar would not prevent the user selecting 31900 // in the row containing the context toolbar. 31901 const isOverlapping = isVerticalOverlap(selectionBounds, box$1(contextbar), -20); 31902 return isOverlapping && !data.isReposition() ? flip : preserve$1; 31903 }); 31904 } 31905 else { 31906 // Attempt to find the best layout to use that won't cause an overlap for the new anchor element 31907 // Note: In fixed positioning mode we need to translate by adding the scroll pos to get the absolute position 31908 const yBounds = data.getMode() === 'fixed' ? bounds.y + get$b().top : bounds.y; 31909 const contextbarHeight = get$d(contextbar) + bubbleSize$1; 31910 return yBounds + contextbarHeight <= selectionBounds.y ? north$1 : south$1; 31911 } 31912 }; 31913 const getAnchorSpec$2 = (editor, mobile, data, position) => { 31914 // IMPORTANT: We lazily determine the layout here so that we only do the calculations if absolutely necessary 31915 const smartInsetLayout = (elem) => (anchor, element, bubbles, placee, bounds) => { 31916 const layout = determineInsetLayout(editor, placee, elem, data, bounds); 31917 // Adjust the anchor box to use the passed y bound coords so that we simulate a "docking" type of behaviour 31918 const newAnchor = { 31919 ...anchor, 31920 y: bounds.y, 31921 height: bounds.height 31922 }; 31923 return { 31924 ...layout(newAnchor, element, bubbles, placee, bounds), 31925 // Ensure this is always the preferred option if no outside layouts fit 31926 alwaysFit: true 31927 }; 31928 }; 31929 const getInsetLayouts = (elem) => shouldUseInsetLayouts(position) ? [smartInsetLayout(elem)] : []; 31930 // On desktop we prioritise north-then-south because it's cleaner, but on mobile we prioritise south to try to avoid overlapping with native context toolbars 31931 const desktopAnchorSpecLayouts = { 31932 onLtr: (elem) => [north$2, south$2, northeast$2, southeast$2, northwest$2, southwest$2].concat(getInsetLayouts(elem)), 31933 onRtl: (elem) => [north$2, south$2, northwest$2, southwest$2, northeast$2, southeast$2].concat(getInsetLayouts(elem)) 31934 }; 31935 const mobileAnchorSpecLayouts = { 31936 onLtr: (elem) => [south$2, southeast$2, southwest$2, northeast$2, northwest$2, north$2].concat(getInsetLayouts(elem)), 31937 onRtl: (elem) => [south$2, southwest$2, southeast$2, northwest$2, northeast$2, north$2].concat(getInsetLayouts(elem)) 31938 }; 31939 return mobile ? mobileAnchorSpecLayouts : desktopAnchorSpecLayouts; 31940 }; 31941 const getAnchorLayout = (editor, position, isTouch, data) => { 31942 if (position === 'line') { 31943 return { 31944 bubble: nu$6(bubbleSize$1, 0, bubbleAlignments$1), 31945 layouts: { 31946 onLtr: () => [east$2, west$2], 31947 onRtl: () => [west$2, east$2] 31948 }, 31949 overrides: anchorOverrides 31950 }; 31951 } 31952 else { 31953 return { 31954 // Ensure that inset layouts use a 1px bubble since we're hiding the bubble arrow 31955 bubble: nu$6(0, bubbleSize$1, bubbleAlignments$1, 1 / bubbleSize$1), 31956 layouts: getAnchorSpec$2(editor, isTouch, data, position), 31957 overrides: anchorOverrides 31958 }; 31959 } 31960 }; 31961 31962 const matchTargetWith = (elem, candidates) => { 31963 const ctxs = filter$2(candidates, (toolbarApi) => toolbarApi.predicate(elem.dom)); 31964 // TODO: somehow type this properly (Arr.partition can't) 31965 // e.g. here pass is Toolbar.ContextToolbar and fail is Toolbar.ContextForm 31966 const { pass, fail } = partition$3(ctxs, (t) => t.type === 'contexttoolbar'); 31967 return { 31968 contextToolbars: pass, 31969 contextForms: fail 31970 }; 31971 }; 31972 const filterByPositionForStartNode = (toolbars) => { 31973 if (toolbars.length <= 1) { 31974 return toolbars; 31975 } 31976 else { 31977 const doesPositionExist = (value) => exists(toolbars, (t) => t.position === value); 31978 const filterToolbarsByPosition = (value) =>
31978 filter$2(toolbars, (t) => t.position === value); 31979 const hasSelectionToolbars = doesPositionExist('selection'); 31980 const hasNodeToolbars = doesPositionExist('node'); 31981 if (hasSelectionToolbars || hasNodeToolbars) { 31982 if (hasNodeToolbars && hasSelectionToolbars) { 31983 // if there's a mix, change the 'selection' toolbars to 'node' so there's no positioning confusion 31984 const nodeToolbars = filterToolbarsByPosition('node'); 31985 const selectionToolbars = map$2(filterToolbarsByPosition('selection'), (t) => ({ ...t, position: 'node' })); 31986 return nodeToolbars.concat(selectionToolbars); 31987 } 31988 else { 31989 return hasSelectionToolbars ? filterToolbarsByPosition('selection') : filterToolbarsByPosition('node'); 31990 } 31991 } 31992 else { 31993 return filterToolbarsByPosition('line'); 31994 } 31995 } 31996 }; 31997 const filterByPositionForAncestorNode = (toolbars) => { 31998 if (toolbars.length <= 1) { 31999 return toolbars; 32000 } 32001 else { 32002 const findPosition = (value) => find$5(toolbars, (t) => t.position === value); 32003 // prioritise position by 'selection' -> 'node' -> 'line' 32004 const basePosition = findPosition('selection') 32005 .orThunk(() => findPosition('node')) 32006 .orThunk(() => findPosition('line')) 32007 .map((t) => t.position); 32008 return basePosition.fold(() => [], (pos) => filter$2(toolbars, (t) => t.position === pos)); 32009 } 32010 }; 32011 const matchStartNode = (elem, nodeCandidates, editorCandidates) => { 32012 // requirements: 32013 // 1. prioritise context forms over context menus 32014 // 2. prioritise node scoped over editor scoped context forms 32015 // 3. only show max 1 context form 32016 // 4. concatenate all available context toolbars if no context form 32017 const nodeMatches = matchTargetWith(elem, nodeCandidates); 32018 if (nodeMatches.contextForms.length > 0) { 32019 return Optional.some({ elem, toolbars: [nodeMatches.contextForms[0]] }); 32020 } 32021 else { 32022 const editorMatches = matchTargetWith(elem, editorCandidates); 32023 if (editorMatches.contextForms.length > 0) { 32024 return Optional.some({ elem, toolbars: [editorMatches.contextForms[0]] }); 32025 } 32026 else if (nodeMatches.contextToolbars.length > 0 || editorMatches.contextToolbars.length > 0) { 32027 const toolbars = filterByPositionForStartNode(nodeMatches.contextToolbars.concat(editorMatches.contextToolbars)); 32028 return Optional.some({ elem, toolbars }); 32029 } 32030 else { 32031 return Optional.none(); 32032 } 32033 } 32034 }; 32035 const matchAncestor = (isRoot, startNode, scopes) => { 32036 // Don't continue to traverse if the start node is the root node 32037 if (isRoot(startNode)) { 32038 return Optional.none(); 32039 } 32040 else { 32041 return ancestor(startNode, (ancestorElem) => { 32042 if (isElement$1(ancestorElem)) { 32043 const { contextToolbars, contextForms } = matchTargetWith(ancestorElem, scopes.inNodeScope); 32044 const toolbars = contextForms.length > 0 ? contextForms : filterByPositionForAncestorNode(contextToolbars); 32045 return toolbars.length > 0 ? Optional.some({ elem: ancestorElem, toolbars }) : Optional.none(); 32046 } 32047 else { 32048 return Optional.none(); 32049 } 32050 }, isRoot); 32051 } 32052 }; 32053 const lookup$1 = (scopes, editor) => { 32054 const rootElem = SugarElement.fromDom(editor.getBody()); 32055 const isRoot = (elem) => eq(elem, rootElem); 32056 const isOutsideRoot = (startNode) => !isRoot(startNode) && !contains(rootElem, startNode); 32057 const startNode = SugarElement.fromDom(editor.selection.getNode()); 32058 // Ensure the lookup doesn't start on a parent or sibling element of the root node 32059 if (isOutsideRoot(startNode)) { 32060 return Optional.none(); 32061 } 32062 return matchStartNode(startNode, scopes.inNodeScope, scopes.inEditorScope).orThunk(() => matchAncestor(isRoot, startNode, scopes)); 32063 }; 32064 32065 const categorise = (contextToolbars, navigate) => { 32066 // TODO: Use foldl/foldr and avoid as much mutation. 32067 const forms = {}; 32068 const inNodeScope = []; 32069 const inEditorScope = []; 32070 const formNavigators = {}; 32071 const lookupTable = {}; 32072 const registerForm = (key, toolbarSpec) => { 32073 const contextForm = getOrDie(createContextForm(toolbarSpec)); 32074 forms[key] = contextForm; 32075 contextForm.launch.map((launch) => { 32076 // Use the original here (pre-boulder), because using as a the spec for toolbar buttons 32077 formNavigators['form:' + key + ''] = { 32078 ...toolbarSpec.launch, 32079 type: (launch.type === 'contextformtogglebutton' ? 'togglebutton' : 'button'), 32080 onAction: () => { 32081 navigate(contextForm); 32082 } 32083 }; 32084 }); 32085 if (contextForm.scope === 'editor') { 32086 inEditorScope.push(contextForm); 32087 } 32088 else { 32089 inNodeScope.push(contextForm); 32090 } 32091 lookupTable[key] = contextForm; 32092 }; 32093 const registerToolbar = (key, toolbarSpec) => { 32094 createContextToolbar(toolbarSpec).each((contextToolbar) => { 32095 if (contextToolbar.launch.isSome()) { 32096 formNavigators['toolbar:' + key + ''] = { 32097 ...toolbarSpec.launch, 32098 type: 'button', 32099 onAction: () => { 32100 navigate(contextToolbar); 32101 } 32102 }; 32103 } 32104 if (toolbarSpec.scope === 'editor') { 32105 inEditorScope.push(contextToolbar); 32106 } 32107 else { 32108 inNodeScope.push(contextToolbar); 32109 } 32110 lookupTable[key] = contextToolbar; 32111 }); 32112 };
32113 const keys$1 = keys(contextToolbars); 32114 each$1(keys$1, (key) => { 32115 const toolbarApi = contextToolbars[key]; 32116 if (toolbarApi.type === 'contextform' || toolbarApi.type === 'contextsliderform' || toolbarApi.type === 'contextsizeinputform') { 32117 registerForm(key, toolbarApi); 32118 } 32119 else if (toolbarApi.type === 'contexttoolbar') { 32120 registerToolbar(key, toolbarApi); 32121 } 32122 }); 32123 return { 32124 forms, 32125 inNodeScope, 32126 inEditorScope, 32127 lookupTable, 32128 formNavigators 32129 }; 32130 }; 32131 32132 const transitionClass = 'tox-pop--transition'; 32133 const isToolbarActionKey = (keyCode) => keyCode === global$1.ENTER || keyCode === global$1.SPACEBAR; 32134 const register$a = (editor, registryContextToolbars, sink, extras) => { 32135 const backstage = extras.backstage; 32136 const sharedBackstage = backstage.shared; 32137 const isTouch = detect$1().deviceType.isTouch; 32138 const lastElement = value$2(); 32139 const lastTrigger = value$2(); 32140 const lastContextPosition = value$2(); 32141 const contextToolbarResult = renderContextToolbar({ 32142 sink, 32143 onEscape: () => { 32144 editor.focus(); 32145 fireContextToolbarClose(editor); 32146 return Optional.some(true); 32147 }, 32148 onHide: () => { 32149 fireContextToolbarClose(editor); 32150 }, 32151 onBack: () => { 32152 fireContextFormSlideBack(editor); 32153 }, 32154 focusElement: (el) => { 32155 if (editor.getBody().contains(el.dom)) { 32156 editor.focus(); 32157 } 32158 else { 32159 focus$4(el); 32160 } 32161 } 32162 }); 32163 const contextbar = build$1(contextToolbarResult.sketch); 32164 const getBounds = () => { 32165 const position = lastContextPosition.get().getOr('node'); 32166 // Use a 1px margin for the bounds to keep the context toolbar from butting directly against 32167 // the header, etc... when switching to inset layouts 32168 const margin = shouldUseInsetLayouts(position) ? 1 : 0; 32169 return getContextToolbarBounds(editor, sharedBackstage, position, margin); 32170 }; 32171 const canLaunchToolbar = () => { 32172 // If a mobile context menu is open, don't launch else they'll probably overlap. For android, specifically. 32173 return !editor.removed && !(isTouch() && backstage.isContextMenuOpen()); 32174 }; 32175 const isSameLaunchElement = (elem) => is$1(lift2(elem, lastElement.get(), eq), true); 32176 const shouldContextToolbarHide = () => { 32177 if (!canLaunchToolbar()) { 32178 return true; 32179 } 32180 else { 32181 const contextToolbarBounds = getBounds(); 32182 // Get the anchor bounds. For node anchors we should always try to use the last element bounds 32183 const anchorBounds = is$1(lastContextPosition.get(), 'node') ? 32184 getAnchorElementBounds(editor, lastElement.get()) : 32185 getSelectionBounds(editor); 32186 // If the anchor bounds aren't overlapping with the context toolbar bounds, then the context toolbar 32187 // should hide. We want the threshold to require some overlap here (+.01), so that as soon as the 32188 // anchor is off-screen, the context toolbar disappers. 32189 return contextToolbarBounds.height <= 0 || !isVerticalOverlap(anchorBounds, contextToolbarBounds, 0.01); 32190 } 32191 }; 32192 const close = () => { 32193 lastElement.clear(); 32194 lastTrigger.clear(); 32195 lastContextPosition.clear(); 32196 InlineView.hide(contextbar); 32197 }; 32198 const hideOrRepositionIfNecessary = () => { 32199 if (InlineView.isOpen(contextbar)) { 32200 const contextBarEle = contextbar.element; 32201 remove$6(contextBarEle, 'display'); 32202 if (shouldContextToolbarHide()) { 32203 set$7(contextBarEle, 'display', 'none'); 32204 } 32205 else { 32206 lastTrigger.set(0 /* TriggerCause.Reposition */); 32207 InlineView.reposition(contextbar); 32208 } 32209 } 32210 }; 32211 const wrapInPopDialog = (toolbarSpec) => ({ 32212 dom: { 32213 tag: 'div', 32214 classes: ['tox-pop__dialog'] 32215 }, 32216 components: [toolbarSpec], 32217 behaviours: derive$1([ 32218 Keying.config({ 32219 mode: 'acyclic' 32220 }), 32221 config('pop-dialog-wrap-events', [ 32222 runOnAttached((comp) => { 32223 editor.shortcuts.add('ctrl+F9', 'focus statusbar', () => Keying.focusIn(comp)); 32224 }), 32225 runOnDetached((_comp) => { 32226 editor.shortcuts.remove('ctrl+F9'); 32227 }) 32228 ]) 32229 ]) 32230 }); 32231 const navigate = (toolbarApi) => { 32232 // ASSUMPTION: This should only ever show one context toolbar since it's used for c
vendor: 4,701 bytes, lines 32232-32313
32232ontext forms hence [toolbarApi] 32233 const alloySpec = buildToolbar([toolbarApi]); 32234 emitWith(contextbar, forwardSlideEvent, { 32235 forwardContents: wrapInPopDialog(alloySpec) 32236 }); 32237 }; 32238 const getScopes = cached(() => categorise(registryContextToolbars, navigate)); 32239 const buildContextToolbarGroups = (allButtons, ctx) => { 32240 return identifyButtons(editor, { buttons: allButtons, toolbar: ctx.items, allowToolbarGroups: false }, extras.backstage, Optional.some(['form:', 'toolbar:'])); 32241 }; 32242 const buildContextFormGroups = (ctx, providers) => ContextForm.buildInitGroups(ctx, providers); 32243 const buildToolbar = (toolbars) => { 32244 const { buttons } = editor.ui.registry.getAll(); 32245 const scopes = getScopes(); 32246 const allButtons = { ...buttons, ...scopes.formNavigators }; 32247 // For context toolbars we don't want to use floating or sliding, so just restrict this 32248 // to scrolling or wrapping (default) 32249 const toolbarType = getToolbarMode(editor) === ToolbarMode$1.scrolling ? ToolbarMode$1.scrolling : ToolbarMode$1.default; 32250 const initGroups = flatten(map$2(toolbars, (ctx) => ctx.type === 'contexttoolbar' ? buildContextToolbarGroups(allButtons, contextToolbarToSpec(ctx)) : buildContextFormGroups(ctx, sharedBackstage.providers))); 32251 return renderToolbar({ 32252 type: toolbarType, 32253 uid: generate$6('context-toolbar'), 32254 initGroups, 32255 onEscape: Optional.none, 32256 cyclicKeying: true, 32257 providers: sharedBackstage.providers 32258 }); 32259 }; 32260 const getAnchor = (position, element) => { 32261 const anchorage = position === 'node' ? sharedBackstage.anchors.node(element) : sharedBackstage.anchors.cursor(); 32262 const anchorLayout = getAnchorLayout(editor, position, isTouch(), { 32263 lastElement: lastElement.get, 32264 isReposition: () => is$1(lastTrigger.get(), 0 /* TriggerCause.Reposition */), 32265 getMode: () => Positioning.getMode(sink) 32266 }); 32267 return deepMerge(anchorage, anchorLayout); 32268 }; 32269 const launchContext = (toolbarApi, elem) => { 32270 launchContextToolbar.cancel(); 32271 // Don't launch if the editor has something else open that would conflict 32272 if (!canLaunchToolbar()) { 32273 return; 32274 } 32275 const toolbarSpec = buildToolbar(toolbarApi); 32276 // TINY-4495 ASSUMPTION: Can only do toolbarApi[0].position because ContextToolbarLookup.filterToolbarsByPosition 32277 // ensures all toolbars returned by ContextToolbarLookup have the same position. 32278 // And everything else that gets toolbars from elsewhere only returns maximum 1 toolbar 32279 const position = toolbarApi[0].position; 32280 const anchor = getAnchor(position, elem); 32281 lastContextPosition.set(position); 32282 lastTrigger.set(1 /* TriggerCause.NewAnchor */); 32283 const contextBarEle = contextbar.element; 32284 remove$6(contextBarEle, 'display'); 32285 // Reset placement and transitions when moving to different elements 32286 if (!isSameLaunchElement(elem)) { 32287 remove$3(contextBarEle, transitionClass); 32288 Positioning.reset(sink, contextbar); 32289 } 32290 // Place the element 32291 InlineView.showWithinBounds(contextbar, wrapInPopDialog(toolbarSpec), { 32292 anchor, 32293 transition: { 32294 classes: [transitionClass], 32295 mode: 'placement' 32296 } 32297 }, () => Optional.some(getBounds())); 32298 // IMPORTANT: This must be stored after the initial render, otherwise the lookup of the last element in the 32299 // anchor placement will be incorrect as it'll reuse the new element as the anchor point. 32300 elem.fold(lastElement.clear, lastElement.set); 32301 // It's possible we may have launched offscreen, if so then hide 32302 if (shouldContextToolbarHide()) { 32303 set$7(contextBarEle, 'display', 'none'); 32304 } 32305 }; 32306 const instantReposition = () => { 32307 // Sometimes when we reposition the toolbar it might be in a transitioning state and 32308 // if we try to reposition while that happens the computed position/width will be incorrect. 32309 set$7(contextbar.element, 'transition', 'none'); 32310 hideOrRepositionIfNecessary(); 32311 remove$6(contextbar.element, 'transition'); 32312 }; 32313 let isDragging = false;
32314 const launchContextToolbar = last(() => { 32315 // Don't launch if the editor doesn't have focus or has been destroyed 32316 if (!editor.hasFocus() || editor.removed || isDragging) { 32317 return; 32318 } 32319 // If currently transitioning then throttle again so we don't interrupt the transition 32320 if (has(contextbar.element, transitionClass)) { 32321 launchContextToolbar.throttle(); 32322 } 32323 else { 32324 const scopes = getScopes(); 32325 lookup$1(scopes, editor).fold(close, (info) => { 32326 launchContext(info.toolbars, Optional.some(info.elem)); 32327 }); 32328 } 32329 }, 17); // 17ms is used as that's about 1 frame at 60fps 32330 editor.on('init', () => { 32331 editor.on('remove', close); 32332 editor.on('ScrollContent ScrollWindow ObjectResized ResizeEditor longpress', hideOrRepositionIfNecessary); 32333 // FIX: Make it go away when the action makes it go away. E.g. deleting a column deletes the table. 32334 editor.on('click focus SetContent', launchContextToolbar.throttle); 32335 editor.on('keyup', (e) => { 32336 // If you use keyboard to press a button in a subtoolbar then the keyup will happen inside the editor and that should not re-render the toolbar 32337 if (!isToolbarActionKey(e.keyCode) || !contextToolbarResult.inSubtoolbar()) { 32338 launchContextToolbar.throttle(); 32339 } 32340 }); 32341 editor.on(hideContextToolbarEvent, close); 32342 editor.on(showContextToolbarEvent, (e) => { 32343 const scopes = getScopes(); 32344 // TODO: Have this stored in a better structure 32345 get$h(scopes.lookupTable, e.toolbarKey).each((ctx) => { 32346 // ASSUMPTION: this is only used to open one specific toolbar at a time, hence [ctx] 32347 launchContext([ctx], someIf(e.target !== editor, e.target)); 32348 focusIn(contextbar); 32349 }); 32350 }); 32351 editor.on('focusout', (_e) => { 32352 global$a.setEditorTimeout(editor, () => { 32353 if (search(sink.element).isNone() && search(contextbar.element).isNone() && !editor.hasFocus()) { 32354 close(); 32355 } 32356 }, 0); 32357 }); 32358 editor.on('SwitchMode', () => { 32359 if (editor.mode.isReadOnly()) { 32360 close(); 32361 } 32362 }); 32363 editor.on('DisabledStateChange', (e) => { 32364 if (e.state) { 32365 close(); 32366 } 32367 }); 32368 // TINY-10640: Firefox was flaking in tests and was not properly dismissing the toolbar could not reproduce it manually but adding this seems to resolve it. 32369 editor.on('ExecCommand', ({ command }) => { 32370 if (command.toLowerCase() === 'toggleview') { 32371 close(); 32372 } 32373 }); 32374 editor.on('AfterProgressState', (event) => { 32375 if (event.state) { 32376 close(); 32377 } 32378 else if (editor.hasFocus()) { 32379 launchContextToolbar.throttle(); 32380 } 32381 }); 32382 editor.on('dragstart', () => { 32383 isDragging = true; 32384 }); 32385 editor.on('dragend drop', () => { 32386 isDragging = false; 32387 }); 32388 editor.on('NodeChange', (_e) => { 32389 if (!contextToolbarResult.inSubtoolbar()) { 32390 search(contextbar.element).fold(launchContextToolbar.throttle, noop); 32391 } 32392 else { 32393 instantReposition(); 32394 } 32395 }); 32396 }); 32397 }; 32398 32399 const register$9 = (editor) => { 32400 const alignToolbarButtons = [ 32401 { name: 'alignleft', text: 'Align left', cmd: 'JustifyLeft', icon: 'align-left' }, 32402 { name: 'aligncenter', text: 'Align center', cmd: 'JustifyCenter', icon: 'align-center' }, 32403 { name: 'alignright', text: 'Align right', cmd: 'JustifyRight', icon: 'align-right' }, 32404 { name: 'alignjustify', text: 'Justify', cmd: 'JustifyFull', icon: 'align-justify' } 32405 ]; 32406 each$1(alignToolbarButtons, (item) => { 32407 editor.ui.registry.addToggleButton(item.name, { 32408 tooltip: item.text, 32409 icon: item.icon, 32410 onAction: onActionExecCommand(editor, item.cmd), 32411 onSetup: onSetupStateToggle(editor, item.name) 32412 }); 32413 }); 32414 editor.ui.registry.addButton('alignnone', { 32415 tooltip: 'No alignment', 32416 icon: 'align-none', 32417 onSetup: onSetupEditableToggle(editor), 32418 onAction: onActionExecCommand(editor, 'JustifyNone') 32419 }); 32420 }; 32421 32422 const registerController = (editor, spec) => { 32423 const getMenuItems = () => { 32424 const options = spec.getOptions(editor); 32425 const initial = spec.getCurrent(editor).map(spec.hash); 32426 const current = value$2(); 32427 return map$2(options, (value) => ({ 32428 type: 'togglemenuitem', 32429 text: spec.display(value),
32430 onSetup: (api) => { 32431 const setActive = (active) => { 32432 if (active) { 32433 current.on((oldApi) => oldApi.setActive(false)); 32434 current.set(api); 32435 } 32436 api.setActive(active); 32437 }; 32438 setActive(is$1(initial, spec.hash(value))); 32439 const unbindWatcher = spec.watcher(editor, value, setActive); 32440 return () => { 32441 current.clear(); 32442 unbindWatcher(); 32443 }; 32444 }, 32445 onAction: () => spec.setCurrent(editor, value) 32446 })); 32447 }; 32448 editor.ui.registry.addMenuButton(spec.name, { 32449 tooltip: spec.text, 32450 icon: spec.icon, 32451 fetch: (callback) => callback(getMenuItems()), 32452 onSetup: spec.onToolbarSetup 32453 }); 32454 editor.ui.registry.addNestedMenuItem(spec.name, { 32455 type: 'nestedmenuitem', 32456 text: spec.text, 32457 getSubmenuItems: getMenuItems, 32458 onSetup: spec.onMenuSetup 32459 }); 32460 }; 32461 const lineHeightSpec = (editor) => ({ 32462 name: 'lineheight', 32463 text: 'Line height', 32464 icon: 'line-height', 32465 getOptions: getLineHeightFormats, 32466 hash: (input) => normalise(input, ['fixed', 'relative', 'empty']).getOr(input), 32467 display: identity, 32468 watcher: (editor, value, callback) => editor.formatter.formatChanged('lineheight', callback, false, { value }).unbind, 32469 getCurrent: (editor) => Optional.from(editor.queryCommandValue('LineHeight')), 32470 setCurrent: (editor, value) => editor.execCommand('LineHeight', false, value), 32471 onToolbarSetup: onSetupEditableToggle(editor), 32472 onMenuSetup: onSetupEditableToggle(editor) 32473 }); 32474 const languageSpec = (editor) => { 32475 const settingsOpt = Optional.from(getContentLanguages(editor)); 32476 return settingsOpt.map((settings) => ({ 32477 name: 'language', 32478 text: 'Language', 32479 icon: 'language', 32480 getOptions: constant$1(settings), 32481 hash: (input) => isUndefined(input.customCode) ? input.code : `${input.code}/${input.customCode}`, 32482 display: (input) => input.title, 32483 watcher: (editor, value, callback) => editor.formatter.formatChanged('lang', callback, false, { value: value.code, customValue: value.customCode ?? null }).unbind, 32484 getCurrent: (editor) => { 32485 const node = SugarElement.fromDom(editor.selection.getNode()); 32486 return closest(node, (n) => Optional.some(n) 32487 .filter(isElement$1) 32488 .bind((ele) => { 32489 const codeOpt = getOpt(ele, 'lang'); 32490 return codeOpt.map((code) => { 32491 const customCode = getOpt(ele, 'data-mce-lang').getOrUndefined(); 32492 return { code, customCode, title: '' }; 32493 }); 32494 })); 32495 }, 32496 setCurrent: (editor, lang) => editor.execCommand('Lang', false, lang), 32497 onToolbarSetup: (api) => { 32498 const unbinder = unbindable(); 32499 api.setActive(editor.formatter.match('lang', {}, undefined, true)); 32500 unbinder.set(editor.formatter.formatChanged('lang', api.setActive, true)); 32501 return composeUnbinders(unbinder.clear, onSetupEditableToggle(editor)(api)); 32502 }, 32503 onMenuSetup: onSetupEditableToggle(editor) 32504 })); 32505 }; 32506 const register$8 = (editor) => { 32507 registerController(editor, lineHeightSpec(editor)); 32508 languageSpec(editor).each((spec) => registerController(editor, spec)); 32509 }; 32510 32511 const register$7 = (editor, backstage) => { 32512 createAlignMenu(editor, backstage); 32513 createFontFamilyMenu(editor, backstage); 32514 createStylesMenu(editor, backstage); 32515 createBlocksMenu(editor, backstage); 32516 createFontSizeMenu(editor, backstage); 32517 }; 32518 32519 const register$6 = (editor) => { 32520 editor.ui.registry.addContext('editable', () => { 32521 return editor.selection.isEditable(); 32522 }); 32523 editor.ui.registry.addContext('mode', (mode) => { 32524 return editor.mode.get() === mode; 32525 }); 32526 editor.ui.registry.addContext('any', always); 32527 editor.ui.registry.addContext('formatting', (format) => { 32528 return editor.formatter.canApply(format); 32529 }); 32530 editor.ui.registry.addContext('insert', (child) => { 32531 return editor.schema.isValidChild(editor.selection.getNode().tagName, child); 32532 }); 32533 }; 32534 32535 const onSetupOutdentState = (editor) => onSetupEvent(editor, 'NodeChange', (api) => { 32536 api.setEnabled(editor.queryCommandState('outdent') && editor.selection.isEditable()); 32537 }); 32538 const registerButtons$2 = (editor) => {
32539 editor.ui.registry.addButton('outdent', { 32540 tooltip: 'Decrease indent', 32541 icon: 'outdent', 32542 onSetup: onSetupOutdentState(editor), 32543 onAction: onActionExecCommand(editor, 'outdent') 32544 }); 32545 editor.ui.registry.addButton('indent', { 32546 tooltip: 'Increase indent', 32547 icon: 'indent', 32548 onSetup: onSetupEditableToggle(editor, () => editor.queryCommandState('indent')), 32549 onAction: onActionExecCommand(editor, 'indent') 32550 }); 32551 }; 32552 const register$5 = (editor) => { 32553 registerButtons$2(editor); 32554 }; 32555 32556 const makeSetupHandler = (editor, pasteAsText) => (api) => { 32557 api.setActive(pasteAsText.get()); 32558 const pastePlainTextToggleHandler = (e) => { 32559 pasteAsText.set(e.state); 32560 api.setActive(e.state); 32561 }; 32562 editor.on('PastePlainTextToggle', pastePlainTextToggleHandler); 32563 return composeUnbinders(() => editor.off('PastePlainTextToggle', pastePlainTextToggleHandler), onSetupEditableToggle(editor)(api)); 32564 }; 32565 const register$4 = (editor) => { 32566 const pasteAsText = Cell(getPasteAsText(editor)); 32567 const onAction = () => editor.execCommand('mceTogglePlainTextPaste'); 32568 editor.ui.registry.addToggleButton('pastetext', { 32569 active: false, 32570 icon: 'paste-text', 32571 tooltip: 'Paste as text', 32572 onAction, 32573 onSetup: makeSetupHandler(editor, pasteAsText) 32574 }); 32575 editor.ui.registry.addToggleMenuItem('pastetext', { 32576 text: 'Paste as text', 32577 icon: 'paste-text', 32578 onAction, 32579 onSetup: makeSetupHandler(editor, pasteAsText) 32580 }); 32581 }; 32582 32583 const onActionToggleFormat = (editor, fmt) => () => { 32584 editor.execCommand('mceToggleFormat', false, fmt); 32585 }; 32586 const registerFormatButtons = (editor) => { 32587 global$2.each([ 32588 { name: 'bold', text: 'Bold', icon: 'bold', shortcut: 'Meta+B' }, 32589 { name: 'italic', text: 'Italic', icon: 'italic', shortcut: 'Meta+I' }, 32590 { name: 'underline', text: 'Underline', icon: 'underline', shortcut: 'Meta+U' }, 32591 { name: 'strikethrough', text: 'Strikethrough', icon: 'strike-through' }, 32592 { name: 'subscript', text: 'Subscript', icon: 'subscript' }, 32593 { name: 'superscript', text: 'Superscript', icon: 'superscript' } 32594 ], (btn, _idx) => { 32595 editor.ui.registry.addToggleButton(btn.name, { 32596 tooltip: btn.text, 32597 icon: btn.icon, 32598 onSetup: onSetupStateToggle(editor, btn.name), 32599 onAction: onActionToggleFormat(editor, btn.name), 32600 shortcut: btn.shortcut 32601 }); 32602 }); 32603 for (let i = 1; i <= 6; i++) { 32604 const name = 'h' + i; 32605 const shortcut = `Access+${i}`; 32606 editor.ui.registry.addToggleButton(name, { 32607 text: name.toUpperCase(), 32608 tooltip: 'Heading ' + i, 32609 onSetup: onSetupStateToggle(editor, name), 32610 onAction: onActionToggleFormat(editor, name), 32611 shortcut 32612 }); 32613 } 32614 }; 32615 const registerCommandButtons = (editor) => { 32616 global$2.each([ 32617 { name: 'copy', text: 'Copy', action: 'Copy', icon: 'copy', context: 'any' }, 32618 { name: 'help', text: 'Help', action: 'mceHelp', icon: 'help', shortcut: 'Alt+0', context: 'any' }, 32619 { name: 'selectall', text: 'Select all', action: 'SelectAll', icon: 'select-all', shortcut: 'Meta+A', context: 'any' }, 32620 { name: 'newdocument', text: 'New document', action: 'mceNewDocument', icon: 'new-document' }, 32621 { name: 'print', text: 'Print', action: 'mcePrint', icon: 'print', shortcut: 'Meta+P', context: 'any' }, 32622 ], (btn) => { 32623 editor.ui.registry.addButton(btn.name, { 32624 tooltip: btn.text, 32625 icon: btn.icon, 32626 onAction: onActionExecCommand(editor, btn.action), 32627 shortcut: btn.shortcut, 32628 context: btn.context 32629 }); 32630 }); 32631 global$2.each([ 32632 { name: 'cut', text: 'Cut', action: 'Cut', icon: 'cut' }, 32633 { name: 'paste', text: 'Paste', action: 'Paste', icon: 'paste' }, 32634 // visualaid was here but also exists in VisualAid.ts? 32635 { name: 'removeformat', text: 'Clear formatting', action: 'RemoveFormat', icon: 'remove-formatting' }, 32636 { name: 'remove', text: 'Remove', action: 'Delete', icon: 'remove' }, 32637 { name: 'hr', text: 'Horizontal line', action: 'InsertHorizontalRule', icon: 'horizontal-rule' } 32638 ], (btn) => { 32639 editor.ui.registry.addButton(btn.name, { 32640 tooltip: btn.text, 32641 icon: btn.icon, 32642 onSetup: onSetupEditableToggle(editor), 32643 onAction: onActionExecCommand(editor, btn.action) 32644 }); 32645 }); 32646 }; 32647 const registerCommandToggleButtons = (editor) => { 32648 global$2.each([ 32649 { name: 'blockquote', text: 'Blockquote', action: 'mceBlockQuote', icon: 'quote' } 32650 ], (btn) => { 32651 editor.ui.registry.addToggleButton(btn.name, { 32652 tooltip: btn.text, 32653 icon: btn.icon, 32654 onAction: onActionExecCommand(editor, btn.action), 32655 onSetup: onSetupStateToggle(editor, btn.name) 32656 }); 32657 }); 32658 }; 32659 const registerButtons$1 = (editor) => { 32660 registerFormatButtons(editor); 32661 registerCommandButtons(editor); 32662 registerCommandToggleButtons(editor); 32663 }; 32664 const registerMenuItems$2 = (editor) => { 32665 global$2.each([ 32666 { name: 'newdocument', text: 'New document', action: 'mceNewDocument', icon: 'new-document' }, 32667 { name: 'copy', text: 'Copy', action: 'Copy', icon: 'copy', shortcut: 'Meta+C', context: 'any' }, 32668 { name: 'selectall', text: 'Select all', action: 'SelectAll', icon: 'select-all', shortcut: 'Meta+A', context: 'any' }, 32669 { name: 'print', text: 'Print...', action: 'mcePrint', icon: 'print', shortcut: 'Meta+P', context: 'any' } 32670 ], (menuitem) => { 32671 editor.ui.registry.addMenuItem(menuitem.name, { 32672 text: menuitem.text, 32673 icon: menuitem.icon, 32674 shortcut: menuitem.shortcut, 32675 onAction: onActionExecCommand(editor, menuitem.action), 32676 context: menuitem.context 32677 }); 32678 }); 32679 global$2.each([ 32680 { name: 'bold', text: 'Bold', action: 'Bold', icon: 'bold', shortcut: 'Meta+B' }, 32681 { name: 'italic', text: 'Italic', action: 'Italic', icon: 'italic', shortcut: 'Meta+I' }, 32682 { name: 'underline', text: 'Underline', action: 'Underline', icon: 'underline', shortcut: 'Meta+U' }, 32683 { name: 'strikethrough', text: 'Strikethrough', action: 'Strikethrough', icon: 'strike-through' }, 32684 { name: 'subscript', text: 'Subscript', action: 'Subscript', icon: 'subscript' }, 32685 { name: 'superscript', text: 'Superscript', action: 'Superscript', icon: 'superscript' }, 32686 { name: 'removeformat', text: 'Clear formatting', action: 'RemoveFormat', icon: 'remove-formatting' }, 32687 { name: 'cut', text: 'Cut', action: 'Cut', icon: 'cut', shortcut: 'Meta+X' }, 32688 { name: 'paste', text: 'Paste', action: 'Paste', icon: 'paste', shortcut: 'Meta+V' }, 32689 { name: 'hr', text: 'Horizontal line', action: 'InsertHorizontalRule', icon: 'horizontal-rule' } 32690 ], (menuitem) => { 32691 editor.ui.registry.addMenuItem(menuitem.name, { 32692 text: menuitem.text, 32693 icon: menuitem.icon, 32694 shortcut: menuitem.shortcut, 32695 onSetup: onSetupEditableToggle(editor), 32696 onAction: onActionExecCommand(editor, menuitem.action) 32697 }); 32698 }); 32699 editor.ui.registry.addMenuItem('codeformat', { 32700 text: 'Code', 32701 icon: 'sourcecode', 32702 onSetup: onSetupEditableToggle(editor), 32703 onAction: onActionToggleFormat(editor, 'code') 32704 }); 32705 }; 32706 const register$3 = (editor) => { 32707 registerButtons$1(editor); 32708 registerMenuItems$2(editor); 32709 }; 32710 32711 const onSetupUndoRedoState = (editor, type) => onSetupEvent(editor, 'Undo Redo AddUndo TypingUndo ClearUndos SwitchMode', (api) => { 32712 api.setEnabled(!editor.mode.isReadOnly() && editor.undoManager[type]()); 32713 }); 32714 const registerMenuItems$1 = (editor) => { 32715 editor.ui.registry.addMenuItem('undo', { 32716 text: 'Undo', 32717 icon: 'undo', 32718 shortcut: 'Meta+Z', 32719 onSetup: onSetupUndoRedoState(editor, 'hasUndo'), 32720 onAction: onActionExecCommand(editor, 'undo') 32721 }); 32722 editor.ui.registry.addMenuItem('redo', { 32723 text: 'Redo', 32724 icon: 'redo', 32725 shortcut: 'Meta+Y', 32726 onSetup: onSetupUndoRedoState(editor, 'hasRedo'), 32727 onAction: onActionExecCommand(editor, 'redo') 32728 }); 32729 };
vendor: 23,232 bytes, lines 32730-33284
32730 // Note: The undo/redo buttons are disabled by default here, as they'll be rendered 32731 // on init generally and it won't have any undo levels at that stage. 32732 const registerButtons = (editor) => { 32733 editor.ui.registry.addButton('undo', { 32734 tooltip: 'Undo', 32735 icon: 'undo', 32736 enabled: false, 32737 onSetup: onSetupUndoRedoState(editor, 'hasUndo'), 32738 onAction: onActionExecCommand(editor, 'undo'), 32739 shortcut: 'Meta+Z' 32740 }); 32741 editor.ui.registry.addButton('redo', { 32742 tooltip: 'Redo', 32743 icon: 'redo', 32744 enabled: false, 32745 onSetup: onSetupUndoRedoState(editor, 'hasRedo'), 32746 onAction: onActionExecCommand(editor, 'redo'), 32747 shortcut: 'Meta+Y' 32748 }); 32749 }; 32750 const register$2 = (editor) => { 32751 registerMenuItems$1(editor); 32752 registerButtons(editor); 32753 }; 32754 32755 const onSetupVisualAidState = (editor) => onSetupEvent(editor, 'VisualAid', (api) => { 32756 api.setActive(editor.hasVisual); 32757 }); 32758 const registerMenuItems = (editor) => { 32759 editor.ui.registry.addToggleMenuItem('visualaid', { 32760 text: 'Visual aids', 32761 onSetup: onSetupVisualAidState(editor), 32762 onAction: onActionExecCommand(editor, 'mceToggleVisualAid'), 32763 context: 'any' 32764 }); 32765 }; 32766 const registerToolbarButton = (editor) => { 32767 editor.ui.registry.addButton('visualaid', { 32768 tooltip: 'Visual aids', 32769 text: 'Visual aids', 32770 onAction: onActionExecCommand(editor, 'mceToggleVisualAid'), 32771 context: 'any' 32772 }); 32773 }; 32774 const register$1 = (editor) => { 32775 registerToolbarButton(editor); 32776 registerMenuItems(editor); 32777 }; 32778 32779 const setup$6 = (editor, backstage) => { 32780 register$9(editor); 32781 register$3(editor); 32782 register$7(editor, backstage); 32783 register$2(editor); 32784 register$d(editor); 32785 register$1(editor); 32786 register$5(editor); 32787 register$8(editor); 32788 register$4(editor); 32789 register$6(editor); 32790 }; 32791 32792 const patchPipeConfig = (config) => isString(config) ? config.split(/[ ,]/) : config; 32793 const option = (name) => (editor) => editor.options.get(name); 32794 const register = (editor) => { 32795 const registerOption = editor.options.register; 32796 registerOption('contextmenu_avoid_overlap', { 32797 processor: 'string', 32798 default: '' 32799 }); 32800 registerOption('contextmenu_never_use_native', { 32801 processor: 'boolean', 32802 default: false 32803 }); 32804 registerOption('contextmenu', { 32805 processor: (value) => { 32806 if (value === false) { 32807 return { value: [], valid: true }; 32808 } 32809 else if (isString(value) || isArrayOf(value, isString)) { 32810 return { value: patchPipeConfig(value), valid: true }; 32811 } 32812 else { 32813 return { valid: false, message: 'Must be false or a string.' }; 32814 } 32815 }, 32816 default: 'link linkchecker image editimage table spellchecker configurepermanentpen' 32817 }); 32818 }; 32819 const shouldNeverUseNative = option('contextmenu_never_use_native'); 32820 const getAvoidOverlapSelector = option('contextmenu_avoid_overlap'); 32821 const isContextMenuDisabled = (editor) => getContextMenu(editor).length === 0; 32822 const getContextMenu = (editor) => { 32823 const contextMenus = editor.ui.registry.getAll().contextMenus; 32824 const contextMenu = editor.options.get('contextmenu'); 32825 if (editor.options.isSet('contextmenu')) { 32826 return contextMenu; 32827 } 32828 else { 32829 // Filter default context menu items when they are not in the registry (e.g. when the plugin is not loaded) 32830 return filter$2(contextMenu, (item) => has$2(contextMenus, item)); 32831 } 32832 }; 32833 32834 const nu = (x, y) => ({ 32835 type: 'makeshift', 32836 x, 32837 y 32838 }); 32839 const transpose = (pos, dx, dy) => { 32840 return nu(pos.x + dx, pos.y + dy); 32841 }; 32842 const isTouchEvent$1 = (e) => e.type === 'longpress' || e.type.indexOf('touch') === 0; 32843 const fromPageXY = (e) => { 32844 if (isTouchEvent$1(e)) { 32845 const touch = e.touches[0]; 32846 return nu(touch.pageX, touch.pageY); 32847 } 32848 else { 32849 return nu(e.pageX, e.pageY); 32850 } 32851 }; 32852 const fromClientXY = (e) => { 32853 if (isTouchEvent$1(e)) { 32854 const touch = e.touches[0]; 32855 return nu(touch.clientX, touch.clientY); 32856 } 32857 else { 32858 return nu(e.clientX, e.clientY); 32859 } 32860 }; 32861 const transposeContentAreaContainer = (element, pos) => { 32862 const containerPos = global$9.DOM.getPos(element); 32863 return transpose(pos, containerPos.x, containerPos.y); 32864 }; 32865 const getPointAnchor = (editor, e) => { 32866 // If the contextmenu event is fired via the editor.dispatch() API or some other means, fall back to selection anchor 32867 if (e.type === 'contextmenu' || e.type === 'longpress') { 32868 if (editor.inline) { 32869 return fromPageXY(e); 32870 } 32871 else { 32872 return transposeContentAreaContainer(editor.getContentAreaContainer(), fromClientXY(e)); 32873 } 32874 } 32875 else { 32876 return getSelectionAnchor(editor); 32877 } 32878 }; 32879 const getSelectionAnchor = (editor) => { 32880 return { 32881 type: 'selection', 32882 root: SugarElement.fromDom(editor.selection.getNode()) 32883 }; 32884 }; 32885 const getNodeAnchor = (editor) => ({ 32886 type: 'node', 32887 node: Optional.some(SugarElement.fromDom(editor.selection.getNode())), 32888 root: SugarElement.fromDom(editor.getBody()) 32889 }); 32890 const getAnchorSpec$1 = (editor, e, anchorType) => { 32891 switch (anchorType) { 32892 case 'node': 32893 return getNodeAnchor(editor); 32894 case 'point': 32895 return getPointAnchor(editor, e); 32896 case 'selection': 32897 return getSelectionAnchor(editor); 32898 } 32899 }; 32900 32901 const initAndShow$1 = (editor, e, buildMenu, backstage, contextmenu, anchorType) => { 32902 const items = buildMenu(); 32903 const anchorSpec = getAnchorSpec$1(editor, e, anchorType); 32904 build(items, ItemResponse$1.CLOSE_ON_EXECUTE, backstage, { 32905 isHorizontalMenu: false, 32906 search: Optional.none() 32907 }).map((menuData) => { 32908 e.preventDefault(); 32909 // show the context menu, with items set to close on click 32910 InlineView.showMenuAt(contextmenu, { anchor: anchorSpec }, { 32911 menu: { 32912 markers: markers('normal') 32913 }, 32914 data: menuData 32915 }); 32916 }); 32917 }; 32918 32919 const layouts = { 32920 onLtr: () => [south$2, southeast$2, southwest$2, northeast$2, northwest$2, north$2, 32921 north$1, south$1, northeast$1, southeast$1, northwest$1, southwest$1], 32922 onRtl: () => [south$2, southwest$2, southeast$2, northwest$2, northeast$2, north$2, 32923 north$1, south$1, northwest$1, southwest$1, northeast$1, southeast$1] 32924 }; 32925 const bubbleSize = 12; 32926 const bubbleAlignments = { 32927 valignCentre: [], 32928 alignCentre: [], 32929 alignLeft: ['tox-pop--align-left'], 32930 alignRight: ['tox-pop--align-right'], 32931 right: ['tox-pop--right'], 32932 left: ['tox-pop--left'], 32933 bottom: ['tox-pop--bottom'], 32934 top: ['tox-pop--top'] 32935 }; 32936 const isTouchWithinSelection = (editor, e) => { 32937 const selection = editor.selection; 32938 if (selection.isCollapsed() || e.touches.length < 1) { 32939 return false; 32940 } 32941 else { 32942 const touch = e.touches[0]; 32943 const rng = selection.getRng(); 32944 const rngRectOpt = getFirstRect(editor.getWin(), SimSelection.domRange(rng)); 32945 return rngRectOpt.exists((rngRect) => rngRect.left <= touch.clientX && 32946 rngRect.right >= touch.clientX && 32947 rngRect.top <= touch.clientY && 32948 rngRect.bottom >= touch.clientY); 32949 } 32950 }; 32951 const setupiOSOverrides = (editor) => { 32952 // iOS will change the selection due to longpress also being a range selection gesture. As such we 32953 // need to reset the selection back to the original selection after the touchend event has fired 32954 const originalSelection = editor.selection.getRng(); 32955 const selectionReset = () => { 32956 global$a.setEditorTimeout(editor, () => { 32957 editor.selection.setRng(originalSelection); 32958 }, 10); 32959 unbindEventListeners(); 32960 }; 32961 editor.once('touchend', selectionReset); 32962 // iPadOS will trigger a mousedown after the longpress which will close open context menus 32963 // so we want to prevent that from running 32964 const preventMousedown = (e) => { 32965 e.preventDefault(); 32966 e.stopImmediatePropagation(); 32967 }; 32968 editor.on('mousedown', preventMousedown, true); 32969 // If a longpresscancel is fired, then a touchmove has occurred so we shouldn't do any overrides 32970 const clearSelectionReset = () => unbindEventListeners(); 32971 editor.once('longpresscancel', clearSelectionReset); 32972 const unbindEventListeners = () => { 32973 editor.off('touchend', selectionReset); 32974 editor.off('longpresscancel', clearSelectionReset); 32975 editor.off('mousedown', preventMousedown); 32976 }; 32977 }; 32978 const getAnchorSpec = (editor, e, anchorType) => { 32979 const anchorSpec = getAnchorSpec$1(editor, e, anchorType); 32980 const bubbleYOffset = anchorType === 'point' ? bubbleSize : 0; 32981 return { 32982 bubble: nu$6(0, bubbleYOffset, bubbleAlignments), 32983 layouts, 32984 overrides: { 32985 maxWidthFunction: expandable(), 32986 maxHeightFunction: expandable$1() 32987 }, 32988 ...anchorSpec 32989 }; 32990 }; 32991 const show = (editor, e, items, backstage, contextmenu, anchorType, highlightImmediately) => { 32992 const anchorSpec = getAnchorSpec(editor, e, anchorType); 32993 build(items, ItemResponse$1.CLOSE_ON_EXECUTE, backstage, { 32994 // MobileContextMenus are the *only* horizontal menus currently (2022-08-16) 32995 isHorizontalMenu: true, 32996 search: Optional.none() 32997 }).map((menuData) => { 32998 e.preventDefault(); 32999 // If we are highlighting immediately, then we want to highlight the menu 33000 // and the item. Otherwise, we don't want to highlight anything. 33001 const highlightOnOpen = highlightImmediately 33002 ? HighlightOnOpen.HighlightMenuAndItem 33003 : HighlightOnOpen.HighlightNone; 33004 // Show the context menu, with items set to close on click 33005 InlineView.showMenuWithinBounds(contextmenu, { anchor: anchorSpec }, { 33006 menu: { 33007 markers: markers('normal'), 33008 highlightOnOpen 33009 }, 33010 data: menuData, 33011 type: 'horizontal' 33012 }, () => Optional.some(getContextToolbarBounds(editor, backstage.shared, anchorType === 'node' ? 'node' : 'selection'))); 33013 // Ensure the context toolbar is hidden 33014 editor.dispatch(hideContextToolbarEvent); 33015 }); 33016 }; 33017 const initAndShow = (editor, e, buildMenu, backstage, contextmenu, anchorType) => { 33018 const detection = detect$1(); 33019 const isiOS = detection.os.isiOS(); 33020 const isMacOS = detection.os.isMacOS(); 33021 const isAndroid = detection.os.isAndroid(); 33022 const isTouch = detection.deviceType.isTouch(); 33023 const shouldHighlightImmediately = () => !(isAndroid || isiOS || (isMacOS && isTouch)); 33024 const open = () => { 33025 const items = buildMenu(); 33026 show(editor, e, items, backstage, contextmenu, anchorType, shouldHighlightImmediately()); 33027 }; 33028 // On iOS/iPadOS if we've long pressed on a ranged selection then we've already selected the content 33029 // and just need to open the menu. Otherwise we need to wait for a selection change to occur as long 33030 // press triggers a ranged selection on iOS. 33031 if ((isMacOS || isiOS) && anchorType !== 'node') { 33032 const openiOS = () => { 33033 setupiOSOverrides(editor); 33034 open(); 33035 }; 33036 if (isTouchWithinSelection(editor, e)) { 33037 openiOS(); 33038 } 33039 else { 33040 editor.once('selectionchange', openiOS); 33041 editor.once('touchend', () => editor.off('selectionchange', openiOS)); 33042 } 33043 } 33044 else { 33045 open(); 33046 } 33047 }; 33048 33049 const isSeparator = (item) => isString(item) ? item === '|' : item.type === 'separator'; 33050 const separator = { 33051 type: 'separator' 33052 }; 33053 const makeContextItem = (item) => { 33054 const commonMenuItem = (item) => ({ 33055 text: item.text, 33056 icon: item.icon, 33057 enabled: item.enabled, 33058 shortcut: item.shortcut, 33059 }); 33060 if (isString(item)) { 33061 return item; 33062 } 33063 else { 33064 switch (item.type) { 33065 case 'separator': 33066 return separator; 33067 case 'submenu': 33068 return { 33069 type: 'nestedmenuitem', 33070 ...commonMenuItem(item), 33071 getSubmenuItems: () => { 33072 const items = item.getSubmenuItems(); 33073 if (isString(items)) { 33074 return items; 33075 } 33076 else { 33077 return map$2(items, makeContextItem); 33078 } 33079 } 33080 }; 33081 default: 33082 // case 'item', or anything else really 33083 const commonItem = item; 33084 return { 33085 type: 'menuitem', 33086 ...commonMenuItem(commonItem), 33087 // disconnect the function from the menu item API bridge defines 33088 onAction: noarg(commonItem.onAction) 33089 }; 33090 } 33091 } 33092 }; 33093 const addContextMenuGroup = (xs, groupItems) => { 33094 // Skip if there are no items 33095 if (groupItems.length === 0) { 33096 return xs; 33097 } 33098 // Only add a separator at the beginning if the last item isn't a separator 33099 const lastMenuItem = last$1(xs).filter((item) => !isSeparator(item)); 33100 const before = lastMenuItem.fold(() => [], (_) => [separator]); 33101 return xs.concat(before).concat(groupItems).concat([separator]); 33102 }; 33103 const generateContextMenu = (contextMenus, menuConfig, selectedElement) => { 33104 const sections = foldl(menuConfig, (acc, name) => { 33105 // Either read and convert the list of items out of the plugin, or assume it's a standard menu item reference 33106 return get$h(contextMenus, name.toLowerCase()).map((menu) => { 33107 const items = menu.update(selectedElement); 33108 if (isString(items) && isNotEmpty(trim$1(items))) { 33109 return addContextMenuGroup(acc, items.split(' ')); 33110 } 33111 else if (isArray(items) && items.length > 0) { 33112 // TODO: Should we add a StructureSchema check here? 33113 const allItems = map$2(items, makeContextItem); 33114 return addContextMenuGroup(acc, allItems); 33115 } 33116 else { 33117 return acc; 33118 } 33119 }).getOrThunk(() => acc.concat([name])); 33120 }, []); 33121 // Strip off any trailing separator 33122 if (sections.length > 0 && isSeparator(sections[sections.length - 1])) { 33123 sections.pop(); 33124 } 33125 return sections; 33126 }; 33127 const isNativeOverrideKeyEvent = (editor, e) => e.ctrlKey && !shouldNeverUseNative(editor); 33128 const isTouchEvent = (e) => e.type === 'longpress' || has$2(e, 'touches'); 33129 const isTriggeredByKeyboard = (editor, e) => 33130 // Different browsers trigger the context menu from keyboards differently, so need to check various different things here. 33131 // If a longpress touch event, always treat it as a pointer event 33132 // Chrome: button = 0, pointerType = undefined & target = the selection range node 33133 // Firefox: button = 0, pointerType = undefined & target = body 33134 // Safari: N/A (Mac's don't expose a contextmenu keyboard shortcut) 33135 !isTouchEvent(e) && (e.button !== 2 || e.target === editor.getBody() && e.pointerType === ''); 33136 const getSelectedElement = (editor, e) => isTriggeredByKeyboard(editor, e) ? editor.selection.getStart(true) : e.target; 33137 const getAnchorType = (editor, e) => { 33138 const selector = getAvoidOverlapSelector(editor); 33139 const anchorType = isTriggeredByKeyboard(editor, e) ? 'selection' : 'point'; 33140 if (isNotEmpty(selector)) { 33141 const target = getSelectedElement(editor, e); 33142 const selectorExists = closest$1(SugarElement.fromDom(target), selector); 33143 return selectorExists ? 'node' : anchorType; 33144 } 33145 else { 33146 return anchorType; 33147 } 33148 }; 33149 const setup$5 = (editor, lazySink, backstage) => { 33150 const detection = detect$1(); 33151 const isTouch = detection.deviceType.isTouch; 33152 const contextmenu = build$1(InlineView.sketch({ 33153 dom: { 33154 tag: 'div' 33155 }, 33156 lazySink, 33157 onEscape: () => editor.focus(), 33158 onShow: () => backstage.setContextMenuState(true), 33159 onHide: () => backstage.setContextMenuState(false), 33160 fireDismissalEventInstead: {}, 33161 inlineBehaviours: derive$1([ 33162 config('dismissContextMenu', [ 33163 run$1(dismissRequested(), (comp, _se) => { 33164 Sandboxing.close(comp); 33165 editor.focus(); 33166 }) 33167 ]) 33168 ]) 33169 })); 33170 const hideContextMenu = () => InlineView.hide(contextmenu); 33171 const showContextMenu = (e) => { 33172 // Prevent the default if we should never use native 33173 if (shouldNeverUseNative(editor)) { 33174 e.preventDefault(); 33175 } 33176 if (isNativeOverrideKeyEvent(editor, e) || isContextMenuDisabled(editor)) { 33177 return; 33178 } 33179 const anchorType = getAnchorType(editor, e); 33180 const buildMenu = () => { 33181 // Use the event target element for touch events, otherwise fallback to the current selection 33182 const selectedElement = getSelectedElement(editor, e); 33183 const registry = editor.ui.registry.getAll(); 33184 const menuConfig = getContextMenu(editor); 33185 return generateContextMenu(registry.contextMenus, menuConfig, selectedElement); 33186 }; 33187 const initAndShow$2 = isTouch() ? initAndShow : initAndShow$1; 33188 initAndShow$2(editor, e, buildMenu, backstage, contextmenu, anchorType); 33189 }; 33190 editor.on('init', () => { 33191 // Hide the context menu when scrolling or resizing 33192 // Except ResizeWindow on mobile which fires when the keyboard appears/disappears 33193 const hideEvents = 'ResizeEditor ScrollContent ScrollWindow longpresscancel' + (isTouch() ? '' : ' ResizeWindow'); 33194 editor.on(hideEvents, hideContextMenu); 33195 editor.on('longpress contextmenu', showContextMenu); 33196 }); 33197 }; 33198 33199 const snapWidth = 40; 33200 const snapOffset = snapWidth / 2; 33201 // const insertDebugDiv = (left, top, width, height, color, clazz) => { 33202 // const debugArea = SugarElement.fromHtml(`<div class="${clazz}"></div>`); 33203 // Css.setAll(debugArea, { 33204 // 'left': left.toString() + 'px', 33205 // 'top': top.toString() + 'px', 33206 // 'background-color': color, 33207 // 'position': 'absolute', 33208 // 'width': width.toString() + 'px', 33209 // 'height': height.toString() + 'px', 33210 // 'opacity': '0.2' 33211 // }); 33212 // Insert.append(SugarBody.body(), debugArea); 33213 // }; 33214 const calcSnap = (selectorOpt, td, x, y, width, height) => selectorOpt.fold(() => Dragging.snap({ 33215 sensor: absolute$1(x - snapOffset, y - snapOffset), 33216 range: SugarPosition(width, height), 33217 output: absolute$1(Optional.some(x), Optional.some(y)), 33218 extra: { 33219 td 33220 } 33221 }), (selectorHandle) => { 33222 const sensorLeft = x - snapOffset; 33223 const sensorTop = y - snapOffset; 33224 const sensorWidth = snapWidth; // box.width(); 33225 const sensorHeight = snapWidth; // box.height(); 33226 const rect = selectorHandle.element.dom.getBoundingClientRect(); 33227 // insertDebugDiv(sensorLeft, sensorTop, sensorWidth, sensorHeight, 'green', 'top-left-snap-debug'); 33228 return Dragging.snap({ 33229 sensor: absolute$1(sensorLeft, sensorTop), 33230 range: SugarPosition(sensorWidth, sensorHeight), 33231 output: absolute$1(Optional.some(x - (rect.width / 2)), Optional.some(y - (rect.height / 2))), 33232 extra: { 33233 td 33234 } 33235 }); 33236 }); 33237 const getSnapsConfig = (getSnapPoints, cell, onChange) => { 33238 // Can't use Optional.is() here since we need to do a dom compare, not an equality compare 33239 const isSameCell = (cellOpt, td) => cellOpt.exists((currentTd) => eq(currentTd, td)); 33240 return { 33241 getSnapPoints, 33242 leftAttr: 'data-drag-left', 33243 topAttr: 'data-drag-top', 33244 onSensor: (component, extra) => { 33245 const td = extra.td; 33246 if (!isSameCell(cell.get(), td)) { 33247 cell.set(td); 33248 onChange(td); 33249 } 33250 }, 33251 mustSnap: true 33252 }; 33253 }; 33254 const createSelector = (snaps) => record(Button.sketch({ 33255 dom: { 33256 tag: 'div', 33257 classes: ['tox-selector'] 33258 }, 33259 buttonBehaviours: derive$1([ 33260 Dragging.config({ 33261 mode: 'mouseOrTouch', 33262 blockerClass: 'blocker', 33263 snaps 33264 }), 33265 Unselecting.config({}) 33266 ]), 33267 eventOrder: { 33268 // Because this is a button, allow dragging. It will stop clicking. 33269 mousedown: ['dragging', 'alloy.base.behaviour'], 33270 touchstart: ['dragging', 'alloy.base.behaviour'] 33271 } 33272 })); 33273 const setup$4 = (editor, sink) => { 33274 const tlTds = Cell([]); 33275 const brTds = Cell([]); 33276 const isVisible = Cell(false); 33277 const startCell = value$2(); 33278 const finishCell = value$2(); 33279 const getTopLeftSnap = (td) => { 33280 const box = absolute$2(td); 33281 return calcSnap(memTopLeft.getOpt(sink), td, box.x, box.y, box.width, box.height); 33282 }; 33283 const getTopLeftSnaps = () => 33284 // const body = SugarBody.body();
33285 // const debugs = SelectorFilter.descendants(body, '.top-left-snap-debug'); 33286 // Arr.each(debugs, (debugArea) => { 33287 // Remove.remove(debugArea); 33288 // }); 33289 map$2(tlTds.get(), (td) => getTopLeftSnap(td)); 33290 const getBottomRightSnap = (td) => { 33291 const box = absolute$2(td); 33292 return calcSnap(memBottomRight.getOpt(sink), td, box.right, box.bottom, box.width, box.height); 33293 }; 33294 const getBottomRightSnaps = () => 33295 // const body = SugarBody.body(); 33296 // const debugs = SelectorFilter.descendants(body, '.bottom-right-snap-debug'); 33297 // Arr.each(debugs, (debugArea) => { 33298 // Remove.remove(debugArea); 33299 // }); 33300 map$2(brTds.get(), (td) => getBottomRightSnap(td)); 33301 const topLeftSnaps = getSnapsConfig(getTopLeftSnaps, startCell, (start) => { 33302 finishCell.get().each((finish) => { 33303 editor.dispatch('TableSelectorChange', { start, finish }); 33304 }); 33305 }); 33306 const bottomRightSnaps = getSnapsConfig(getBottomRightSnaps, finishCell, (finish) => { 33307 startCell.get().each((start) => { 33308 editor.dispatch('TableSelectorChange', { start, finish }); 33309 }); 33310 }); 33311 const memTopLeft = createSelector(topLeftSnaps); 33312 const memBottomRight = createSelector(bottomRightSnaps); 33313 const topLeft = build$1(memTopLeft.asSpec()); 33314 const bottomRight = build$1(memBottomRight.asSpec()); 33315 const showOrHideHandle = (selector, cell, isAbove, isBelow) => { 33316 const cellRect = cell.dom.getBoundingClientRect(); 33317 remove$6(selector.element, 'display'); 33318 const viewportHeight = defaultView(SugarElement.fromDom(editor.getBody())).dom.innerHeight; 33319 const aboveViewport = isAbove(cellRect); 33320 const belowViewport = isBelow(cellRect, viewportHeight); 33321 if (aboveViewport || belowViewport) { 33322 set$7(selector.element, 'display', 'none'); 33323 } 33324 }; 33325 const snapTo = (selector, cell, getSnapConfig, pos) => { 33326 const snap = getSnapConfig(cell); 33327 Dragging.snapTo(selector, snap); 33328 const isAbove = (rect) => rect[pos] < 0; 33329 const isBelow = (rect, viewportHeight) => rect[pos] > viewportHeight; 33330 showOrHideHandle(selector, cell, isAbove, isBelow); 33331 }; 33332 const snapTopLeft = (cell) => snapTo(topLeft, cell, getTopLeftSnap, 'top'); 33333 const snapLastTopLeft = () => startCell.get().each(snapTopLeft); 33334 const snapBottomRight = (cell) => snapTo(bottomRight, cell, getBottomRightSnap, 'bottom'); 33335 const snapLastBottomRight = () => finishCell.get().each(snapBottomRight); 33336 // TODO: Make this work for desktop maybe? 33337 if (detect$1().deviceType.isTouch()) {
33338 const domToSugar = (arr) => map$2(arr, SugarElement.fromDom); 33339 editor.on('TableSelectionChange', (e) => { 33340 if (!isVisible.get()) { 33341 attach(sink, topLeft); 33342 attach(sink, bottomRight); 33343 isVisible.set(true); 33344 } 33345 const start = SugarElement.fromDom(e.start); 33346 const finish = SugarElement.fromDom(e.finish); 33347 startCell.set(start); 33348 finishCell.set(finish); 33349 Optional.from(e.otherCells).each((otherCells) => { 33350 tlTds.set(domToSugar(otherCells.upOrLeftCells)); 33351 brTds.set(domToSugar(otherCells.downOrRightCells)); 33352 snapTopLeft(start); 33353 snapBottomRight(finish); 33354 }); 33355 }); 33356 editor.on('ResizeEditor ResizeWindow ScrollContent', () => { 33357 snapLastTopLeft(); 33358 snapLastBottomRight(); 33359 }); 33360 editor.on('TableSelectionClear', () => { 33361 if (isVisible.get()) { 33362 detach(topLeft); 33363 detach(bottomRight); 33364 isVisible.set(false); 33365 } 33366 startCell.clear(); 33367 finishCell.clear(); 33368 }); 33369 } 33370 }; 33371 33372 var Logo = "<svg height=\"16\" viewBox=\"0 0 80 16\" width=\"80\" xmlns=\"http://www.w3.org/2000/svg\"><g opacity=\".8\"><path d=\"m80 3.537v-2.202h-7.976v11.585h7.976v-2.25h-5.474v-2.621h4.812v-2.069h-4.812v-2.443zm-10.647 6.929c-.493.217-1.13.337-1.864.337s-1.276-.156-1.805-.47a3.732 3.732 0 0 1 -1.3-1.298c-.324-.554-.48-1.191-.48-1.877s.156-1.335.48-1.877a3.635 3.635 0 0 1 1.3-1.299 3.466 3.466 0 0 1 1.805-.481c.65 0 .914.06 1.263.18.36.12.698.277.986.47.289.192.578.384.842.6l.12.085v-2.586l-.023-.024c-.385-.35-.855-.614-1.384-.818-.53-.205-1.155-.313-1.877-.313-.721 0-1.6.144-2.333.445a5.773 5.773 0 0 0 -1.937 1.251 5.929 5.929 0 0 0 -1.324 1.9c-.324.735-.48 1.565-.48 2.455s.156 1.72.48 2.454c.325.734.758 1.383 1.324 1.913.553.53 1.215.938 1.937 1.25a6.286 6.286 0 0 0 2.333.434c.819 0 1.384-.108 1.961-.313.59-.216 1.083-.505 1.468-.866l.024-.024v-2.49l-.12.096c-.41.337-.878.626-1.396.866zm-14.869-4.15-4.8-5.04-.024-.025h-.902v11.67h2.502v-6.847l2.827 3.08.385.409.397-.41 2.791-3.067v6.845h2.502v-11.679h-.902l-4.788 5.052z\"/><path clip-rule=\"evenodd\" d=\"m15.543 5.137c0-3.032-2.466-5.113-4.957-5.137-.36 0-.745.024-1.094.096-.157.024-3.85.758-3.85.758-3.032.602-4.62 2.466-4.704 4.788-.024.89-.024 4.27-.024 4.27.036 3.165 2.406 5.138 5.017 5.126.337 0 1.119-.109 1.287-.145.144-.024.385-.084.746-.144.661-.12 1.684-.325 3.067-.602 2.37-.409 4.103-2.009 4.44-4.33.156-1.023.084-4.692.084-4.692zm-3.213 3.308-2.346.457v2.31l-5.859 1.143v-5.75l2.346-.458v3.441l3.513-.686v-3.44l-3.513.685v-2.297l5.859-1.143v5.75zm20.09-3.296-.083-1.023h-2.13v8.794h2.346v-4.884c0-1.107.95-1.985 2.057-1.997 1.095 0 1.901.89 1.901 1.997v4.884h2.346v-5.245c-.012-2.105-1.588-3.777-3.67-3.765a3.764 3.764 0 0 0 -2.778 1.25l.012-.011zm-6.014-4.102 2.346-.458v2.298l-2.346.457z\" fill-rule=\"evenodd\"/><path d=\"m28.752 4.126h-2.346v8.794h2.346z\"/><path clip-rule=\"evenodd\" d=\"m43.777 15.483 4.043-11.357h-2.418l-1.54 4.355-.445 1.324-.36-1.324-1.54-4.355h-2.418l3.151 8.794-1.083 3.08zm-21.028-5.51c0 .722.541 1.034.878 1.034s.638-.048.95-.144l.518 1.708c-.217.145-.879.518-2.13.518a2.565 2.565 0 0 1 -2.562-2.587c-.024-1.082-.024-2.49 0-4.21h-1.54v-2.142h1.54v-1.912l2.346-.458v2.37h2.201v2.142h-2.2v3.693-.012z\" fill-rule=\"evenodd\"/></g></svg>\n"; 33373 33374 const isHidden = (elm) => elm.nodeName === 'BR' || !!elm.getAttribute('data-mce-bogus') || elm.getAttribute('data-mce-type') === 'bookmark'; 33375 const renderElementPath = (editor, settings, providersBackstage) => { 33376 const delimiter = settings.delimiter ?? '\u203A'; 33377 const renderElement = (name, element, index) => Button.sketch({ 33378 dom: { 33379 tag: 'div', 33380 classes: ['tox-statusbar__path-item'], 33381 attributes: { 33382 'data-index': index, 33383 } 33384 }, 33385 components: [ 33386 text$2(name) 33387 ], 33388 action: (_btn) => { 33389 editor.focus(); 33390 editor.selection.select(element); 33391 editor.nodeChanged(); 33392 }, 33393 buttonBehaviours: derive$1([ 33394 Tooltipping.config({ 33395 ...providersBackstage.tooltips.getConfig({ 33396 tooltipText: providersBackstage.translate(['Select the {0} element', element.nodeName.toLowerCase()]), 33397 onShow: (comp, tooltip) => { 33398 describedBy(comp.element, tooltip.element); 33399 }, 33400 onHide: (comp) => { 33401 remove$1(comp.element); 33402 } 33403 }), 33404 }), 33405 DisablingConfigs.button(providersBackstage.isDisabled), 33406 toggleOnReceive(() => providersBackstage.checkUiComponentContext('any')) 33407 ]) 33408 }); 33409 const renderDivider = () => ({ 33410 dom: { 33411 tag: 'div', 33412 classes: ['tox-statusbar__path-divider'], 33413 attributes: { 33414 'aria-hidden': true 33415 } 33416 }, 33417 components: [ 33418 text$2(` ${delimiter} `) 33419 ] 33420 }); 33421 const renderPathData = (data) => foldl(data, (acc, path, index) => { 33422 const element = renderElement(path.name, path.element, index); 33423 if (index === 0) { 33424 return acc.concat([element]); 33425 } 33426 else { 33427 return acc.concat([renderDivider(), element]); 33428 } 33429 }, []);
33430 const updatePath = (parents) => { 33431 const newPath = []; 33432 let i = parents.length; 33433 while (i-- > 0) { 33434 const parent = parents[i]; 33435 if (parent.nodeType === 1 && !isHidden(parent)) { 33436 const args = fireResolveName(editor, parent); 33437 if (!args.isDefaultPrevented()) { 33438 newPath.push({ name: args.name, element: parent }); 33439 } 33440 if (args.isPropagationStopped()) { 33441 break; 33442 } 33443 } 33444 } 33445 return newPath; 33446 }; 33447 return { 33448 dom: { 33449 tag: 'div', 33450 classes: ['tox-statusbar__path'], 33451 attributes: { 33452 'role': 'group', 33453 'aria-label': providersBackstage.translate('Element Path') 33454 } 33455 }, 33456 behaviours: derive$1([ 33457 Keying.config({ 33458 mode: 'flow', 33459 selector: 'div[role=button]' 33460 }), 33461 Disabling.config({ 33462 disabled: providersBackstage.isDisabled 33463 }), 33464 toggleOnReceive(() => providersBackstage.checkUiComponentContext('any')), 33465 Tabstopping.config({}), 33466 Replacing.config({}), 33467 config('elementPathEvents', [ 33468 runOnAttached((comp, _e) => { 33469 // NOTE: If statusbar ever gets re-rendered, we will need to free this. 33470 editor.shortcuts.add('alt+F11', 'focus statusbar elementpath', () => Keying.focusIn(comp)); 33471 editor.on('NodeChange', (e) => { 33472 const newPath = updatePath(e.parents); 33473 const newChildren = newPath.length > 0 ? renderPathData(newPath) : []; 33474 Replacing.set(comp, newChildren); 33475 }); 33476 }) 33477 ]) 33478 ]), 33479 components: [] 33480 }; 33481 }; 33482 33483 var ResizeTypes; 33484 (function (ResizeTypes) { 33485 ResizeTypes[ResizeTypes["None"] = 0] = "None"; 33486 ResizeTypes[ResizeTypes["Both"] = 1] = "Both"; 33487 ResizeTypes[ResizeTypes["Vertical"] = 2] = "Vertical"; 33488 })(ResizeTypes || (ResizeTypes = {})); 33489 const getOriginalDimensions = (editor) => { 33490 const container = SugarElement.fromDom(editor.getContainer()); 33491 const originalHeight = get$d(container); 33492 const originalWidth = get$c(container); 33493 return { 33494 height: originalHeight, 33495 width: originalWidth, 33496 }; 33497 }; 33498 const getDimensions = (editor, deltas, resizeType, originalDimentions) => { 33499 const height = calcCappedSize(originalDimentions.height + deltas.top, getMinHeightOption(editor), getMaxHeightOption(editor)); 33500 if (resizeType === ResizeTypes.Both) { 33501 return { 33502 height, 33503 width: calcCappedSize(originalDimentions.width + deltas.left, getMinWidthOption(editor), getMaxWidthOption(editor)) 33504 }; 33505 } 33506 return { height }; 33507 }; 33508 const resize = (editor, deltas, resizeType) => { 33509 const container = SugarElement.fromDom(editor.getContainer()); 33510 const originalDimensions = getOriginalDimensions(editor); 33511 const dimensions = getDimensions(editor, deltas, resizeType, originalDimensions); 33512 each(dimensions, (val, dim) => { 33513 if (isNumber(val)) { 33514 set$7(container, dim, numToPx(val)); 33515 } 33516 }); 33517 fireResizeEditor(editor); 33518 return dimensions; 33519 }; 33520 33521 const keyboardResizeStepInPx = 20; 33522 const getResizeType = (editor) => { 33523 const resize = getResize(editor); 33524 if (resize === false) { 33525 return ResizeTypes.None; 33526 } 33527 else if (resize === 'both') { 33528 return ResizeTypes.Both; 33529 } 33530 else { 33531 return ResizeTypes.Vertical; 33532 } 33533 }; 33534 const getAriaValuetext = (dimensions, resizeType) => { 33535 return resizeType === ResizeTypes.Both 33536 ? global$6.translate([`Editor's height: {0} pixels, Editor's width: {1} pixels`, dimensions.height, dimensions.width]) 33537 : global$6.translate([`Editor's height: {0} pixels`, dimensions.height]); 33538 }; 33539 const setAriaDimensions = (comp, dimensions, resizeType, minHeight, maxHeight) => { 33540 set$9(comp.element, 'aria-valuetext', getAriaValuetext(dimensions, resizeType)); 33541 // aria-valuenow/valuemin/valuemax are single numeric values, so they can only 33542 // meaningfully represent one dimension. In 'both' mode the handle resizes both 33543 // width and height, which has no sensible single-value representation, so we only 33544 // set these attributes for vertical resizing and rely on aria-valuetext otherwise. 33545 if (resizeType === ResizeTypes.Vertical) { 33546 set$9(comp.element, 'aria-valuenow', dimensions.height); 33547 set$9(comp.element, 'aria-valuemin', minHeight.getOr(0)); 33548 set$9(comp.element, 'aria-valuemax', maxHeight.getOr(dimensions.height + keyboardResizeStepInPx)); 33549 } 33550 };
33551 const keyboardHandler = (editor, comp, resizeType, x, y, minHeight, maxHeight) => { 33552 const delta = SugarPosition(x * keyboardResizeStepInPx, y * keyboardResizeStepInPx); 33553 const newDimentions = resize(editor, delta, resizeType); 33554 setAriaDimensions(comp, newDimentions, resizeType, minHeight, maxHeight); 33555 return Optional.some(true); 33556 }; 33557 const renderResizeHandler = (editor, providersBackstage) => { 33558 const resizeType = getResizeType(editor); 33559 if (resizeType === ResizeTypes.None) { 33560 return Optional.none(); 33561 } 33562 const minHeight = getMinHeightOption(editor); 33563 const maxHeight = getMaxHeightOption(editor); 33564 const resizeLabel = resizeType === ResizeTypes.Both 33565 ? global$6.translate('Press the arrow keys to resize the editor.') 33566 : global$6.translate('Press the Up and Down arrow keys to resize the editor.'); 33567 const cursorClass = resizeType === ResizeTypes.Both 33568 ? 'tox-statusbar__resize-cursor-both' 33569 : 'tox-statusbar__resize-cursor-default'; 33570 return Optional.some(render$4('resize-handle', { 33571 tag: 'div', 33572 classes: ['tox-statusbar__resize-handle', cursorClass], 33573 attributes: { 33574 'aria-label': providersBackstage.translate(resizeLabel), 33575 'data-mce-name': 'resize-handle', 33576 'role': 'separator' 33577 }, 33578 behaviours: [ 33579 Dragging.config({ 33580 mode: 'pointer', 33581 repositionTarget: false, 33582 onDragStart: (comp) => { 33583 Tooltipping.immediateOpenClose(comp, false); 33584 Tooltipping.setEnabled(comp, false); 33585 }, 33586 onDrag: (comp, _target, delta) => { 33587 const newDimentions = resize(editor, delta, resizeType); 33588 setAriaDimensions(comp, newDimentions, resizeType, minHeight, maxHeight); 33589 }, 33590 onDrop: (comp) => { 33591 Tooltipping.setEnabled(comp, true); 33592 }, 33593 }), 33594 Keying.config({ 33595 mode: 'special', 33596 onLeft: (comp) => keyboardHandler(editor, comp, resizeType, -1, 0, minHeight, maxHeight), 33597 onRight: (comp) => keyboardHandler(editor, comp, resizeType, 1, 0, minHeight, maxHeight), 33598 onUp: (comp) => keyboardHandler(editor, comp, resizeType, 0, -1, minHeight, maxHeight), 33599 onDown: (comp) => keyboardHandler(editor, comp, resizeType, 0, 1, minHeight, maxHeight), 33600 }), 33601 Tabstopping.config({}), 33602 Focusing.config({}), 33603 Tooltipping.config(providersBackstage.tooltips.getConfig({ 33604 tooltipText: providersBackstage.translate('Resize') 33605 })), 33606 config('set-aria-valuetext', [ 33607 runOnAttached((comp) => { 33608 const setInitialValuetext = () => { 33609 setAriaDimensions(comp, getOriginalDimensions(editor), resizeType, minHeight, maxHeight); 33610 }; 33611 if (editor._skinLoaded) { 33612 setInitialValuetext(); 33613 } 33614 else { 33615 editor.once('SkinLoaded', setInitialValuetext); 33616 } 33617 }) 33618 ]) 33619 ], 33620 eventOrder: { 33621 [attachedToDom()]: ['add-focusable', 'set-aria-valuetext'] 33622 } 33623 }, providersBackstage.icons)); 33624 }; 33625 33626 const renderWordCount = (editor, providersBackstage) => { 33627 const replaceCountText = (comp, count, mode) => Replacing.set(comp, [text$2(providersBackstage.translate(['{0} ' + mode, count[mode]]))]); 33628 return Button.sketch({ 33629 dom: { 33630 // The tag for word count was changed to 'button' as Jaws does not read out spans. 33631 // Word count is just a toggle and changes modes between words and characters. 33632 tag: 'button', 33633 classes: ['tox-statusbar__wordcount'] 33634 }, 33635 components: [], 33636 buttonBehaviours: derive$1([ 33637 DisablingConfigs.button(providersBackstage.isDisabled), 33638 toggleOnReceive(() => providersBackstage.checkUiComponentContext('any')), 33639 Tabstopping.config({}), 33640 Replacing.config({}), 33641 Representing.config({ 33642 store: { 33643 mode: 'memory', 33644 initialValue: { 33645 mode: "words" /* WordCountMode.Words */, 33646 count: { words: 0, characters: 0 } 33647 } 33648 } 33649 }), 33650 config('wordcount-events', [ 33651 runOnExecute$1((comp) => { 33652 const currentVal = Representing.getValue(comp); 33653 const newMode = currentVal.mode === "words" /* WordCountMode.Words */ ? "characters" /* WordCountMode.Characters */ : "words" /* WordCountMode.Words */; 33654 Representing.setValue(comp, { mode: newMode, count: currentVal.count }); 33655 replaceCountText(comp, currentVal.count, newMode); 33656 }), 33657 runOnAttached((comp) => { 33658 editor.on('wordCountUpdate', (e) => { 33659 const { mode } = Representing.getValue(comp); 33660 Representing.setValue(comp, { mode, count: e.wordCount }); 33661 replaceCountText(comp, e.wordCount, mode); 33662 }); 33663 }) 33664 ]) 33665 ]), 33666 eventOrder: { 33667 [execute$5()]: ['disabling', 'alloy.base.behaviour', 'wordcount-events'] 33668 } 33669 }); 33670 }; 33671 33672 const renderStatusbar = (editor, providersBackstage) => { 33673 const renderBranding = () => { 33674 return { 33675 dom: { 33676 tag: 'span', 33677 classes: ['tox-statusbar__branding'], 33678 }, 33679 components: [ 33680 { 33681 dom: { 33682 tag: 'a', 33683 attributes: { 33684 'href': 'https://www.tiny.cloud/powered-by-tiny?utm_campaign=p
33684oweredby&utm_source=tiny&utm_medium=referral&utm_content=v7', 33685 'rel': 'noopener', 33686 'target': '_blank', 33687 'aria-label': editor.translate(['Build with {0}', 'TinyMCE']) 33688 }, 33689 innerHtml: editor.translate(['Build with {0}', Logo.trim()]) 33690 }, 33691 behaviours: derive$1([ 33692 Focusing.config({}) 33693 ]) 33694 } 33695 ] 33696 }; 33697 }; 33698 const renderHelpAccessibility = () => { 33699 const shortcutText = convertText('Alt+0'); 33700 const text = `Press {0} for help`; 33701 return { 33702 dom: { 33703 tag: 'div', 33704 classes: ['tox-statusbar__help-text'], 33705 }, 33706 components: [ 33707 text$2(global$6.translate([text, shortcutText])) 33708 ] 33709 }; 33710 }; 33711 const renderRightContainer = () => { 33712 const components = []; 33713 if (editor.hasPlugin('wordcount')) { 33714 components.push(renderWordCount(editor, providersBackstage)); 33715 } 33716 if (useBranding(editor)) { 33717 components.push(renderBranding()); 33718 } 33719 return { 33720 dom: { 33721 tag: 'div', 33722 classes: ['tox-statusbar__right-container'] 33723 }, 33724 components 33725 }; 33726 }; 33727 const getTextComponents = () => { 33728 const components = []; 33729 const shouldRenderHelp = useHelpAccessibility(editor); 33730 const shouldRenderElementPath = useElementPath(editor); 33731 const shouldRenderRightContainer = useBranding(editor) || editor.hasPlugin('wordcount'); 33732 const getTextComponentClasses = () => { 33733 const flexStart = 'tox-statusbar__text-container--flex-start'; 33734 const flexEnd = 'tox-statusbar__text-container--flex-end'; 33735 const spaceAround = 'tox-statusbar__text-container--space-around'; 33736 if (shouldRenderHelp) { 33737 const container3Columns = 'tox-statusbar__text-container-3-cols'; 33738 if (!shouldRenderRightContainer && !shouldRenderElementPath) { 33739 return [container3Columns, spaceAround]; 33740 } 33741 if (shouldRenderRightContainer && !shouldRenderElementPath) { 33742 return [container3Columns, flexEnd]; 33743 } 33744 return [container3Columns, flexStart]; 33745 } 33746 return [shouldRenderRightContainer && !shouldRenderElementPath ? flexEnd : flexStart]; 33747 }; 33748 if (shouldRenderElementPath) { 33749 components.push(renderElementPath(editor, {}, providersBackstage)); 33750 } 33751 if (shouldRenderHelp) { 33752 components.push(renderHelpAccessibility()); 33753 } 33754 if (shouldRenderRightContainer) { 33755 components.push(renderRightContainer()); 33756 } 33757 if (components.length > 0) { 33758 return [{ 33759 dom: { 33760 tag: 'div', 33761 classes: ['tox-statusbar__text-container', ...getTextComponentClasses()] 33762 }, 33763 components 33764 }]; 33765 } 33766 return []; 33767 }; 33768 const getComponents = () => { 33769 const components = getTextComponents(); 33770 const resizeHandler = renderResizeHandler(editor, providersBackstage); 33771 return components.concat(resizeHandler.toArray()); 33772 }; 33773 return { 33774 dom: { 33775 tag: 'div', 33776 classes: ['tox-statusbar'] 33777 }, 33778 components: getComponents() 33779 }; 33780 }; 33781 33782 const getLazyMothership = (label, singleton) => singleton.get().getOrDie(`UI for ${label} has not been rendered`); 33783 const setup$3 = (editor, setupForTheme) => { 33784 const isInline = editor.inline; 33785 const mode = isInline ? Inline : Iframe;
vendor: 25,447 bytes, lines 33786-34331
33786 // We use a different component for creating the sticky toolbar behaviour. The 33787 // most important difference is it needs "Docking" configured and all of the 33788 // ripple effects that creates. 33789 const header = isStickyToolbar(editor) ? StickyHeader : StaticHeader; 33790 const lazyUiRefs = LazyUiReferences(); 33791 // Importantly, this is outside the setup function. 33792 const lazyMothership = value$2(); 33793 const lazyDialogMothership = value$2(); 33794 const lazyPopupMothership = value$2(); 33795 const platform = detect$1(); 33796 const isTouch = platform.deviceType.isTouch(); 33797 const touchPlatformClass = 'tox-platform-touch'; 33798 const deviceClasses = isTouch ? [touchPlatformClass] : []; 33799 const isToolbarBottom = isToolbarLocationBottom(editor); 33800 const toolbarMode = getToolbarMode(editor); 33801 const memAnchorBar = record({ 33802 dom: { 33803 tag: 'div', 33804 classes: ['tox-anchorbar'] 33805 } 33806 }); 33807 const memBottomAnchorBar = record({ 33808 dom: { 33809 tag: 'div', 33810 classes: ['tox-bottom-anchorbar'] 33811 } 33812 }); 33813 const lazyHeader = () => lazyUiRefs.mainUi.get() 33814 .map((ui) => ui.outerContainer) 33815 .bind(OuterContainer.getHeader); 33816 const lazyDialogSinkResult = () => Result.fromOption(lazyUiRefs.dialogUi.get().map((ui) => ui.sink), 'UI has not been rendered'); 33817 const lazyPopupSinkResult = () => Result.fromOption(lazyUiRefs.popupUi.get().map((ui) => ui.sink), '(popup) UI has not been rendered'); 33818 const lazyAnchorBar = lazyUiRefs.lazyGetInOuterOrDie('anchor bar', memAnchorBar.getOpt); 33819 const lazyBottomAnchorBar = lazyUiRefs.lazyGetInOuterOrDie('bottom anchor bar', memBottomAnchorBar.getOpt); 33820 const lazyToolbar = lazyUiRefs.lazyGetInOuterOrDie('toolbar', OuterContainer.getToolbar); 33821 const lazyThrobber = lazyUiRefs.lazyGetInOuterOrDie('throbber', OuterContainer.getThrobber); 33822 // Here, we build the backstage. The backstage is going to use different sinks for dialog 33823 // vs popup. 33824 const backstages = init({ 33825 popup: lazyPopupSinkResult, 33826 dialog: lazyDialogSinkResult 33827 }, editor, lazyAnchorBar, lazyBottomAnchorBar); 33828 const makeHeaderPart = () => { 33829 const verticalDirAttributes = { 33830 attributes: { 33831 [Attribute]: isToolbarBottom ? 33832 AttributeValue.BottomToTop : 33833 AttributeValue.TopToBottom 33834 } 33835 }; 33836 const partMenubar = OuterContainer.parts.menubar({ 33837 dom: { 33838 tag: 'div', 33839 classes: ['tox-menubar'] 33840 }, 33841 // TINY-9223: The menu bar should scroll with the editor. 33842 backstage: backstages.popup, 33843 onEscape: () => { 33844 editor.focus(); 33845 } 33846 }); 33847 const partToolbar = OuterContainer.parts.toolbar({ 33848 dom: { 33849 tag: 'div', 33850 classes: ['tox-toolbar'] 33851 }, 33852 getSink: backstages.popup.shared.getSink, 33853 providers: backstages.popup.shared.providers, 33854 onEscape: () => { 33855 editor.focus(); 33856 }, 33857 onToolbarToggled: (state) => { 33858 fireToggleToolbarDrawer(editor, state); 33859 }, 33860 type: toolbarMode, 33861 lazyToolbar, 33862 lazyHeader: () => lazyHeader().getOrDie('Could not find header element'), 33863 ...verticalDirAttributes 33864 }); 33865 const partMultipleToolbar = OuterContainer.parts['multiple-toolbar']({ 33866 dom: { 33867 tag: 'div', 33868 classes: ['tox-toolbar-overlord'] 33869 }, 33870 providers: backstages.popup.shared.providers, 33871 onEscape: () => { 33872 editor.focus(); 33873 }, 33874 type: toolbarMode 33875 }); 33876 // False should stop the menubar and toolbar rendering altogether 33877 const hasMultipleToolbar = isMultipleToolbars(editor); 33878 const hasToolbar = isToolbarEnabled(editor); 33879 const hasMenubar = isMenubarEnabled(editor); 33880 const shouldHavePromotionLink = promotionEnabled(editor); 33881 const partPromotion = makePromotion(shouldHavePromotionLink); 33882 const hasAnyContents = hasMultipleToolbar || hasToolbar || hasMenubar; 33883 const getPartToolbar = () => { 33884 if (hasMultipleToolbar) { 33885 return [partMultipleToolbar]; 33886 } 33887 else if (hasToolbar) { 33888 return [partToolbar]; 33889 } 33890 else { 33891 return []; 33892 } 33893 }; 33894 const menubarCollection = [partPromotion, partMenubar]; 33895 return OuterContainer.parts.header({ 33896 dom: { 33897 tag: 'div', 33898 classes: ['tox-editor-header'] 33899 .concat(hasAnyContents ? [] : ['tox-editor-header--empty']), 33900 ...verticalDirAttributes 33901 }, 33902 components: flatten([ 33903 hasMenubar ? menubarCollection : [], 33904 getPartToolbar(), 33905 // fixed_toolbar_container anchors to the editable area, else add an anchor bar 33906 useFixedContainer(editor) ? [] : [memAnchorBar.asSpec()] 33907 ]), 33908 sticky: isStickyToolbar(editor), 33909 editor, 33910 // TINY-9223: If using a sticky toolbar, which sink should it really go in? 33911 sharedBackstage: backstages.popup.shared 33912 }); 33913 }; 33914 const makePromotion = (promotionLink) => { 33915 return OuterContainer.parts.promotion({ 33916 dom: { 33917 tag: 'div', 33918 classes: ['tox-promotion'], 33919 }, 33920 promotionLink 33921 }); 33922 }; 33923 const makeSidebarDefinition = () => { 33924 const partSocket = OuterContainer.parts.socket({ 33925 dom: { 33926 tag: 'div', 33927 classes: ['tox-edit-area'] 33928 } 33929 }); 33930 const partSidebar = OuterContainer.parts.sidebar({ 33931 dom: { 33932 tag: 'div', 33933 classes: ['tox-sidebar'] 33934 } 33935 }); 33936 return { 33937 dom: { 33938 tag: 'div', 33939 classes: ['tox-sidebar-wrap'] 33940 }, 33941 components: [ 33942 partSocket, 33943 partSidebar 33944 ] 33945 }; 33946 }; 33947 // TINY-14384: we want to restrict the bounds to the host element, rather than the entire window when the sink is attached in a ShadowDOM (ie: webcomponent) 33948 const getSinkBounds = (sinkOpt) => sinkOpt.bind(({ sink }) => { 33949 const rootNode = getRootNode(sink.element); 33950 if (isShadowRoot(rootNode)) { 33951 return Optional.some(box$1(getShadowHost(rootNode))); 33952 } 33953 return Optional.none(); 33954 }).getOr(win()); 33955 const renderDialogUi = () => { 33956 const uiContainer = getUiContainer(editor); 33957 // TINY-3321: When the body is using a grid layout, we need to ensure the sink width is manually set 33958 const isGridUiContainer = eq(body(), uiContainer) && get$e(uiContainer, 'display') === 'grid'; 33959 const sinkSpec = { 33960 dom: { 33961 tag: 'div', 33962 classes: ['tox', 'tox-silver-sink', 'tox-tinymce-aux'].concat(deviceClasses), 33963 attributes: { 33964 ...global$6.isRtl() ? { dir: 'rtl' } : {} 33965 } 33966 }, 33967 behaviours: derive$1([ 33968 Positioning.config({ 33969 useFixed: () => header.isDocked(lazyHeader), 33970 getBounds: () => getSinkBounds(lazyUiRefs.dialogUi.get()) 33971 }), 33972 ]) 33973 }; 33974 const reactiveWidthSpec = { 33975 dom: { 33976 styles: { width: document.body.clientWidth + 'px' } 33977 }, 33978 events: derive$2([ 33979 run$1(windowResize(), (comp) => { 33980 set$7(comp.element, 'width', document.body.clientWidth + 'px'); 33981 }) 33982 ]) 33983 }; 33984 const sink = build$1(deepMerge(sinkSpec, isGridUiContainer ? reactiveWidthSpec : {})); 33985 const uiMothership = takeover(sink); 33986 lazyDialogMothership.set(uiMothership); 33987 return { sink, mothership: uiMothership }; 33988 }; 33989 const renderPopupUi = () => { 33990 // TINY-9226: Because the popupUi is going to be placed adjacent to the editor, we aren't currently 33991 // implementing the behaviour to reset widths based on window sizing. It is a workaround that 33992 // is mainly targeted at Ui containers in the root. However, we may need to revisit this 33993 // if the ui_mode: split setting is commonly used when the editor is at the root level, and the 33994 // page has size-unfriendly CSS for sinks (like CSS grid) 33995 const sinkSpec = { 33996 dom: { 33997 tag: 'div', 33998 classes: ['tox', 'tox-silver-sink', 'tox-silver-popup-sink', 'tox-tinymce-aux'].concat(deviceClasses), 33999 attributes: { 34000 ...global$6.isRtl() ? { dir: 'rtl' } : {} 34001 } 34002 }, 34003 behaviours: derive$1([ 34004 Positioning.config({ 34005 useFixed: () => header.isDocked(lazyHeader), 34006 // TINY-9226: We want to limit the popup sink's bounds based on its scrolling environment. We don't 34007 // want it to try to position things outside of its scrolling viewport, because they will 34008 // just appear offscreen (hidden by the current scroll values) 34009 getBounds: () => setupForTheme.getPopupSinkBounds() 34010 }) 34011 ]) 34012 }; 34013 const sink = build$1(sinkSpec); 34014 const uiMothership = takeover(sink); 34015 lazyPopupMothership.set(uiMothership); 34016 return { sink, mothership: uiMothership }; 34017 }; 34018 const renderMainUi = () => { 34019 const partHeader = makeHeaderPart(); 34020 const sidebarContainer = makeSidebarDefinition(); 34021 const partThrobber = OuterContainer.parts.throbber({ 34022 dom: { 34023 tag: 'div', 34024 classes: ['tox-throbber'] 34025 }, 34026 backstage: backstages.popup 34027 }); 34028 const partViewWrapper = OuterContainer.parts.viewWrapper({ 34029 backstage: backstages.popup 34030 }); 34031 const statusbar = useStatusBar(editor) && !isInline ? Optional.some(renderStatusbar(editor, backstages.popup.shared.providers)) : Optional.none(); 34032 // We need the statusbar to be separate to everything else so resizing works properly 34033 const editorComponents = flatten([ 34034 isToolbarBottom ? [] : [partHeader], 34035 // Inline mode does not have a socket/sidebar 34036 isInline ? [] : [sidebarContainer], 34037 isToolbarBottom ? [partHeader] : [] 34038 ]); 34039 const editorContainer = OuterContainer.parts.editorContainer({ 34040 components: flatten([ 34041 editorComponents, 34042 isInline ? [] : [memBottomAnchorBar.asSpec()] 34043 ]) 34044 }); 34045 // Hide the outer container if using inline mode and there's no menubar or toolbar 34046 const isHidden = isDistractionFree(editor); 34047 const attributes = { 34048 role: 'application', 34049 ...global$6.isRtl() ? { dir: 'rtl' } : {}, 34050 ...isHidden ? { 'aria-hidden': 'true' } : {} 34051 }; 34052 const outerContainer = build$1(OuterContainer.sketch({ 34053 dom: { 34054 tag: 'div', 34055 classes: ['tox', 'tox-tinymce'] 34056 .concat(isInline ? ['tox-tinymce-inline'] : []) 34057 .concat(isToolbarBottom ? ['tox-tinymce--toolbar-bottom'] : []) 34058 .concat(deviceClasses), 34059 styles: { 34060 // This is overridden by the skin, it helps avoid FOUC 34061 visibility: 'hidden', 34062 // Hide the container if needed, but don't use "display: none" so that it still has a position 34063 ...isHidden ? { opacity: '0', border: '0' } : {} 34064 }, 34065 attributes 34066 }, 34067 components: [ 34068 editorContainer, 34069 // Inline mode does not have a status bar 34070 ...isInline ? [] : [partViewWrapper, ...statusbar.toArray()], 34071 partThrobber, 34072 ], 34073 behaviours: derive$1([ 34074 toggleOnReceive(() => backstages.popup.shared.providers.checkUiComponentContext('any')), 34075 Disabling.config({ 34076 disableClass: 'tox-tinymce--disabled' 34077 }), 34078 Keying.config({ 34079 mode: 'cyclic', 34080 selector: '.tox-menubar, .tox-toolbar, .tox-toolbar__primary, .tox-toolbar__overflow--open, .tox-sidebar__overflow--open, .tox-statusbar__path, .tox-statusbar__wordcount, .tox-statusbar__branding a, .tox-statusbar__resize-handle' 34081 }) 34082 ]) 34083 })); 34084 const mothership = takeover(outerContainer); 34085 lazyMothership.set(mothership); 34086 return { mothership, outerContainer }; 34087 }; 34088 const setEditorSize = (outerContainer) => { 34089 // Set height and width if they were given, though height only applies to iframe mode 34090 const parsedHeight = numToPx(getHeightWithFallback(editor)); 34091 const parsedWidth = numToPx(getWidthWithFallback(editor)); 34092 if (!editor.inline) { 34093 // Update the width 34094 if (isValidValue$1('div', 'width', parsedWidth)) { 34095 set$7(outerContainer.element, 'width', parsedWidth); 34096 } 34097 // Update the height 34098 if (isValidValue$1('div', 'height', parsedHeight)) { 34099 set$7(outerContainer.element, 'height', parsedHeight); 34100 } 34101 else { 34102 set$7(outerContainer.element, 'height', '400px'); 34103 } 34104 } 34105 return parsedHeight; 34106 }; 34107 const setupShortcutsAndCommands = (outerContainer) => { 34108 editor.addShortcut('alt+F9', 'focus menubar', () => { 34109 OuterContainer.focusMenubar(outerContainer); 34110 }); 34111 editor.addShortcut('alt+F10', 'focus toolbar', () => { 34112 OuterContainer.focusToolbar(outerContainer); 34113 }); 34114 editor.addCommand('ToggleToolbarDrawer', (_ui, options, args) => { 34115 if (options?.skipFocus) { 34116 logFeatureDeprecationWarning('skipFocus'); 34117 OuterContainer.toggleToolbarDrawerWithoutFocusing(outerContainer); 34118 } 34119 else if (args?.skip_focus) { 34120 OuterContainer.toggleToolbarDrawerWithoutFocusing(outerContainer); 34121 } 34122 else { 34123 OuterContainer.toggleToolbarDrawer(outerContainer); 34124 } 34125 }); 34126 editor.addQueryStateHandler('ToggleToolbarDrawer', () => OuterContainer.isToolbarDrawerToggled(outerContainer)); 34127 editor.on('blur', () => { 34128 if (getToolbarMode(editor) === ToolbarMode$1.floating && OuterContainer.isToolbarDrawerToggled(outerContainer)) { 34129 OuterContainer.toggleToolbarDrawerWithoutFocusing(outerContainer); 34130 } 34131 }); 34132 }; 34133 const renderUIWithRefs = (uiRefs) => { 34134 const { mainUi, popupUi, uiMotherships } = uiRefs; 34135 map$1(getToolbarGroups(editor), (toolbarGroupButtonConfig, name) => { 34136 editor.ui.registry.addGroupToolbarButton(name, toolbarGroupButtonConfig); 34137 }); 34138 // Apply Bridge types 34139 const { buttons, menuItems, contextToolbars, sidebars, views } = editor.ui.registry.getAll(); 34140 const toolbarOpt = getMultipleToolbarsOption(editor); 34141 const rawUiConfig = { 34142 menuItems, 34143 menus: getMenus(editor), 34144 menubar: getMenubar(editor), 34145 toolbar: toolbarOpt.getOrThunk(() => getToolbar(editor)), 34146 allowToolbarGroups: toolbarMode === ToolbarMode$1.floating, 34147 buttons, 34148 sidebar: sidebars, 34149 views 34150 }; 34151 setupShortcutsAndCommands(mainUi.outerContainer); 34152 setup$b(editor, mainUi.mothership, uiMotherships); 34153 // This backstage needs to kept in sync with the one passed to the Header part. 34154 header.setup(editor, backstages.popup.shared, lazyHeader); 34155 // This backstage is probably needed for just the bespoke dropdowns 34156 setup$6(editor, backstages.popup); 34157 setup$5(editor, backstages.popup.shared.getSink, backstages.popup); 34158 setup$8(editor); 34159 setup$7(editor, lazyThrobber, backstages.popup.shared); 34160 register$a(editor, contextToolbars, popupUi.sink, { backstage: backstages.popup }); 34161 setup$4(editor, popupUi.sink); 34162 const elm = editor.getElement(); 34163 const height = setEditorSize(mainUi.outerContainer); 34164 const args = { targetNode: elm, height }; 34165 // The only components that use backstages.dialog currently are the Modal dialogs. 34166 return mode.render(editor, uiRefs, rawUiConfig, backstages.popup, args); 34167 }; 34168 const reuseDialogUiForPopuUi = (dialogUi) => { 34169 lazyPopupMothership.set(dialogUi.mothership); 34170 return dialogUi; 34171 }; 34172 const renderUI = () => { 34173 const mainUi = renderMainUi(); 34174 const dialogUi = renderDialogUi(); 34175 // If dialogUi and popupUi are the same, LazyUiReferences should handle deduplicating then 34176 // get calling getUiMotherships 34177 const popupUi = isSplitUiMode(editor) ? renderPopupUi() : reuseDialogUiForPopuUi(dialogUi); 34178 lazyUiRefs.dialogUi.set(dialogUi); 34179 lazyUiRefs.popupUi.set(popupUi); 34180 lazyUiRefs.mainUi.set(mainUi); 34181 // From this point on, we shouldn't use LazyReferences any more. 34182 const uiRefs = { 34183 popupUi, 34184 dialogUi, 34185 mainUi, 34186 uiMotherships: lazyUiRefs.getUiMotherships() 34187 }; 34188 return renderUIWithRefs(uiRefs); 34189 }; 34190 // We don't have uiRefs here, so we have to rely on cells that are set by renderUI unfortunately. 34191 return { 34192 popups: { 34193 backstage: backstages.popup, 34194 getMothership: () => getLazyMothership('popups', lazyPopupMothership) 34195 }, 34196 dialogs: { 34197 backstage: backstages.dialog, 34198 getMothership: () => getLazyMothership('dialogs', lazyDialogMothership) 34199 }, 34200 renderUI 34201 }; 34202 }; 34203 34204 const toValidValues = (values) => { 34205 const errors = []; 34206 const result = {}; 34207 each(values, (value, name) => { 34208 value.fold(() => { 34209 errors.push(name); 34210 }, (v) => { 34211 result[name] = v; 34212 }); 34213 }); 34214 return errors.length > 0 ? Result.error(errors) : 34215 Result.value(result); 34216 }; 34217 34218 const renderBodyPanel = (spec, dialogData, backstage, getCompByName) => { 34219 const memForm = record(Form.sketch((parts) => ({ 34220 dom: { 34221 tag: 'div', 34222 classes: ['tox-form'].concat(spec.classes) 34223 }, 34224 // All of the items passed through the form need to be put through the interpreter 34225 // with their form part preserved. 34226 components: map$2(spec.items, (item) => interpretInForm(parts, item, dialogData, backstage, getCompByName)) 34227 }))); 34228 return { 34229 dom: { 34230 tag: 'div', 34231 classes: ['tox-dialog__body'] 34232 }, 34233 components: [ 34234 { 34235 dom: { 34236 tag: 'div', 34237 classes: ['tox-dialog__body-content'] 34238 }, 34239 components: [ 34240 memForm.asSpec() 34241 ] 34242 } 34243 ], 34244 behaviours: derive$1([ 34245 Keying.config({ 34246 mode: 'acyclic', 34247 useTabstopAt: not(isPseudoStop) 34248 }), 34249 ComposingConfigs.memento(memForm), 34250 memento(memForm, { 34251 postprocess: (formValue) => toValidValues(formValue).fold((err) => { 34252 // eslint-disable-next-line no-console 34253 console.error(err); 34254 return {}; 34255 }, identity) 34256 }), 34257 config('dialog-body-panel', [ 34258 // TINY-10101: This is to cater for the case where clicks are made into the dialog instead using keyboard navigation, as FocusShifted would not be triggered in that case. 34259 run$1(focusin(), (comp, se) => { 34260 comp.getSystem().broadcastOn([dialogFocusShiftedChannel], { 34261 newFocus: Optional.some(se.event.target) 34262 }); 34263 }), 34264 ]) 34265 ]) 34266 }; 34267 }; 34268 34269 const measureHeights = (allTabs, tabview, tabviewComp) => map$2(allTabs, (_tab, i) => { 34270 Replacing.set(tabviewComp, allTabs[i].view()); 34271 const rect = tabview.dom.getBoundingClientRect(); 34272 Replacing.set(tabviewComp, []); 34273 return rect.height; 34274 }); 34275 const getMaxHeight = (heights) => head(sort(heights, (a, b) => { 34276 if (a > b) { 34277 return -1; 34278 } 34279 else if (a < b) { 34280 return +1; 34281 } 34282 else { 34283 return 0; 34284 } 34285 })); 34286 const getMaxTabviewHeight = (dialog, tabview, tablist) => { 34287 const documentElement$1 = documentElement(dialog).dom; 34288 const rootElm = ancestor$1(dialog, '.tox-dialog-wrap').getOr(dialog); 34289 const isFixed = get$e(rootElm, 'position') === 'fixed'; 34290 // Get the document or window/viewport height 34291 let maxHeight; 34292 if (isFixed) { 34293 maxHeight = Math.max(documentElement$1.clientHeight, window.innerHeight); 34294 } 34295 else { 34296 maxHeight = Math.max(documentElement$1.offsetHeight, documentElement$1.scrollHeight); 34297 } 34298 // Determine the current height taken up by the tabview panel 34299 const tabviewHeight = get$d(tabview); 34300 const isTabListBeside = tabview.dom.offsetLeft >= tablist.dom.offsetLeft + get$c(tablist); 34301 const currentTabHeight = isTabListBeside ? Math.max(get$d(tablist), tabviewHeight) : tabviewHeight; 34302 // Get the dialog height, making sure to account for any margins on the dialog 34303 const dialogTopMargin = parseInt(get$e(dialog, 'margin-top'), 10) || 0; 34304 const dialogBottomMargin = parseInt(get$e(dialog, 'margin-bottom'), 10) || 0; 34305 const dialogHeight = get$d(dialog) + dialogTopMargin + dialogBottomMargin; 34306 const chromeHeight = dialogHeight - currentTabHeight; 34307 return maxHeight - chromeHeight; 34308 }; 34309 const showTab = (allTabs, comp) => { 34310 head(allTabs).each((tab) => TabSection.showTab(comp, tab.value)); 34311 }; 34312 const setTabviewHeight = (tabview, height) => { 34313 // Set both height and flex-basis as some browsers don't support flex-basis. 34314 set$7(tabview, 'height', height + 'px'); 34315 set$7(tabview, 'flex-basis', height + 'px'); 34316 }; 34317 const updateTabviewHeight = (dialogBody, tabview, maxTabHeight) => { 34318 ancestor$1(dialogBody, '[role="dialog"]').each((dialog) => { 34319 descendant(dialog, '[role="tablist"]').each((tablist) => { 34320 maxTabHeight.get().map((height) => { 34321 // Set the tab view height to 0, so we can calculate the max tabview height, without worrying about overflows 34322 set$7(tabview, 'height', '0'); 34323 set$7(tabview, 'flex-basis', '0'); 34324 return Math.min(height, getMaxTabviewHeight(dialog, tabview, tablist)); 34325 }).each((height) => { 34326 setTabviewHeight(tabview, height); 34327 }); 34328 }); 34329 }); 34330 }; 34331 const getTabview = (dialog) =>
34331 descendant(dialog, '[role="tabpanel"]'); 34332 const smartMode = (allTabs) => { 34333 const maxTabHeight = value$2(); 34334 const extraEvents = [ 34335 runOnAttached((comp) => { 34336 const dialog = comp.element; 34337 getTabview(dialog).each((tabview) => { 34338 set$7(tabview, 'visibility', 'hidden'); 34339 // Determine the maximum heights of each tab 34340 comp.getSystem().getByDom(tabview).toOptional().each((tabviewComp) => { 34341 const heights = measureHeights(allTabs, tabview, tabviewComp); 34342 // Calculate the maximum tab height and store it 34343 const maxTabHeightOpt = getMaxHeight(heights); 34344 maxTabHeightOpt.fold(maxTabHeight.clear, maxTabHeight.set); 34345 }); 34346 // Set an initial height, based on the current size 34347 updateTabviewHeight(dialog, tabview, maxTabHeight); 34348 // Show the tabs 34349 remove$6(tabview, 'visibility'); 34350 showTab(allTabs, comp); 34351 // Use a delay here and recalculate the height, as we need all the components attached 34352 // to be able to properly calculate the max height 34353 requestAnimationFrame(() => { 34354 updateTabviewHeight(dialog, tabview, maxTabHeight); 34355 }); 34356 }); 34357 }), 34358 run$1(windowResize(), (comp) => { 34359 const dialog = comp.element; 34360 getTabview(dialog).each((tabview) => { 34361 updateTabviewHeight(dialog, tabview, maxTabHeight); 34362 }); 34363 }), 34364 run$1(formResizeEvent, (comp, _se) => { 34365 const dialog = comp.element; 34366 getTabview(dialog).each((tabview) => { 34367 const oldFocus = active$1(getRootNode(tabview)); 34368 set$7(tabview, 'visibility', 'hidden'); 34369 const oldHeight = getRaw(tabview, 'height').map((h) => parseInt(h, 10)); 34370 remove$6(tabview, 'height'); 34371 remove$6(tabview, 'flex-basis'); 34372 const newHeight = tabview.dom.getBoundingClientRect().height; 34373 const hasGrown = oldHeight.forall((h) => newHeight > h); 34374 if (hasGrown) { 34375 maxTabHeight.set(newHeight); 34376 updateTabviewHeight(dialog, tabview, maxTabHeight); 34377 } 34378 else { 34379 oldHeight.each((h) => { 34380 setTabviewHeight(tabview, h); 34381 }); 34382 } 34383 remove$6(tabview, 'visibility'); 34384 oldFocus.each(focus$4); 34385 }); 34386 }) 34387 ]; 34388 const selectFirst = false; 34389 return { 34390 extraEvents, 34391 selectFirst 34392 }; 34393 }; 34394 // Set tab heights within a dialog to vary according to their contents 34395 const naiveMode = (_allTabs) => { 34396 const extraEvents = []; 34397 const selectFirst = true; 34398 return { 34399 extraEvents, 34400 selectFirst 34401 }; 34402 }; 34403 34404 const SendDataToSectionChannel = 'send-data-to-section'; 34405 const SendDataToViewChannel = 'send-data-to-view'; 34406 const renderTabPanel = (spec, dialogData, backstage, getCompByName) => { 34407 const storedValue = Cell({}); 34408 const updateDataWithForm = (form) => { 34409 const formData = Representing.getValue(form); 34410 const validData = toValidValues(formData).getOr({}); 34411 const currentData = storedValue.get(); 34412 const newData = deepMerge(currentData, validData); 34413 storedValue.set(newData); 34414 }; 34415 const setDataOnForm = (form) => { 34416 const tabData = storedValue.get(); 34417 Representing.setValue(form, tabData); 34418 }; 34419 const oldTab = Cell(null); 34420 const allTabs = map$2(spec.tabs, (tab) => { 34421 return { 34422 value: tab.name, 34423 dom: { 34424 tag: 'div', 34425 classes: ['tox-dialog__body-nav-item'] 34426 }, 34427 components: [ 34428 text$2(backstage.shared.providers.translate(tab.title)) 34429 ], 34430 view: () => { 34431 return [ 34432 // Dupe with SilverDialog
vendor: 10,858 bytes, lines 34433-34719
34433 Form.sketch((parts) => ({ 34434 dom: { 34435 tag: 'div', 34436 classes: ['tox-form'] 34437 }, 34438 components: map$2(tab.items, (item) => interpretInForm(parts, item, dialogData, backstage, getCompByName)), 34439 formBehaviours: derive$1([ 34440 Keying.config({ 34441 mode: 'acyclic', 34442 useTabstopAt: not(isPseudoStop) 34443 }), 34444 config('TabView.form.events', [ 34445 runOnAttached(setDataOnForm), 34446 runOnDetached(updateDataWithForm) 34447 ]), 34448 Receiving.config({ 34449 channels: wrapAll([ 34450 { 34451 key: SendDataToSectionChannel, 34452 value: { 34453 onReceive: updateDataWithForm 34454 } 34455 }, 34456 { 34457 key: SendDataToViewChannel, 34458 value: { 34459 onReceive: setDataOnForm 34460 } 34461 } 34462 ]) 34463 }) 34464 ]) 34465 })) 34466 ]; 34467 } 34468 }; 34469 }); 34470 // Assign fixed height or variable height to the tabs 34471 const tabMode = spec.dynamicHeight 34472 ? naiveMode() // Height is determined by the content of the tab, which may cause layout shifts when switching tabs. 34473 : smartMode(allTabs); // Height is determined by the tallest tab, which prevents layout shifts but may cause excessive whitespace in shorter tabs. 34474 return TabSection.sketch({ 34475 dom: { 34476 tag: 'div', 34477 classes: ['tox-dialog__body'] 34478 }, 34479 onChangeTab: (section, button, _viewItems) => { 34480 const name = Representing.getValue(button); 34481 emitWith(section, formTabChangeEvent, { 34482 name, 34483 oldName: oldTab.get() 34484 }); 34485 oldTab.set(name); 34486 }, 34487 tabs: allTabs, 34488 components: [ 34489 TabSection.parts.tabbar({ 34490 dom: { 34491 tag: 'div', 34492 classes: ['tox-dialog__body-nav'] 34493 }, 34494 components: [ 34495 Tabbar.parts.tabs({}) 34496 ], 34497 markers: { 34498 tabClass: 'tox-tab', 34499 selectedClass: 'tox-dialog__body-nav-item--active' 34500 }, 34501 tabbarBehaviours: derive$1([ 34502 Tabstopping.config({}) 34503 ]) 34504 }), 34505 TabSection.parts.tabview({ 34506 dom: { 34507 tag: 'div', 34508 classes: ['tox-dialog__body-content'] 34509 } 34510 }) 34511 ], 34512 selectFirst: tabMode.selectFirst, 34513 tabSectionBehaviours: derive$1([ 34514 config('tabpanel', tabMode.extraEvents), 34515 Keying.config({ 34516 mode: 'acyclic' 34517 }), 34518 // INVESTIGATE: Is this necessary? Probably used by getCompByName. 34519 Composing.config({ 34520 // TODO: Think about this 34521 find: (comp) => head(TabSection.getViewItems(comp)) 34522 }), 34523 withComp(Optional.none(), (tsection) => { 34524 // NOTE: Assumes synchronous updating of store. 34525 tsection.getSystem().broadcastOn([SendDataToSectionChannel], {}); 34526 return storedValue.get(); 34527 }, (tsection, value) => { 34528 storedValue.set(value); 34529 tsection.getSystem().broadcastOn([SendDataToViewChannel], {}); 34530 }) 34531 ]) 34532 }); 34533 }; 34534 34535 // ariaAttrs is being passed through to silver inline dialog 34536 // from the WindowManager as a property of 'params' 34537 const renderBody = (spec, dialogId, contentId, backstage, ariaAttrs, getCompByName) => { 34538 const renderComponents = (incoming) => { 34539 const body = incoming.body; 34540 switch (body.type) { 34541 case 'tabpanel': { 34542 return [ 34543 renderTabPanel(body, incoming.initialData, backstage, getCompByName) 34544 ]; 34545 } 34546 default: { 34547 return [ 34548 renderBodyPanel(body, incoming.initialData, backstage, getCompByName) 34549 ]; 34550 } 34551 } 34552 }; 34553 const updateState = (_comp, incoming) => Optional.some({ 34554 isTabPanel: () => incoming.body.type === 'tabpanel' 34555 }); 34556 const ariaAttributes = { 34557 'aria-live': 'polite' 34558 }; 34559 return { 34560 dom: { 34561 tag: 'div', 34562 classes: ['tox-dialog__content-js'], 34563 attributes: { 34564 ...contentId.map((x) => ({ id: x })).getOr({}), 34565 ...ariaAttrs ? ariaAttributes : {} 34566 } 34567 }, 34568 components: [], 34569 behaviours: derive$1([ 34570 ComposingConfigs.childAt(0), 34571 Reflecting.config({ 34572 channel: `${bodyChannel}-${dialogId}`, 34573 updateState, 34574 renderComponents, 34575 initialData: spec 34576 }) 34577 ]) 34578 }; 34579 }; 34580 const renderInlineBody = (spec, dialogId, contentId, backstage, ariaAttrs, getCompByName) => renderBody(spec, dialogId, Optional.some(contentId), backstage, ariaAttrs, getCompByName); 34581 const renderModalBody = (spec, dialogId, backstage, getCompByName) => { 34582 const bodySpec = renderBody(spec, dialogId, Optional.none(), backstage, false, getCompByName); 34583 return ModalDialog.parts.body(bodySpec); 34584 }; 34585 const renderIframeBody = (spec) => { 34586 const bodySpec = { 34587 dom: { 34588 tag: 'div', 34589 classes: ['tox-dialog__content-js'] 34590 }, 34591 components: [ 34592 { 34593 dom: { 34594 tag: 'div', 34595 classes: ['tox-dialog__body-iframe'] 34596 }, 34597 components: [ 34598 craft(Optional.none(), { 34599 dom: { 34600 tag: 'iframe', 34601 attributes: { 34602 src: spec.url 34603 } 34604 }, 34605 behaviours: derive$1([ 34606 Tabstopping.config({}), 34607 Focusing.config({}) 34608 ]) 34609 }) 34610 ] 34611 } 34612 ], 34613 behaviours: derive$1([ 34614 Keying.config({ 34615 mode: 'acyclic', 34616 useTabstopAt: not(isPseudoStop) 34617 }) 34618 ]) 34619 }; 34620 return ModalDialog.parts.body(bodySpec); 34621 }; 34622 34623 const isTouch = global$7.deviceType.isTouch(); 34624 const hiddenHeader = (title, close) => ({ 34625 dom: { 34626 tag: 'div', 34627 styles: { display: 'none' }, 34628 classes: ['tox-dialog__header'] 34629 }, 34630 components: [ 34631 title, 34632 close 34633 ] 34634 }); 34635 const pClose = (onClose, providersBackstage) => ModalDialog.parts.close( 34636 // Need to find a way to make it clear in the docs whether parts can be sketches 34637 Button.sketch({ 34638 dom: { 34639 tag: 'button', 34640 classes: ['tox-button', 'tox-button--icon', 'tox-button--naked'], 34641 attributes: { 34642 'type': 'button', 34643 'aria-label': providersBackstage.translate('Close') 34644 } 34645 }, 34646 action: onClose, 34647 buttonBehaviours: derive$1([ 34648 Tabstopping.config({}) 34649 ]) 34650 })); 34651 const pUntitled = () => ModalDialog.parts.title({ 34652 dom: { 34653 tag: 'div', 34654 classes: ['tox-dialog__title'], 34655 innerHtml: '', 34656 styles: { 34657 display: 'none' 34658 } 34659 } 34660 }); 34661 const pBodyMessage = (message, providersBackstage) => ModalDialog.parts.body({ 34662 dom: { 34663 tag: 'div', 34664 classes: ['tox-dialog__body'] 34665 }, 34666 components: [ 34667 { 34668 dom: { 34669 tag: 'div', 34670 classes: ['tox-dialog__body-content'] 34671 }, 34672 components: [ 34673 { 34674 dom: fromHtml(`<p>${sanitizeHtmlString(providersBackstage.translate(message))}</p>`) 34675 } 34676 ] 34677 } 34678 ] 34679 }); 34680 const pFooter = (buttons) => ModalDialog.parts.footer({ 34681 dom: { 34682 tag: 'div', 34683 classes: ['tox-dialog__footer'] 34684 }, 34685 components: buttons 34686 }); 34687 const pFooterGroup = (startButtons, endButtons) => [ 34688 Container.sketch({ 34689 dom: { 34690 tag: 'div', 34691 classes: ['tox-dialog__footer-start'] 34692 }, 34693 components: startButtons 34694 }), 34695 Container.sketch({ 34696 dom: { 34697 tag: 'div', 34698 classes: ['tox-dialog__footer-end'] 34699 }, 34700 components: endButtons 34701 }) 34702 ]; 34703 const renderDialog$1 = (spec) => { 34704 const dialogClass = 'tox-dialog'; 34705 const blockerClass = dialogClass + '-wrap'; 34706 const blockerBackdropClass = blockerClass + '__backdrop'; 34707 const scrollLockClass = dialogClass + '__disable-scroll'; 34708 return ModalDialog.sketch({ 34709 role: spec.role, 34710 lazySink: spec.lazySink, 34711 onEscape: (comp) => { 34712 spec.onEscape(comp); 34713 // TODO: Make a strong type for Handled KeyEvent 34714 return Optional.some(true); 34715 }, 34716 useTabstopAt: (elem) => !isPseudoStop(elem), 34717 firstTabstop: spec.firstTabstop, 34718 dom: { 34719 tag: 'div',
34720 classes: [dialogClass].concat(spec.extraClasses), 34721 styles: { 34722 position: 'relative', 34723 ...spec.extraStyles 34724 } 34725 }, 34726 components: [ 34727 spec.header, 34728 spec.body, 34729 ...spec.footer.toArray() 34730 ], 34731 parts: { 34732 blocker: { 34733 dom: fromHtml(`<div class="${blockerClass}"></div>`), 34734 components: [ 34735 { 34736 dom: { 34737 tag: 'div', 34738 classes: (isTouch ? [blockerBackdropClass, blockerBackdropClass + '--opaque'] : [blockerBackdropClass]) 34739 } 34740 } 34741 ] 34742 } 34743 }, 34744 dragBlockClass: blockerClass, 34745 modalBehaviours: derive$1([ 34746 Focusing.config({}), 34747 config('dialog-events', spec.dialogEvents.concat([ 34748 // Note: `runOnSource` here will only listen to the event at the outer component level. 34749 // Using just `run` instead will cause an infinite loop as `focusIn` would fire a `focusin` which would then get responded to and so forth. 34750 runOnSource(focusin(), (comp, _se) => { 34751 Blocking.isBlocked(comp) ? noop() : Keying.focusIn(comp); 34752 }), 34753 run$1(focusShifted(), (comp, se) => { 34754 comp.getSystem().broadcastOn([dialogFocusShiftedChannel], { 34755 newFocus: se.event.newFocus 34756 }); 34757 }) 34758 ])), 34759 config('scroll-lock', [ 34760 runOnAttached(() => { 34761 add$2(body(), scrollLockClass); 34762 }), 34763 runOnDetached(() => { 34764 remove$3(body(), scrollLockClass); 34765 }) 34766 ]), 34767 ...spec.extraBehaviours 34768 ]), 34769 eventOrder: { 34770 [execute$5()]: ['dialog-events'], 34771 [attachedToDom()]: ['scroll-lock', 'dialog-events', 'alloy.base.behaviour'], 34772 [detachedFromDom()]: ['alloy.base.behaviour', 'dialog-events', 'scroll-lock'], 34773 ...spec.eventOrder 34774 } 34775 }); 34776 }; 34777 34778 const renderClose = (providersBackstage) => Button.sketch({ 34779 dom: { 34780 tag: 'button', 34781 classes: ['tox-button', 'tox-button--icon', 'tox-button--naked'], 34782 attributes: { 34783 'type': 'button', 34784 'aria-label': providersBackstage.translate('Close'), 34785 'data-mce-name': 'close' 34786 } 34787 }, 34788 buttonBehaviours: derive$1([ 34789 Tabstopping.config({}), 34790 Tooltipping.config(providersBackstage.tooltips.getConfig({ 34791 tooltipText: providersBackstage.translate('Close') 34792 })) 34793 ]), 34794 components: [ 34795 render$4('close', { tag: 'span', classes: ['tox-icon'] }, providersBackstage.icons) 34796 ], 34797 action: (comp) => { 34798 emit(comp, formCancelEvent); 34799 }, 34800 }); 34801 const renderTitle = (spec, dialogId, titleId, providersBackstage) => { 34802 const renderComponents = (data) => [text$2(providersBackstage.translate(data.title))]; 34803 return { 34804 dom: { 34805 tag: 'h1', 34806 classes: ['tox-dialog__title'], 34807 attributes: { 34808 ...titleId.map((x) => ({ id: x })).getOr({}) 34809 } 34810 }, 34811 components: [], 34812 behaviours: derive$1([ 34813 Reflecting.config({ 34814 channel: `${titleChannel}-${dialogId}`, 34815 initialData: spec, 34816 renderComponents 34817 }) 34818 ]) 34819 }; 34820 }; 34821 const renderDragHandle = () => ({ 34822 dom: fromHtml('<div class="tox-dialog__draghandle"></div>') 34823 }); 34824 const renderInlineHeader = (spec, dialogId, titleId, providersBackstage) => Container.sketch({ 34825 dom: fromHtml('<div class="tox-dialog__header"></div>'), 34826 components: [ 34827 renderTitle(spec, dialogId, Optional.some(titleId), providersBackstage), 34828 renderDragHandle(), 34829 renderClose(providersBackstage) 34830 ], 34831 containerBehaviours: derive$1([ 34832 Dragging.config({ 34833 mode: 'mouse', 34834 blockerClass: 'blocker', 34835 getTarget: (handle) => { 34836 return closest$3(handle, '[role="dialog"]').getOrDie(); 34837 }, 34838 snaps: { 34839 getSnapPoints: () => [], 34840 leftAttr: 'data-drag-left', 34841 topAttr: 'data-drag-top'
34842 }, 34843 onDrag: (comp, target) => { 34844 comp.getSystem().broadcastOn([repositionPopups()], { target }); 34845 } 34846 }) 34847 ]) 34848 }); 34849 const renderModalHeader = (spec, dialogId, providersBackstage) => { 34850 const pTitle = ModalDialog.parts.title(renderTitle(spec, dialogId, Optional.none(), providersBackstage)); 34851 const pHandle = ModalDialog.parts.draghandle(renderDragHandle()); 34852 const pClose = ModalDialog.parts.close(renderClose(providersBackstage)); 34853 const components = [pTitle].concat(spec.draggable ? [pHandle] : []).concat([pClose]); 34854 return Container.sketch({ 34855 dom: fromHtml('<div class="tox-dialog__header"></div>'), 34856 components 34857 }); 34858 }; 34859 34860 const getHeader = (title, dialogId, backstage) => renderModalHeader({ 34861 title: backstage.shared.providers.translate(title), 34862 draggable: backstage.dialog.isDraggableModal() 34863 }, dialogId, backstage.shared.providers); 34864 const getBusySpec = (message, bs, providers, headerHeight) => ({ 34865 dom: { 34866 tag: 'div', 34867 classes: ['tox-dialog__busy-spinner'], 34868 attributes: { 34869 'aria-label': providers.translate(message) 34870 }, 34871 styles: { 34872 left: '0px', 34873 right: '0px', 34874 bottom: '0px', 34875 top: `${headerHeight.getOr(0)}px`, 34876 position: 'absolute' 34877 } 34878 }, 34879 behaviours: bs, 34880 components: [{ 34881 dom: fromHtml('<div class="tox-spinner"><div></div><div></div><div></div></div>') 34882 }] 34883 }); 34884 const getEventExtras = (lazyDialog, providers, extra) => ({ 34885 onClose: () => extra.closeWindow(), 34886 onBlock: (blockEvent) => { 34887 const headerHeight = descendant(lazyDialog().element, '.tox-dialog__header').map((header) => get$d(header)); 34888 ModalDialog.setBusy(lazyDialog(), (_comp, bs) => getBusySpec(blockEvent.message, bs, providers, headerHeight)); 34889 }, 34890 onUnblock: () => { 34891 ModalDialog.setIdle(lazyDialog()); 34892 } 34893 }); 34894 const fullscreenClass = 'tox-dialog--fullscreen'; 34895 const largeDialogClass = 'tox-dialog--width-lg'; 34896 const mediumDialogClass = 'tox-dialog--width-md'; 34897 const getDialogSizeClass = (size) => { 34898 switch (size) { 34899 case 'large': 34900 return Optional.some(largeDialogClass); 34901 case 'medium': 34902 return Optional.some(mediumDialogClass); 34903 default: 34904 return Optional.none(); 34905 } 34906 }; 34907 const updateDialogSizeClass = (size, component) => { 34908 const dialogBody = SugarElement.fromDom(component.element.dom); 34909 if (!has(dialogBody, fullscreenClass)) { 34910 remove$2(dialogBody, [largeDialogClass, mediumDialogClass]); 34911 getDialogSizeClass(size).each((dialogSizeClass) => add$2(dialogBody, dialogSizeClass)); 34912 } 34913 }; 34914 const toggleFullscreen = (comp, currentSize) => { 34915 const dialogBody = SugarElement.fromDom(comp.element.dom); 34916 const classes = get$7(dialogBody); 34917 const currentSizeClass = find$5(classes, (c) => c === largeDialogClass || c === mediumDialogClass).or(getDialogSizeClass(currentSize)); 34918 toggle$2(dialogBody, [fullscreenClass, ...currentSizeClass.toArray()]); 34919 }; 34920 const renderModalDialog = (spec, dialogEvents, backstage) => build$1(renderDialog$1({ 34921 ...spec, 34922 firstTabstop: 1, 34923 lazySink: backstage.shared.getSink, 34924 extraBehaviours: [ 34925 memory({}), 34926 ...spec.extraBehaviours 34927 ], 34928 onEscape: (comp) => { 34929 emit(comp, formCancelEvent); 34930 }, 34931 dialogEvents, 34932 eventOrder: { 34933 [receive()]: [Reflecting.name(), Receiving.name()], 34934 [attachedToDom()]: ['scroll-lock', Reflecting.name(), 'messages', 'dialog-events', 'alloy.base.behaviour'], 34935 [detachedFromDom()]: ['alloy.base.behaviour', 'dialog-events', 'messages', Reflecting.name(), 'scroll-lock'] 34936 } 34937 })); 34938 const mapMenuButtons = (buttons, menuItemStates = {}) => { 34939 const mapItems = (button) => { 34940 const items = map$2(button.items, (item) => { 34941 const cell = get$h(menuItemStates, item.name).getOr(Cell(false)); 34942 return { 34943 ...item, 34944 storage: cell 34945 }; 34946 }); 34947 return { 34948 ...button, 34949 items 34950 }; 34951 }; 34952 return map$2(buttons, (button) => { 34953 return button.type === 'menu' ? mapItems(button) : button; 34954 }); 34955 }; 34956 const extractCellsToObject = (buttons) => foldl(buttons, (acc, button) => { 34957 if (button.type === 'menu') { 34958 const menuButton = button; 34959 return foldl(menuButton.items, (innerAcc, item) => { 34960 innerAcc[item.name] = item.storage; 34961 return innerAcc; 34962 }, acc); 34963 } 34964 return acc; 34965 }, {}); 34966 34967 const focusFirstTabbable = (getSink, component) => { 34968 // TODO: add a test for focusIn (TINY-10125) 34969 const focusIn = () => component.getSystem().isConnected() ? Keying.focusIn(component) : undefined; 34970 const isDisabled = (focused) => has$1(focused, 'disabled') || getOpt(focused, 'aria-disabled').exists((val) => val === 'true'); 34971 const rootNode = getRootNode(component.element); 34972 const current = active$1(rootNode); 34973 active$1(rootNode).fold(focusIn, (focused) => { 34974 // We need to check if the focused element is disabled because apparently firefox likes to leave focus on disabled elements. 34975 if (isDisabled(focused)) { 34976 focusIn(); 34977 // And we need the below check for IE, which likes to leave focus on the parent of disabled elements 34978 } 34979 else if (current.exists((cur) => contains(focused, cur) && isDisabled(cur))) { 34980 focusIn(); 34981 // Lastly if something outside the sink has focus then return the focus back to the dialog 34982 } 34983 else { 34984 getSink().toOptional() 34985 .filter((sink) => !contains(sink.element, focused)) 34986 .each(focusIn); 34987 } 34988 }); 34989 };
vendor: 7,079 bytes, lines 34990-35145
34990 const initCommonEvents = (fireApiEvent, extras, getSink) => [ 34991 // When focus moves onto a tab-placeholder, skip to the next thing in the tab sequence 34992 runWithTarget(focusin(), onFocus), 34993 // TODO: Test if disabled first. 34994 fireApiEvent(formCloseEvent, (_api, spec, _event, self) => { 34995 // TINY-9148: Safari scrolls down to the sink if the dialog is selected before removing, 34996 // so we should blur the currently active element beforehand. 34997 if (hasFocus(self.element)) { 34998 active$1(getRootNode(self.element)).each(blur$1); 34999 } 35000 extras.onClose(); 35001 spec.onClose(); 35002 }), 35003 // TODO: Test if disabled first. 35004 fireApiEvent(formCancelEvent, (api, spec, _event, self) => { 35005 spec.onCancel(api); 35006 emit(self, formCloseEvent); 35007 }), 35008 run$1(formUnblockEvent, (component, _se) => { 35009 extras.onUnblock(); 35010 focusFirstTabbable(getSink, component); 35011 }), 35012 run$1(formBlockEvent, (_c, se) => extras.onBlock(se.event)) 35013 ]; 35014 const initUrlDialog = (getInstanceApi, extras, getSink) => { 35015 const fireApiEvent = (eventName, f) => run$1(eventName, (c, se) => { 35016 withSpec(c, (spec, _c) => { 35017 f(getInstanceApi(), spec, se.event, c); 35018 }); 35019 }); 35020 const withSpec = (c, f) => { 35021 Reflecting.getState(c).get().each((currentDialog) => { 35022 f(currentDialog, c); 35023 }); 35024 }; 35025 return [ 35026 ...initCommonEvents(fireApiEvent, extras, getSink), 35027 fireApiEvent(formActionEvent, (api, spec, event) => { 35028 spec.onAction(api, { name: event.name }); 35029 }) 35030 ]; 35031 }; 35032 const initDialog = (getInstanceApi, extras, getSink) => { 35033 const fireApiEvent = (eventName, f) => run$1(eventName, (c, se) => { 35034 withSpec(c, (spec, _c) => { 35035 f(getInstanceApi(), spec, se.event, c); 35036 }); 35037 }); 35038 const withSpec = (c, f) => { 35039 Reflecting.getState(c).get().each((currentDialogInit) => { 35040 f(currentDialogInit.internalDialog, c); 35041 }); 35042 }; 35043 return [ 35044 ...initCommonEvents(fireApiEvent, extras, getSink), 35045 fireApiEvent(formSubmitEvent, (api, spec) => spec.onSubmit(api)), 35046 fireApiEvent(formChangeEvent, (api, spec, event) => { 35047 spec.onChange(api, { name: event.name }); 35048 }), 35049 fireApiEvent(formActionEvent, (api, spec, event, component) => { 35050 spec.onAction(api, { name: event.name, value: event.value }); 35051 focusFirstTabbable(getSink, component); 35052 }), 35053 fireApiEvent(formTabChangeEvent, (api, spec, event) => { 35054 spec.onTabChange(api, { newTabName: event.name, oldTabName: event.oldName }); 35055 }), 35056 // When the dialog is being closed, store the current state of the form 35057 runOnDetached((component) => { 35058 const api = getInstanceApi(); 35059 Representing.setValue(component, api.getData()); 35060 }) 35061 ]; 35062 }; 35063 35064 const makeButton = (button, backstage) => renderFooterButton(button, button.type, backstage); 35065 const lookup = (compInSystem, footerButtons, buttonName) => find$5(footerButtons, (button) => button.name === buttonName) 35066 .bind((memButton) => memButton.memento.getOpt(compInSystem)); 35067 const renderComponents = (_data, state) => { 35068 // default group is 'end' 35069 const footerButtons = state.map((s) => s.footerButtons).getOr([]); 35070 const buttonGroups = partition$3(footerButtons, (button) => button.align === 'start'); 35071 const makeGroup = (edge, buttons) => Container.sketch({ 35072 dom: { 35073 tag: 'div', 35074 classes: [`tox-dialog__footer-${edge}`] 35075 }, 35076 components: map$2(buttons, (button) => button.memento.asSpec()) 35077 }); 35078 const startButtons = makeGroup('start', buttonGroups.pass); 35079 const endButtons = makeGroup('end', buttonGroups.fail); 35080 return [startButtons, endButtons]; 35081 }; 35082 const renderFooter = (initSpec, dialogId, backstage) => { 35083 const updateState = (comp, data) => { 35084 const footerButtons = map$2(data.buttons, (button) => { 35085 const memButton = record(makeButton(button, backstage)); 35086 return { 35087 name: button.name, 35088 align: button.align, 35089 memento: memButton 35090 }; 35091 }); 35092 const lookupByName = (buttonName) => lookup(comp, footerButtons, buttonName); 35093 return Optional.some({ 35094 lookupByName, 35095 footerButtons 35096 }); 35097 }; 35098 return { 35099 dom: fromHtml('<div class="tox-dialog__footer"></div>'), 35100 components: [], 35101 behaviours: derive$1([ 35102 Reflecting.config({ 35103 channel: `${footerChannel}-${dialogId}`, 35104 initialData: initSpec, 35105 updateState, 35106 renderComponents 35107 }) 35108 ]) 35109 }; 35110 }; 35111 const renderInlineFooter = (initSpec, dialogId, backstage) => renderFooter(initSpec, dialogId, backstage); 35112 const renderModalFooter = (initSpec, dialogId, backstage) => ModalDialog.parts.footer(renderFooter(initSpec, dialogId, backstage)); 35113 35114 const getCompByName = (access, name) => { 35115 // TODO: Add API to alloy to find the inner most component of a Composing chain. 35116 const root = access.getRoot(); 35117 // This is just to avoid throwing errors if the dialog closes before this. We should take it out 35118 // while developing (probably), and put it back in for the real thing. 35119 if (root.getSystem().isConnected()) { 35120 const form = Composing.getCurrent(access.getFormWrapper()).getOr(access.getFormWrapper()); 35121 return Form.getField(form, name).orThunk(() => { 35122 const footer = access.getFooter(); 35123 const footerState = footer.bind((f) => Reflecting.getState(f).get()); 35124 return footerState.bind((f) => f.lookupByName(name)); 35125 }); 35126 } 35127 else { 35128 return Optional.none(); 35129 } 35130 }; 35131 const validateData$1 = (access, data) => { 35132 const root = access.getRoot(); 35133 return Reflecting.getState(root).get().map((dialogState) => getOrDie(asRaw('data', dialogState.dataValidator, data))).getOr(data); 35134 }; 35135 const getDialogApi = (access, doRedial, menuItemStates) => { 35136 const withRoot = (f) => { 35137 const root = access.getRoot(); 35138 if (root.getSystem().isConnected()) { 35139 f(root); 35140 } 35141 }; 35142 const getData = () => { 35143 const root = access.getRoot(); 35144 const valueComp = root.getSystem().isConnected() ? access.getFormWrapper() : root; 35145 const representedValues = Representing.getValue(value
35145Comp); 35146 const menuItemCurrentState = map$1(menuItemStates, (cell) => cell.get()); 35147 return { 35148 ...representedValues, 35149 ...menuItemCurrentState 35150 }; 35151 }; 35152 const setData = (newData) => { 35153 // Currently, the decision is to ignore setData calls that fire after the dialog is closed 35154 withRoot((_) => { 35155 const prevData = instanceApi.getData(); 35156 const mergedData = deepMerge(prevData, newData); 35157 const newInternalData = validateData$1(access, mergedData); 35158 const form = access.getFormWrapper(); 35159 Representing.setValue(form, newInternalData); 35160 each(menuItemStates, (v, k) => { 35161 if (has$2(mergedData, k)) { 35162 v.set(mergedData[k]); 35163 } 35164 }); 35165 }); 35166 }; 35167 const setEnabled = (name, state) => { 35168 getCompByName(access, name).each(state ? Disabling.enable : Disabling.disable); 35169 }; 35170 const focus = (name) => { 35171 getCompByName(access, name).each(Focusing.focus); 35172 }; 35173 const block = (message) => { 35174 if (!isString(message)) { 35175 throw new Error('The dialogInstanceAPI.block function should be passed a blocking message of type string as an argument'); 35176 } 35177 withRoot((root) => { 35178 emitWith(root, formBlockEvent, { message }); 35179 }); 35180 }; 35181 const unblock = () => { 35182 withRoot((root) => { 35183 emit(root, formUnblockEvent); 35184 }); 35185 }; 35186 const showTab = (name) => { 35187 withRoot((_) => { 35188 const body = access.getBody(); 35189 const bodyState = Reflecting.getState(body); 35190 if (bodyState.get().exists((b) => b.isTabPanel())) { 35191 Composing.getCurrent(body).each((tabSection) => { 35192 TabSection.showTab(tabSection, name); 35193 }); 35194 } 35195 }); 35196 }; 35197 const redial = (d) => { 35198 withRoot((root) => { 35199 const id = access.getId(); 35200 const dialogInit = doRedial(d); 35201 const storedMenuButtons = mapMenuButtons(dialogInit.internalDialog.buttons, menuItemStates); 35202 // TINY-9223: We only need to broadcast to the mothership containing the dialog 35203 root.getSystem().broadcastOn([`${dialogChannel}-${id}`], dialogInit); 35204 // NOTE: Reflecting does not have any smart handling of nested reflecting components, 35205 // and the order of receiving a broadcast is non-deterministic. Here we use separate 35206 // channels for each section (title, body, footer), and make those broadcasts *after* 35207 // we've already sent the overall dialog broadcast. The overall dialog broadcast 35208 // doesn't actually change the components ... its Reflecting config just stores state, 35209 // but these Reflecting configs (title, body, footer) do change the components based on 35210 // the received broadcasts. 35211 root.getSystem().broadcastOn([`${titleChannel}-${id}`], dialogInit.internalDialog); 35212 root.getSystem().broadcastOn([`${bodyChannel}-${id}`], dialogInit.internalDialog); 35213 root.getSystem().broadcastOn([`${footerChannel}-${id}`], { 35214 ...dialogInit.internalDialog, 35215 buttons: storedMenuButtons 35216 }); 35217 instanceApi.setData(dialogInit.initialData); 35218 }); 35219 }; 35220 const close = () => { 35221 withRoot((root) => { 35222 emit(root, formCloseEvent); 35223 }); 35224 }; 35225 const instanceApi = { 35226 getData, 35227 setData, 35228 setEnabled, 35229 focus, 35230 block, 35231 unblock, 35232 showTab, 35233 redial, 35234 close, 35235 toggleFullscreen: access.toggleFullscreen 35236 }; 35237 return instanceApi; 35238 }; 35239 35240 const renderDialog = (dialogInit, extra, backstage) => { 35241 const dialogId = generate$6('dialog'); 35242 const internalDialog = dialogInit.internalDialog; 35243 const header = getHeader(internalDialog.title, dialogId, backstage); 35244 const dialogSize = Cell(internalDialog.size); 35245 const getCompByName$1 = (name) => getCompByName(modalAccess, name); 35246 const dialogSizeClasses = getDialogSizeClass(dialogSize.get()).toArray(); 35247 const updateState = (comp, incoming) => { 35248 dialogSize.set(incoming.internalDialog.size); 35249 updateDialogSizeClass(incoming.internalDialog.size, comp); 35250 return Optional.some(incoming); 35251 }; 35252 const body = renderModalBody({ 35253 body: internalDialog.body, 35254 initialData: internalDialog.initialData 35255 }, dialogId, backstage, getCompByName$1); 35256 const storedMenuButtons = mapMenuButtons(internalDialog.buttons); 35257 const objOfCells = extractCellsToObject(storedMenuButtons); 35258 const footer = someIf(storedMenuButtons.length !== 0, renderModalFooter({ buttons: storedMenuButtons }, dialogId, backstage)); 35259 const dialogEvents = initDialog(() => instanceApi, getEventExtras(() => dialog, backstage.shared.providers, extra), backstage.shared.getSink); 35260 const spec = { 35261 id: dialogId, 35262 header, 35263 body, 35264 footer,
35265 extraClasses: dialogSizeClasses, 35266 extraBehaviours: [ 35267 Reflecting.config({ 35268 channel: `${dialogChannel}-${dialogId}`, 35269 updateState, 35270 initialData: dialogInit 35271 }), 35272 ], 35273 extraStyles: {} 35274 }; 35275 const dialog = renderModalDialog(spec, dialogEvents, backstage); 35276 const modalAccess = (() => { 35277 const getForm = () => { 35278 const outerForm = ModalDialog.getBody(dialog); 35279 return Composing.getCurrent(outerForm).getOr(outerForm); 35280 }; 35281 const toggleFullscreen$1 = () => { 35282 toggleFullscreen(dialog, dialogSize.get()); 35283 }; 35284 return { 35285 getId: constant$1(dialogId), 35286 getRoot: constant$1(dialog), 35287 getBody: () => ModalDialog.getBody(dialog), 35288 getFooter: () => ModalDialog.getFooter(dialog), 35289 getFormWrapper: getForm, 35290 toggleFullscreen: toggleFullscreen$1 35291 }; 35292 })(); 35293 // TODO: Get the validator from the dialog state. 35294 const instanceApi = getDialogApi(modalAccess, extra.redial, objOfCells); 35295 return { 35296 dialog, 35297 instanceApi 35298 }; 35299 }; 35300 35301 // DUPE with SilverDialog. Cleaning up. 35302 const renderInlineDialog = (dialogInit, extra, backstage, ariaAttrs = false, refreshDocking) => { 35303 const dialogId = generate$6('dialog'); 35304 const dialogLabelId = generate$6('dialog-label'); 35305 const dialogContentId = generate$6('dialog-content'); 35306 const internalDialog = dialogInit.internalDialog; 35307 const getCompByName$1 = (name) => getCompByName(modalAccess, name); 35308 const dialogSize = Cell(internalDialog.size); 35309 const dialogSizeClass = getDialogSizeClass(dialogSize.get()).toArray(); 35310 // Reflecting behaviour broadcasts on dialog channel only on redial. 35311 const updateState = (comp, incoming) => { 35312 // Update dialog size and position upon redial. 35313 dialogSize.set(incoming.internalDialog.size); 35314 updateDialogSizeClass(incoming.internalDialog.size, comp); 35315 refreshDocking(); 35316 return Optional.some(incoming); 35317 }; 35318 const memHeader = record(renderInlineHeader({ 35319 title: internalDialog.title, 35320 draggable: true 35321 }, dialogId, dialogLabelId, backstage.shared.providers)); 35322 const memBody = record(renderInlineBody({ 35323 body: internalDialog.body, 35324 initialData: internalDialog.initialData, 35325 }, dialogId, dialogContentId, backstage, ariaAttrs, getCompByName$1)); 35326 const storagedMenuButtons = mapMenuButtons(internalDialog.buttons); 35327 const objOfCells = extractCellsToObject(storagedMenuButtons); 35328 const optMemFooter = someIf(storagedMenuButtons.length !== 0, record(renderInlineFooter({ 35329 buttons: storagedMenuButtons 35330 }, dialogId, backstage))); 35331 const dialogEvents = initDialog(() => instanceApi, { 35332 onBlock: (event) => { 35333 Blocking.block(dialog, (_comp, bs) => { 35334 const headerHeight = memHeader.getOpt(dialog).map((dialog) => get$d(dialog.element)); 35335 return getBusySpec(event.message, bs, backstage.shared.providers, headerHeight); 35336 }); 35337 }, 35338 onUnblock: () => { 35339 Blocking.unblock(dialog); 35340 }, 35341 onClose: () => extra.closeWindow() 35342 }, backstage.shared.getSink); 35343 const inlineClass = 'tox-dialog-inline'; 35344 const os = detect$1().os; 35345 // TODO: Disable while validating? 35346 const dialog = build$1({ 35347 dom: { 35348 tag: 'div', 35349 classes: ['tox-dialog', inlineClass, ...dialogSizeClass], 35350 attributes: { 35351 role: 'dialog', 35352 // TINY-10808 - Workaround to address the dialog header not being announced on VoiceOver with aria-labelledby, ideally we should use the aria-labelledby 35353 ...os.isMacOS() ? { 'aria-label': internalDialog.title } : { 'aria-labelledby': dialogLabelId } 35354 } 35355 }, 35356 eventOrder: { 35357 [receive()]: [Reflecting.name(), Receiving.name()], 35358 [execute$5()]: ['execute-on-form'], 35359 [attachedToDom()]: ['reflecting', 'execute-on-form'] 35360 }, 35361 // Dupe with SilverDialog. 35362 behaviours: derive$1([ 35363 Keying.config({ 35364 mode: 'cyclic', 35365 onEscape: (c) => { 35366 emit(c, formCloseEvent); 35367 return Optional.some(true); 35368 }, 35369 useTabstopAt: (elem) => !isPseudoStop(elem) && (name$3(elem) !== 'button' || get$g(elem, 'disabled') !== 'disabled'), 35370 firstTabstop: 1 35371 }), 35372 Reflecting.config({ 35373 channel: `${dialogChannel}-${dialogId}`, 35374 updateState, 35375 initialData: dialogInit 35376 }), 35377 Focusing.config({}), 35378 config('execute-on-form', dialogEvents.concat([ 35379 // Note: `runOnSource` here will only listen to the event at the outer component level. 35380 // Using just `run` instead will cause an infinite loop as `focusIn` would fire a `focusin` which would then get responded to and so forth. 35381 runOnSource(focusin(), (comp, _se) => { 35382 Keying.focusIn(comp); 35383 }), 35384 run$1(focusShifted(), (comp, se) => { 35385 comp.getSystem().broadcastOn([dialogFocusShiftedChannel], {
35386 newFocus: se.event.newFocus 35387 }); 35388 }) 35389 ])), 35390 Blocking.config({ getRoot: () => Optional.some(dialog) }), 35391 Replacing.config({}), 35392 memory({}) 35393 ]), 35394 components: [ 35395 memHeader.asSpec(), 35396 memBody.asSpec(), 35397 ...optMemFooter.map((memFooter) => memFooter.asSpec()).toArray() 35398 ] 35399 }); 35400 const toggleFullscreen$1 = () => { 35401 toggleFullscreen(dialog, dialogSize.get()); 35402 }; 35403 // TODO: Clean up the dupe between this (InlineDialog) and SilverDialog 35404 const modalAccess = { 35405 getId: constant$1(dialogId), 35406 getRoot: constant$1(dialog), 35407 getFooter: () => optMemFooter.map((memFooter) => memFooter.get(dialog)), 35408 getBody: () => memBody.get(dialog), 35409 getFormWrapper: () => { 35410 const body = memBody.get(dialog); 35411 return Composing.getCurrent(body).getOr(body); 35412 }, 35413 toggleFullscreen: toggleFullscreen$1 35414 }; 35415 const instanceApi = getDialogApi(modalAccess, extra.redial, objOfCells); 35416 return { 35417 dialog, 35418 instanceApi 35419 }; 35420 }; 35421 35422 var global = tinymce.util.Tools.resolve('tinymce.util.URI'); 35423 35424 const getUrlDialogApi = (root) => { 35425 const withRoot = (f) => { 35426 if (root.getSystem().isConnected()) { 35427 f(root); 35428 } 35429 }; 35430 const block = (message) => { 35431 if (!isString(message)) { 35432 throw new Error('The urlDialogInstanceAPI.block function should be passed a blocking message of type string as an argument'); 35433 } 35434 withRoot((root) => { 35435 emitWith(root, formBlockEvent, { message }); 35436 }); 35437 }; 35438 const unblock = () => { 35439 withRoot((root) => { 35440 emit(root, formUnblockEvent); 35441 }); 35442 }; 35443 const close = () => { 35444 withRoot((root) => { 35445 emit(root, formCloseEvent); 35446 }); 35447 }; 35448 const sendMessage = (data) => { 35449 withRoot((root) => { 35450 root.getSystem().broadcastOn([bodySendMessageChannel], data); 35451 }); 35452 }; 35453 return { 35454 block, 35455 unblock, 35456 close, 35457 sendMessage 35458 }; 35459 }; 35460 35461 // A list of supported message actions 35462 const SUPPORTED_MESSAGE_ACTIONS = ['insertContent', 'setContent', 'execCommand', 'close', 'block', 'unblock']; 35463 const isSupportedMessage = (data) => isObject(data) && SUPPORTED_MESSAGE_ACTIONS.indexOf(data.mceAction) !== -1; 35464 const isCustomMessage = (data) => !isSupportedMessage(data) && isObject(data) && has$2(data, 'mceAction'); 35465 const handleMessage = (editor, api, data) => { 35466 switch (data.mceAction) { 35467 case 'insertContent': 35468 editor.insertContent(data.content); 35469 break; 35470 case 'setContent': 35471 editor.setContent(data.content); 35472 break; 35473 case 'execCommand': 35474 const ui = isBoolean(data.ui) ? data.ui : false; 35475 editor.execCommand(data.cmd, ui, data.value); 35476 break; 35477 case 'close': 35478 api.close(); 35479 break; 35480 case 'block': 35481 api.block(data.message); 35482 break; 35483 case 'unblock': 35484 api.unblock(); 35485 break; 35486 } 35487 }; 35488 const renderUrlDialog = (internalDialog, extra, editor, backstage) => { 35489 const dialogId = generate$6('dialog'); 35490 const header = getHeader(internalDialog.title, dialogId, backstage); 35491 const body = renderIframeBody(internalDialog); 35492 const footer = internalDialog.buttons.bind((buttons) => { 35493 // Don't render a footer if no buttons are specified 35494 if (buttons.length === 0) { 35495 return Optional.none(); 35496 } 35497 else { 35498 return Optional.some(renderModalFooter({ buttons }, dialogId, backstage)); 35499 } 35500 }); 35501 const dialogEvents = initUrlDialog(() => instanceApi, getEventExtras(() => dialog, backstage.shared.providers, extra), backstage.shared.getSink); 35502 // Add the styles for the modal width/height 35503 const styles = { 35504 ...internalDialog.height.fold(() => ({}), (height) => ({ 'height': height + 'px', 'max-height': height + 'px' })), 35505 ...internalDialog.width.fold(() => ({}), (width) => ({ 'width': width + 'px', 'max-width': width + 'px' })) 35506 }; 35507 // Default back to using a large sized dialog, if no dimensions are specified 35508 const classes = internalDialog.width.isNone() && internalDialog.height.isNone() ? ['tox-dialog--width-lg'] : []; 35509 // Determine the iframe urls domain, so we can target that specifically when sending messages 35510 const iframeUri = new global(internalDialog.url, { base_uri: new global(window.location.href) }); 35511 const iframeDomain = `${iframeUri.protocol}://${iframeUri.host}${iframeUri.port ? ':' + iframeUri.port : ''}`; 35512 const messageHandlerUnbinder = unbindable(); 35513 const updateState = (_comp, incoming) => Optional.some(incoming); 35514 // Setup the behaviours for dealing with messages between the iframe and current window 35515 const extraBehaviours = [ 35516 // Because this doesn't define `renderComponents`, all this does is update the state. 35517 // We use the state for the initialData. The other parts (body etc.) render the 35518 // components based on what reflecting receives. 35519 Reflecting.config({ 35520 channel: `${dialogChannel}-${dialogId}`, 35521 updateState, 35522 initialData: internalDialog 35523 }), 35524 config('messages', [ 35525 // When the dialog is opened, bind a window message listener for the spec url 35526 runOnAttached(() => { 35527 const unbind = bind$1(SugarElement.fromDom(window), 'message', (e) => { 35528 // Validate that the request came from the correct domain 35529 if (iframeUri.isSameOrigin(new global(e.raw.origin))) { 35530 const data = e.raw.data; 35531 // Handle the message if it has the 'mceAction' key, otherwise just ignore it 35532 if (isSupportedMessage(data)) { 35533 handleMessage(editor, instanceApi, data); 35534 } 35535 else if (isCustomMessage(data)) { 35536 internalDialog.onMessage(instanceApi, data); 35537 } 35538 } 35539 }); 35540 messageHandlerUnbinder.set(unbind); 35541 }), 35542 // When the dialog is closed, unbind the window message listener 35543 runOnDetached(messageHandlerUnbinder.clear) 35544 ]), 35545 Receiving.config({ 35546 channels: { 35547 [bodySendMessageChannel]: { 35548 onReceive: (comp, data) => { 35549 // Send the message to the iframe via postMessage 35550 descendant(comp.element, 'iframe').each((iframeEle) => { 35551 const iframeWin = iframeEle.dom.contentWindow; 35552 if (isNonNullable(iframeWin)) { 35553 iframeWin.postMessage(data, iframeDomain); 35554 } 35555 }); 35556 } 35557 } 35558 } 35559 }) 35560 ]; 35561 const spec = { 35562 id: dialogId, 35563 header, 35564 body, 35565 footer,
vendor: 9,455 bytes, lines 35566-35778
35566 extraClasses: classes, 35567 extraBehaviours, 35568 extraStyles: styles 35569 }; 35570 const dialog = renderModalDialog(spec, dialogEvents, backstage); 35571 const instanceApi = getUrlDialogApi(dialog); 35572 return { 35573 dialog, 35574 instanceApi 35575 }; 35576 }; 35577 35578 const setup$2 = (backstage) => { 35579 const sharedBackstage = backstage.shared; 35580 const open = (message, callback) => { 35581 const closeDialog = () => { 35582 ModalDialog.hide(alertDialog); 35583 callback(); 35584 }; 35585 const memFooterClose = record(renderFooterButton({ 35586 context: 'any', 35587 name: 'close-alert', 35588 text: 'OK', 35589 primary: true, 35590 buttonType: Optional.some('primary'), 35591 align: 'end', 35592 enabled: true, 35593 icon: Optional.none() 35594 }, 'cancel', backstage)); 35595 const titleSpec = pUntitled(); 35596 const closeSpec = pClose(closeDialog, sharedBackstage.providers); 35597 const alertDialog = build$1(renderDialog$1({ 35598 role: 'alertdialog', 35599 lazySink: () => sharedBackstage.getSink(), 35600 header: hiddenHeader(titleSpec, closeSpec), 35601 body: pBodyMessage(message, sharedBackstage.providers), 35602 footer: Optional.some(pFooter(pFooterGroup([], [ 35603 memFooterClose.asSpec() 35604 ]))), 35605 onEscape: closeDialog, 35606 extraClasses: ['tox-alert-dialog'], 35607 extraBehaviours: [], 35608 extraStyles: {}, 35609 dialogEvents: [ 35610 run$1(formCancelEvent, closeDialog), 35611 runOnAttached((c) => { 35612 const bodyElm = ModalDialog.getBody(c); 35613 describedBy(c.element, bodyElm.element); 35614 }), 35615 ], 35616 eventOrder: {} 35617 })); 35618 ModalDialog.show(alertDialog); 35619 const footerCloseButton = memFooterClose.get(alertDialog); 35620 Focusing.focus(footerCloseButton); 35621 }; 35622 return { 35623 open 35624 }; 35625 }; 35626 35627 const setup$1 = (backstage) => { 35628 const sharedBackstage = backstage.shared; 35629 // FIX: Extreme dupe with Alert dialog 35630 const open = (message, callback) => { 35631 const closeDialog = (state) => { 35632 ModalDialog.hide(confirmDialog); 35633 callback(state); 35634 }; 35635 const memFooterYes = record(renderFooterButton({ 35636 context: 'any', 35637 name: 'yes', 35638 text: 'Yes', 35639 primary: true, 35640 buttonType: Optional.some('primary'), 35641 align: 'end', 35642 enabled: true, 35643 icon: Optional.none() 35644 }, 'submit', backstage)); 35645 const footerNo = renderFooterButton({ 35646 context: 'any', 35647 name: 'no', 35648 text: 'No', 35649 primary: false, 35650 buttonType: Optional.some('secondary'), 35651 align: 'end', 35652 enabled: true, 35653 icon: Optional.none() 35654 }, 'cancel', backstage); 35655 const titleSpec = pUntitled(); 35656 const closeSpec = pClose(() => closeDialog(false), sharedBackstage.providers); 35657 const confirmDialog = build$1(renderDialog$1({ 35658 role: 'alertdialog', 35659 lazySink: () => sharedBackstage.getSink(), 35660 header: hiddenHeader(titleSpec, closeSpec), 35661 body: pBodyMessage(message, sharedBackstage.providers), 35662 footer: Optional.some(pFooter(pFooterGroup([], [ 35663 footerNo, 35664 memFooterYes.asSpec() 35665 ]))), 35666 onEscape: () => closeDialog(false), 35667 extraClasses: ['tox-confirm-dialog'], 35668 extraBehaviours: [], 35669 extraStyles: {}, 35670 dialogEvents: [ 35671 run$1(formCancelEvent, () => closeDialog(false)), 35672 run$1(formSubmitEvent, () => closeDialog(true)), 35673 runOnAttached((c) => { 35674 const bodyElm = ModalDialog.getBody(c); 35675 describedBy(c.element, bodyElm.element); 35676 }), 35677 ], 35678 eventOrder: {} 35679 })); 35680 ModalDialog.show(confirmDialog); 35681 const footerYesButton = memFooterYes.get(confirmDialog); 35682 Focusing.focus(footerYesButton); 35683 }; 35684 return { 35685 open 35686 }; 35687 }; 35688 35689 const validateData = (data, validator) => getOrDie(asRaw('data', validator, data)); 35690 const isAlertOrConfirmDialog = (target) => closest$1(target, '.tox-alert-dialog') || closest$1(target, '.tox-confirm-dialog'); 35691 const inlineAdditionalBehaviours = (editor, isStickyToolbar, isToolbarLocationTop, onHide) => { 35692 // When using sticky toolbars it already handles the docking behaviours so applying docking would 35693 // do nothing except add additional processing when scrolling, so we don't want to include it here 35694 // (Except when the toolbar is located at the bottom since the anchor will be at the top) 35695 if (isStickyToolbar && isToolbarLocationTop) { 35696 return []; 35697 } 35698 else { 35699 return [ 35700 Docking.config({ 35701 contextual: { 35702 lazyContext: () => Optional.some(box$1(SugarElement.fromDom(editor.getContentAreaContainer()))), 35703 fadeInClass: 'tox-dialog-dock-fadein', 35704 fadeOutClass: 'tox-dialog-dock-fadeout', 35705 transitionClass: 'tox-dialog-dock-transition', 35706 onHide 35707 }, 35708 modes: ['top'], 35709 lazyViewport: (comp) => { 35710 // If we don't have a special scrolling environment, then just use the default 35711 // viewport of (window) 35712 const optScrollingContext = detectWhenSplitUiMode(editor, comp.element); 35713 return optScrollingContext 35714 .map((sc) => { 35715 const combinedBounds = getBoundsFrom(sc); 35716 return { 35717 bounds: combinedBounds, 35718 optScrollEnv: Optional.some({ 35719 currentScrollTop: sc.element.dom.scrollTop, 35720 scrollElmTop: absolute$3(sc.element).top 35721 }) 35722 }; 35723 }).getOrThunk(() => ({ 35724 bounds: win(), 35725 optScrollEnv: Optional.none() 35726 })); 35727 } 35728 }) 35729 ]; 35730 } 35731 }; 35732 const setup = (extras) => { 35733 const editor = extras.editor; 35734 const isStickyToolbar$1 = isStickyToolbar(editor); 35735 // Alert and Confirm dialogs are Modal Dialogs 35736 const alertDialog = setup$2(extras.backstages.dialog); 35737 const confirmDialog = setup$1(extras.backstages.dialog); 35738 const open = (config, params, closeWindow) => { 35739 if (!isUndefined(params)) { 35740 if (params.inline === 'toolbar') { 35741 return openInlineDialog(config, extras.backstages.popup.shared.anchors.inlineDialog(), closeWindow, params); 35742 } 35743 else if (params.inline === 'bottom') { 35744 return openBottomInlineDialog(config, extras.backstages.popup.shared.anchors.inlineBottomDialog(), closeWindow, params); 35745 } 35746 else if (params.inline === 'cursor') { 35747 return openInlineDialog(config, extras.backstages.popup.shared.anchors.cursor(), closeWindow, params); 35748 } 35749 } 35750 return openModalDialog(config, closeWindow); 35751 }; 35752 const openUrl = (config, closeWindow) => openModalUrlDialog(config, closeWindow); 35753 const openModalUrlDialog = (config, closeWindow) => { 35754 const factory = (contents) => { 35755 const dialog = renderUrlDialog(contents, { 35756 closeWindow: () => { 35757 ModalDialog.hide(dialog.dialog); 35758 closeWindow(dialog.instanceApi); 35759 } 35760 }, editor, extras.backstages.dialog); 35761 ModalDialog.show(dialog.dialog); 35762 return dialog.instanceApi; 35763 }; 35764 return DialogManager.openUrl(factory, config); 35765 }; 35766 const openModalDialog = (config, closeWindow) => { 35767 const factory = (contents, internalInitialData, dataValidator) => { 35768 // We used to validate data here, but it's done by the instanceApi.setData call below. 35769 const initialData = internalInitialData; 35770 const dialogInit = { 35771 dataValidator, 35772 initialData, 35773 internalDialog: contents 35774 }; 35775 const dialog = renderDialog(dialogInit, { 35776 redial: DialogManager.redial, 35777 closeWindow: () => { 35778 ModalDialog.hide(dialog.dialog);
35779 closeWindow(dialog.instanceApi); 35780 } 35781 }, extras.backstages.dialog); 35782 ModalDialog.show(dialog.dialog); 35783 dialog.instanceApi.setData(initialData); 35784 return dialog.instanceApi; 35785 }; 35786 return DialogManager.open(factory, config); 35787 }; 35788 const openInlineDialog = (config$1, anchor, closeWindow, windowParams) => { 35789 const factory = (contents, internalInitialData, dataValidator) => { 35790 const initialData = validateData(internalInitialData, dataValidator); 35791 const inlineDialog = value$2(); 35792 const isToolbarLocationTop = extras.backstages.popup.shared.header.isPositionedAtTop(); 35793 const dialogInit = { 35794 dataValidator, 35795 initialData, 35796 internalDialog: contents 35797 }; 35798 const refreshDocking = () => inlineDialog.on((dialog) => { 35799 InlineView.reposition(dialog); 35800 if (!isStickyToolbar$1 || !isToolbarLocationTop) { 35801 Docking.refresh(dialog); 35802 } 35803 }); 35804 const dialogUi = renderInlineDialog(dialogInit, { 35805 redial: DialogManager.redial, 35806 closeWindow: () => { 35807 inlineDialog.on(InlineView.hide); 35808 editor.off('ResizeEditor', refreshDocking); 35809 editor.off('ScrollWindow', repositionPopups$1); 35810 inlineDialog.clear(); 35811 closeWindow(dialogUi.instanceApi); 35812 } 35813 }, extras.backstages.popup, windowParams.ariaAttrs, refreshDocking); 35814 const repositionPopups$1 = () => dialogUi.dialog.getSystem().broadcastOn([repositionPopups()], { target: dialogUi.dialog.element }); 35815 const dismissPopups$1 = () => dialogUi.dialog.getSystem().broadcastOn([dismissPopups()], { target: dialogUi.dialog.element }); 35816 const inlineDialogComp = build$1(InlineView.sketch({ 35817 lazySink: extras.backstages.popup.shared.getSink, 35818 dom: { 35819 tag: 'div', 35820 classes: [] 35821 }, 35822 // Fires the default dismiss event. 35823 fireDismissalEventInstead: (windowParams.persistent ? { event: 'doNotDismissYet' } : {}), 35824 // TINY-9412: The docking behaviour for inline dialogs is inconsistent 35825 // for toolbar_location: bottom. We need to clarify exactly what the behaviour 35826 // should be. The intent here might have been that they shouldn't automatically 35827 // reposition at all because they aren't visually connected to the toolbar 35828 // (i.e. inline "toolbar" dialogs still display at the top, even when the 35829 // toolbar_location is bottom), but it's unclear. 35830 ...isToolbarLocationTop ? {} : { fireRepositionEventInstead: {} }, 35831 inlineBehaviours: derive$1([ 35832 config('window-manager-inline-events', [ 35833 run$1(dismissRequested(), (_comp, _se) => { 35834 emit(dialogUi.dialog, formCancelEvent); 35835 }) 35836 ]), 35837 ...inlineAdditionalBehaviours(editor, isStickyToolbar$1, isToolbarLocationTop, dismissPopups$1) 35838 ]), 35839 // Treat alert or confirm dialogs as part of the inline dialog 35840 isExtraPart: (_comp, target) => isAlertOrConfirmDialog(target) 35841 })); 35842 inlineDialog.set(inlineDialogComp); 35843 const getInlineDialogBounds = () => { 35844 // At the moment the inline dialog is just put anywhere in the body, and docking is what is used to make 35845 // sure that it stays onscreen 35846 const elem = editor.inline ? body() : SugarElement.fromDom(editor.getContainer()); 35847 const bounds = box$1(elem); 35848 return Optional.some(bounds); 35849 }; 35850 // Position the inline dialog 35851 InlineView.showWithinBounds(inlineDialogComp, premade(dialogUi.dialog), { anchor }, getInlineDialogBounds); 35852 // Refresh the docking position if not using a sticky toolbar 35853 if (!isStickyToolbar$1 || !isToolbarLocationTop) { 35854 Docking.refresh(inlineDialogComp); 35855 // Bind to the editor resize event and update docking as needed. We don't need to worry about 35856 // 'ResizeWindow` as that's handled by docking already. 35857 editor.on('ResizeEditor', refreshDocking); 35858 } 35859 editor.on('ScrollWindow', repositionPopups$1); 35860 // Set the initial data in the dialog and focus the first focusable item 35861 dialogUi.instanceApi.setData(initialData); 35862 Keying.focusIn(dialogUi.dialog); 35863 return dialogUi.instanceApi; 35864 }; 35865 return DialogManager.open(factory, config$1); 35866 }; 35867 const openBottomInlineDialog = (config$1, anchor, closeWindow, windowParams) => { 35868 const factory = (contents, internalInitialData, dataValidator) => { 35869 const initialData = validateData(internalInitialData, dataValidator); 35870 const inlineDialog = value$2(); 35871 const isToolbarLocationTop = extras.backstages.popup.shared.header.isPositionedAtTop(); 35872 const dialogInit = { 35873 dataValidator, 35874 initialData, 35875 internalDialog: contents 35876 }; 35877 const refreshDocking = () => inlineDialog.on((dialog) => { 35878 InlineView.reposition(dialog); 35879 Docking.refresh(dialog); 35880 }); 35881 const dialogUi = renderInlineDialog(dialogInit, { 35882 redial: DialogManager.redial, 35883 closeWindow: () => { 35884 inlineDialog.on(InlineView.hide); 35885 editor.off('ResizeEditor ScrollWindow ElementScroll', refreshDocking); 35886 inlineDialog.clear(); 35887 closeWindow(dialogUi.instanceApi); 35888 } 35889 }, extras.backstages.popup, windowParams.ariaAttrs, refreshDocking); 35890 const inlineDialogComp = build$1(InlineView.sketch({ 35891 lazySink: extras.backstages.popup.shared.getSink, 35892 dom: { 35893 tag: 'div', 35894 classes: [] 35895 }, 35896 // Fires the default dismiss event. 35897 fireDismissalEventInstead: (windowParams.persistent ? { event: 'doNotDismissYet' } : {}), 35898 ...isToolbarLocationTop ? {} : { fireRepositionEventInstead: {} }, 35899 inlineBehaviours: derive$1([ 35900 config('window-manager-inline-events', [ 35901 run$1(dismissRequested(), (_comp, _se) => { 35902 emit(dialogUi.dialog, formCancelEvent); 35903 }) 35904 ]), 35905 Docking.config({ 35906 contextual: { 35907 lazyContext: () => Optional.some(box$1(SugarElement.fromDom(editor.getContentAreaContainer()))), 35908 fadeInClass: 'tox-dialog-dock-fadein', 35909 fadeOutClass: 'tox-dialog-dock-fadeout', 35910 transitionClass: 'tox-dialog-dock-transition' 35911 }, 35912 modes: ['top', 'bottom'], 35913 lazyViewport: (comp) => { 35914 const optScrollingContext = detectWhenSplitUiMode(editor, comp.element); 35915 return optScrollingContext.map((sc) => { 35916 const combinedBounds = getBoundsFrom(sc); 35917 return { 35918 bounds: combinedBounds, 35919 optScrollEnv: Optional.some({ 35920 currentScrollTop: sc.element.dom.scrollTop, 35921 scrollElmTop: absolute$3(sc.element).top 35922 }) 35923 }; 35924 }).getOrThunk(() => ({ 35925 bounds: win(), 35926 optScrollEnv: Optional.none() 35927 })); 35928 } 35929 }) 35930 ]), 35931 // Treat alert or confirm dialogs as part of the inline dialog 35932 isExtraPart: (_comp, target) => isAlertOrConfirmDialog(target) 35933 })); 35934 inlineDialog.set(inlineDialogComp); 35935 const getInlineDialogBounds = () => { 35936 return extras.backstages.popup.shared.getSink().toOptional().bind((s) => { 35937 const optScrollingContext = detectWhenSplitUiMode(editor, s.element); 35938 // Margin between element and the bottom of the screen or the editor content area container 35939 const margin = 15; 35940 const bounds$1 = optScrollingContext.map((sc) => getBoundsFrom(sc)).getOr(win()); 35941 const contentAreaContainer = box$1(SugarElement.fromDom(editor.getContentAreaContainer())); 35942 const constrainedBounds = constrain(contentAreaContainer, bounds$1); 35943 return Optional.some(bounds(constrainedBounds.x, constrainedBounds.y, constrainedBounds.width, constrainedBounds.height - margin)); 35944 }); 35945 }; 35946 // Position the inline dialog 35947 InlineView.showWithinBounds(inlineDialogComp, premade(dialogUi.dialog), { anchor }, getInlineDialogBounds); 35948 Docking.refresh(inlineDialogComp); 35949 editor.on('ResizeEditor ScrollWindow ElementScroll ResizeWindow', refreshDocking); 35950 // Set the initial data in the dialog and focus the first focusable item 35951 dialogUi.instanceApi.setData(initialData); 35952 Keying.focusIn(dialogUi.dialog); 35953 return dialogUi.instanceApi; 35954 }; 35955 return DialogManager.open(factory, config$1); 35956 }; 35957 const confirm = (message, callback) => { 35958 confirmDialog.open(message, callback); 35959 }; 35960 const alert = (message, callback) => { 35961 alertDialog.open(message, callback); 35962 }; 35963 const close = (instanceApi) => { 35964 instanceApi.close(); 35965 }; 35966 return { 35967 open, 35968 openUrl, 35969 alert, 35970 close, 35971 confirm 35972 }; 35973 }; 35974 35975 const registerOptions = (editor) => { 35976 register$f(editor); 35977 register$e(editor); 35978 register(editor); 35979 }; 35980 var Theme = () => { 35981 global$b.add('silver', (editor) => { 35982 registerOptions(editor); 35983 // When using the ui_mode: split, the popup sink is placed as a sibling to the 35984 // editor, which means that it might be subject to any scrolling environments 35985 // that the editor has. Therefore, we want to make the popup sink have an overall 35986 // bounds that is dependent on its scrolling environment. We don't know that ahead 35987 // of time, so we use a mutable variable whose value will change if there is a scrolling context. 35988 let popupSinkBounds = () => win(); 35989 const { dialogs, popups, renderUI: renderModeUI } = setup$3(editor, { 35990 // consult the mutable variable to find out the bounds for the popup sink. When renderUI is 35991 // called, this mutable variable might be reassigned 35992 getPopupSinkBounds: () => popupSinkBounds() 35993 }); 35994 // We wrap the `renderModeUI` function being returned by Render so that we can update 35995 // the getPopupSinkBounds mutable variable if required. 35996 // DON'T define this function as `async`; otherwise, it will slow down the rendering process and cause flickering if the editor is repeatedly removed and re-initialized. 35997 const renderUI = () => { 35998 const renderResult = renderModeUI(); 35999 const optScrollingContext = detectWhenSplitUiMode(editor, popups.getMothership().element); 36000 optScrollingContext.each((sc) => { 36001 popupSinkBounds = () => { 36002 // At this stage, it looks like we need to calculate the bounds each time, just in 36003 // case the scrolling context details have changed since the last time. The bounds considers 36004 // the Boxes.box sizes, which might change over time. 36005 return getBoundsFrom(sc); 36006 }; 36007 }); 36008 return renderResult; 36009 }; 36010 Autocompleter.register(editor, popups.backstage.shared); 36011 const windowMgr = setup({ 36012 editor, 36013 backstages: { 36014 popup: popups.backstage, 36015 dialog: dialogs.backstage 36016 } 36017 }); 36018 const notificationRegion = value$2(); 36019 // The NotificationManager uses the popup mothership (and sink) 36020 const getNotificationManagerImpl = () => NotificationManagerImpl(editor, { backstage: popups.backstage }, popups.getMothership(), notificationRegion); 36021 const getPromotionElement = () => { 36022 return descendant(SugarElement.fromDom(editor.getContainer()), '.tox-promotion').map((promotion) => promotion.dom).getOrNull(); 36023 }; 36024 const getSinkElement = (type) => type === 'dialog' ? dialogs.getMothership().element.dom : popups.getMothership().element.dom; 36025 return { 36026 renderUI, 36027 getWindowManagerImpl: constant$1(windowMgr), 36028 getNotificationManagerImpl, 36029 getPromotionElement, 36030 getSinkElement 36031 }; 36032 }); 36033 }; 36034 36035 Theme(); 36036 /** ***** 36037 * DO NOT EXPORT ANYTHING 36038 * 36039 * IF YOU DO ROLLUP WILL LEAVE A GLOBAL ON THE PAGE 36040 *******/ 36041 36042})();
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.