restrictededitingmodeediting.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  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/restrictededitingmodeediting
  7. */
  8. import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  9. import RestrictedEditingNavigationCommand from './restrictededitingmodenavigationcommand';
  10. import {
  11. extendMarkerOnTypingPostFixer,
  12. resurrectCollapsedMarkerPostFixer,
  13. setupExceptionHighlighting,
  14. upcastHighlightToMarker
  15. } from './restrictededitingmode/converters';
  16. import { getMarkerAtPosition, isSelectionInMarker } from './restrictededitingmode/utils';
  17. /**
  18. * The Restricted Editing Mode editing feature.
  19. *
  20. * * It introduces the exception marker group that renders to `<spans>` with the `restricted-editing-exception` CSS class.
  21. * * It registers the `'goToPreviousRestrictedEditingException'` and `'goToNextRestrictedEditingException'` commands.
  22. * * Also enables highlighting exception markers that are selected.
  23. *
  24. * @extends module:core/plugin~Plugin
  25. */
  26. export default class RestrictedEditingModeEditing extends Plugin {
  27. /**
  28. * @inheritDoc
  29. */
  30. static get pluginName() {
  31. return 'RestrictedEditingModeEditing';
  32. }
  33. /**
  34. * @inheritDoc
  35. */
  36. constructor( editor ) {
  37. super( editor );
  38. editor.config.define( 'restrictedEditing', {
  39. allowedCommands: [ 'bold', 'italic', 'link', 'unlink' ]
  40. } );
  41. /**
  42. * Command names that are enabled outside non-restricted regions.
  43. *
  44. * @type {Set.<String>}
  45. * @private
  46. */
  47. this._alwaysEnabled = new Set( [ 'undo', 'redo', 'goToPreviousRestrictedEditingException', 'goToNextRestrictedEditingException' ] );
  48. /**
  49. * Commands allowed in non-restricted areas.
  50. *
  51. * Commands always enabled combines typing feature commands: `'typing'`, `'delete'` and `'forwardDelete'` with commands defined
  52. * in the feature configuration.
  53. *
  54. * @type {Set<string>}
  55. * @private
  56. */
  57. this._allowedInException = new Set( [ 'input', 'delete', 'forwardDelete' ] );
  58. }
  59. /**
  60. * @inheritDoc
  61. */
  62. init() {
  63. const editor = this.editor;
  64. const allowedCommands = editor.config.get( 'restrictedEditing.allowedCommands' );
  65. allowedCommands.forEach( commandName => this._allowedInException.add( commandName ) );
  66. this._setupConversion();
  67. this._setupCommandsToggling();
  68. // Commands & keystrokes that allow navigation in the content.
  69. editor.commands.add( 'goToPreviousRestrictedEditingException', new RestrictedEditingNavigationCommand( editor, 'backward' ) );
  70. editor.commands.add( 'goToNextRestrictedEditingException', new RestrictedEditingNavigationCommand( editor, 'forward' ) );
  71. editor.keystrokes.set( 'Tab', getCommandExecuter( editor, 'goToNextRestrictedEditingException' ) );
  72. editor.keystrokes.set( 'Shift+Tab', getCommandExecuter( editor, 'goToPreviousRestrictedEditingException' ) );
  73. // Block clipboard completely in restricted mode.
  74. this.listenTo( this.editor.editing.view.document, 'clipboardInput', evt => {
  75. evt.stop();
  76. }, { priority: 'highest' } );
  77. this.listenTo( this.editor.editing.view.document, 'clipboardOutput', ( evt, data ) => {
  78. if ( data.method == 'cut' ) {
  79. evt.stop();
  80. }
  81. }, { priority: 'highest' } );
  82. }
  83. /**
  84. * Setups restricted mode editing conversion:
  85. *
  86. * * ucpast & downcast converters
  87. * * marker highlighting in the edting area
  88. * * marker post-fixers
  89. *
  90. * @private
  91. */
  92. _setupConversion() {
  93. const editor = this.editor;
  94. const model = editor.model;
  95. const doc = model.document;
  96. // The restricted editing does not attach additional data to the zones so there's no need for smarter markers management.
  97. // Also, the markers will only be created when when loading the data.
  98. let markerNumber = 0;
  99. editor.conversion.for( 'upcast' ).add( upcastHighlightToMarker( {
  100. view: {
  101. name: 'span',
  102. classes: 'restricted-editing-exception'
  103. },
  104. model: () => {
  105. markerNumber++; // Starting from restrictedEditingException:1 marker.
  106. return `restrictedEditingException:${ markerNumber }`;
  107. }
  108. } ) );
  109. // Currently the marker helpers are tied to other use-cases and do not render collapsed marker as highlight.
  110. // That's why there are 2 downcast converters for them:
  111. // 1. The default marker-to-highlight will wrap selected text with `<span>`.
  112. editor.conversion.for( 'downcast' ).markerToHighlight( {
  113. model: 'restrictedEditingException',
  114. // Use callback to return new object every time new marker instance is created - otherwise it will be seen as the same marker.
  115. view: () => {
  116. return {
  117. name: 'span',
  118. classes: 'restricted-editing-exception',
  119. priority: -10
  120. };
  121. }
  122. } );
  123. // 2. But for collapsed marker we need to render it as an element.
  124. // Additionally the editing pipeline should always display a collapsed markers.
  125. editor.conversion.for( 'editingDowncast' ).markerToElement( {
  126. model: 'restrictedEditingException',
  127. view: ( markerData, viewWriter ) => {
  128. return viewWriter.createUIElement( 'span', {
  129. class: 'restricted-editing-exception restricted-editing-exception_collapsed'
  130. } );
  131. }
  132. } );
  133. editor.conversion.for( 'dataDowncast' ).markerToElement( {
  134. model: 'restrictedEditingException',
  135. view: ( markerData, viewWriter ) => {
  136. return viewWriter.createEmptyElement( 'span', {
  137. class: 'restricted-editing-exception'
  138. } );
  139. }
  140. } );
  141. doc.registerPostFixer( extendMarkerOnTypingPostFixer( editor ) );
  142. doc.registerPostFixer( resurrectCollapsedMarkerPostFixer( editor ) );
  143. setupExceptionHighlighting( editor );
  144. }
  145. /**
  146. * Setups the commands toggling - enables or disables commands based on user selection.
  147. *
  148. * @private
  149. */
  150. _setupCommandsToggling() {
  151. const editor = this.editor;
  152. const model = editor.model;
  153. const doc = model.document;
  154. this._disableCommands( editor );
  155. this.listenTo( doc.selection, 'change', this._checkCommands.bind( this ) );
  156. this.listenTo( doc, 'change:data', this._checkCommands.bind( this ) );
  157. }
  158. /**
  159. * Checks if commands should be enabled or disabled based on current selection.
  160. *
  161. * @private
  162. */
  163. _checkCommands() {
  164. const editor = this.editor;
  165. const selection = editor.model.document.selection;
  166. if ( selection.rangeCount > 1 ) {
  167. this._disableCommands( editor );
  168. return;
  169. }
  170. const marker = getMarkerAtPosition( editor, selection.focus );
  171. this._disableCommands();
  172. if ( isSelectionInMarker( selection, marker ) ) {
  173. this._enableCommands( marker );
  174. }
  175. }
  176. /**
  177. * Enables commands in non-restricted regions.
  178. *
  179. * @returns {module:engine/model/markercollection~Marker} marker
  180. * @private
  181. */
  182. _enableCommands( marker ) {
  183. const editor = this.editor;
  184. const commands = this._getCommandNamesToToggle( editor, this._allowedInException )
  185. .filter( name => this._allowedInException.has( name ) )
  186. .filter( filterDeleteCommandsOnMarkerBoundaries( editor.model.document.selection, marker.getRange() ) )
  187. .map( name => editor.commands.get( name ) );
  188. for ( const command of commands ) {
  189. command.clearForceDisabled( 'RestrictedEditingMode' );
  190. }
  191. }
  192. /**
  193. * Disables commands outside non-restricted regions.
  194. *
  195. * @private
  196. */
  197. _disableCommands() {
  198. const editor = this.editor;
  199. const commands = this._getCommandNamesToToggle( editor )
  200. .map( name => editor.commands.get( name ) );
  201. for ( const command of commands ) {
  202. command.forceDisabled( 'RestrictedEditingMode' );
  203. }
  204. }
  205. /**
  206. * Returns command names that should be toggleable.
  207. *
  208. * @param {module:core/editor/editor~Editor} editor
  209. * @returns {Array.<String>}
  210. * @private
  211. */
  212. _getCommandNamesToToggle( editor ) {
  213. return Array.from( editor.commands.names() )
  214. .filter( name => !this._alwaysEnabled.has( name ) );
  215. }
  216. }
  217. // Helper method for executing enabled commands only.
  218. function getCommandExecuter( editor, commandName ) {
  219. return ( data, cancel ) => {
  220. const command = editor.commands.get( commandName );
  221. if ( command.isEnabled ) {
  222. editor.execute( commandName );
  223. }
  224. cancel();
  225. };
  226. }
  227. // Additional filtering rule for enabling "delete" and "forwardDelete" commands if selection is on range boundaries:
  228. //
  229. // Does not allow to enable command when selection focus is:
  230. // - is on marker start - "delete" - to prevent removing content before marker
  231. // - is on marker end - "forwardDelete" - to prevent removing content after marker
  232. function filterDeleteCommandsOnMarkerBoundaries( selection, markerRange ) {
  233. return name => {
  234. if ( name == 'delete' && markerRange.start.isEqual( selection.focus ) ) {
  235. return false;
  236. }
  237. // Only for collapsed selection - non-collapsed seleciton that extends over a marker is handled elsewhere.
  238. if ( name == 'forwardDelete' && selection.isCollapsed && markerRange.end.isEqual( selection.focus ) ) {
  239. return false;
  240. }
  241. return true;
  242. };
  243. }