restrictededitingediting.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  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/restrictededitingediting
  7. */
  8. import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  9. import Matcher from '@ckeditor/ckeditor5-engine/src/view/matcher';
  10. /**
  11. * @extends module:core/plugin~Plugin
  12. */
  13. export default class RestrictedEditingEditing extends Plugin {
  14. /**
  15. * @inheritDoc
  16. */
  17. static get pluginName() {
  18. return 'RestrictedEditingEditing';
  19. }
  20. /**
  21. * @inheritDoc
  22. */
  23. constructor( editor ) {
  24. super( editor );
  25. this._alwaysEnabled = new Set( [ 'undo', 'redo' ] );
  26. this._allowedInException = new Set( [ 'bold', 'italic', 'link', 'input', 'delete', 'forwardDelete' ] );
  27. }
  28. /**
  29. * @inheritDoc
  30. */
  31. init() {
  32. const editor = this.editor;
  33. let createdMarkers = 0;
  34. editor.conversion.for( 'upcast' ).add( upcastHighlightToMarker( {
  35. view: {
  36. name: 'span',
  37. classes: 'ck-restricted-editing-exception'
  38. },
  39. model: () => {
  40. createdMarkers++;
  41. return `restricted-editing-exception:${ createdMarkers }`;
  42. }
  43. } ) );
  44. editor.conversion.for( 'downcast' ).markerToHighlight( {
  45. model: 'restricted-editing-exception',
  46. // Use callback to return new object every time new marker instance is created - otherwise it will be seen as the same marker.
  47. view: () => ( {
  48. name: 'span',
  49. classes: 'ck-restricted-editing-exception',
  50. priority: -10
  51. } )
  52. } );
  53. this._disableCommands( editor );
  54. const selection = editor.model.document.selection;
  55. this.listenTo( selection, 'change', this._checkCommands.bind( this ) );
  56. this.listenTo( editor.model.document, 'change:data', this._checkCommands.bind( this ) );
  57. editor.model.document.registerPostFixer( writer => {
  58. let changeApplied = false;
  59. for ( const change of editor.model.document.differ.getChanges() ) {
  60. if ( change.type == 'insert' && change.name == '$text' && change.length === 1 ) {
  61. changeApplied = this._tryExtendMarkedEnd( change, writer, changeApplied ) || changeApplied;
  62. changeApplied = this._tryExtendMarkerStart( change, writer, changeApplied ) || changeApplied;
  63. }
  64. }
  65. return changeApplied;
  66. } );
  67. }
  68. _tryExtendMarkerStart( change, writer, changeApplied ) {
  69. const markerAtStart = this._getMarker( change.position.getShiftedBy( 1 ) );
  70. if ( markerAtStart && markerAtStart.getStart().isEqual( change.position.getShiftedBy( 1 ) ) ) {
  71. writer.updateMarker( markerAtStart, {
  72. range: writer.createRange( markerAtStart.getStart().getShiftedBy( -1 ), markerAtStart.getEnd() )
  73. } );
  74. changeApplied = true;
  75. }
  76. return changeApplied;
  77. }
  78. _tryExtendMarkedEnd( change, writer, changeApplied ) {
  79. const markerAtEnd = this._getMarker( change.position );
  80. if ( markerAtEnd && markerAtEnd.getEnd().isEqual( change.position ) ) {
  81. writer.updateMarker( markerAtEnd, {
  82. range: writer.createRange( markerAtEnd.getStart(), markerAtEnd.getEnd().getShiftedBy( 1 ) )
  83. } );
  84. changeApplied = true;
  85. }
  86. return changeApplied;
  87. }
  88. _checkCommands() {
  89. const editor = this.editor;
  90. const selection = editor.model.document.selection;
  91. if ( selection.rangeCount > 1 ) {
  92. this._disableCommands( editor );
  93. return;
  94. }
  95. const marker = this._getMarker( selection.focus );
  96. if ( isSelectionInExceptionMarker( marker, selection ) ) {
  97. this._enableCommands( marker );
  98. } else {
  99. this._disableCommands();
  100. }
  101. }
  102. _getMarker( position ) {
  103. const editor = this.editor;
  104. for ( const marker of editor.model.markers ) {
  105. const markerRange = marker.getRange();
  106. if ( isPositionInRangeOrOnRangeBoundary( markerRange, position ) ) {
  107. if ( marker.name.startsWith( 'restricted-editing-exception:' ) ) {
  108. return marker;
  109. }
  110. }
  111. }
  112. }
  113. _enableCommands( marker ) {
  114. const editor = this.editor;
  115. const exceptionDisable = [];
  116. const commands = this._getCommandNamesToToggle( editor, this._allowedInException )
  117. .filter( name => this._allowedInException.has( name ) )
  118. .filter( name => {
  119. const selection = editor.model.document.selection;
  120. const markerRange = marker.getRange();
  121. if ( name == 'delete' && markerRange.start.isEqual( selection.focus ) ) {
  122. exceptionDisable.push( name );
  123. return false;
  124. }
  125. if ( name == 'forwardDelete' && selection.isCollapsed && markerRange.end.isEqual( selection.focus ) ) {
  126. exceptionDisable.push( name );
  127. return false;
  128. }
  129. return true;
  130. } )
  131. .map( name => editor.commands.get( name ) );
  132. for ( const command of commands ) {
  133. command.clearForceDisabled( 'RestrictedMode' );
  134. }
  135. for ( const command of exceptionDisable.map( name => editor.commands.get( name ) ) ) {
  136. command.forceDisabled( 'RestrictedMode' );
  137. }
  138. }
  139. _disableCommands() {
  140. const editor = this.editor;
  141. const commands = this._getCommandNamesToToggle( editor )
  142. .map( name => editor.commands.get( name ) );
  143. for ( const command of commands ) {
  144. command.forceDisabled( 'RestrictedMode' );
  145. }
  146. }
  147. _getCommandNamesToToggle( editor ) {
  148. return Array.from( editor.commands.names() )
  149. .filter( name => !this._alwaysEnabled.has( name ) );
  150. }
  151. }
  152. function upcastHighlightToMarker( config ) {
  153. return dispatcher => dispatcher.on( 'element:span', ( evt, data, conversionApi ) => {
  154. const { writer } = conversionApi;
  155. const matcher = new Matcher( config.view );
  156. const matcherResult = matcher.match( data.viewItem );
  157. // If there is no match, this callback should not do anything.
  158. if ( !matcherResult ) {
  159. return;
  160. }
  161. const match = matcherResult.match;
  162. // Force consuming element's name (taken from upcast helpers elementToElement converter).
  163. match.name = true;
  164. const { modelRange: convertedChildrenRange } = conversionApi.convertChildren( data.viewItem, data.modelCursor );
  165. conversionApi.consumable.consume( data.viewItem, match );
  166. const markerName = config.model( data.viewItem );
  167. const fakeMarkerStart = writer.createElement( '$marker', { 'data-name': markerName } );
  168. const fakeMarkerEnd = writer.createElement( '$marker', { 'data-name': markerName } );
  169. // Insert in reverse order to use converter content positions directly (without recalculating).
  170. writer.insert( fakeMarkerEnd, convertedChildrenRange.end );
  171. writer.insert( fakeMarkerStart, convertedChildrenRange.start );
  172. data.modelRange = writer.createRange(
  173. writer.createPositionBefore( fakeMarkerStart ),
  174. writer.createPositionAfter( fakeMarkerEnd )
  175. );
  176. data.modelCursor = data.modelRange.end;
  177. } );
  178. }
  179. function isSelectionInExceptionMarker( marker, selection ) {
  180. if ( !marker ) {
  181. return false;
  182. }
  183. const markerRange = marker.getRange();
  184. if ( selection.isCollapsed ) {
  185. return isPositionInRangeOrOnRangeBoundary( markerRange, selection.focus );
  186. }
  187. return markerRange.containsRange( selection.getFirstRange(), true );
  188. }
  189. function isPositionInRangeOrOnRangeBoundary( range, position ) {
  190. return (
  191. range.containsPosition( position ) ||
  192. range.end.isEqual( position ) ||
  193. range.start.isEqual( position )
  194. );
  195. }