Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@ckeditor/ckeditor5-typing

Package Overview
Dependencies
Maintainers
1
Versions
708
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@ckeditor/ckeditor5-typing - npm Package Compare versions

Comparing version 0.9.1 to 0.10.0

.eslintrc.js

21

CHANGELOG.md
Changelog
=========
## [0.10.0](https://github.com/ckeditor/ckeditor5-typing/compare/v0.9.1...v0.10.0) (2017-09-03)
### Bug fixes
* Fixed a range of issues when typing or using a spellchecker on styled words leads to errors. Closes [#100](https://github.com/ckeditor/ckeditor5-typing/issues/100). Closes ckeditor/ckeditor5[#491](https://github.com/ckeditor/ckeditor5-typing/issues/491). ([c30dbf8](https://github.com/ckeditor/ckeditor5-typing/commit/c30dbf8))
* Prevent from modifying document by `Input` feature when `InputCommand` is disabled. Closes [#107](https://github.com/ckeditor/ckeditor5-typing/issues/107). ([f935d66](https://github.com/ckeditor/ckeditor5-typing/commit/f935d66))
### Features
* Pressing <kbd>Backspace</kbd> or <kbd>Delete</kbd> in an empty content will reset the current block to a paragraph. Closes [#61](https://github.com/ckeditor/ckeditor5-typing/issues/61). ([bb07bc6](https://github.com/ckeditor/ckeditor5-typing/commit/bb07bc6))
* The viewport will be scrolled to the selection upon user input. See ckeditor/ckeditor5-engine#660. ([2cdf02f](https://github.com/ckeditor/ckeditor5-typing/commit/2cdf02f))
### Other changes
* Aligned the implementation to the new Command API (see https://github.com/ckeditor/ckeditor5-core/issues/88). ([b241ac6](https://github.com/ckeditor/ckeditor5-typing/commit/b241ac6))
### BREAKING CHANGES
* The command API has been changed.
## [0.9.1](https://github.com/ckeditor/ckeditor5-typing/compare/v0.9.0...v0.9.1) (2017-05-07)

@@ -5,0 +26,0 @@

11

gulpfile.js

@@ -6,3 +6,3 @@ /**

/* jshint browser: false, node: true, strict: true */
/* eslint-env node */

@@ -12,3 +12,4 @@ 'use strict';

const gulp = require( 'gulp' );
const ckeditor5Lint = require( '@ckeditor/ckeditor5-dev-lint' )( {
const ckeditor5Lint = require( '@ckeditor/ckeditor5-dev-lint' );
const options = {
// Files ignored by `gulp lint` task.

@@ -19,6 +20,6 @@ // Files from .gitignore will be added automatically during task execution.

]
} );
};
gulp.task( 'lint', ckeditor5Lint.lint );
gulp.task( 'lint-staged', ckeditor5Lint.lintStaged );
gulp.task( 'lint', () => ckeditor5Lint.lint( options ) );
gulp.task( 'lint-staged', () => ckeditor5Lint.lintStaged( options ) );
gulp.task( 'pre-commit', [ 'lint-staged' ] );
{
"name": "@ckeditor/ckeditor5-typing",
"version": "0.9.1",
"version": "0.10.0",
"description": "Typing feature for CKEditor 5.",
"keywords": [],
"dependencies": {
"@ckeditor/ckeditor5-core": "^0.8.1",
"@ckeditor/ckeditor5-engine": "^0.10.0",
"@ckeditor/ckeditor5-utils": "^0.9.1"
"@ckeditor/ckeditor5-core": "^0.9.0",
"@ckeditor/ckeditor5-engine": "^0.11.0",
"@ckeditor/ckeditor5-utils": "^0.10.0"
},
"devDependencies": {
"@ckeditor/ckeditor5-dev-lint": "^2.0.2",
"@ckeditor/ckeditor5-basic-styles": "^0.8.1",
"@ckeditor/ckeditor5-editor-classic": "^0.7.3",
"@ckeditor/ckeditor5-enter": "^0.9.1",
"@ckeditor/ckeditor5-heading": "^0.9.1",
"@ckeditor/ckeditor5-paragraph": "^0.8.0",
"@ckeditor/ckeditor5-undo": "^0.8.1",
"@ckeditor/ckeditor5-presets": "^0.2.2",
"gulp": "^3.9.0",
"@ckeditor/ckeditor5-dev-lint": "^3.1.0",
"@ckeditor/ckeditor5-basic-styles": "^0.9.0",
"@ckeditor/ckeditor5-editor-classic": "^0.8.0",
"@ckeditor/ckeditor5-enter": "^0.10.0",
"@ckeditor/ckeditor5-heading": "^0.10.0",
"@ckeditor/ckeditor5-link": "^0.8.0",
"@ckeditor/ckeditor5-paragraph": "^0.9.0",
"@ckeditor/ckeditor5-undo": "^0.9.0",
"@ckeditor/ckeditor5-presets": "^0.3.0",
"eslint-config-ckeditor5": "^1.0.5",
"gulp": "^3.9.1",
"guppy-pre-commit": "^0.4.0"

@@ -22,0 +24,0 @@ },

@@ -1,4 +0,5 @@

CKEditor 5 Typing Feature
CKEditor 5 typing feature
========================================
[![Join the chat at https://gitter.im/ckeditor/ckeditor5](https://badges.gitter.im/ckeditor/ckeditor5.svg)](https://gitter.im/ckeditor/ckeditor5?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![npm version](https://badge.fury.io/js/%40ckeditor%2Fckeditor5-typing.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-typing)

@@ -5,0 +6,0 @@ [![Build Status](https://travis-ci.org/ckeditor/ckeditor5-typing.svg?branch=master)](https://travis-ci.org/ckeditor/ckeditor5-typing)

@@ -64,3 +64,3 @@ /**

/**
* Whether the buffer is locked. The locked buffer cannot be reset unless it gets unlocked.
* Whether the buffer is locked. A locked buffer cannot be reset unless it gets unlocked.
*

@@ -101,3 +101,3 @@ * @readonly

/**
* The callback to document selection change:attribute and change:range events which resets the buffer.
* The callback to document selection `change:attribute` and `change:range` events which resets the buffer.
*

@@ -104,0 +104,0 @@ * @private

@@ -24,3 +24,3 @@ /**

static get pluginName() {
return 'typing/delete';
return 'Delete';
}

@@ -34,10 +34,11 @@

editor.commands.set( 'forwardDelete', new DeleteCommand( editor, 'forward' ) );
editor.commands.set( 'delete', new DeleteCommand( editor, 'backward' ) );
editor.commands.add( 'forwardDelete', new DeleteCommand( editor, 'forward' ) );
editor.commands.add( 'delete', new DeleteCommand( editor, 'backward' ) );
this.listenTo( editingView, 'delete', ( evt, data ) => {
editor.execute( data.direction == 'forward' ? 'forwardDelete' : 'delete', { unit: data.unit } );
editor.execute( data.direction == 'forward' ? 'forwardDelete' : 'delete', { unit: data.unit, sequence: data.sequence } );
data.preventDefault();
editingView.scrollToTheSelection();
} );
}
}

@@ -10,4 +10,7 @@ /**

import Command from '@ckeditor/ckeditor5-core/src/command/command';
import Command from '@ckeditor/ckeditor5-core/src/command';
import Selection from '@ckeditor/ckeditor5-engine/src/model/selection';
import Element from '@ckeditor/ckeditor5-engine/src/model/element';
import Position from '@ckeditor/ckeditor5-engine/src/model/position';
import Range from '@ckeditor/ckeditor5-engine/src/model/range';
import ChangeBuffer from './changebuffer';

@@ -20,3 +23,3 @@ import count from '@ckeditor/ckeditor5-utils/src/count';

*
* @extends core.command.Command
* @extends module:core/command~Command
*/

@@ -57,7 +60,9 @@ export default class DeleteCommand extends Command {

*
* @fires execute
* @param {Object} [options] The command options.
* @param {'character'} [options.unit='character'] See {@link module:engine/controller/modifyselection~modifySelection}'s
* options.
* @param {'character'} [options.unit='character'] See {@link module:engine/controller/modifyselection~modifySelection}'s options.
* @param {Number} [options.sequence=1] A number describing which subsequent delete event it is without the key being released.
* See the {@link module:engine/view/document~Document#event:delete} event data.
*/
_doExecute( options = {} ) {
execute( options = {} ) {
const doc = this.editor.document;

@@ -71,2 +76,9 @@ const dataController = this.editor.data;

// Do not replace the whole selected content if selection was collapsed.
// This prevents such situation:
//
// <h1></h1><p>[]</p> --> <h1>[</h1><p>]</p> --> <p></p>
// starting content --> after `modifySelection` --> after `deleteContent`.
const doNotResetEntireContent = selection.isCollapsed;
// Try to extend the selection in the specified direction.

@@ -77,2 +89,9 @@ if ( selection.isCollapsed ) {

// Check if deleting in an empty editor. See #61.
if ( this._shouldEntireContentBeReplacedWithParagraph( options.sequence || 1 ) ) {
this._replaceEntireContentWithParagraph();
return;
}
// If selection is still collapsed, then there's nothing to delete.

@@ -85,3 +104,3 @@ if ( selection.isCollapsed ) {

selection.getFirstRange().getMinimalFlatRanges().forEach( ( range ) => {
selection.getFirstRange().getMinimalFlatRanges().forEach( range => {
changeCount += count(

@@ -92,3 +111,3 @@ range.getWalker( { singleCharacters: true, ignoreElementEnd: true, shallow: true } )

dataController.deleteContent( selection, this._buffer.batch, { merge: true } );
dataController.deleteContent( selection, this._buffer.batch, { doNotResetEntireContent } );
this._buffer.input( changeCount );

@@ -101,2 +120,71 @@

}
/**
* If the user keeps <kbd>Backspace</kbd> or <kbd>Delete</kbd> key pressed, the content of the current
* editable will be cleared. However, this will not yet lead to resetting the remaining block to a paragraph
* (which happens e.g. when the user does <kbd>Ctrl</kbd> + <kbd>A</kbd>, <kbd>Backspace</kbd>).
*
* But, if the user pressed the key in an empty editable for the first time,
* we want to replace the entire content with a paragraph if:
*
* * the current limit element is empty,
* * the paragraph is allowed in the limit element,
* * the limit doesn't already have a paragraph inside.
*
* See https://github.com/ckeditor/ckeditor5-typing/issues/61.
*
* @private
* @param {Number} sequence A number describing which subsequent delete event it is without the key being released.
* @returns {Boolean}
*/
_shouldEntireContentBeReplacedWithParagraph( sequence ) {
// Does nothing if user pressed and held the "Backspace" or "Delete" key.
if ( sequence > 1 ) {
return false;
}
const document = this.editor.document;
const selection = document.selection;
const limitElement = document.schema.getLimitElement( selection );
// If a collapsed selection contains the whole content it means that the content is empty
// (from the user perspective).
const limitElementIsEmpty = selection.isCollapsed && selection.containsEntireContent( limitElement );
if ( !limitElementIsEmpty ) {
return false;
}
if ( !document.schema.check( { name: 'paragraph', inside: limitElement.name } ) ) {
return false;
}
const limitElementFirstChild = limitElement.getChild( 0 );
// Does nothing if the limit element already contains only a paragraph.
// We ignore the case when paragraph might have some inline elements (<p><inlineWidget>[]</inlineWidget></p>)
// because we don't support such cases yet and it's unclear whether inlineWidget shouldn't be a limit itself.
if ( limitElementFirstChild && limitElementFirstChild.name === 'paragraph' ) {
return false;
}
return true;
}
/**
* The entire content is replaced with the paragraph. Selection is moved inside the paragraph.
*
* @private
*/
_replaceEntireContentWithParagraph() {
const document = this.editor.document;
const selection = document.selection;
const limitElement = document.schema.getLimitElement( selection );
const paragraph = new Element( 'paragraph' );
this._buffer.batch.remove( Range.createIn( limitElement ) );
this._buffer.batch.insert( Position.createAt( limitElement ), paragraph );
selection.setCollapsedAt( paragraph );
}
}

@@ -17,3 +17,3 @@ /**

*
* @extends engine.view.observer.Observer
* @extends module:engine/view/observer/observer~Observer
*/

@@ -24,2 +24,10 @@ export default class DeleteObserver extends Observer {

let sequence = 0;
document.on( 'keyup', ( evt, data ) => {
if ( data.keyCode == keyCodes.delete || data.keyCode == keyCodes.backspace ) {
sequence = 0;
}
} );
document.on( 'keydown', ( evt, data ) => {

@@ -39,2 +47,3 @@ const deleteData = {};

deleteData.unit = data.altKey ? 'word' : deleteData.unit;
deleteData.sequence = ++sequence;

@@ -61,2 +70,4 @@ document.fire( 'delete', new DomEventData( document, data.domEvent, deleteData ) );

* @param {'character'|'word'} data.unit The "amount" of content that should be deleted.
* @param {Number} data.sequence A number describing which subsequent delete event it is without the key being released.
* If it's 2 or more it means that the key was pressed and hold.
*/

@@ -17,2 +17,3 @@ /**

import { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard';
import DomConverter from '@ckeditor/ckeditor5-engine/src/view/domconverter';
import InputCommand from './inputcommand';

@@ -30,3 +31,3 @@

static get pluginName() {
return 'typing/input';
return 'Input';
}

@@ -44,6 +45,6 @@

editor.commands.set( 'input', inputCommand );
editor.commands.add( 'input', inputCommand );
this.listenTo( editingView, 'keydown', ( evt, data ) => {
this._handleKeydown( data, inputCommand.buffer );
this._handleKeydown( data, inputCommand );
}, { priority: 'lowest' } );

@@ -70,7 +71,17 @@

* @param {module:engine/view/observer/keyobserver~KeyEventData} evtData
* @param {module:typing/changebuffer~ChangeBuffer} buffer
* @param {module:typing/inputcommand~InputCommand} inputCommand
*/
_handleKeydown( evtData, buffer ) {
_handleKeydown( evtData, inputCommand ) {
const doc = this.editor.document;
const buffer = inputCommand.buffer;
// By relying on the state of the input command we allow disabling the entire input easily
// by just disabling the input command. We could’ve used here the delete command but that
// would mean requiring the delete feature which would block loading one without the other.
// We could also check the editor.isReadOnly property, but that wouldn't allow to block
// the input without blocking other features.
if ( !inputCommand.isEnabled ) {
return;
}
if ( isSafeKeystroke( evtData ) || doc.selection.isCollapsed ) {

@@ -93,3 +104,4 @@ return;

* @private
* @param {Array.<module:engine/view/document~MutatatedText|module:engine/view/document~MutatatedChildren>} mutations
* @param {Array.<module:engine/view/observer/mutationobserver~MutatedText|
* module:engine/view/observer/mutationobserver~MutatedChildren>} mutations
* @param {module:engine/view/selection~Selection|null} viewSelection

@@ -135,13 +147,106 @@ */

* @param {Array.<module:engine/view/observer/mutationobserver~MutatedText|
* module:engine/view/observer/mutationobserver~MutatatedChildren>} mutations
* module:engine/view/observer/mutationobserver~MutatedChildren>} mutations
* @param {module:engine/view/selection~Selection|null} viewSelection
*/
handle( mutations, viewSelection ) {
for ( let mutation of mutations ) {
// Fortunately it will never be both.
this._handleTextMutation( mutation, viewSelection );
this._handleTextNodeInsertion( mutation );
if ( containerChildrenMutated( mutations ) ) {
this._handleContainerChildrenMutations( mutations, viewSelection );
} else {
for ( const mutation of mutations ) {
// Fortunately it will never be both.
this._handleTextMutation( mutation, viewSelection );
this._handleTextNodeInsertion( mutation );
}
}
}
/**
* Handles situations when container's children mutated during input. This can happen when
* the browser is trying to "fix" DOM in certain situations. For example, when the user starts to type
* in `<p><a href=""><i>Link{}</i></a></p>`, the browser might change the order of elements
* to `<p><i><a href="">Link</a>x{}</i></p>`. A similar situation happens when the spell checker
* replaces a word wrapped with `<strong>` with a word wrapped with a `<b>` element.
*
* To handle such situations, the common DOM ancestor of all mutations is converted to the model representation
* and then compared with the current model to calculate the proper text change.
*
* Note: Single text node insertion is handled in {@link #_handleTextNodeInsertion} and text node mutation is handled
* in {@link #_handleTextMutation}).
*
* @private
* @param {Array.<module:engine/view/observer/mutationobserver~MutatedText|
* module:engine/view/observer/mutationobserver~MutatedChildren>} mutations
* @param {module:engine/view/selection~Selection|null} viewSelection
*/
_handleContainerChildrenMutations( mutations, viewSelection ) {
// Get common ancestor of all mutations.
const mutationsCommonAncestor = getMutationsContainer( mutations );
// Quit if there is no common ancestor.
if ( !mutationsCommonAncestor ) {
return;
}
const domConverter = this.editor.editing.view.domConverter;
// Get common ancestor in DOM.
const domMutationCommonAncestor = domConverter.mapViewToDom( mutationsCommonAncestor );
if ( !domMutationCommonAncestor ) {
return;
}
// Create fresh DomConverter so it will not use existing mapping and convert current DOM to model.
// This wouldn't be needed if DomConverter would allow to create fresh view without checking any mappings.
const freshDomConverter = new DomConverter();
const modelFromCurrentDom = this.editor.data.toModel( freshDomConverter.domToView( domMutationCommonAncestor ) ).getChild( 0 );
// Current model.
const currentModel = this.editor.editing.mapper.toModelElement( mutationsCommonAncestor );
// Get children from both ancestors.
const modelFromDomChildren = Array.from( modelFromCurrentDom.getChildren() );
const currentModelChildren = Array.from( currentModel.getChildren() );
// Skip situations when common ancestor has any elements (cause they are too hard).
if ( !hasOnlyTextNodes( modelFromDomChildren ) || !hasOnlyTextNodes( currentModelChildren ) ) {
return;
}
// Replace &nbsp; inserted by the browser with normal space.
// See comment in `_handleTextMutation`.
const newText = modelFromDomChildren.map( item => item.data ).join( '' ).replace( /\u00A0/g, ' ' );
const oldText = currentModelChildren.map( item => item.data ).join( '' );
// Do nothing if mutations created same text.
if ( oldText === newText ) {
return;
}
const diffResult = diff( oldText, newText );
const { firstChangeAt, insertions, deletions } = calculateChanges( diffResult );
// Try setting new model selection according to passed view selection.
let modelSelectionRange = null;
if ( viewSelection ) {
modelSelectionRange = this.editing.mapper.toModelRange( viewSelection.getFirstRange() );
}
const insertText = newText.substr( firstChangeAt, insertions );
const removeRange = ModelRange.createFromParentsAndOffsets(
currentModel,
firstChangeAt,
currentModel,
firstChangeAt + deletions
);
this.editor.execute( 'input', {
text: insertText,
range: removeRange,
resultRange: modelSelectionRange
} );
}
_handleTextMutation( mutation, viewSelection ) {

@@ -167,34 +272,4 @@ if ( mutation.type != 'text' ) {

// Index where the first change happens. Used to set the position from which nodes will be removed and where will be inserted.
let firstChangeAt = null;
// Index where the last change happens. Used to properly count how many characters have to be removed and inserted.
let lastChangeAt = null;
const { firstChangeAt, insertions, deletions } = calculateChanges( diffResult );
// Get `firstChangeAt` and `lastChangeAt`.
for ( let i = 0; i < diffResult.length; i++ ) {
const change = diffResult[ i ];
if ( change != 'equal' ) {
firstChangeAt = firstChangeAt === null ? i : firstChangeAt;
lastChangeAt = i;
}
}
// How many characters, starting from `firstChangeAt`, should be removed.
let deletions = 0;
// How many characters, starting from `firstChangeAt`, should be inserted (basing on mutation.newText).
let insertions = 0;
for ( let i = firstChangeAt; i <= lastChangeAt; i++ ) {
// If there is no change (equal) or delete, the character is existing in `oldText`. We count it for removing.
if ( diffResult[ i ] != 'insert' ) {
deletions++;
}
// If there is no change (equal) or insert, the character is existing in `newText`. We count it for inserting.
if ( diffResult[ i ] != 'delete' ) {
insertions++;
}
}
// Try setting new model selection according to passed view selection.

@@ -225,23 +300,3 @@ let modelSelectionRange = null;

// One new node.
if ( mutation.newChildren.length - mutation.oldChildren.length != 1 ) {
return;
}
// Which is text.
const diffResult = diff( mutation.oldChildren, mutation.newChildren, compareChildNodes );
const changes = diffToChanges( diffResult, mutation.newChildren );
// In case of [ delete, insert, insert ] the previous check will not exit.
if ( changes.length > 1 ) {
return;
}
const change = changes[ 0 ];
// Which is text.
if ( !( change.values[ 0 ] instanceof ViewText ) ) {
return;
}
const change = getSingleTextNodeChange( mutation );
const viewPos = new ViewPosition( mutation.node, change.index );

@@ -267,3 +322,3 @@ const modelPos = this.editing.mapper.toModelPosition( viewPos );

getCode( 'arrowLeft' ),
9, // Tab
9, // Tab
16, // Shift

@@ -290,2 +345,3 @@ 17, // Ctrl

//
// @private
// @param {engine.view.observer.keyObserver.KeyEventData} keyData

@@ -311,1 +367,126 @@ // @returns {Boolean}

}
// Returns change made to a single text node. Returns `undefined` if more than a single text node was changed.
//
// @private
// @param mutation
function getSingleTextNodeChange( mutation ) {
// One new node.
if ( mutation.newChildren.length - mutation.oldChildren.length != 1 ) {
return;
}
// Which is text.
const diffResult = diff( mutation.oldChildren, mutation.newChildren, compareChildNodes );
const changes = diffToChanges( diffResult, mutation.newChildren );
// In case of [ delete, insert, insert ] the previous check will not exit.
if ( changes.length > 1 ) {
return;
}
const change = changes[ 0 ];
// Which is text.
if ( !( change.values[ 0 ] instanceof ViewText ) ) {
return;
}
return change;
}
// Returns first common ancestor of all mutations that is either {@link module:engine/view/containerelement~ContainerElement}
// or {@link module:engine/view/rootelement~RootElement}.
//
// @private
// @param {Array.<module:engine/view/observer/mutationobserver~MutatedText|
// module:engine/view/observer/mutationobserver~MutatedChildren>} mutations
// @returns {module:engine/view/containerelement~ContainerElement|engine/view/rootelement~RootElement|undefined}
function getMutationsContainer( mutations ) {
const lca = mutations
.map( mutation => mutation.node )
.reduce( ( commonAncestor, node ) => {
return commonAncestor.getCommonAncestor( node, { includeSelf: true } );
} );
if ( !lca ) {
return;
}
// We need to look for container and root elements only, so check all LCA's
// ancestors (starting from itself).
return lca.getAncestors( { includeSelf: true, parentFirst: true } )
.find( element => element.is( 'containerElement' ) || element.is( 'rootElement' ) );
}
// Returns true if container children have mutated and more than a single text node was changed. Single text node
// child insertion is handled in {@link module:typing/input~MutationHandler#_handleTextNodeInsertion} and text
// mutation is handled in {@link module:typing/input~MutationHandler#_handleTextMutation}.
//
// @private
// @param {Array.<module:engine/view/observer/mutationobserver~MutatedText|
// module:engine/view/observer/mutationobserver~MutatedChildren>} mutations
// @returns {Boolean}
function containerChildrenMutated( mutations ) {
if ( mutations.length == 0 ) {
return false;
}
// Check if all mutations are `children` type, and there is no single text node mutation.
for ( const mutation of mutations ) {
if ( mutation.type !== 'children' || getSingleTextNodeChange( mutation ) ) {
return false;
}
}
return true;
}
// Returns true if provided array contains only {@link module:engine/model/text~Text model text nodes}.
//
// @param {Array.<module:engine/model/node~Node>} children
// @returns {Boolean}
function hasOnlyTextNodes( children ) {
return children.every( child => child.is( 'text' ) );
}
// Calculates first change index and number of characters that should be inserted and deleted starting from that index.
//
// @private
// @param diffResult
// @return {{insertions: number, deletions: number, firstChangeAt: *}}
function calculateChanges( diffResult ) {
// Index where the first change happens. Used to set the position from which nodes will be removed and where will be inserted.
let firstChangeAt = null;
// Index where the last change happens. Used to properly count how many characters have to be removed and inserted.
let lastChangeAt = null;
// Get `firstChangeAt` and `lastChangeAt`.
for ( let i = 0; i < diffResult.length; i++ ) {
const change = diffResult[ i ];
if ( change != 'equal' ) {
firstChangeAt = firstChangeAt === null ? i : firstChangeAt;
lastChangeAt = i;
}
}
// How many characters, starting from `firstChangeAt`, should be removed.
let deletions = 0;
// How many characters, starting from `firstChangeAt`, should be inserted.
let insertions = 0;
for ( let i = firstChangeAt; i <= lastChangeAt; i++ ) {
// If there is no change (equal) or delete, the character is existing in `oldText`. We count it for removing.
if ( diffResult[ i ] != 'insert' ) {
deletions++;
}
// If there is no change (equal) or insert, the character is existing in `newText`. We count it for inserting.
if ( diffResult[ i ] != 'delete' ) {
insertions++;
}
}
return { insertions, deletions, firstChangeAt };
}

@@ -10,3 +10,3 @@ /**

import Command from '@ckeditor/ckeditor5-core/src/command/command';
import Command from '@ckeditor/ckeditor5-core/src/command';
import ChangeBuffer from './changebuffer';

@@ -17,3 +17,3 @@

*
* @extends module:core/command/command~Command
* @extends module:core/command~Command
*/

@@ -42,2 +42,11 @@ export default class InputCommand extends Command {

/**
* The current change buffer.
*
* @type {module:typing/changebuffer~ChangeBuffer}
*/
get buffer() {
return this._buffer;
}
/**
* @inheritDoc

@@ -49,28 +58,19 @@ */

this._buffer.destroy();
this._buffer = null;
}
/**
* The current change buffer.
*
* @type {module:typing/changebuffer~ChangeBuffer}
*/
get buffer() {
return this._buffer;
}
/**
* Executes the input command. It replaces the content within the given range with the given text.
* Replacing is a two step process, first content within the range is removed and then new text is inserted
* on the beginning of the range (which after removal is a collapsed range).
* Replacing is a two step process, first the content within the range is removed and then the new text is inserted
* at the beginning of the range (which after the removal is a collapsed range).
*
* @fires execute
* @param {Object} [options] The command options.
* @param {String} [options.text=''] Text to be inserted.
* @param {module:engine/model/range~Range} [options.range] Range in which the text is inserted. Defaults
* @param {String} [options.text=''] The text to be inserted.
* @param {module:engine/model/range~Range} [options.range] The range in which the text is inserted. Defaults
* to the first range in the current selection.
* @param {module:engine/model/range~Range} [options.resultRange] Range at which the selection
* @param {module:engine/model/range~Range} [options.resultRange] The range where the selection
* should be placed after the insertion. If not specified, the selection will be placed right after
* the inserted text.
*/
_doExecute( options = {} ) {
execute( options = {} ) {
const doc = this.editor.document;

@@ -97,3 +97,3 @@ const text = options.text || '';

// If range was collapsed just shift the selection by the number of inserted characters.
this.editor.data.model.selection.collapse( range.start.getShiftedBy( textInsertions ) );
this.editor.data.model.selection.setCollapsedAt( range.start.getShiftedBy( textInsertions ) );
}

@@ -100,0 +100,0 @@

@@ -15,3 +15,3 @@ /**

/**
* The typing feature. Handles typing.
* The typing feature. It handles typing.
*

@@ -29,4 +29,34 @@ * @extends module:core/plugin~Plugin

static get pluginName() {
return 'typing/typing';
return 'Typing';
}
}
/**
* The configuration of the typing features. Used by the features from the `@ckeditor/ckeditor5-typing` package.
*
* Read more in {@link module:typing/typing~TypingConfig}.
*
* @member {module:typing/typing~TypingConfig} module:core/editor/editorconfig~EditorConfig#typing
*/
/**
* The configuration of the typing features. Used by the typing features in `@ckeditor/ckeditor5-typing` package.
*
* ClassicEditor
* .create( {
* typing: ... // Typing feature options.
* } )
* .then( ... )
* .catch( ... );
*
* See {@link module:core/editor/editorconfig~EditorConfig all editor options}.
*
* @interface TypingConfig
*/
/**
* The granularity of undo/redo for typing and deleting. The value `20` means (more or less) that a new undo step
* is created every 20 characters are inserted or deleted.
*
* @member {Number} [module:typing/typing~TypingConfig#undoStep=20]
*/

@@ -25,10 +25,11 @@ /**

return ClassicTestEditor.create( editorElement, {
plugins: [ Typing, Paragraph, Bold ]
} )
.then( newEditor => {
editor = newEditor;
domRoot = editor.editing.view.getDomRoot();
mutationObserver = editor.editing.view.getObserver( MutationObserver );
} );
return ClassicTestEditor
.create( editorElement, {
plugins: [ Typing, Paragraph, Bold ]
} )
.then( newEditor => {
editor = newEditor;
domRoot = editor.editing.view.getDomRoot();
mutationObserver = editor.editing.view.getObserver( MutationObserver );
} );
} );

@@ -46,3 +47,3 @@

it( 'space is inserted on the end of the line (paragraph)', ( done ) => {
it( 'space is inserted on the end of the line (paragraph)', done => {
editor.document.enqueueChanges( () => {

@@ -61,3 +62,3 @@ editor.editing.view.getDomRoot().focus();

it( 'space is inserted on the end of the line (empty paragraph)', ( done ) => {
it( 'space is inserted on the end of the line (empty paragraph)', done => {
editor.document.enqueueChanges( () => {

@@ -76,3 +77,3 @@ editor.editing.view.getDomRoot().focus();

it( 'space is inserted on the end of the line (bold)', ( done ) => {
it( 'space is inserted on the end of the line (bold)', done => {
editor.document.enqueueChanges( () => {

@@ -95,3 +96,3 @@ editor.editing.view.getDomRoot().focus();

it( 'space is inserted on the end of the paragraph', ( done ) => {
it( 'space is inserted on the end of the paragraph', done => {
editor.document.enqueueChanges( () => {

@@ -122,3 +123,3 @@ editor.editing.view.getDomRoot().focus();

it( 'space is inserted on the end of the line paragraph (with bogus br)', ( done ) => {
it( 'space is inserted on the end of the line paragraph (with bogus br)', done => {
editor.document.enqueueChanges( () => {

@@ -135,3 +136,3 @@ editor.editing.view.getDomRoot().focus();

const paragraph = domRoot.childNodes[ 0 ];
const text = paragraph.childNodes [ 0 ];
const text = paragraph.childNodes[ 0 ];
const br = document.createElement( 'br' );

@@ -153,3 +154,3 @@

it( 'word is properly corrected on the end of the block element (with bogus br)', ( done ) => {
it( 'word is properly corrected on the end of the block element (with bogus br)', done => {
editor.document.enqueueChanges( () => {

@@ -166,3 +167,3 @@ editor.editing.view.getDomRoot().focus();

const paragraph = domRoot.childNodes[ 0 ];
const text = paragraph.childNodes [ 0 ];
const text = paragraph.childNodes[ 0 ];
const br = document.createElement( 'br' );

@@ -197,6 +198,6 @@

removedNodes: [],
target: target,
type: type
target,
type
};
}
} );

@@ -233,3 +233,3 @@ /**

describe( 'destroy', () => {
describe( 'destroy()', () => {
it( 'offs the buffer from the document', () => {

@@ -236,0 +236,0 @@ const batch1 = buffer.batch;

@@ -14,5 +14,4 @@ /**

beforeEach( () => {
return VirtualTestEditor.create( {
plugins: [ Delete ]
} )
return VirtualTestEditor
.create( { plugins: [ Delete ] } )
.then( newEditor => {

@@ -40,7 +39,8 @@ editor = newEditor;

direction: 'forward',
unit: 'character'
unit: 'character',
sequence: 1
} ) );
expect( spy.calledOnce ).to.be.true;
expect( spy.calledWithMatch( 'forwardDelete', { unit: 'character' } ) ).to.be.true;
expect( spy.calledWithMatch( 'forwardDelete', { unit: 'character', sequence: 1 } ) ).to.be.true;

@@ -51,9 +51,23 @@ expect( domEvt.preventDefault.calledOnce ).to.be.true;

direction: 'backward',
unit: 'character'
unit: 'character',
sequence: 5
} ) );
expect( spy.calledTwice ).to.be.true;
expect( spy.calledWithMatch( 'delete', { unit: 'character' } ) ).to.be.true;
expect( spy.calledWithMatch( 'delete', { unit: 'character', sequence: 5 } ) ).to.be.true;
} );
it( 'scrolls the editing document to the selection after executing the command', () => {
const scrollSpy = sinon.stub( editingView, 'scrollToTheSelection' );
const executeSpy = editor.execute = sinon.spy();
editingView.fire( 'delete', new DomEventData( editingView, getDomEvent(), {
direction: 'backward',
unit: 'character'
} ) );
sinon.assert.calledOnce( scrollSpy );
sinon.assert.callOrder( executeSpy, scrollSpy );
} );
function getDomEvent() {

@@ -60,0 +74,0 @@ return {

@@ -15,6 +15,5 @@ /**

beforeEach( () => {
return ModelTestEditor.create( {
plugins: [
UndoEngine
],
return ModelTestEditor
.create( {
plugins: [ UndoEngine ],
typing: {

@@ -29,5 +28,8 @@ undoStep: 3

const command = new DeleteCommand( editor, 'backward' );
editor.commands.set( 'delete', command );
editor.commands.add( 'delete', command );
doc.schema.registerItem( 'p', '$block' );
// Mock paragraph feature.
doc.schema.registerItem( 'paragraph', '$block' );
doc.schema.allow( { name: 'paragraph', inside: '$block' } );
doc.schema.registerItem( 'img', '$inline' );

@@ -48,5 +50,5 @@ doc.schema.allow( { name: '$text', inside: 'img' } );

describe( 'DeleteCommand integration', () => {
describe( 'with undo', () => {
it( 'deletes characters (and group changes in batches) and rollbacks', () => {
setData( doc, '<p>123456789[]</p>' );
setData( doc, '<paragraph>123456789[]</paragraph>' );

@@ -59,7 +61,7 @@ for ( let i = 0; i < 3; ++i ) {

assertOutput( '<p>123456789[]</p>' );
assertOutput( '<paragraph>123456789[]</paragraph>' );
} );
it( 'deletes characters (and group changes in batches) and rollbacks - test step', () => {
setData( doc, '<p>123456789[]</p>' );
setData( doc, '<paragraph>123456789[]</paragraph>' );

@@ -72,11 +74,11 @@ for ( let i = 0; i < 6; ++i ) {

assertOutput( '<p>123456[]</p>' );
assertOutput( '<paragraph>123456[]</paragraph>' );
editor.execute( 'undo' );
assertOutput( '<p>123456789[]</p>' );
assertOutput( '<paragraph>123456789[]</paragraph>' );
} );
it( 'deletes elements (and group changes in batches) and rollbacks', () => {
setData( doc, '<p><img>1</img><img>2</img><img>3</img><img>4</img><img>5</img><img>6</img>[]</p>' );
setData( doc, '<paragraph><img>1</img><img>2</img><img>3</img><img>4</img><img>5</img><img>6</img>[]</paragraph>' );

@@ -89,7 +91,7 @@ for ( let i = 0; i < 3; ++i ) {

assertOutput( '<p><img>1</img><img>2</img><img>3</img><img>4</img><img>5</img><img>6</img>[]</p>' );
assertOutput( '<paragraph><img>1</img><img>2</img><img>3</img><img>4</img><img>5</img><img>6</img>[]</paragraph>' );
} );
it( 'merges elements (and group changes in batches) and rollbacks', () => {
setData( doc, '<p>123456</p><p>[]78</p>' );
setData( doc, '<paragraph>123456</paragraph><paragraph>[]78</paragraph>' );

@@ -102,13 +104,15 @@ for ( let i = 0; i < 6; ++i ) {

// Deleted 6,5,4, <P> does not count.
// Deleted 6,5,4, <paragraph> does not count.
// It's not the most elegant solution, but is the best if we don't want to make complicated algorithm.
assertOutput( '<p>123[]78</p>' );
assertOutput( '<paragraph>123[]78</paragraph>' );
editor.execute( 'undo' );
assertOutput( '<p>123456</p><p>[]78</p>' );
// Selection restoing in undo is not 100% correct so slight miss-settings are expected as long as
// the selection makes any sense and is near the correct position.
assertOutput( '<paragraph>123456</paragraph><paragraph>78[]</paragraph>' );
} );
it( 'merges elements (and group changes in batches) and rollbacks - non-collapsed selection', () => {
setData( doc, '<p>12345[6</p><p>7]8</p>' );
setData( doc, '<paragraph>12345[6</paragraph><paragraph>7]8</paragraph>' );

@@ -121,9 +125,39 @@ editor.execute( 'delete' );

assertOutput( '<p>1234[]8</p>' );
assertOutput( '<paragraph>1234[]8</paragraph>' );
editor.execute( 'undo' );
assertOutput( '<p>12345[6</p><p>7]8</p>' );
assertOutput( '<paragraph>12345[6</paragraph><paragraph>7]8</paragraph>' );
} );
} );
describe( 'with DataController.deleteContent', () => {
beforeEach( () => {
doc.schema.registerItem( 'h1', '$block' );
} );
it( 'should replace content with paragraph - if whole content is selected', () => {
setData( doc, '<h1>[foo</h1><paragraph>bar]</paragraph>' );
editor.execute( 'delete' );
assertOutput( '<paragraph>[]</paragraph>' );
} );
it( 'should not replace content with paragraph - if not whole content is selected', () => {
setData( doc, '<h1>f[oo</h1><paragraph>bar]</paragraph>' );
editor.execute( 'delete' );
assertOutput( '<h1>f[]</h1>' );
} );
it( 'should not replace content with paragraph - if selection was collapsed', () => {
setData( doc, '<h1></h1><paragraph>[]</paragraph>' );
editor.execute( 'delete' );
assertOutput( '<h1>[]</h1>' );
} );
} );
} );

@@ -23,5 +23,6 @@ /**

const command = new DeleteCommand( editor, 'backward' );
editor.commands.set( 'delete', command );
editor.commands.add( 'delete', command );
doc.schema.registerItem( 'p', '$block' );
doc.schema.registerItem( 'paragraph', '$block' );
doc.schema.registerItem( 'heading1', '$block' );
} );

@@ -40,15 +41,20 @@ } );

describe( 'execute', () => {
describe( 'execute()', () => {
it( 'uses enqueueChanges', () => {
setData( doc, '<p>foo[]bar</p>' );
setData( doc, '<paragraph>foo[]bar</paragraph>' );
const spy = testUtils.sinon.spy( doc, 'enqueueChanges' );
doc.enqueueChanges( () => {
editor.execute( 'delete' );
editor.execute( 'delete' );
// We expect that command is executed in enqueue changes block. Since we are already in
// an enqueued block, the command execution will be postponed. Hence, no changes.
expect( getData( doc ) ).to.equal( '<paragraph>foo[]bar</paragraph>' );
} );
expect( spy.calledOnce ).to.be.true;
// After all enqueued changes are done, the command execution is reflected.
expect( getData( doc ) ).to.equal( '<paragraph>fo[]bar</paragraph>' );
} );
it( 'locks buffer when executing', () => {
setData( doc, '<p>foo[]bar</p>' );
setData( doc, '<paragraph>foo[]bar</paragraph>' );

@@ -66,23 +72,23 @@ const buffer = editor.commands.get( 'delete' )._buffer;

it( 'deletes previous character when selection is collapsed', () => {
setData( doc, '<p>foo[]bar</p>' );
setData( doc, '<paragraph>foo[]bar</paragraph>' );
editor.execute( 'delete' );
expect( getData( doc, { selection: true } ) ).to.equal( '<p>fo[]bar</p>' );
expect( getData( doc ) ).to.equal( '<paragraph>fo[]bar</paragraph>' );
} );
it( 'deletes selection contents', () => {
setData( doc, '<p>fo[ob]ar</p>' );
setData( doc, '<paragraph>fo[ob]ar</paragraph>' );
editor.execute( 'delete' );
expect( getData( doc, { selection: true } ) ).to.equal( '<p>fo[]ar</p>' );
expect( getData( doc ) ).to.equal( '<paragraph>fo[]ar</paragraph>' );
} );
it( 'merges elements', () => {
setData( doc, '<p>foo</p><p>[]bar</p>' );
setData( doc, '<paragraph>foo</paragraph><paragraph>[]bar</paragraph>' );
editor.execute( 'delete' );
expect( getData( doc, { selection: true } ) ).to.equal( '<p>foo[]bar</p>' );
expect( getData( doc ) ).to.equal( '<paragraph>foo[]bar</paragraph>' );
} );

@@ -94,7 +100,7 @@

editor.data.on( 'deleteContent', spy );
setData( doc, '<p>[]foo</p>' );
setData( doc, '<paragraph>[]foo</paragraph>' );
editor.execute( 'delete' );
expect( getData( doc, { selection: true } ) ).to.equal( '<p>[]foo</p>' );
expect( getData( doc ) ).to.equal( '<paragraph>[]foo</paragraph>' );
expect( spy.callCount ).to.equal( 0 );

@@ -107,3 +113,3 @@ } );

editor.data.on( 'modifySelection', spy );
setData( doc, '<p>foo[]bar</p>' );
setData( doc, '<paragraph>foo[]bar</paragraph>' );

@@ -116,7 +122,132 @@ editor.commands.get( 'delete' ).direction = 'forward';

const modifyOpts = spy.args[ 0 ][ 1 ].options;
const modifyOpts = spy.args[ 0 ][ 1 ][ 1 ];
expect( modifyOpts ).to.have.property( 'direction', 'forward' );
expect( modifyOpts ).to.have.property( 'unit', 'word' );
} );
it( 'passes options to deleteContent #1', () => {
const spy = sinon.spy();
editor.data.on( 'deleteContent', spy );
setData( doc, '<paragraph>foo[]bar</paragraph>' );
editor.execute( 'delete' );
expect( spy.callCount ).to.equal( 1 );
const deleteOpts = spy.args[ 0 ][ 1 ][ 2 ];
expect( deleteOpts ).to.have.property( 'doNotResetEntireContent', true );
} );
it( 'passes options to deleteContent #2', () => {
const spy = sinon.spy();
editor.data.on( 'deleteContent', spy );
setData( doc, '<paragraph>[foobar]</paragraph>' );
editor.execute( 'delete' );
expect( spy.callCount ).to.equal( 1 );
const deleteOpts = spy.args[ 0 ][ 1 ][ 2 ];
expect( deleteOpts ).to.have.property( 'doNotResetEntireContent', false );
} );
it( 'leaves an empty paragraph after removing the whole content from editor', () => {
setData( doc, '<heading1>[Header 1</heading1><paragraph>Some text.]</paragraph>' );
editor.execute( 'delete' );
expect( getData( doc ) ).to.equal( '<paragraph>[]</paragraph>' );
} );
it( 'leaves an empty paragraph after removing the whole content inside limit element', () => {
doc.schema.registerItem( 'section', '$root' );
doc.schema.limits.add( 'section' );
doc.schema.allow( { name: 'section', inside: '$root' } );
setData( doc,
'<heading1>Foo</heading1>' +
'<section>' +
'<heading1>[Header 1</heading1>' +
'<paragraph>Some text.]</paragraph>' +
'</section>' +
'<paragraph>Bar.</paragraph>'
);
editor.execute( 'delete' );
expect( getData( doc ) ).to.equal(
'<heading1>Foo</heading1>' +
'<section>' +
'<paragraph>[]</paragraph>' +
'</section>' +
'<paragraph>Bar.</paragraph>'
);
} );
it( 'leaves an empty paragraph after removing another paragraph from block element', () => {
doc.schema.registerItem( 'section', '$block' );
doc.schema.registerItem( 'blockQuote', '$block' );
doc.schema.limits.add( 'section' );
doc.schema.allow( { name: 'section', inside: '$root' } );
doc.schema.allow( { name: 'paragraph', inside: 'section' } );
doc.schema.allow( { name: 'blockQuote', inside: 'section' } );
doc.schema.allow( { name: 'paragraph', inside: 'blockQuote' } );
setData( doc, '<section><blockQuote><paragraph>[]</paragraph></blockQuote></section>' );
editor.execute( 'delete' );
expect( getData( doc ) ).to.equal( '<section><paragraph>[]</paragraph></section>' );
} );
it( 'leaves an empty paragraph after removing the whole content when root element was not added as Schema.limits', () => {
doc.schema.limits.delete( '$root' );
setData( doc, '<heading1>[]</heading1>' );
editor.execute( 'delete' );
expect( getData( doc ) ).to.equal( '<paragraph>[]</paragraph>' );
} );
it( 'replaces an empty element with paragraph', () => {
setData( doc, '<heading1>[]</heading1>' );
editor.execute( 'delete' );
expect( getData( doc ) ).to.equal( '<paragraph>[]</paragraph>' );
} );
it( 'does not replace an element when Backspace or Delete key is held', () => {
setData( doc, '<heading1>Bar[]</heading1>' );
for ( let sequence = 1; sequence < 10; ++sequence ) {
editor.execute( 'delete', { sequence } );
}
expect( getData( doc ) ).to.equal( '<heading1>[]</heading1>' );
} );
it( 'does not replace with paragraph in another paragraph already occurs in limit element', () => {
setData( doc, '<paragraph>[]</paragraph>' );
const element = doc.getRoot().getNodeByPath( [ 0 ] );
editor.execute( 'delete' );
expect( element ).is.equal( doc.getRoot().getNodeByPath( [ 0 ] ) );
} );
it( 'does not replace an element if a paragraph is not allowed in current position', () => {
doc.schema.disallow( { name: 'paragraph', inside: '$root' } );
setData( doc, '<heading1>[]</heading1>' );
editor.execute( 'delete' );
expect( getData( doc ) ).to.equal( '<heading1>[]</heading1>' );
} );
} );
} );

@@ -14,7 +14,7 @@ /**

describe( 'DeleteObserver', () => {
let viewDocument, observer;
let viewDocument;
beforeEach( () => {
viewDocument = new ViewDocument();
observer = viewDocument.addObserver( DeleteObserver );
viewDocument.addObserver( DeleteObserver );
} );

@@ -44,2 +44,3 @@

expect( data ).to.have.property( 'unit', 'character' );
expect( data ).to.have.property( 'sequence', 1 );
} );

@@ -62,2 +63,3 @@

expect( data ).to.have.property( 'unit', 'word' );
expect( data ).to.have.property( 'sequence', 1 );
} );

@@ -76,2 +78,97 @@

} );
it( 'is fired with a proper sequence number', () => {
const spy = sinon.spy();
viewDocument.on( 'delete', spy );
// Simulate that a user keeps the "Delete" key.
for ( let i = 0; i < 5; ++i ) {
viewDocument.fire( 'keydown', new DomEventData( viewDocument, getDomEvent(), {
keyCode: getCode( 'delete' )
} ) );
}
expect( spy.callCount ).to.equal( 5 );
expect( spy.args[ 0 ][ 1 ] ).to.have.property( 'sequence', 1 );
expect( spy.args[ 1 ][ 1 ] ).to.have.property( 'sequence', 2 );
expect( spy.args[ 2 ][ 1 ] ).to.have.property( 'sequence', 3 );
expect( spy.args[ 3 ][ 1 ] ).to.have.property( 'sequence', 4 );
expect( spy.args[ 4 ][ 1 ] ).to.have.property( 'sequence', 5 );
} );
it( 'clears the sequence when the key was released', () => {
const spy = sinon.spy();
viewDocument.on( 'delete', spy );
// Simulate that a user keeps the "Delete" key.
for ( let i = 0; i < 3; ++i ) {
viewDocument.fire( 'keydown', new DomEventData( viewDocument, getDomEvent(), {
keyCode: getCode( 'delete' )
} ) );
}
// Then the user has released the key.
viewDocument.fire( 'keyup', new DomEventData( viewDocument, getDomEvent(), {
keyCode: getCode( 'delete' )
} ) );
// And pressed it once again.
viewDocument.fire( 'keydown', new DomEventData( viewDocument, getDomEvent(), {
keyCode: getCode( 'delete' )
} ) );
expect( spy.callCount ).to.equal( 4 );
expect( spy.args[ 0 ][ 1 ] ).to.have.property( 'sequence', 1 );
expect( spy.args[ 1 ][ 1 ] ).to.have.property( 'sequence', 2 );
expect( spy.args[ 2 ][ 1 ] ).to.have.property( 'sequence', 3 );
expect( spy.args[ 3 ][ 1 ] ).to.have.property( 'sequence', 1 );
} );
it( 'works fine with Backspace key', () => {
const spy = sinon.spy();
viewDocument.on( 'delete', spy );
viewDocument.fire( 'keydown', new DomEventData( viewDocument, getDomEvent(), {
keyCode: getCode( 'backspace' )
} ) );
viewDocument.fire( 'keyup', new DomEventData( viewDocument, getDomEvent(), {
keyCode: getCode( 'backspace' )
} ) );
viewDocument.fire( 'keydown', new DomEventData( viewDocument, getDomEvent(), {
keyCode: getCode( 'backspace' )
} ) );
expect( spy.callCount ).to.equal( 2 );
expect( spy.args[ 0 ][ 1 ] ).to.have.property( 'sequence', 1 );
expect( spy.args[ 1 ][ 1 ] ).to.have.property( 'sequence', 1 );
} );
it( 'does not reset the sequence if other than Backspace or Delete key was released', () => {
const spy = sinon.spy();
viewDocument.on( 'delete', spy );
viewDocument.fire( 'keydown', new DomEventData( viewDocument, getDomEvent(), {
keyCode: getCode( 'delete' )
} ) );
viewDocument.fire( 'keyup', new DomEventData( viewDocument, getDomEvent(), {
keyCode: getCode( 'A' )
} ) );
viewDocument.fire( 'keydown', new DomEventData( viewDocument, getDomEvent(), {
keyCode: getCode( 'delete' )
} ) );
expect( spy.args[ 0 ][ 1 ] ).to.have.property( 'sequence', 1 );
expect( spy.args[ 1 ][ 1 ] ).to.have.property( 'sequence', 2 );
} );
} );

@@ -78,0 +175,0 @@

@@ -6,5 +6,11 @@ /*

/* global document */
import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
import Bold from '@ckeditor/ckeditor5-basic-styles/src/boldengine';
import Italic from '@ckeditor/ckeditor5-basic-styles/src/italicengine';
import LinkEngine from '@ckeditor/ckeditor5-link/src/linkengine';
import Input from '../src/input';

@@ -19,3 +25,5 @@

import ViewElement from '@ckeditor/ckeditor5-engine/src/view/element';
import ViewContainerElement from '@ckeditor/ckeditor5-engine/src/view/containerelement';
import ViewSelection from '@ckeditor/ckeditor5-engine/src/view/selection';
import MutationObserver from '@ckeditor/ckeditor5-engine/src/view/observer/mutationobserver';

@@ -25,3 +33,3 @@ import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';

import { getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';

@@ -37,3 +45,4 @@

return VirtualTestEditor.create( {
return VirtualTestEditor
.create( {
plugins: [ Input, Paragraph ]

@@ -204,3 +213,3 @@ } )

const viewSelection = new ViewSelection();
viewSelection.collapse( viewRoot.getChild( 0 ).getChild( 0 ), 6 );
viewSelection.setCollapsedAt( viewRoot.getChild( 0 ).getChild( 0 ), 6 );

@@ -225,3 +234,3 @@ view.fire( 'mutations',

const viewSelection = new ViewSelection();
viewSelection.collapse( viewRoot.getChild( 0 ).getChild( 0 ), 6 );
viewSelection.setCollapsedAt( viewRoot.getChild( 0 ).getChild( 0 ), 6 );

@@ -250,3 +259,3 @@ testUtils.sinon.spy( Batch.prototype, 'weakInsert' );

const viewSelection = new ViewSelection();
viewSelection.collapse( viewRoot.getChild( 0 ).getChild( 0 ), 9 );
viewSelection.setCollapsedAt( viewRoot.getChild( 0 ).getChild( 0 ), 9 );

@@ -272,3 +281,3 @@ view.fire( 'mutations',

const viewSelection = new ViewSelection();
viewSelection.collapse( viewRoot.getChild( 0 ).getChild( 0 ), 8 );
viewSelection.setCollapsedAt( viewRoot.getChild( 0 ).getChild( 0 ), 8 );

@@ -294,3 +303,3 @@ view.fire( 'mutations',

const viewSelection = new ViewSelection();
viewSelection.collapse( viewRoot.getChild( 0 ).getChild( 0 ), 9 );
viewSelection.setCollapsedAt( viewRoot.getChild( 0 ).getChild( 0 ), 9 );

@@ -315,4 +324,4 @@ view.fire( 'mutations',

const viewSelection = new ViewSelection();
viewSelection.collapse( viewRoot.getChild( 0 ).getChild( 0 ), 8 );
viewSelection.setFocus( viewRoot.getChild( 0 ).getChild( 0 ), 9 );
viewSelection.setCollapsedAt( viewRoot.getChild( 0 ).getChild( 0 ), 8 );
viewSelection.moveFocusTo( viewRoot.getChild( 0 ).getChild( 0 ), 9 );

@@ -403,3 +412,12 @@ view.fire( 'mutations',

expect( getModelData( model ) ).to.equal( '<paragraph>fo[]ar</paragraph>' );
}, { priority: 'lowest' } );
} );
// #97
it( 'should remove contents and merge blocks', () => {
setModelData( model, '<paragraph>fo[o</paragraph><paragraph>b]ar</paragraph>' );
listenter.listenTo( view, 'keydown', () => {
expect( getModelData( model ) ).to.equal( '<paragraph>fo[]ar</paragraph>' );
view.fire( 'mutations', [

@@ -427,3 +445,3 @@ {

view.fire( 'keydown', { keyCode: getCode( 'arrowright' ) } );
view.fire( 'keydown', { keyCode: getCode( 'arrowdown' ) } );

@@ -528,4 +546,420 @@ expect( getModelData( model ) ).to.equal( '<paragraph>fo[ob]ar</paragraph>' );

} );
it( 'should not modify document when input command is disabled and selection is collapsed', () => {
setModelData( model, '<paragraph>foo[]bar</paragraph>' );
editor.commands.get( 'input' ).isEnabled = false;
view.fire( 'keydown', { keyCode: getCode( 'b' ) } );
expect( getModelData( model ) ).to.equal( '<paragraph>foo[]bar</paragraph>' );
} );
it( 'should not modify document when input command is disabled and selection is non-collapsed', () => {
setModelData( model, '<paragraph>fo[ob]ar</paragraph>' );
editor.commands.get( 'input' ).isEnabled = false;
view.fire( 'keydown', { keyCode: getCode( 'b' ) } );
expect( getModelData( model ) ).to.equal( '<paragraph>fo[ob]ar</paragraph>' );
} );
} );
// NOTE: In all these tests we need to simulate the mutations. However, it's really tricky to tell what
// should be in "newChildren" because we don't have yet access to these nodes. We pass new instances,
// but this means that DomConverter which is used somewhere internally may return a different instance
// (which wouldn't happen in practice because it'd cache it). Besides, it's really hard to tell if the
// browser will keep the instances of the old elements when modifying the tree when the user is typing
// or if it will create new instances itself too.
// However, the code handling these mutations doesn't really care what's inside new/old children. It
// just needs the mutations common ancestor to understand how big fragment of the tree has changed.
describe( '#100', () => {
let domElement, domRoot;
beforeEach( () => {
domElement = document.createElement( 'div' );
document.body.appendChild( domElement );
return ClassicTestEditor.create( domElement, { plugins: [ Input, Paragraph, Bold, Italic, LinkEngine ] } )
.then( newEditor => {
editor = newEditor;
model = editor.document;
modelRoot = model.getRoot();
view = editor.editing.view;
viewRoot = view.getRoot();
domRoot = view.getDomRoot();
// Mock image feature.
newEditor.document.schema.registerItem( 'image', '$inline' );
buildModelConverter().for( newEditor.data.modelToView, newEditor.editing.modelToView )
.fromElement( 'image' )
.toElement( 'img' );
buildViewConverter().for( newEditor.data.viewToModel )
.fromElement( 'img' )
.toElement( 'image' );
// Disable MO completely and in a way it won't be reenabled on some Document#render() call.
const mutationObserver = view.getObserver( MutationObserver );
mutationObserver.disable();
mutationObserver.enable = () => {};
} );
} );
afterEach( () => {
domElement.remove();
return editor.destroy();
} );
// This happens when browser automatically switches parent and child nodes.
it( 'should handle mutations switching inner and outer node when adding new text node after', () => {
setModelData( model,
'<paragraph>' +
'<$text italic="true" linkHref="foo">' +
'text[]' +
'</$text>' +
'</paragraph>'
);
expect( getViewData( view ) ).to.equal( '<p><a href="foo"><i>text{}</i></a></p>' );
const paragraph = viewRoot.getChild( 0 );
const link = paragraph.getChild( 0 );
const italic = link.getChild( 0 );
const text = italic.getChild( 0 );
// Simulate mutations and DOM change.
domRoot.childNodes[ 0 ].innerHTML = '<i><a href="foo">text</a>x</i>';
view.fire( 'mutations', [
// First mutation - remove all children from link element.
{
type: 'children',
node: link,
oldChildren: [ italic ],
newChildren: []
},
// Second mutation - remove link from paragraph and put italic there.
{
type: 'children',
node: paragraph,
oldChildren: [ link ],
newChildren: [ new ViewElement( 'i' ) ]
},
// Third mutation - italic's new children.
{
type: 'children',
node: italic,
oldChildren: [ text ],
newChildren: [ new ViewElement( 'a', null, text ), new ViewText( 'x' ) ]
}
] );
expect( getViewData( view ) ).to.equal( '<p><a href="foo"><i>textx{}</i></a></p>' );
} );
it( 'should handle mutations switching inner and outer node when adding new text node before', () => {
setModelData( model,
'<paragraph>' +
'<$text italic="true" linkHref="foo">' +
'[]text' +
'</$text>' +
'</paragraph>'
);
expect( getViewData( view ) ).to.equal( '<p><a href="foo"><i>{}text</i></a></p>' );
const paragraph = viewRoot.getChild( 0 );
const link = paragraph.getChild( 0 );
const italic = link.getChild( 0 );
const text = italic.getChild( 0 );
// Simulate mutations and DOM change.
domRoot.childNodes[ 0 ].innerHTML = '<i>x<a href="foo">text</a></i>';
view.fire( 'mutations', [
// First mutation - remove all children from link element.
{
type: 'children',
node: link,
oldChildren: [ italic ],
newChildren: []
},
// Second mutation - remove link from paragraph and put italic there.
{
type: 'children',
node: paragraph,
oldChildren: [ link ],
newChildren: [ new ViewElement( 'i' ) ]
},
// Third mutation - italic's new children.
{
type: 'children',
node: italic,
oldChildren: [ text ],
newChildren: [ new ViewText( 'x' ), new ViewElement( 'a', null, 'text' ) ]
}
] );
expect( getViewData( view ) ).to.equal( '<p><a href="foo"><i>x{}text</i></a></p>' );
} );
it( 'should handle mutations switching inner and outer node - with text before', () => {
setModelData( model,
'<paragraph>' +
'xxx<$text italic="true" linkHref="foo">' +
'text[]' +
'</$text>' +
'</paragraph>'
);
expect( getViewData( view ) ).to.equal( '<p>xxx<a href="foo"><i>text{}</i></a></p>' );
const paragraph = viewRoot.getChild( 0 );
const textBefore = paragraph.getChild( 0 );
const link = paragraph.getChild( 1 );
const italic = link.getChild( 0 );
const text = italic.getChild( 0 );
// Simulate mutations and DOM change.
domRoot.childNodes[ 0 ].innerHTML = 'xxx<i><a href="foo">text</a>x</i>';
view.fire( 'mutations', [
// First mutation - remove all children from link element.
{
type: 'children',
node: link,
oldChildren: [ italic ],
newChildren: []
},
// Second mutation - remove link from paragraph and put italic there.
{
type: 'children',
node: paragraph,
oldChildren: [ textBefore, link ],
newChildren: [ new ViewText( 'xxx' ), new ViewElement( 'i' ) ]
},
// Third mutation - italic's new children.
{
type: 'children',
node: italic,
oldChildren: [ text ],
newChildren: [ new ViewElement( 'a', null, 'text' ), new ViewText( 'x' ) ]
}
] );
expect( getViewData( view ) ).to.equal( '<p>xxx<a href="foo"><i>textx{}</i></a></p>' );
} );
// This happens when spell checker is applied on <strong> element and changes it to <b>.
it( 'should handle mutations replacing node', () => {
setModelData( model,
'<paragraph>' +
'<$text bold="true">' +
'text[]' +
'</$text>' +
'</paragraph>'
);
expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
const paragraph = viewRoot.getChild( 0 );
const strong = paragraph.getChild( 0 );
// Simulate mutations and DOM change.
domRoot.childNodes[ 0 ].innerHTML = '<b>fixed text</b>';
view.fire( 'mutations', [
// Replace `<strong>` with `<b>`.
{
type: 'children',
node: paragraph,
oldChildren: [ strong ],
newChildren: [ new ViewElement( 'b', null, 'fixed text' ) ]
}
] );
expect( getViewData( view, { withoutSelection: true } ) ).to.equal( '<p><strong>fixed text</strong></p>' );
} );
// Spell checker splits text inside attributes to two text nodes.
it( 'should handle mutations inside attribute element', () => {
setModelData( model,
'<paragraph>' +
'<$text bold="true">' +
'this is foo text[]' +
'</$text>' +
'</paragraph>'
);
expect( getViewData( view ) ).to.equal( '<p><strong>this is foo text{}</strong></p>' );
const paragraph = viewRoot.getChild( 0 );
const strong = paragraph.getChild( 0 );
const text = strong.getChild( 0 );
// Simulate mutations and DOM change.
domRoot.childNodes[ 0 ].childNodes[ 0 ].innerHTML = 'this is bar text';
view.fire( 'mutations', [
{
type: 'children',
node: strong,
oldChildren: [ text ],
newChildren: [ new ViewText( 'this is bar' ), new ViewText( ' text' ) ]
}
] );
expect( getViewData( view, { withoutSelection: true } ) ).to.equal( '<p><strong>this is bar text</strong></p>' );
} );
it( 'should do nothing if elements mutated', () => {
setModelData( model,
'<paragraph>' +
'<$text bold="true">' +
'text[]' +
'</$text>' +
'</paragraph>'
);
expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
const paragraph = viewRoot.getChild( 0 );
const strong = paragraph.getChild( 0 );
// Simulate mutations and DOM change.
domRoot.childNodes[ 0 ].innerHTML = '<strong>text</strong><img />';
view.fire( 'mutations', [
{
type: 'children',
node: paragraph,
oldChildren: [ strong ],
newChildren: [
new ViewElement( 'strong', null, new ViewText( 'text' ) ),
new ViewElement( 'img' )
]
}
] );
expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
} );
it( 'should do nothing if text is not changed', () => {
setModelData( model,
'<paragraph>' +
'<$text bold="true">' +
'text[]' +
'</$text>' +
'</paragraph>'
);
expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
const paragraph = viewRoot.getChild( 0 );
const strong = paragraph.getChild( 0 );
// Simulate mutations and DOM change.
domRoot.childNodes[ 0 ].innerHTML = '<strong>text</strong>';
view.fire( 'mutations', [
{
type: 'children',
node: paragraph,
oldChildren: [ strong ],
newChildren: [ new ViewElement( 'strong', null, new ViewText( 'text' ) ) ]
}
] );
expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
} );
it( 'should do nothing on empty mutations', () => {
setModelData( model,
'<paragraph>' +
'<$text bold="true">' +
'text[]' +
'</$text>' +
'</paragraph>'
);
expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
// Simulate mutations and DOM change.
domRoot.childNodes[ 0 ].innerHTML = '<strong>text</strong>';
view.fire( 'mutations', [] );
expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
} );
it( 'should do nothing if mutations does not have common ancestor', () => {
setModelData( model,
'<paragraph>' +
'<$text bold="true">' +
'text[]' +
'</$text>' +
'</paragraph>'
);
expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
const paragraph = viewRoot.getChild( 0 );
const strong = paragraph.getChild( 0 );
// Simulate mutations and DOM change.
domRoot.childNodes[ 0 ].innerHTML = '<strong>text</strong>';
view.fire( 'mutations', [
{
type: 'children',
node: paragraph,
oldChildren: [ strong ],
newChildren: [ strong ]
},
{
type: 'children',
node: new ViewContainerElement( 'div' ),
oldChildren: [],
newChildren: [ new ViewText( 'foo' ), new ViewText( 'bar' ) ]
}
] );
expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
} );
it( 'should handle view selection if one is returned from mutations', () => {
setModelData( model,
'<paragraph>' +
'<$text bold="true">' +
'text[]' +
'</$text>' +
'</paragraph>'
);
expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
const paragraph = viewRoot.getChild( 0 );
const strong = paragraph.getChild( 0 );
const viewSelection = new ViewSelection();
viewSelection.setCollapsedAt( paragraph, 0 );
// Simulate mutations and DOM change.
domRoot.childNodes[ 0 ].innerHTML = '<b>textx</b>';
view.fire( 'mutations', [
// Replace `<strong>` with `<b>`.
{
type: 'children',
node: paragraph,
oldChildren: [ strong ],
newChildren: [ new ViewElement( 'b', null, new ViewText( 'textx' ) ) ]
}
], viewSelection );
expect( getModelData( model ) ).to.equal( '<paragraph><$text bold="true">[]textx</$text></paragraph>' );
expect( getViewData( view ) ).to.equal( '<p><strong>{}textx</strong></p>' );
} );
} );
} );

@@ -20,4 +20,3 @@ /**

import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
import { getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
import { setData as setModelData, getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';

@@ -32,14 +31,15 @@

return ClassicTestEditor.create( editorElement, {
plugins: [ Typing, Paragraph, Undo, Bold, Italic, Enter ],
typing: { undoStep: 3 }
} )
.then( newEditor => {
editor = newEditor;
doc = editor.document;
viewDocument = editor.editing.view;
return ClassicTestEditor
.create( editorElement, {
plugins: [ Typing, Paragraph, Undo, Bold, Italic, Enter ],
typing: { undoStep: 3 }
} )
.then( newEditor => {
editor = newEditor;
doc = editor.document;
viewDocument = editor.editing.view;
boldView = editor.ui.componentFactory.create( 'bold' );
italicView = editor.ui.componentFactory.create( 'italic' );
} );
boldView = editor.ui.componentFactory.create( 'bold' );
italicView = editor.ui.componentFactory.create( 'italic' );
} );
} );

@@ -60,3 +60,3 @@

// While typing, every character is an atomic change.
text.split( '' ).forEach( ( character ) => {
text.split( '' ).forEach( character => {
editor.execute( 'input', {

@@ -70,3 +70,3 @@ text: character

// Use longer text at once in input command.
batches.forEach( ( batch ) => {
batches.forEach( batch => {
editor.execute( 'input', {

@@ -198,4 +198,6 @@ text: batch

expectOutput( '<paragraph>Foo <$text bold="true">B</$text><$text italic="true"><$text bold="true">a</$text></$text>z[] Bar</paragraph>',
'<p>Foo <strong>B</strong><em><strong>a</strong></em>z{} Bar</p>' );
expectOutput(
'<paragraph>Foo <$text bold="true">B<$text italic="true">a</$text></$text>z[] Bar</paragraph>',
'<p>Foo <strong>B</strong><i><strong>a</strong></i>z{} Bar</p>'
);

@@ -206,6 +208,5 @@ editor.execute( 'undo' );

'<paragraph>' +
'Foo <$text bold="true">B</$text><$text italic="true"><$text bold="true">a</$text></$text>' +
'<$text bold="true" italic="true">[]</$text> Bar' +
'Foo <$text bold="true">B<$text italic="true">a[]</$text></$text> Bar' +
'</paragraph>',
'<p>Foo <strong>B</strong><em><strong>a{}</strong></em> Bar</p>'
'<p>Foo <strong>B</strong><i><strong>a{}</strong></i> Bar</p>'
);

@@ -212,0 +213,0 @@

@@ -21,3 +21,3 @@ /**

before( () => {
beforeEach( () => {
return ModelTestEditor.create()

@@ -29,5 +29,6 @@ .then( newEditor => {

const inputCommand = new InputCommand( editor, 20 );
editor.commands.set( 'input', inputCommand );
editor.commands.add( 'input', inputCommand );
buffer = inputCommand.buffer;
buffer.size = 0;

@@ -39,10 +40,6 @@ doc.schema.registerItem( 'p', '$block' );

after( () => {
afterEach( () => {
return editor.destroy();
} );
beforeEach( () => {
buffer.size = 0;
} );
describe( 'buffer', () => {

@@ -58,8 +55,9 @@ it( 'has buffer getter', () => {

it( 'has a buffer configured to config.typing.undoStep', () => {
return VirtualTestEditor.create( {
plugins: [ Input ],
typing: {
undoStep: 5
}
} )
return VirtualTestEditor
.create( {
plugins: [ Input ],
typing: {
undoStep: 5
}
} )
.then( editor => {

@@ -71,13 +69,16 @@ expect( editor.commands.get( 'input' )._buffer ).to.have.property( 'limit', 5 );

describe( 'execute', () => {
describe( 'execute()', () => {
it( 'uses enqueueChanges', () => {
setData( doc, '<p>foo[]bar</p>' );
const spy = testUtils.sinon.spy( doc, 'enqueueChanges' );
doc.enqueueChanges( () => {
editor.execute( 'input', { text: 'x' } );
editor.execute( 'input', {
text: ''
// We expect that command is executed in enqueue changes block. Since we are already in
// an enqueued block, the command execution will be postponed. Hence, no changes.
expect( getData( doc ) ).to.be.equal( '<p>foo[]bar</p>' );
} );
expect( spy.calledOnce ).to.be.true;
// After all enqueued changes are done, the command execution is reflected.
expect( getData( doc ) ).to.be.equal( '<p>foox[]bar</p>' );
} );

@@ -209,7 +210,4 @@

const spy = testUtils.sinon.spy( doc, 'enqueueChanges' );
editor.execute( 'input' );
expect( spy.callCount ).to.be.equal( 1 );
expect( getData( doc, { selection: true } ) ).to.be.equal( '<p>[]obar</p>' );

@@ -222,7 +220,4 @@ expect( buffer.size ).to.be.equal( 0 );

const spy = testUtils.sinon.spy( doc, 'enqueueChanges' );
editor.execute( 'input' );
expect( spy.callCount ).to.be.equal( 1 );
expect( getData( doc, { selection: true } ) ).to.be.equal( '<p>fo[]obar</p>' );

@@ -241,5 +236,4 @@ expect( buffer.size ).to.be.equal( 0 );

expect( destroy.calledOnce ).to.be.true;
expect( command._buffer ).to.be.null;
} );
} );
} );

@@ -8,3 +8,3 @@ /**

import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classic';
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
import Heading from '@ckeditor/ckeditor5-heading/src/heading';

@@ -16,11 +16,12 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';

ClassicEditor.create( document.querySelector( '#editor' ), {
plugins: [ EssentialsPreset, Paragraph, Bold, Italic, Heading ],
toolbar: [ 'headings', 'bold', 'italic', 'undo', 'redo' ]
} )
.then( editor => {
window.editor = editor;
} )
.catch( err => {
console.error( err.stack );
} );
ClassicEditor
.create( document.querySelector( '#editor' ), {
plugins: [ EssentialsPreset, Paragraph, Bold, Italic, Heading ],
toolbar: [ 'headings', 'bold', 'italic', 'undo', 'redo' ]
} )
.then( editor => {
window.editor = editor;
} )
.catch( err => {
console.error( err.stack );
} );

@@ -8,3 +8,3 @@ /**

import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classic';
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
import Heading from '@ckeditor/ckeditor5-heading/src/heading';

@@ -16,11 +16,12 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';

ClassicEditor.create( document.querySelector( '#editor' ), {
plugins: [ EssentialsPreset, Paragraph, Bold, Italic, Heading ],
toolbar: [ 'headings', 'bold', 'italic', 'undo', 'redo' ]
} )
.then( editor => {
window.editor = editor;
} )
.catch( err => {
console.error( err.stack );
} );
ClassicEditor
.create( document.querySelector( '#editor' ), {
plugins: [ EssentialsPreset, Paragraph, Bold, Italic, Heading ],
toolbar: [ 'headings', 'bold', 'italic', 'undo', 'redo' ]
} )
.then( editor => {
window.editor = editor;
} )
.catch( err => {
console.error( err.stack );
} );

@@ -8,3 +8,3 @@ /**

import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classic';
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
import Enter from '@ckeditor/ckeditor5-enter/src/enter';

@@ -14,11 +14,12 @@ import Typing from '../../../src/typing';

ClassicEditor.create( document.querySelector( '#editor' ), {
plugins: [ Enter, Typing, Heading ],
toolbar: [ 'headings' ]
} )
.then( editor => {
window.editor = editor;
} )
.catch( err => {
console.error( err.stack );
} );
ClassicEditor
.create( document.querySelector( '#editor' ), {
plugins: [ Enter, Typing, Heading ],
toolbar: [ 'headings' ]
} )
.then( editor => {
window.editor = editor;
} )
.catch( err => {
console.error( err.stack );
} );

@@ -8,3 +8,3 @@ /**

import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classic';
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
import Heading from '@ckeditor/ckeditor5-heading/src/heading';

@@ -21,11 +21,12 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';

ClassicEditor.create( document.querySelector( '#editor' ), {
plugins: [ EssentialsPreset, Paragraph, Bold, Italic, Heading ],
toolbar: [ 'headings', 'bold', 'italic', 'undo', 'redo' ]
} )
.then( editor => {
window.editor = editor;
} )
.catch( err => {
console.error( err.stack );
} );
ClassicEditor
.create( document.querySelector( '#editor' ), {
plugins: [ EssentialsPreset, Paragraph, Bold, Italic, Heading ],
toolbar: [ 'headings', 'bold', 'italic', 'undo', 'redo' ]
} )
.then( editor => {
window.editor = editor;
} )
.catch( err => {
console.error( err.stack );
} );

@@ -8,3 +8,3 @@ /**

import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classic';
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
import Typing from '../../../src/typing';

@@ -14,11 +14,12 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';

ClassicEditor.create( document.querySelector( '#editor' ), {
plugins: [ Typing, Paragraph, Bold ],
toolbar: [ 'bold' ]
} )
.then( editor => {
window.editor = editor;
} )
.catch( err => {
console.error( err.stack );
} );
ClassicEditor
.create( document.querySelector( '#editor' ), {
plugins: [ Typing, Paragraph, Bold ],
toolbar: [ 'bold' ]
} )
.then( editor => {
window.editor = editor;
} )
.catch( err => {
console.error( err.stack );
} );

@@ -8,3 +8,3 @@ /**

import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classic';
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
import Heading from '@ckeditor/ckeditor5-heading/src/heading';

@@ -19,11 +19,12 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';

ClassicEditor.create( document.querySelector( '#editor' ), {
plugins: [ EssentialsPreset, Paragraph, Heading ],
toolbar: [ 'headings', 'undo', 'redo' ]
} )
.then( editor => {
window.editor = editor;
} )
.catch( err => {
console.error( err.stack );
} );
ClassicEditor
.create( document.querySelector( '#editor' ), {
plugins: [ EssentialsPreset, Paragraph, Heading ],
toolbar: [ 'headings', 'undo', 'redo' ]
} )
.then( editor => {
window.editor = editor;
} )
.catch( err => {
console.error( err.stack );
} );

@@ -8,3 +8,3 @@ /**

import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classic';
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
import EssentialsPreset from '@ckeditor/ckeditor5-presets/src/essentials';

@@ -18,11 +18,12 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';

ClassicEditor.create( document.querySelector( '#editor' ), {
plugins: [ EssentialsPreset, Paragraph ],
toolbar: [ 'undo', 'redo' ]
} )
.then( editor => {
window.editor = editor;
} )
.catch( err => {
console.error( err.stack );
} );
ClassicEditor
.create( document.querySelector( '#editor' ), {
plugins: [ EssentialsPreset, Paragraph ],
toolbar: [ 'undo', 'redo' ]
} )
.then( editor => {
window.editor = editor;
} )
.catch( err => {
console.error( err.stack );
} );

@@ -8,3 +8,3 @@ /**

import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classic';
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
import Heading from '@ckeditor/ckeditor5-heading/src/heading';

@@ -23,11 +23,12 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';

ClassicEditor.create( document.querySelector( '#editor' ), {
plugins: [ EssentialsPreset, Paragraph, Bold, Italic, Heading ],
toolbar: [ 'headings', 'bold', 'italic', 'undo', 'redo' ]
} )
.then( editor => {
window.editor = editor;
} )
.catch( err => {
console.error( err.stack );
} );
ClassicEditor
.create( document.querySelector( '#editor' ), {
plugins: [ EssentialsPreset, Paragraph, Bold, Italic, Heading ],
toolbar: [ 'headings', 'bold', 'italic', 'undo', 'redo' ]
} )
.then( editor => {
window.editor = editor;
} )
.catch( err => {
console.error( err.stack );
} );

@@ -6,2 +6,6 @@ ## Delete feature

* collapsed selection (by letter, by word, whole line),
* non-collapsed selections.
* non-collapsed selections,
* put the selection at the end of **Heading 1**, **press and hold** the Backspace.
After releasing the key you should be able typing inside the header.
* clear the whole editor and choose **Heading 1** from the dropdown. Press the Backspace.
After typing, your text should be wrapped in a paragraph.

@@ -8,3 +8,3 @@ /**

import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classic';
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
import Heading from '@ckeditor/ckeditor5-heading/src/heading';

@@ -21,11 +21,12 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';

ClassicEditor.create( document.querySelector( '#editor' ), {
plugins: [ EssentialsPreset, Paragraph, Bold, Italic, Heading ],
toolbar: [ 'headings', 'bold', 'italic', 'undo', 'redo' ]
} )
.then( editor => {
window.editor = editor;
} )
.catch( err => {
console.error( err.stack );
} );
ClassicEditor
.create( document.querySelector( '#editor' ), {
plugins: [ EssentialsPreset, Paragraph, Bold, Italic, Heading ],
toolbar: [ 'headings', 'bold', 'italic', 'undo', 'redo' ]
} )
.then( editor => {
window.editor = editor;
} )
.catch( err => {
console.error( err.stack );
} );

@@ -6,3 +6,4 @@ ## Input (typing) feature

* normal typing,
* typing into non-collapsed selection.
* typing into non-collapsed selection,
* typing when the entire content is selected - new content should be wrapped in a paragraph.

@@ -9,0 +10,0 @@ ### IME

@@ -8,3 +8,3 @@ /**

import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classic';
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
import Enter from '@ckeditor/ckeditor5-enter/src/enter';

@@ -16,35 +16,36 @@ import Typing from '../../src/typing';

ClassicEditor.create( document.querySelector( '#editor' ), {
plugins: [ Enter, Typing, Paragraph ],
toolbar: []
} )
.then( editor => {
window.editor = editor;
ClassicEditor
.create( document.querySelector( '#editor' ), {
plugins: [ Enter, Typing, Paragraph ],
toolbar: []
} )
.then( editor => {
window.editor = editor;
editor.document.schema.allow( { name: '$text', inside: '$root' } );
editor.document.schema.allow( { name: '$text', inside: '$root' } );
const editable = editor.ui.view.editableElement;
const editable = editor.ui.view.editableElement;
document.querySelector( '#nbsp' ).addEventListener( 'click', () => {
editor.document.enqueueChanges( () => {
editor.document.selection.collapseToStart();
editor.document.batch().weakInsert( editor.document.selection.getFirstPosition(), '\u00A0' );
document.querySelector( '#nbsp' ).addEventListener( 'click', () => {
editor.document.enqueueChanges( () => {
editor.document.selection.collapseToStart();
editor.document.batch().weakInsert( editor.document.selection.getFirstPosition(), '\u00A0' );
} );
} );
} );
editor.document.on( 'changesDone', () => {
console.clear();
editor.document.on( 'changesDone', () => {
console.clear();
const modelData = getModelData( editor.document, { withoutSelection: true } );
console.log( 'model:', modelData.replace( /\u00A0/g, '&nbsp;' ) );
const modelData = getModelData( editor.document, { withoutSelection: true } );
console.log( 'model:', modelData.replace( /\u00A0/g, '&nbsp;' ) );
const viewData = getViewData( editor.editing.view, { withoutSelection: true } );
console.log( 'view:', viewData.replace( /\u00A0/g, '&nbsp;' ) );
const viewData = getViewData( editor.editing.view, { withoutSelection: true } );
console.log( 'view:', viewData.replace( /\u00A0/g, '&nbsp;' ) );
console.log( 'dom:', editable.innerHTML );
console.log( 'editor.getData', editor.getData() );
}, { priority: 'lowest' } );
} )
.catch( err => {
console.error( err.stack );
} );
console.log( 'dom:', editable.innerHTML );
console.log( 'editor.getData', editor.getData() );
}, { priority: 'lowest' } );
} )
.catch( err => {
console.error( err.stack );
} );

@@ -8,3 +8,3 @@ /**

import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classic';
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
import Heading from '@ckeditor/ckeditor5-heading/src/heading';

@@ -89,20 +89,22 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';

ClassicEditor.create( document.querySelector( '#editor1' ), config )
.then( editor => {
window.editor1 = editor;
ClassicEditor
.create( document.querySelector( '#editor1' ), config )
.then( editor => {
window.editor1 = editor;
// Editable doesn't automatically get this attribute right now.
// https://github.com/ckeditor/ckeditor5-editor-classic/issues/32
editor.editing.view.getDomRoot().setAttribute( 'dir', 'rtl' );
} )
.catch( err => {
console.error( err.stack );
} );
// Editable doesn't automatically get this attribute right now.
// https://github.com/ckeditor/ckeditor5-editor-classic/issues/32
editor.editing.view.getDomRoot().setAttribute( 'dir', 'rtl' );
} )
.catch( err => {
console.error( err.stack );
} );
ClassicEditor.create( document.querySelector( '#editor2' ), config )
.then( editor => {
window.editor2 = editor;
} )
.catch( err => {
console.error( err.stack );
} );
ClassicEditor
.create( document.querySelector( '#editor2' ), config )
.then( editor => {
window.editor2 = editor;
} )
.catch( err => {
console.error( err.stack );
} );

@@ -8,3 +8,3 @@ /**

import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classic';
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';

@@ -14,11 +14,12 @@ import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';

ClassicEditor.create( document.querySelector( '#editor' ), {
plugins: [ EssentialsPreset, Paragraph, Bold ],
toolbar: [ 'bold' ]
} )
.then( editor => {
window.editor = editor;
} )
.catch( err => {
console.error( err.stack );
} );
ClassicEditor
.create( document.querySelector( '#editor' ), {
plugins: [ EssentialsPreset, Paragraph, Bold ],
toolbar: [ 'bold' ]
} )
.then( editor => {
window.editor = editor;
} )
.catch( err => {
console.error( err.stack );
} );

@@ -8,3 +8,3 @@ /**

import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classic';
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
import Heading from '@ckeditor/ckeditor5-heading/src/heading';

@@ -11,0 +11,0 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';

@@ -8,3 +8,3 @@ /**

import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classic';
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
import Enter from '@ckeditor/ckeditor5-enter/src/enter';

@@ -14,11 +14,12 @@ import Typing from '../../src/typing';

ClassicEditor.create( document.querySelector( '#editor' ), {
plugins: [ Enter, Typing, Paragraph ],
toolbar: []
} )
.then( editor => {
window.editor = editor;
} )
.catch( err => {
console.error( err.stack );
} );
ClassicEditor
.create( document.querySelector( '#editor' ), {
plugins: [ Enter, Typing, Paragraph ],
toolbar: []
} )
.then( editor => {
window.editor = editor;
} )
.catch( err => {
console.error( err.stack );
} );

@@ -8,3 +8,3 @@ /*

import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classic';
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
import Enter from '@ckeditor/ckeditor5-enter/src/enter';

@@ -30,8 +30,9 @@ import Typing from '../src/typing';

return ClassicEditor.create( container, {
plugins: [ Enter, Typing, Paragraph, Bold, Undo ]
} )
.then( newEditor => {
editor = newEditor;
} );
return ClassicEditor
.create( container, {
plugins: [ Enter, Typing, Paragraph, Bold, Undo ]
} )
.then( newEditor => {
editor = newEditor;
} );
} );

@@ -128,3 +129,3 @@

it( 'should replace with longer word (collapsed)', ( done ) => {
it( 'should replace with longer word (collapsed)', done => {
onChangesDone = () => {

@@ -144,3 +145,3 @@ expectContent( editor,

it( 'should replace with longer word (non-collapsed)', ( done ) => {
it( 'should replace with longer word (non-collapsed)', done => {
onChangesDone = () => {

@@ -160,3 +161,3 @@ expectContent( editor,

it( 'should replace with shorter word (merging letter after - collapsed)', ( done ) => {
it( 'should replace with shorter word (merging letter after - collapsed)', done => {
onChangesDone = () => {

@@ -176,3 +177,3 @@ expectContent( editor,

it( 'should replace with shorter word (merging letter after - non-collapsed)', ( done ) => {
it( 'should replace with shorter word (merging letter after - non-collapsed)', done => {
onChangesDone = () => {

@@ -192,3 +193,3 @@ expectContent( editor,

it( 'should replace with same length text', ( done ) => {
it( 'should replace with same length text', done => {
onChangesDone = () => {

@@ -208,3 +209,3 @@ expectContent( editor,

it( 'should replace with longer word on the paragraph end', ( done ) => {
it( 'should replace with longer word on the paragraph end', done => {
onChangesDone = () => {

@@ -224,3 +225,3 @@ expectContent( editor,

it( 'should replace with shorter word on the paragraph end', ( done ) => {
it( 'should replace with shorter word on the paragraph end', done => {
onChangesDone = () => {

@@ -247,3 +248,3 @@ expectContent( editor,

viewSelection.collapse( viewRoot.getChild( nodeIndex ).getChild( 0 ), resultPositionIndex );
viewSelection.setCollapsedAt( viewRoot.getChild( nodeIndex ).getChild( 0 ), resultPositionIndex );

@@ -253,4 +254,4 @@ view.fire( 'mutations',

type: 'text',
oldText: oldText,
newText: newText,
oldText,
newText,
node: viewRoot.getChild( nodeIndex ).getChild( 0 )

@@ -257,0 +258,0 @@ } ],

@@ -8,3 +8,3 @@ /**

import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classic';
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
import Typing from '../../src/typing';

@@ -23,8 +23,9 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';

return ClassicEditor.create( container, {
plugins: [ Typing, Paragraph, Bold ]
} )
.then( newEditor => {
editor = newEditor;
} );
return ClassicEditor
.create( container, {
plugins: [ Typing, Paragraph, Bold ]
} )
.then( newEditor => {
editor = newEditor;
} );
} );

@@ -31,0 +32,0 @@

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc