@ckeditor/ckeditor5-engine 27.1.0 → 29.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +3 -3
  3. package/package.json +22 -21
  4. package/src/controller/datacontroller.js +28 -7
  5. package/src/controller/editingcontroller.js +1 -1
  6. package/src/conversion/conversion.js +4 -4
  7. package/src/conversion/downcastdispatcher.js +6 -2
  8. package/src/conversion/downcasthelpers.js +48 -39
  9. package/src/conversion/mapper.js +1 -0
  10. package/src/conversion/modelconsumable.js +10 -5
  11. package/src/conversion/upcastdispatcher.js +6 -6
  12. package/src/conversion/upcasthelpers.js +34 -30
  13. package/src/dataprocessor/dataprocessor.jsdoc +5 -5
  14. package/src/dataprocessor/htmldataprocessor.js +38 -9
  15. package/src/dataprocessor/xmldataprocessor.js +5 -5
  16. package/src/index.js +1 -0
  17. package/src/model/element.js +3 -3
  18. package/src/model/liveposition.js +1 -1
  19. package/src/model/model.js +5 -5
  20. package/src/model/node.js +3 -3
  21. package/src/model/range.js +5 -3
  22. package/src/model/schema.js +103 -39
  23. package/src/model/selection.js +1 -1
  24. package/src/model/treewalker.js +3 -4
  25. package/src/model/utils/deletecontent.js +17 -4
  26. package/src/model/utils/insertcontent.js +15 -15
  27. package/src/model/utils/selection-post-fixer.js +1 -1
  28. package/src/view/documentselection.js +2 -2
  29. package/src/view/domconverter.js +150 -72
  30. package/src/view/downcastwriter.js +2 -1
  31. package/src/view/element.js +3 -2
  32. package/src/view/filler.js +4 -4
  33. package/src/view/matcher.js +419 -93
  34. package/src/view/observer/focusobserver.js +7 -3
  35. package/src/view/observer/mouseobserver.js +1 -1
  36. package/src/view/renderer.js +9 -1
  37. package/src/view/selection.js +2 -2
  38. package/src/view/styles/background.js +2 -0
  39. package/src/view/styles/border.js +107 -21
  40. package/src/view/styles/utils.js +1 -1
  41. package/src/view/stylesmap.js +45 -5
  42. package/src/view/upcastwriter.js +12 -11
  43. package/src/view/view.js +5 -0
@@ -7,6 +7,10 @@
7
7
  * @module engine/view/matcher
8
8
  */
9
9
 
10
+ import { isPlainObject } from 'lodash-es';
11
+
12
+ import { logWarning } from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
13
+
10
14
  /**
11
15
  * View matcher class.
12
16
  * Instance of this class can be used to find {@link module:engine/view/element~Element elements} that match given pattern.
@@ -72,11 +76,6 @@ export default class Matcher {
72
76
  item = { name: item };
73
77
  }
74
78
 
75
- // Single class name/RegExp can be provided.
76
- if ( item.classes && ( typeof item.classes == 'string' || item.classes instanceof RegExp ) ) {
77
- item.classes = [ item.classes ];
78
- }
79
-
80
79
  this._patterns.push( item );
81
80
  }
82
81
  }
@@ -242,40 +241,202 @@ function matchName( pattern, name ) {
242
241
  return pattern === name;
243
242
  }
244
243
 
245
- // Checks if attributes of provided element can be matched against provided patterns.
244
+ // Checks if an array of key/value pairs can be matched against provided patterns.
246
245
  //
247
- // @param {Object} patterns Object with information about attributes to match. Each key of the object will be
248
- // used as attribute name. Value of each key can be a string or regular expression to match against attribute value.
249
- // @param {module:engine/view/element~Element} element Element which attributes will be tested.
246
+ // Patterns can be provided in a following ways:
247
+ // - a boolean value matches any attribute with any value (or no value):
248
+ //
249
+ // pattern: true
250
+ //
251
+ // - a RegExp expression or object matches any attribute name:
252
+ //
253
+ // pattern: /h[1-6]/
254
+ //
255
+ // - an object matches any attribute that has the same name as the object item's key, where object item's value is:
256
+ // - equal to `true`, which matches any attribute value:
257
+ //
258
+ // pattern: {
259
+ // required: true
260
+ // }
261
+ //
262
+ // - a string that is equal to attribute value:
263
+ //
264
+ // pattern: {
265
+ // rel: 'nofollow'
266
+ // }
267
+ //
268
+ // - a regular expression that matches attribute value,
269
+ //
270
+ // pattern: {
271
+ // src: /https.*/
272
+ // }
273
+ //
274
+ // - an array with items, where the item is:
275
+ // - a string that is equal to attribute value:
276
+ //
277
+ // pattern: [ 'data-property-1', 'data-property-2' ],
278
+ //
279
+ // - an object with `key` and `value` property, where `key` is a regular expression matching attribute name and
280
+ // `value` is either regular expression matching attribute value or a string equal to attribute value:
281
+ //
282
+ // pattern: [
283
+ // { key: /data-property-.*/, value: true },
284
+ // // or:
285
+ // { key: /data-property-.*/, value: 'foobar' },
286
+ // // or:
287
+ // { key: /data-property-.*/, value: /foo.*/ }
288
+ // ]
289
+ //
290
+ // @param {Object} patterns Object with information about attributes to match.
291
+ // @param {Iterable.<String>} keys Attribute, style or class keys.
292
+ // @param {Function} valueGetter A function providing value for a given item key.
250
293
  // @returns {Array|null} Returns array with matched attribute names or `null` if no attributes were matched.
251
- function matchAttributes( patterns, element ) {
294
+ function matchPatterns( patterns, keys, valueGetter ) {
295
+ const normalizedPatterns = normalizePatterns( patterns );
296
+ const normalizedItems = Array.from( keys );
252
297
  const match = [];
253
298
 
254
- for ( const name in patterns ) {
255
- const pattern = patterns[ name ];
299
+ normalizedPatterns.forEach( ( [ patternKey, patternValue ] ) => {
300
+ normalizedItems.forEach( itemKey => {
301
+ if (
302
+ isKeyMatched( patternKey, itemKey ) &&
303
+ isValueMatched( patternValue, itemKey, valueGetter )
304
+ ) {
305
+ match.push( itemKey );
306
+ }
307
+ } );
308
+ } );
256
309
 
257
- if ( element.hasAttribute( name ) ) {
258
- const attribute = element.getAttribute( name );
310
+ // Return matches only if there are at least as many of them as there are patterns.
311
+ // The RegExp pattern can match more than one item.
312
+ if ( !normalizedPatterns.length || match.length < normalizedPatterns.length ) {
313
+ return null;
314
+ }
259
315
 
260
- if ( pattern === true ) {
261
- match.push( name );
262
- } else if ( pattern instanceof RegExp ) {
263
- if ( pattern.test( attribute ) ) {
264
- match.push( name );
265
- } else {
266
- return null;
316
+ return match;
317
+ }
318
+
319
+ // Bring all the possible pattern forms to an array of arrays where first item is a key and second is a value.
320
+ //
321
+ // Examples:
322
+ //
323
+ // Boolean pattern value:
324
+ //
325
+ // true
326
+ //
327
+ // to
328
+ //
329
+ // [ [ true, true ] ]
330
+ //
331
+ // Textual pattern value:
332
+ //
333
+ // 'attribute-name-or-class-or-style'
334
+ //
335
+ // to
336
+ //
337
+ // [ [ 'attribute-name-or-class-or-style', true ] ]
338
+ //
339
+ // Regular expression:
340
+ //
341
+ // /^data-.*$/
342
+ //
343
+ // to
344
+ //
345
+ // [ [ /^data-.*$/, true ] ]
346
+ //
347
+ // Objects (plain or with `key` and `value` specified explicitly):
348
+ //
349
+ // {
350
+ // src: /^https:.*$/
351
+ // }
352
+ //
353
+ // or
354
+ //
355
+ // [ {
356
+ // key: 'src',
357
+ // value: /^https:.*$/
358
+ // } ]
359
+ //
360
+ // to:
361
+ //
362
+ // [ [ 'src', /^https:.*$/ ] ]
363
+ //
364
+ // @param {Object|Array} patterns
365
+ // @returns {Array|null} Returns an array of objects or null if provided patterns were not in an expected form.
366
+ function normalizePatterns( patterns ) {
367
+ if ( Array.isArray( patterns ) ) {
368
+ return patterns.map( pattern => {
369
+ if ( isPlainObject( pattern ) ) {
370
+ if ( pattern.key === undefined || pattern.value === undefined ) {
371
+ // Documented at the end of matcher.js.
372
+ logWarning( 'matcher-pattern-missing-key-or-value', pattern );
267
373
  }
268
- } else if ( attribute === pattern ) {
269
- match.push( name );
270
- } else {
271
- return null;
374
+
375
+ return [ pattern.key, pattern.value ];
272
376
  }
273
- } else {
274
- return null;
377
+
378
+ // Assume the pattern is either String or RegExp.
379
+ return [ pattern, true ];
380
+ } );
381
+ }
382
+
383
+ if ( isPlainObject( patterns ) ) {
384
+ return Object.entries( patterns );
385
+ }
386
+
387
+ // Other cases (true, string or regexp).
388
+ return [ [ patterns, true ] ];
389
+ }
390
+
391
+ // @param {String|RegExp} patternKey A pattern representing a key we want to match.
392
+ // @param {String} itemKey An actual item key (e.g. `'src'`, `'background-color'`, `'ck-widget'`) we're testing against pattern.
393
+ // @returns {Boolean}
394
+ function isKeyMatched( patternKey, itemKey ) {
395
+ return patternKey === true ||
396
+ patternKey === itemKey ||
397
+ patternKey instanceof RegExp && patternKey.test( itemKey );
398
+ }
399
+
400
+ // @param {String|RegExp} patternValue A pattern representing a value we want to match.
401
+ // @param {String} itemKey An item key, e.g. `background`, `href`, 'rel', etc.
402
+ // @param {Function} valueGetter A function used to provide a value for a given `itemKey`.
403
+ // @returns {Boolean}
404
+ function isValueMatched( patternValue, itemKey, valueGetter ) {
405
+ if ( patternValue === true ) {
406
+ return true;
407
+ }
408
+
409
+ const itemValue = valueGetter( itemKey );
410
+
411
+ return patternValue === itemValue || patternValue instanceof RegExp && patternValue.test( itemValue );
412
+ }
413
+
414
+ // Checks if attributes of provided element can be matched against provided patterns.
415
+ //
416
+ // @param {Object} patterns Object with information about attributes to match. Each key of the object will be
417
+ // used as attribute name. Value of each key can be a string or regular expression to match against attribute value.
418
+ // @param {module:engine/view/element~Element} element Element which attributes will be tested.
419
+ // @returns {Array|null} Returns array with matched attribute names or `null` if no attributes were matched.
420
+ function matchAttributes( patterns, element ) {
421
+ const attributeKeys = new Set( element.getAttributeKeys() );
422
+
423
+ // `style` and `class` attribute keys are deprecated. Only allow them in object pattern
424
+ // for backward compatibility.
425
+ if ( isPlainObject( patterns ) ) {
426
+ if ( patterns.style !== undefined ) {
427
+ // Documented at the end of matcher.js.
428
+ logWarning( 'matcher-pattern-deprecated-attributes-style-key', patterns );
275
429
  }
430
+ if ( patterns.class !== undefined ) {
431
+ // Documented at the end of matcher.js.
432
+ logWarning( 'matcher-pattern-deprecated-attributes-class-key', patterns );
433
+ }
434
+ } else {
435
+ attributeKeys.delete( 'style' );
436
+ attributeKeys.delete( 'class' );
276
437
  }
277
438
 
278
- return match;
439
+ return matchPatterns( patterns, attributeKeys, key => element.getAttribute( key ) );
279
440
  }
280
441
 
281
442
  // Checks if classes of provided element can be matched against provided patterns.
@@ -284,29 +445,8 @@ function matchAttributes( patterns, element ) {
284
445
  // @param {module:engine/view/element~Element} element Element which classes will be tested.
285
446
  // @returns {Array|null} Returns array with matched class names or `null` if no classes were matched.
286
447
  function matchClasses( patterns, element ) {
287
- const match = [];
288
-
289
- for ( const pattern of patterns ) {
290
- if ( pattern instanceof RegExp ) {
291
- const classes = element.getClassNames();
292
-
293
- for ( const name of classes ) {
294
- if ( pattern.test( name ) ) {
295
- match.push( name );
296
- }
297
- }
298
-
299
- if ( match.length === 0 ) {
300
- return null;
301
- }
302
- } else if ( element.hasClass( pattern ) ) {
303
- match.push( pattern );
304
- } else {
305
- return null;
306
- }
307
- }
308
-
309
- return match;
448
+ // We don't need `getter` here because patterns for classes are always normalized to `[ className, true ]`.
449
+ return matchPatterns( patterns, element.getClassNames() );
310
450
  }
311
451
 
312
452
  // Checks if styles of provided element can be matched against provided patterns.
@@ -316,31 +456,7 @@ function matchClasses( patterns, element ) {
316
456
  // @param {module:engine/view/element~Element} element Element which styles will be tested.
317
457
  // @returns {Array|null} Returns array with matched style names or `null` if no styles were matched.
318
458
  function matchStyles( patterns, element ) {
319
- const match = [];
320
-
321
- for ( const name in patterns ) {
322
- const pattern = patterns[ name ];
323
-
324
- if ( element.hasStyle( name ) ) {
325
- const style = element.getStyle( name );
326
-
327
- if ( pattern instanceof RegExp ) {
328
- if ( pattern.test( style ) ) {
329
- match.push( name );
330
- } else {
331
- return null;
332
- }
333
- } else if ( style === pattern ) {
334
- match.push( name );
335
- } else {
336
- return null;
337
- }
338
- } else {
339
- return null;
340
- }
341
- }
342
-
343
- return match;
459
+ return matchPatterns( patterns, element.getStyleNames( true ), key => element.getStyle( key ) );
344
460
  }
345
461
 
346
462
  /**
@@ -358,45 +474,187 @@ function matchStyles( patterns, element ) {
358
474
  * const pattern = /^p/;
359
475
  *
360
476
  * If `MatcherPattern` is given as an `Object`, all the object's properties will be matched with view element properties.
477
+ * If the view element does not meet all of the object's pattern properties, the match will not happen.
478
+ * Available `Object` matching properties:
479
+ *
480
+ * Matching view element:
361
481
  *
362
- * // Match view element's name.
363
- * const pattern = { name: /^p/ };
482
+ * // Match view element's name using String:
483
+ * const pattern = { name: 'p' };
364
484
  *
365
- * // Match view element which has matching attributes.
485
+ * // or by providing RegExp:
486
+ * const pattern = { name: /^(ul|ol)$/ };
487
+ *
488
+ * // The name can also be skipped to match any view element with matching attributes:
366
489
  * const pattern = {
367
490
  * attributes: {
368
- * title: 'foobar', // Attribute title should equal 'foobar'.
369
- * foo: /^\w+/, // Attribute foo should match /^\w+/ regexp.
370
- * bar: true // Attribute bar should be set (can be empty).
491
+ * 'title': true
371
492
  * }
372
493
  * };
373
494
  *
374
- * // Match view element which has given class.
495
+ * Matching view element attributes:
496
+ *
497
+ * // Match view element with any attribute value.
375
498
  * const pattern = {
376
- * classes: 'foobar'
499
+ * name: 'p',
500
+ * attributes: true
377
501
  * };
378
502
  *
379
- * // Match view element class using regular expression.
503
+ * // Match view element which has matching attributes (String).
380
504
  * const pattern = {
381
- * classes: /foo.../
505
+ * name: 'figure',
506
+ * attributes: 'title' // Match title attribute (can be empty).
382
507
  * };
383
508
  *
384
- * // Multiple classes to match.
509
+ * // Match view element which has matching attributes (RegExp).
385
510
  * const pattern = {
386
- * classes: [ 'baz', 'bar', /foo.../ ]
511
+ * name: 'figure',
512
+ * attributes: /^data-.*$/ // Match attributes starting with `data-` e.g. `data-foo` with any value (can be empty).
387
513
  * };
388
514
  *
389
- * // Match view element which has given styles.
515
+ * // Match view element which has matching attributes (Object).
390
516
  * const pattern = {
391
- * styles: {
392
- * position: 'absolute',
393
- * color: /^\w*blue$/
517
+ * name: 'figure',
518
+ * attributes: {
519
+ * title: 'foobar', // Match `title` attribute with 'foobar' value.
520
+ * alt: true, // Match `alt` attribute with any value (can be empty).
521
+ * 'data-type': /^(jpg|png)$/ // Match `data-type` attribute with `jpg` or `png` value.
522
+ * }
523
+ * };
524
+ *
525
+ * // Match view element which has matching attributes (Array).
526
+ * const pattern = {
527
+ * name: 'figure',
528
+ * attributes: [
529
+ * 'title', // Match `title` attribute (can be empty).
530
+ * /^data-*$/, // Match attributes starting with `data-` e.g. `data-foo` with any value (can be empty).
531
+ * ]
532
+ * };
533
+ *
534
+ * // Match view element which has matching attributes (key-value pairs).
535
+ * const pattern = {
536
+ * name: 'input',
537
+ * attributes: [
538
+ * {
539
+ * key: 'type', // Match `type` as an attribute key.
540
+ * value: /^(text|number|date)$/ }, // Match `text`, `number` or `date` values.
541
+ * {
542
+ * key: /^data-.*$/, // Match attributes starting with `data-` e.g. `data-foo`.
543
+ * value: true // Match any value (can be empty).
544
+ * }
545
+ * ]
546
+ * };
547
+ *
548
+ * Matching view element styles:
549
+ *
550
+ * // Match view element with any style.
551
+ * const pattern = {
552
+ * name: 'p',
553
+ * styles: true
554
+ * };
555
+ *
556
+ * // Match view element which has matching styles (String).
557
+ * const pattern = {
558
+ * name: 'p',
559
+ * styles: 'color' // Match attributes with `color` style.
560
+ * };
561
+ *
562
+ * // Match view element which has matching styles (RegExp).
563
+ * const pattern = {
564
+ * name: 'p',
565
+ * styles: /^border.*$/ // Match view element with any border style.
566
+ * };
567
+ *
568
+ * // Match view element which has matching styles (Object).
569
+ * const pattern = {
570
+ * name: 'p',
571
+ * attributes: {
572
+ * color: /rgb\((\d{1,3}), (\d{1,3}), (\d{1,3})\)/, // Match `color` in RGB format only.
573
+ * 'font-weight': 600, // Match `font-weight` only if it's `600`.
574
+ * 'text-decoration': true // Match any text decoration.
575
+ * }
576
+ * };
577
+ *
578
+ * // Match view element which has matching styles (Array).
579
+ * const pattern = {
580
+ * name: 'p',
581
+ * attributes: [
582
+ * 'color', // Match `color` with any value.
583
+ * /^border.*$/, // Match all border properties.
584
+ * ]
585
+ * };
586
+ *
587
+ * // Match view element which has matching styles (key-value pairs).
588
+ * const pattern = {
589
+ * name: 'p',
590
+ * attributes: [
591
+ * {
592
+ * key: 'color', // Match `color` as an property key.
593
+ * value: /rgb\((\d{1,3}), (\d{1,3}), (\d{1,3})\)/, // Match RGB format only.
594
+ * {
595
+ * key: /^border.*$/, // Match any border style.
596
+ * value: true // Match any value.
597
+ * }
598
+ * ]
599
+ * };
600
+ *
601
+ * Matching view element classes:
602
+ *
603
+ * // Match view element with any class.
604
+ * const pattern = {
605
+ * name: 'p',
606
+ * classes: true
607
+ * };
608
+ *
609
+ * // Match view element which has matching class (String).
610
+ * const pattern = {
611
+ * name: 'p',
612
+ * classes: 'highlighted' // Match `highlighted` class.
613
+ * };
614
+ *
615
+ * // Match view element which has matching classes (RegExp).
616
+ * const pattern = {
617
+ * name: 'figure',
618
+ * classes: /^image-side-(left|right)$/ // Match `image-side-left` or `image-side-right` class.
619
+ * };
620
+ *
621
+ * // Match view element which has matching classes (Object).
622
+ * const pattern = {
623
+ * name: 'p',
624
+ * classes: {
625
+ * highlighted: true, // Match `highlighted` class.
626
+ * marker: true // Match `marker` class.
394
627
  * }
395
628
  * };
396
629
  *
397
- * // Pattern with multiple properties.
630
+ * // Match view element which has matching classes (Array).
631
+ * const pattern = {
632
+ * name: 'figure',
633
+ * classes: [
634
+ * 'image', // Match `image` class.
635
+ * /^image-side-(left|right)$/ // Match `image-side-left` or `image-side-right` class.
636
+ * ]
637
+ * };
638
+ *
639
+ * // Match view element which has matching classes (key-value pairs).
640
+ * const pattern = {
641
+ * name: 'figure',
642
+ * classes: [
643
+ * {
644
+ * key: 'image', // Match `image` class.
645
+ * value: true
646
+ * {
647
+ * key: /^image-side-(left|right)$/, // Match `image-side-left` or `image-side-right` class.
648
+ * value: true
649
+ * }
650
+ * ]
651
+ * };
652
+ *
653
+ * Pattern can combine multiple properties allowing for more complex view element matching:
654
+ *
398
655
  * const pattern = {
399
656
  * name: 'span',
657
+ * attributes: [ 'title' ],
400
658
  * styles: {
401
659
  * 'font-weight': 'bold'
402
660
  * },
@@ -444,3 +702,71 @@ function matchStyles( patterns, element ) {
444
702
  * @property {Object} [attributes] Object with key-value pairs representing attributes to match.
445
703
  * Each object key represents attribute name. Value can be given as `String` or `RegExp`.
446
704
  */
705
+
706
+ /**
707
+ * The key-value matcher pattern is missing key or value. Both must be present.
708
+ * Refer the documentation: {@link module:engine/view/matcher~MatcherPattern}.
709
+ *
710
+ * @param {Object} pattern Pattern with missing properties.
711
+ * @error matcher-pattern-missing-key-or-value
712
+ */
713
+
714
+ /**
715
+ * The key-value matcher pattern for `attributes` option is using deprecated `style` key.
716
+ *
717
+ * Use `styles` matcher pattern option instead:
718
+ *
719
+ * // Instead of:
720
+ * const pattern = {
721
+ * attributes: {
722
+ * key1: 'value1',
723
+ * key2: 'value2',
724
+ * style: /^border.*$/
725
+ * }
726
+ * }
727
+ *
728
+ * // Use:
729
+ * const pattern = {
730
+ * attributes: {
731
+ * key1: 'value1',
732
+ * key2: 'value2'
733
+ * },
734
+ * styles: /^border.*$/
735
+ * }
736
+ *
737
+ * Refer to the {@glink builds/guides/migration/migration-to-29##migration-to-ckeditor-5-v2910 Migration to v29.1.0} guide
738
+ * and {@link module:engine/view/matcher~MatcherPattern} documentation.
739
+ *
740
+ * @param {Object} pattern Pattern with missing properties.
741
+ * @error matcher-pattern-deprecated-attributes-style-key
742
+ */
743
+
744
+ /**
745
+ * The key-value matcher pattern for `attributes` option is using deprecated `class` key.
746
+ *
747
+ * Use `classes` matcher pattern option instead:
748
+ *
749
+ * // Instead of:
750
+ * const pattern = {
751
+ * attributes: {
752
+ * key1: 'value1',
753
+ * key2: 'value2',
754
+ * class: 'foobar'
755
+ * }
756
+ * }
757
+ *
758
+ * // Use:
759
+ * const pattern = {
760
+ * attributes: {
761
+ * key1: 'value1',
762
+ * key2: 'value2'
763
+ * },
764
+ * classes: 'foobar'
765
+ * }
766
+ *
767
+ * Refer to the {@glink builds/guides/migration/migration-to-29##migration-to-ckeditor-5-v2910 Migration to v29.1.0} guide
768
+ * and the {@link module:engine/view/matcher~MatcherPattern} documentation.
769
+ *
770
+ * @param {Object} pattern Pattern with missing properties.
771
+ * @error matcher-pattern-deprecated-attributes-class-key
772
+ */
@@ -37,7 +37,10 @@ export default class FocusObserver extends DomEventObserver {
37
37
  // overwrite new DOM selection with selection from the view.
38
38
  // See https://github.com/ckeditor/ckeditor5-engine/issues/795 for more details.
39
39
  // Long timeout is needed to solve #676 and https://github.com/ckeditor/ckeditor5-engine/issues/1157 issues.
40
- this._renderTimeoutId = setTimeout( () => view.forceRender(), 50 );
40
+ //
41
+ // Using `view.change()` instead of `view.forceRender()` to prevent double rendering
42
+ // in a situation where `selectionchange` already caused selection change.
43
+ this._renderTimeoutId = setTimeout( () => view.change( () => {} ), 50 );
41
44
  } );
42
45
 
43
46
  document.on( 'blur', ( evt, data ) => {
@@ -46,8 +49,9 @@ export default class FocusObserver extends DomEventObserver {
46
49
  if ( selectedEditable === null || selectedEditable === data.target ) {
47
50
  document.isFocused = false;
48
51
 
49
- // Re-render the document to update view elements.
50
- view.forceRender();
52
+ // Re-render the document to update view elements
53
+ // (changing document.isFocused already marked view as changed since last rendering).
54
+ view.change( () => {} );
51
55
  }
52
56
  } );
53
57
 
@@ -21,7 +21,7 @@ export default class MouseObserver extends DomEventObserver {
21
21
  constructor( view ) {
22
22
  super( view );
23
23
 
24
- this.domEventType = [ 'mousedown', 'mouseup' ];
24
+ this.domEventType = [ 'mousedown', 'mouseup', 'mouseover', 'mouseout' ];
25
25
  }
26
26
 
27
27
  onDomEvent( domEvent ) {
@@ -259,7 +259,15 @@ export default class Renderer {
259
259
  return;
260
260
  }
261
261
 
262
- const actualDomChildren = this.domConverter.mapViewToDom( viewElement ).childNodes;
262
+ // Removing nodes from the DOM as we iterate can cause `actualDomChildren`
263
+ // (which is a live-updating `NodeList`) to get out of sync with the
264
+ // indices that we compute as we iterate over `actions`.
265
+ // This would produce incorrect element mappings.
266
+ //
267
+ // Converting live list to an array to make the list static.
268
+ const actualDomChildren = Array.from(
269
+ this.domConverter.mapViewToDom( viewElement ).childNodes
270
+ );
263
271
  const expectedDomChildren = Array.from(
264
272
  this.domConverter.viewChildrenToDom( viewElement, domElement.ownerDocument, { withChildren: false } )
265
273
  );
@@ -134,7 +134,7 @@ export default class Selection {
134
134
  * Returns true if selection instance is marked as `fake`.
135
135
  *
136
136
  * @see #setTo
137
- * @returns {Boolean}
137
+ * @type {Boolean}
138
138
  */
139
139
  get isFake() {
140
140
  return this._isFake;
@@ -144,7 +144,7 @@ export default class Selection {
144
144
  * Returns fake selection label.
145
145
  *
146
146
  * @see #setTo
147
- * @returns {String}
147
+ * @type {String}
148
148
  */
149
149
  get fakeSelectionLabel() {
150
150
  return this._fakeSelectionLabel;
@@ -41,6 +41,8 @@ export function addBackgroundRules( stylesProcessor ) {
41
41
 
42
42
  return ret;
43
43
  } );
44
+
45
+ stylesProcessor.setStyleRelation( 'background', [ 'background-color' ] );
44
46
  }
45
47
 
46
48
  function normalizeBackground( value ) {