restrictededitingediting.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. /**
  2. * @license Copyright (c) 2003-2019, 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/restrictedediting
  7. */
  8. import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  9. import Matcher from '@ckeditor/ckeditor5-engine/src/view/matcher';
  10. import RestrictedEditingNavigationCommand from './restrictededitingnavigationcommand';
  11. const HIGHLIGHT_CLASS = 'ck-restricted-editing-exception_selected';
  12. /**
  13. * The Restricted Editing editing feature.
  14. *
  15. * * It introduces the exception marker group that renders to `<spans>` with the `ck-restricted-editing-exception` CSS class.
  16. * * It registers the `'goToPreviousRestrictedEditingRegion'` and `'goToNextRestrictedEditingRegion'` commands.
  17. * * Also enables highlighting exception markers that are selected.
  18. *
  19. * @extends module:core/plugin~Plugin
  20. */
  21. export default class RestrictedEditingEditing extends Plugin {
  22. /**
  23. * @inheritDoc
  24. */
  25. static get pluginName() {
  26. return 'RestrictedEditingEditing';
  27. }
  28. /**
  29. * @inheritDoc
  30. */
  31. init() {
  32. const editor = this.editor;
  33. // Commands that allow navigation in the content.
  34. editor.commands.add( 'goToPreviousRestrictedEditingRegion', new RestrictedEditingNavigationCommand( editor, 'backward' ) );
  35. editor.commands.add( 'goToNextRestrictedEditingRegion', new RestrictedEditingNavigationCommand( editor, 'forward' ) );
  36. let createdMarkers = 0;
  37. editor.conversion.for( 'upcast' ).add( upcastHighlightToMarker( {
  38. view: {
  39. name: 'span',
  40. classes: 'ck-restricted-editing-exception'
  41. },
  42. model: () => {
  43. createdMarkers++;
  44. return `restricted-editing-exception:${ createdMarkers }`;
  45. }
  46. } ) );
  47. editor.conversion.for( 'downcast' ).markerToHighlight( {
  48. model: 'restricted-editing-exception',
  49. // Use callback to return new object every time new marker instance is created - otherwise it will be seen as the same marker.
  50. view: () => ( {
  51. name: 'span',
  52. classes: 'ck-restricted-editing-exception',
  53. priority: -10
  54. } )
  55. } );
  56. const getCommandExecuter = commandName => {
  57. return ( data, cancel ) => {
  58. const command = this.editor.commands.get( commandName );
  59. if ( command.isEnabled ) {
  60. this.editor.execute( commandName );
  61. }
  62. cancel();
  63. };
  64. };
  65. editor.keystrokes.set( 'Tab', getCommandExecuter( 'goToNextRestrictedEditingRegion' ) );
  66. editor.keystrokes.set( 'Shift+Tab', getCommandExecuter( 'goToPreviousRestrictedEditingRegion' ) );
  67. this._setupExceptionHighlighting();
  68. }
  69. /**
  70. * Adds a visual highlight style to a restricted editing exception the selection is anchored to.
  71. *
  72. * Highlight is turned on by adding the `.ck-restricted-editing-exception_selected` class to the
  73. * exception in the view:
  74. *
  75. * * The class is removed before the conversion has started, as callbacks added with the `'highest'` priority
  76. * to {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher} events.
  77. * * The class is added in the view post fixer, after other changes in the model tree were converted to the view.
  78. *
  79. * This way, adding and removing the highlight does not interfere with conversion.
  80. *
  81. * @private
  82. */
  83. _setupExceptionHighlighting() {
  84. const editor = this.editor;
  85. const view = editor.editing.view;
  86. const model = editor.model;
  87. const highlightedMarkers = new Set();
  88. // Adding the class.
  89. view.document.registerPostFixer( writer => {
  90. const modelSelection = model.document.selection;
  91. for ( const marker of model.markers.getMarkersAtPosition( modelSelection.anchor ) ) {
  92. for ( const viewElement of editor.editing.mapper.markerNameToElements( marker.name ) ) {
  93. writer.addClass( HIGHLIGHT_CLASS, viewElement );
  94. highlightedMarkers.add( viewElement );
  95. }
  96. }
  97. } );
  98. // Removing the class.
  99. editor.conversion.for( 'editingDowncast' ).add( dispatcher => {
  100. // Make sure the highlight is removed on every possible event, before conversion is started.
  101. dispatcher.on( 'insert', removeHighlight, { priority: 'highest' } );
  102. dispatcher.on( 'remove', removeHighlight, { priority: 'highest' } );
  103. dispatcher.on( 'attribute', removeHighlight, { priority: 'highest' } );
  104. dispatcher.on( 'selection', removeHighlight, { priority: 'highest' } );
  105. function removeHighlight() {
  106. view.change( writer => {
  107. for ( const item of highlightedMarkers.values() ) {
  108. writer.removeClass( HIGHLIGHT_CLASS, item );
  109. highlightedMarkers.delete( item );
  110. }
  111. } );
  112. }
  113. } );
  114. }
  115. }
  116. function upcastHighlightToMarker( config ) {
  117. return dispatcher => dispatcher.on( 'element:span', ( evt, data, conversionApi ) => {
  118. const { writer } = conversionApi;
  119. const matcher = new Matcher( config.view );
  120. const matcherResult = matcher.match( data.viewItem );
  121. // If there is no match, this callback should not do anything.
  122. if ( !matcherResult ) {
  123. return;
  124. }
  125. const match = matcherResult.match;
  126. // Force consuming element's name (taken from upcast helpers elementToElement converter).
  127. match.name = true;
  128. const { modelRange: convertedChildrenRange } = conversionApi.convertChildren( data.viewItem, data.modelCursor );
  129. conversionApi.consumable.consume( data.viewItem, match );
  130. const markerName = config.model( data.viewItem );
  131. const fakeMarkerStart = writer.createElement( '$marker', { 'data-name': markerName } );
  132. const fakeMarkerEnd = writer.createElement( '$marker', { 'data-name': markerName } );
  133. // Insert in reverse order to use converter content positions directly (without recalculating).
  134. writer.insert( fakeMarkerEnd, convertedChildrenRange.end );
  135. writer.insert( fakeMarkerStart, convertedChildrenRange.start );
  136. data.modelRange = writer.createRange(
  137. writer.createPositionBefore( fakeMarkerStart ),
  138. writer.createPositionAfter( fakeMarkerEnd )
  139. );
  140. data.modelCursor = data.modelRange.end;
  141. } );
  142. }