blockautoformatediting.js 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  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 autoformat/blockautoformatediting
  7. */
  8. import LiveRange from '@ckeditor/ckeditor5-engine/src/model/liverange';
  9. /**
  10. * The block autoformatting engine. It allows to format various block patterns. For example,
  11. * it can be configured to turn a paragraph starting with `*` and followed by a space into a list item.
  12. *
  13. * The autoformatting operation is integrated with the undo manager,
  14. * so the autoformatting step can be undone if the user's intention was not to format the text.
  15. *
  16. * See the constructors documentation to learn how to create custom inline 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. export default class BlockAutoformatEditing {
  21. /**
  22. * @inheritDoc
  23. */
  24. static get pluginName() {
  25. return 'BlockAutoformatEditing';
  26. }
  27. /**
  28. * Creates a listener triggered on `change` event in the document.
  29. * Calls the callback when inserted text matches the regular expression or the command name
  30. * if provided instead of the callback.
  31. *
  32. * Examples of usage:
  33. *
  34. * To convert a paragraph to heading 1 when `- ` is typed, using just the command name:
  35. *
  36. * new BlockAutoformatEditing( editor, /^\- $/, 'heading1' );
  37. *
  38. * To convert a paragraph to heading 1 when `- ` is typed, using just the callback:
  39. *
  40. * new BlockAutoformatEditing( editor, /^\- $/, ( context ) => {
  41. * const { match } = context;
  42. * const headingLevel = match[ 1 ].length;
  43. *
  44. * editor.execute( 'heading', {
  45. * formatId: `heading${ headingLevel }`
  46. * } );
  47. * } );
  48. *
  49. * @param {module:core/editor/editor~Editor} editor The editor instance.
  50. * @param {RegExp} pattern The regular expression to execute on just inserted text.
  51. * @param {Function|String} callbackOrCommand The callback to execute or the command to run when the text is matched.
  52. * In case of providing the callback, it receives the following parameter:
  53. * * {Object} match RegExp.exec() result of matching the pattern to inserted text.
  54. */
  55. constructor( editor, pattern, callbackOrCommand ) {
  56. let callback;
  57. let command = null;
  58. if ( typeof callbackOrCommand == 'function' ) {
  59. callback = callbackOrCommand;
  60. } else {
  61. // We assume that the actual command name was provided.
  62. command = editor.commands.get( callbackOrCommand );
  63. callback = () => {
  64. editor.execute( callbackOrCommand );
  65. };
  66. }
  67. editor.model.document.on( 'change', ( evt, batch ) => {
  68. if ( command && !command.isEnabled ) {
  69. return;
  70. }
  71. if ( batch.type == 'transparent' ) {
  72. return;
  73. }
  74. const changes = Array.from( editor.model.document.differ.getChanges() );
  75. const entry = changes[ 0 ];
  76. // Typing is represented by only a single change.
  77. if ( changes.length != 1 || entry.type !== 'insert' || entry.name != '$text' || entry.length != 1 ) {
  78. return;
  79. }
  80. const blockToFormat = entry.position.parent;
  81. // Block formatting should trigger only if the entire content of a paragraph is a single text node... (see ckeditor5#5671).
  82. if ( !blockToFormat.is( 'paragraph' ) || blockToFormat.childCount !== 1 ) {
  83. return;
  84. }
  85. const match = pattern.exec( blockToFormat.getChild( 0 ).data );
  86. // ...and this text node's data match the pattern.
  87. if ( !match ) {
  88. return;
  89. }
  90. // Use enqueueChange to create new batch to separate typing batch from the auto-format changes.
  91. editor.model.enqueueChange( writer => {
  92. // Matched range.
  93. const start = writer.createPositionAt( blockToFormat, 0 );
  94. const end = writer.createPositionAt( blockToFormat, match[ 0 ].length );
  95. const range = new LiveRange( start, end );
  96. const wasChanged = callback( { match } );
  97. // Remove matched text.
  98. if ( wasChanged !== false ) {
  99. writer.remove( range );
  100. }
  101. range.detach();
  102. } );
  103. } );
  104. }
  105. }