uielement.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module engine/view/uielement
  7. */
  8. import Element from './element';
  9. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  10. import Node from './node';
  11. import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
  12. /**
  13. * UIElement class. It is used to represent UI not a content of the document.
  14. * This element can't be split and selection can't be placed inside this element.
  15. */
  16. export default class UIElement extends Element {
  17. /**
  18. * Creates new instance of UIElement.
  19. *
  20. * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-uielement-cannot-add` when third parameter is passed,
  21. * to inform that usage of UIElement is incorrect (adding child nodes to UIElement is forbidden).
  22. *
  23. * @param {String} name Node name.
  24. * @param {Object|Iterable} [attributes] Collection of attributes.
  25. */
  26. constructor( name, attributes, children ) {
  27. super( name, attributes, children );
  28. /**
  29. * Returns `null` because filler is not needed for UIElements.
  30. *
  31. * @method #getFillerOffset
  32. * @returns {null} Always returns null.
  33. */
  34. this.getFillerOffset = getFillerOffset;
  35. }
  36. /**
  37. * @inheritDoc
  38. */
  39. is( type, name = null ) {
  40. if ( !name ) {
  41. return type == 'uiElement' || super.is( type );
  42. } else {
  43. return ( type == 'uiElement' && name == this.name ) || super.is( type, name );
  44. }
  45. }
  46. /**
  47. * Overrides {@link module:engine/view/element~Element#insertChildren} method.
  48. * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-uielement-cannot-add` to prevent adding any child nodes
  49. * to UIElement.
  50. */
  51. insertChildren( index, nodes ) {
  52. if ( nodes && ( nodes instanceof Node || Array.from( nodes ).length > 0 ) ) {
  53. /**
  54. * Cannot add children to {@link module:engine/view/uielement~UIElement}.
  55. *
  56. * @error view-uielement-cannot-add
  57. */
  58. throw new CKEditorError( 'view-uielement-cannot-add: Cannot add child nodes to UIElement instance.' );
  59. }
  60. }
  61. /**
  62. * Renders this {@link module:engine/view/uielement~UIElement} to DOM. This method is called by
  63. * {@link module:engine/view/domconverter~DomConverter}.
  64. *
  65. * @param {Document} domDocument
  66. * @return {HTMLElement}
  67. */
  68. render( domDocument ) {
  69. const domElement = domDocument.createElement( this.name );
  70. for ( const key of this.getAttributeKeys() ) {
  71. domElement.setAttribute( key, this.getAttribute( key ) );
  72. }
  73. return domElement;
  74. }
  75. }
  76. /**
  77. * This function injects UI element handling to the given {@link module:engine/view/document~Document document}.
  78. *
  79. * A callback is added to {@link module:engine/view/document~Document#event:keydown document keydown event}.
  80. * The callback handles the situation when right arrow key is pressed and selection is collapsed before a UI element.
  81. * Without this handler, it would be impossible to "jump over" UI element using right arrow key.
  82. *
  83. * @param {module:engine/view/document~Document} document Document to which the quirks handling will be injected.
  84. */
  85. export function injectUiElementHandling( document ) {
  86. document.on( 'keydown', ( evt, data ) => jumpOverUiElement( evt, data, document.domConverter ) );
  87. }
  88. // Returns `null` because block filler is not needed for UIElements.
  89. //
  90. // @returns {null}
  91. function getFillerOffset() {
  92. return null;
  93. }
  94. // Selection cannot be placed in a `UIElement`. Whenever it is placed there, it is moved before it. This
  95. // causes a situation when it is impossible to jump over `UIElement` using right arrow key, because the selection
  96. // ends up in ui element (in DOM) and is moved back to the left. This handler fixes this situation.
  97. function jumpOverUiElement( evt, data, domConverter ) {
  98. if ( data.keyCode == keyCodes.arrowright ) {
  99. const domSelection = data.domTarget.ownerDocument.defaultView.getSelection();
  100. if ( domSelection.rangeCount == 1 && domSelection.getRangeAt( 0 ).collapsed ) {
  101. const domParent = domSelection.getRangeAt( 0 ).startContainer;
  102. const domOffset = domSelection.getRangeAt( 0 ).startOffset;
  103. const viewPosition = domConverter.domPositionToView( domParent, domOffset );
  104. // In case if dom element is not converted to view or is not mapped or something. Happens for example in some tests.
  105. if ( viewPosition === null ) {
  106. return;
  107. }
  108. // Skip all following ui elements.
  109. const nextViewPosition = viewPosition.getLastMatchingPosition( value => value.item.is( 'uiElement' ) );
  110. // If anything has been skipped, fix position.
  111. // This `if` could be possibly omitted but maybe it is better not to mess with DOM selection if not needed.
  112. if ( !viewPosition.isEqual( nextViewPosition ) ) {
  113. const newDomPosition = domConverter.viewPositionToDom( nextViewPosition );
  114. domSelection.collapse( newDomPosition.parent, newDomPosition.offset );
  115. }
  116. }
  117. }
  118. }