blockautoformatediting.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  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. import LiveRange from '@ckeditor/ckeditor5-engine/src/model/liverange';
  6. import first from '@ckeditor/ckeditor5-utils/src/first';
  7. import Position from '@ckeditor/ckeditor5-engine/src/model/position';
  8. /**
  9. * The block autoformatting engine. It allows to format various block patterns. For example,
  10. * it can be configured to turn a paragraph starting with `*` and followed by a space into a list item.
  11. *
  12. * The autoformatting operation is integrated with the undo manager,
  13. * so the autoformatting step can be undone if the user's intention was not to format the text.
  14. *
  15. * See the {@link module:autoformat/blockautoformatediting~blockAutoformatEditing `blockAutoformatEditing`} documentation
  16. * to learn how to create custom block autoformatters. You can also use
  17. * the {@link module:autoformat/autoformat~Autoformat} feature which enables a set of default autoformatters
  18. * (lists, headings, bold and italic).
  19. *
  20. * @module autoformat/blockautoformatediting
  21. */
  22. /**
  23. * Creates a listener triggered on {@link module:engine/model/document~Document#event:change:data `change:data`} event in the document.
  24. * Calls the callback when inserted text matches the regular expression or the command name
  25. * if provided instead of the callback.
  26. *
  27. * Examples of usage:
  28. *
  29. * To convert a paragraph to heading 1 when `- ` is typed, using just the command name:
  30. *
  31. * blockAutoformatEditing( editor, plugin, /^\- $/, 'heading1' );
  32. *
  33. * To convert a paragraph to heading 1 when `- ` is typed, using just the callback:
  34. *
  35. * blockAutoformatEditing( editor, plugin, /^\- $/, ( context ) => {
  36. * const { match } = context;
  37. * const headingLevel = match[ 1 ].length;
  38. *
  39. * editor.execute( 'heading', {
  40. * formatId: `heading${ headingLevel }`
  41. * } );
  42. * } );
  43. *
  44. * @param {module:core/editor/editor~Editor} editor The editor instance.
  45. * @param {module:autoformat/autoformat~Autoformat} plugin The autoformat plugin instance.
  46. * @param {RegExp} pattern The regular expression to execute on just inserted text.
  47. * @param {Function|String} callbackOrCommand The callback to execute or the command to run when the text is matched.
  48. * In case of providing the callback, it receives the following parameter:
  49. * * {Object} match RegExp.exec() result of matching the pattern to inserted text.
  50. */
  51. export default function blockAutoformatEditing( editor, plugin, pattern, callbackOrCommand ) {
  52. let callback;
  53. let command = null;
  54. if ( typeof callbackOrCommand == 'function' ) {
  55. callback = callbackOrCommand;
  56. } else {
  57. // We assume that the actual command name was provided.
  58. command = editor.commands.get( callbackOrCommand );
  59. callback = () => {
  60. editor.execute( callbackOrCommand );
  61. };
  62. }
  63. editor.model.document.on( 'change:data', ( evt, batch ) => {
  64. if ( command && !command.isEnabled || !plugin.isEnabled ) {
  65. return;
  66. }
  67. if ( batch.type == 'transparent' ) {
  68. return;
  69. }
  70. const changes = Array.from( editor.model.document.differ.getChanges() );
  71. const entry = changes[ 0 ];
  72. // Typing is represented by only a single change.
  73. if ( changes.length != 1 || entry.type !== 'insert' || entry.name != '$text' || entry.length != 1 ) {
  74. return;
  75. }
  76. const blockToFormat = entry.position.parent;
  77. // Block formatting should be disabled in codeBlocks (#5800).
  78. if ( blockToFormat.is( 'codeBlock' ) ) {
  79. return;
  80. }
  81. // In case a command is bound, do not re-execute it over an existing block style which would result with a style removal.
  82. // Instead just drop processing so that autoformat trigger text is not lost. E.g. writing "# " in a level 1 heading.
  83. if ( command && command.value === true ) {
  84. return;
  85. }
  86. const firstNode = blockToFormat.getChild( 0 );
  87. const match = pattern.exec( firstNode.data );
  88. // ...and this text node's data match the pattern.
  89. if ( !match ) {
  90. return;
  91. }
  92. const range = first( editor.model.document.selection.getRanges() );
  93. // We're only handling a collapsed selection that is right after the matched text (#5671).
  94. const expectedPosition = ( new Position( range.root, firstNode.getPath() ) ).getShiftedBy( match[ 0 ].length );
  95. if ( !( range.isCollapsed && range.end.isEqual( expectedPosition ) ) ) {
  96. return;
  97. }
  98. // Use enqueueChange to create new batch to separate typing batch from the auto-format changes.
  99. editor.model.enqueueChange( writer => {
  100. // Matched range.
  101. const start = writer.createPositionAt( blockToFormat, 0 );
  102. const end = writer.createPositionAt( blockToFormat, match[ 0 ].length );
  103. const range = new LiveRange( start, end );
  104. const wasChanged = callback( { match } );
  105. // Remove matched text.
  106. if ( wasChanged !== false ) {
  107. writer.remove( range );
  108. }
  109. range.detach();
  110. } );
  111. } );
  112. }