converters.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. /**
  2. * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /**
  6. * @module restricted-editing/restrictededitingmode/converters
  7. */
  8. import Matcher from '@ckeditor/ckeditor5-engine/src/view/matcher';
  9. import { getMarkerAtPosition } from './utils';
  10. const HIGHLIGHT_CLASS = 'restricted-editing-exception_selected';
  11. /**
  12. * Adds a visual highlight style to a restricted editing exception that the selection is anchored to.
  13. *
  14. * The highlight is turned on by adding the `.ck-restricted-editing-exception_selected` class to the
  15. * exception in the view:
  16. *
  17. * * The class is removed before the conversion starts, as callbacks added with the `'highest'` priority
  18. * to {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher} events.
  19. * * The class is added in the view post-fixer, after other changes in the model tree are converted to the view.
  20. *
  21. * This way, adding and removing the highlight does not interfere with conversion.
  22. *
  23. * @param {module:core/editor/editor~Editor} editor
  24. */
  25. export function setupExceptionHighlighting( editor ) {
  26. const view = editor.editing.view;
  27. const model = editor.model;
  28. const highlightedMarkers = new Set();
  29. // Adding the class.
  30. view.document.registerPostFixer( writer => {
  31. const modelSelection = model.document.selection;
  32. const marker = getMarkerAtPosition( editor, modelSelection.anchor );
  33. if ( !marker ) {
  34. return;
  35. }
  36. for ( const viewElement of editor.editing.mapper.markerNameToElements( marker.name ) ) {
  37. writer.addClass( HIGHLIGHT_CLASS, viewElement );
  38. highlightedMarkers.add( viewElement );
  39. }
  40. } );
  41. // Removing the class.
  42. editor.conversion.for( 'editingDowncast' ).add( dispatcher => {
  43. // Make sure the highlight is removed on every possible event, before conversion is started.
  44. dispatcher.on( 'insert', removeHighlight, { priority: 'highest' } );
  45. dispatcher.on( 'remove', removeHighlight, { priority: 'highest' } );
  46. dispatcher.on( 'attribute', removeHighlight, { priority: 'highest' } );
  47. dispatcher.on( 'selection', removeHighlight, { priority: 'highest' } );
  48. function removeHighlight() {
  49. view.change( writer => {
  50. for ( const item of highlightedMarkers.values() ) {
  51. writer.removeClass( HIGHLIGHT_CLASS, item );
  52. highlightedMarkers.delete( item );
  53. }
  54. } );
  55. }
  56. } );
  57. }
  58. /**
  59. * A post-fixer that prevents removing a collapsed marker from the document.
  60. *
  61. * @param {module:core/editor/editor~Editor} editor
  62. * @returns {Function}
  63. */
  64. export function resurrectCollapsedMarkerPostFixer( editor ) {
  65. // This post-fixer shouldn't be necessary after https://github.com/ckeditor/ckeditor5/issues/5778.
  66. return writer => {
  67. let changeApplied = false;
  68. for ( const [ name, data ] of editor.model.document.differ._changedMarkers ) {
  69. if ( name.startsWith( 'restrictedEditingException' ) && data.newRange.root.rootName == '$graveyard' ) {
  70. writer.updateMarker( name, {
  71. range: writer.createRange( writer.createPositionAt( editor.model.document.selection.focus ) )
  72. } );
  73. changeApplied = true;
  74. }
  75. }
  76. return changeApplied;
  77. };
  78. }
  79. /**
  80. * A post-fixer that extends a marker when the user types on its boundaries.
  81. *
  82. * @param {module:core/editor/editor~Editor} editor
  83. * @returns {Function}
  84. */
  85. export function extendMarkerOnTypingPostFixer( editor ) {
  86. // This post-fixer shouldn't be necessary after https://github.com/ckeditor/ckeditor5/issues/5778.
  87. return writer => {
  88. let changeApplied = false;
  89. for ( const change of editor.model.document.differ.getChanges() ) {
  90. if ( change.type == 'insert' && change.name == '$text' ) {
  91. changeApplied = _tryExtendMarkerStart( editor, change.position, change.length, writer ) || changeApplied;
  92. changeApplied = _tryExtendMarkedEnd( editor, change.position, change.length, writer ) || changeApplied;
  93. }
  94. }
  95. return false;
  96. };
  97. }
  98. /**
  99. * A view highlight-to-marker conversion helper.
  100. *
  101. * @param {Object} config Conversion configuration.
  102. * @param {module:engine/view/matcher~MatcherPattern} [config.view] A pattern matching all view elements which should be converted. If not
  103. * set, the converter will fire for every view element.
  104. * @param {String|module:engine/model/element~Element|Function} config.model The name of the model element, a model element
  105. * instance or a function that takes a view element and returns a model element. The model element will be inserted in the model.
  106. * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
  107. */
  108. export function upcastHighlightToMarker( config ) {
  109. return dispatcher => dispatcher.on( 'element:span', ( evt, data, conversionApi ) => {
  110. const { writer } = conversionApi;
  111. const matcher = new Matcher( config.view );
  112. const matcherResult = matcher.match( data.viewItem );
  113. // If there is no match, this callback should not do anything.
  114. if ( !matcherResult ) {
  115. return;
  116. }
  117. const match = matcherResult.match;
  118. // Force consuming element's name (taken from upcast helpers elementToElement converter).
  119. match.name = true;
  120. const { modelRange: convertedChildrenRange } = conversionApi.convertChildren( data.viewItem, data.modelCursor );
  121. conversionApi.consumable.consume( data.viewItem, match );
  122. const markerName = config.model( data.viewItem );
  123. const fakeMarkerStart = writer.createElement( '$marker', { 'data-name': markerName } );
  124. const fakeMarkerEnd = writer.createElement( '$marker', { 'data-name': markerName } );
  125. // Insert in reverse order to use converter content positions directly (without recalculating).
  126. writer.insert( fakeMarkerEnd, convertedChildrenRange.end );
  127. writer.insert( fakeMarkerStart, convertedChildrenRange.start );
  128. data.modelRange = writer.createRange(
  129. writer.createPositionBefore( fakeMarkerStart ),
  130. writer.createPositionAfter( fakeMarkerEnd )
  131. );
  132. data.modelCursor = data.modelRange.end;
  133. } );
  134. }
  135. // Extend marker if change detected on marker's start position.
  136. function _tryExtendMarkerStart( editor, position, length, writer ) {
  137. const markerAtStart = getMarkerAtPosition( editor, position.getShiftedBy( length ) );
  138. if ( markerAtStart && markerAtStart.getStart().isEqual( position.getShiftedBy( length ) ) ) {
  139. writer.updateMarker( markerAtStart, {
  140. range: writer.createRange( markerAtStart.getStart().getShiftedBy( -length ), markerAtStart.getEnd() )
  141. } );
  142. return true;
  143. }
  144. return false;
  145. }
  146. // Extend marker if change detected on marker's end position.
  147. function _tryExtendMarkedEnd( editor, position, length, writer ) {
  148. const markerAtEnd = getMarkerAtPosition( editor, position );
  149. if ( markerAtEnd && markerAtEnd.getEnd().isEqual( position ) ) {
  150. writer.updateMarker( markerAtEnd, {
  151. range: writer.createRange( markerAtEnd.getStart(), markerAtEnd.getEnd().getShiftedBy( length ) )
  152. } );
  153. return true;
  154. }
  155. return false;
  156. }